Intermittent Radio Traffic

feed admin

Missile Guidance in From the Depths

on 2018-05-02 by frosty-nee

Intro

A while back I bought the game From the Depths on the recommendation of a friend. It's a kind of maritime voxel-based vehicle building sandbox. My friend and I had some fun but eventually got bored because the game, being in Early Access, wasn't very finished.

Skipping forward a bit, some updates were pushed out that got me interested again. This, plus my discovery of a "LUA block" that allowed you to insert custom code to control various aspects of vehicles such as navigation and weapons, piqued my interest. So I updated the game and started reading the in-game documentation.

Lua

Going in, I didn't know anything about Lua other than, "Arrays are 1-indexed rather than 0-indexed like everyone else in the universe". I really wasn't sure what to expect going in, having to learn the language from scratch while also trying to learn how to use the game's API. Plus dealing with the less-than-ideal documentation of it. Fortunately, the language only had a few curveballs, with most of them being related to the sparsity of data structures (what do you mean all I have are tables, doubles, and strings). Even the 1-indexing turned out to be only a convention, and the FtD API 0-indexed all their arrays in spite of it.

Missiles

One of the more powerful weapon systems in the game are missiles. Extremely easy to use, flexible and packing a punch when you needed it. I found myself using missiles for a variety of use cases. However, they're plagued by a flaw. The stock missile guidance provided by the game does no predictive guidance and just points the missile directly at its target.

The red lines in the video indicate where the missile is currently aiming, you can see it tracking the target without leading it. This causes misses and generally poor performance against fast-moving targets. Be they aircraft, incoming hostile missiles, or fast moving ships that are difficult for torpedoes to track effectively. Frustrated by this, I started to dig through the documentation to see what I could use to improve the situation.

The API and the LUA Block

Lua Blocks, basically, provide a text area for you to paste in code and some documentation of the API. That's all that's provided though, and there's no importing of code that's not in the standard library. The main tools available are the Mathf and Unity's Vector3 and Quaternion libraries.

The API provides you with an interface for mainframes placed on the ship, which can be attached to various detection devices that are used to detect and track targets. Using these you can get target information such as current position and velocity as Vector3 objects which contain Cartesian coordinates for position and velocity. With this information, you can form a rather good picture of both where the target is and where it's going.

The first iteration of target navigation prediction used only the instantaneous values and projected an expected future target location by adding the velocity values, multiplied by our expected time until impact, with the current position of the target.

function AimpointUpdate(I, TIndex, MIndex)
    local missile = I:GetLuaControlledMissileInfo(TIndex,MIndex)
    local tgt = Targets[ActiveMissileTargets[missile.Id]]
    if tgt ~= nil then
    local x,y,z = TargetNavigationPrediction(I,tgt.Id)
        I:SetLuaControlledMissileAimPoint(TIndex,MIndex,x,y,z)
    end
end

function TargetNavigationPrediction(I, Id)
    local mainframeindex, targetindex = GetTargetIndexById(I, Id)
    local TargetInfo = I:GetTargetInfo(mainframeindex, targetindex)
    local TargetPositionInfo = I:GetTargetPositionInfo(mainframeindex, targetindex)
    local x,y,z = TargetInfo.Position.x + TargetInfo.Velocity.x, TargetInfo.Position.y + TargetInfo.Velocity.y, TargetInfo.Position.z + TargetInfo.Velocity.z
    return x,y,z
end

Expected time until impact is estimated by subtracting the missile velocity from the target velocity, which Vector3s allow you to do very simply. Then dividing the distance between missile and target by the magnitude of the relative velocity.

function EstimateTimeToImpact(I,TargetInfo, missile)
    -- distances are in meters, velocities are in m/s
    local rvel = TargetInfo.Velocity - missile.Velocity
    return Vector3.Distance(TargetInfo.Position, missile.Position)
    / Mathf.Sqrt(Mathf.Pow(rvel.x,2) + Mathf.Pow(rvel.y,2) + Mathf.Pow(rvel.z,2))
end

You can see that the red lines indicating the missile's projected path are now leading the target, but there's significant jitter due to using only the instantaneous velocity. Due to this, the missile initially misses the target. I can do better!

For my next iteration, I've started tracking historical position and velocity data so that we can average it to reduce jitter and be generally more confident about our predictions. We've had to implement a ring buffer in order to not keep all of the historical velocity data and keep performance good. Additionally, we've also added a feature which checks the distance between the missile and its target and will detonate the missile if it misses but is close enough to still deal damage.

function AimpointUpdate(I, TIndex, MIndex)
    local missile = I:GetLuaControlledMissileInfo(TIndex,MIndex)
    local tgt = Targets[ActiveMissileTargets[missile.Id]]
    if tgt ~= nil then
        local x,y,z = TargetNavigationPrediction(I,tgt, EstimateTimeToImpact(I,tgt,missile))
        I:SetLuaControlledMissileAimPoint(TIndex,MIndex, x, y, z)
        --detonates missile if it's close enough and has missed
        if Vector3.Distance(missile.Position, tgt.Position) < 10 then
            distance = Vector3.Distance(tgt.Position, missile.Position)
            if ActiveMissileDistance[missile.Id] ~= nil and distance > ActiveMissileDistance[missile.Id] then
                I:DetonateLuaControlledMissile(TIndex,MIndex)
            end
            ActiveMissileDistance[missile.Id] = distance
        end
    end
end

function UpdateTargetLocations(I, Targets)
    for k,v in pairs(Targets) do
        if HistoricalTargetLocations[k] == nil then
            HistoricalTargetLocations[k] = {}
            HistoricalTargetLocations[k][0] = 0
        end
        --only keep the last 40 ticks/ 1s of location data
        local index = HistoricalTargetLocations[k][0] % 40 + 1
        HistoricalTargetLocations[k][index] = v
        HistoricalTargetLocations[k][0] = HistoricalTargetLocations[k][0] + 1
    end
end

You can see that even against a more lively target, jitter is greatly reduced and there are no more close misses due to it, even during hard maneuvering by the target. Improvements to be made in the future are mostly performance oriented, and potentially scaling the timeframe I average velocity over as time till impact approaches 0.

GitHub repo here!