Not a member yet? Why not Sign up today
Create an account  

  • 13 Vote(s) - 4.15 Average
  • 1
  • 2
  • 3
  • 4
  • 5
 
Hover AI: differential yaw/pitch, attack runs, pitch for altitude

#1
A drop-in replacement for the stock AI.
Is much more stable, aims hardmounts better, keeps its distance and even evades cannon fire.
Can use all jets/dediblades/spinblock rotors as long as they provide a force pointing from their base to their top on positive input.
If your craft is too jumpy/reacts too slowly take a look at the PID configurations at the start.

Steam workshop: [url]http://steamcommunity.com/sharedfiles/fi...=709627548[/url]

What's new, 2017-09-05:
  • Differential thrust for yaw and pitch
  • Attack run for bombers/fighters
  • Can use pitch to control altitude

Important!
  • If you want to use dedispinners make sure that they create a force pointing towards their top on positive input.
    The simplest way is just making a prefab out of a spinner on the demo crafts and extending that as neccessary.
  • Only watches AI mode on the first mainframe listed in the 'C' menu. If you want to use a different one remove every other mainframe until only that one remains then put the removed ones back.

AI modes:
  • Off: Parking mode, hovers in place
  • On: Manual mode, only controls altitude/pitch/roll, you can drive around like it was a boat just in the air
  • Combat:
    Falls back to patrolling if there are no enemies around

    - maintains a preset distance and always faces the target, performs evasive manouvers if it's configured to do so
    - in orbiter mode circles optimalDistance away from the target, can evade by wildly changing pitch
    - in attackrun mode does head-on attack passes
    - in repairmode helps damaged allies, when everyone is at full health falls back to fleetmove instead of patrol if the craft isn't a flagship
  • Patrol: follows waypoints and stabilizes at 0 speed when not fighting, follows waypoints while facing the target otherwise
  • Fleetmove: it's glued to its position in the fleet(relative to the flagship). Out of combat matches the direction the flagship is facing, in combat faces the target and uses side/forward thrusters to follow the flagship without turning.

Altitude control:
  • If you add a hydrofoil it'll be used to control the base altitude in 'Off'/'On'/'Combat' modes.
    -45° is the minimum altitude, 45° the max(I recommend binding the hydrofoil to a complex controller).

"StabilityTest"/"OrbiterDummy" blueprint manual controls:
- I, K: forward, backward
- J, L: turn left, turn right
- U, O: move left, move right
- uparrow, downarrow: increase/decrease altitude


















[Image: h6ZUs8Dl.jpg] [Image: K3ruS2Xl.jpg]

Code:
-- Possible modes:
--   "frontal":    turns towards the target, keeps the distance and dodges if configured to do so
--   "orbiter":    circles around the target
--   "attackrun":  flies towards the target, breaks off at a set distance and reengages after it got far enough or spent enough time flying away
--   "left":       orbiter that turns the left side towards the target
--   "right":      orbiter that turns the right side towards the target
mode = "frontal"

-- Propulsion components used
useJets         = true
useDediSpinners = true
useAllSpinners  = false

-- General behavior
matchAltitude      = false        -- In combat will match the target's altitude(evasion will use this altitude as base, minAltitude and maxAltitude still apply).
repairMode         = false        -- Won't fight but try to repair friendlies
isSatellite        = false        -- Will aim at the target with the bottom
repairClosestFirst = true         -- In repair mode goes to help the most damaged ally first on false
minDamageNoticed   = 1            -- 0.95 means the craft won't bother with anything above 95% health

-- Altitude
optimalAltitude         = 300     -- Move at this height when in combat/stay this many meters above target in repair mode(can be negative for repair)
optimalDistance         = 1500    -- Try to maintain this distance from target in both modes(doesn't take altitude into account). If you want to get close consider reducing maxPitchMagnitude. -1 means staying stationary.
minAltitude             = 30      -- Measured at center of mass, leave some clearance
maxAltitude             = 400     -- Without space captain above 300 the atmosphere starts to thin out. Use space propulsion if you want to go much higher
altitudeFeedbackMeters  = 10      -- Writes the new target altitude to HUD in off/on/combat mode, if it changed by at least this much. 0 turns feedback off.

-- Pitch/roll/yaw
maxPitchMagnitude       = 30      -- Pitch with a bigger absolute value will be clamped to match this
maxRoll                 = 0       -- Roll with a bigger absolute value will be clamped to match this
maxYawDifferential      = 1       -- Values over 0 enable differential trust for yawing(an example: 0.5 means that a left-of-the-CoM, backwards facing jet's output will be increased by up to 50% when turning right, decreased by 50% when turning left)
maxPitchDifferential    = 0       -- Values over 0 enable differential trust for pitching
pitchMovementAssist     = 0       -- Max angle to pitch at to help forward/backwards movement
rollMovementAssist      = 0       -- Max angle to roll at to help left/right movement

-- Evasion
evasiveAltitudeChange   = 0       -- Vertical evasive manouvers add/subtract this to/from altitude
evasiveLeftPeriod       = 4       -- Go left for this amount of time(seconds)
evasiveRightPeriod      = 4       -- Go right for this amount of time(seconds)

-- Balloons
useBalloons             = true    -- Set it to false when not using balloons for water start
ballonDeployAltitude    = 0       -- Deploy balloons below this one
ballonReleaseAltitude   = 100     -- Cut balloons above this

-- false:  uses upwards propulsion to control altitude, hovers in place when idle
-- true:   uses pitch to control altitude, flies in circles when idle(only works in orbiter and attackrun modes)
pitchForAltitude = false

-- Attack run configuration
attackRunBreakDistance    = 300     -- Will get this close before turning
attackRunPitchDistance    = 0       -- Will pitch to match the target at this distance
attackRunReengageDistance = 1000    -- Will reengage after getting this far
attackRunReengageTime     = 30      -- Will reengage after running away for this many seconds
attackRunRollToTurn       = false  -- Will roll into turns when in attackrun mode

-- Used for getting a serial number(that's not used for anything right now)
blueprintName = "HoverAI"

-- Tune PIDs here
--                       P,        D,       I,    OutMax,   OutMin,    IMax,    IMin
rollPIDData     = {   0.05,     0.01,     0.0,         1,       -1,       1,      -1}
pitchPIDData    = {    0.1,     0.02,     0.0,         1,       -1,       1,      -1}
altitudePIDData = {    0.2,     0.05,    0.01,         1,       -1,       1,      -1}
yawPIDData      = {    0.1,     0.02,     0.0,         1,       -1,       1,      -1}
slipPIDData     = {   0.05,     0.05,     0.0,         1,       -1,       1,      -1}
distancePIDData = {    0.1,      0.5,     0.0,         1,       -1,       1,      -1}

function Init(I)
    -- forgive me for I have sinned
    API = I

    if not VehicleState then
        VehicleState = {}
    end
    VehicleState.centerOfMass       = API:GetConstructCenterOfMass()
    VehicleState.pitch              = -API:GetConstructPitch()
    VehicleState.roll               = API:GetConstructRoll()
    VehicleState.forwardVector      = API:GetConstructForwardVector()
    VehicleState.upVector           = API:GetConstructUpVector()
    VehicleState.yaw                = API:GetConstructYaw()
    VehicleState.speed              = API:GetForwardsVelocityMagnitude()
    VehicleState.currentTime        = API:GetTime()
    VehicleState.velocity           = API:GetVelocityVector()
    VehicleState.AICount            = API:GetNumberOfMainframes()
    VehicleState.flagship           = API.Fleet.Flagship

    propulsionAngleTolerance        = 0.5
    local horizontalProjection      = Vector3(VehicleState.forwardVector.x, 0, VehicleState.forwardVector.z)

    VehicleState.projectionRotation = Quaternion.Inverse(Quaternion.LookRotation(horizontalProjection, VehicleState.upVector))
    VehicleState.normalRotation     = Quaternion.Inverse(Quaternion.LookRotation(VehicleState.forwardVector, VehicleState.upVector))

    VehicleState.target = GetCombatTarget()
    if repairMode then
        VehicleState.repairTarget = GetRepairTarget()
    end

    VehicleState.targetPosition = nil

    if API.AIMode == "fleetmove" and API.IsFlagship then
        VehicleState.AIMode = "patrol"
    else
        VehicleState.AIMode = API.AIMode
    end

    if VehicleState.pitch<-180 then
        VehicleState.pitch = VehicleState.pitch+360
    end
    if VehicleState.roll>180 then
        VehicleState.roll = VehicleState.roll-360
    end

    if not VehicleState.serial then
        SetSerial()
        InitPIDs()
        
        if API:Component_GetCount(8) == 0 then
            VehicleState.hasAltitudeControl = false
            VehicleState.adjustedAltitude   = optimalAltitude
        else
            VehicleState.hasAltitudeControl = true
            SetAltitudeHydrofoil(optimalAltitude)
        end
    end

    if VehicleState.hasAltitudeControl then
        SetupAltitudeControl()
    end
end

function SetupAltitudeControl()
    local altitudeFlag = API:Component_GetFloatLogic(8, 0) + 45
    local newAltitude  = minAltitude + ((maxAltitude-minAltitude) * altitudeFlag / 90)

    if altitudeFeedbackMeters > 0 then
        local promptAltitude = math.floor(newAltitude / altitudeFeedbackMeters) * altitudeFeedbackMeters

        if not VehicleState.lastAltitudePrompt or math.abs(promptAltitude - VehicleState.lastAltitudePrompt) >= altitudeFeedbackMeters then
            if VehicleState.AIMode == "on" or VehicleState.AIMode == "off" or VehicleState.AIMode == "combat" then
                API:LogToHud("Altitude: " .. promptAltitude .. "m")
            end
            VehicleState.lastAltitudePrompt = promptAltitude
        end
    end
    VehicleState.adjustedAltitude = newAltitude
end

function SetAltitudeHydrofoil(newAltitude)
    local relativeAltitude = math.min(math.max(newAltitude, minAltitude), maxAltitude) - minAltitude
    local altitudeFraction = relativeAltitude / (maxAltitude-minAltitude)
    local normalizedRange  = altitudeFraction - 0.5
    local fullAngle        = normalizedRange * 90

    -- initialize altitude flag hydrofoil angle
    API:Component_SetFloatLogic(8, 0, math.min(math.max(fullAngle, -45), 45))
end

-- Take over all jets, not just vertical ones
function EnableAllJets()
    VehicleState.jetsAtMax = 0

    if useJets then
        for i=0,11 do
            API:RequestThrustControl(i)
        end
    end
end

function ResetJetStrengths()
    if not VehicleState.jetsAtMax or VehicleState.jetsAtMax == 0 then
        API:Component_SetFloatLogicAll(9,1)
        VehicleState.jetsAtMax = 1
    end
end

function SetupPropulsion()
    -- Send an 'on' signal to vertical jets
    if useJets then
        -- up/down
        API:RequestThrustControl(4)
        API:RequestThrustControl(5)
        -- pitch up/down
        API:RequestControl(2, 4, 1)
        API:RequestControl(2, 5, 1)
    end

    if VehicleState.AIMode == "on" or VehicleState.AIMode == "off" then
        -- Manual mode, lay off side/forward propulsion strengths
        ResetJetStrengths()
    else
        -- Automatic, toggle everything on
        EnableAllJets()
    end

    JetManager = {
        propulsionCount = API:Component_GetCount(9),
        GetComponent    = function (serial) return API:Component_GetBlockInfo(9,serial) end,
        SetImpulse      = function (serial,impulse) API:Component_SetFloatLogic(9,serial,math.min(math.max(impulse, 0),1)) end,
        GetFowardVector = function (propulsion) return propulsion.Forwards end,
        type            = "jet"
    }
    RotorManager = {
        propulsionCount = API:GetSpinnerCount(),
        GetComponent    = function (serial) return API:GetSpinnerInfo(serial) end,
        SetImpulse      = function (serial,impulse) if useAllSpinners or API:IsSpinnerDedicatedHelispinner(serial) then API:SetSpinnerInstaSpin(serial, math.min(math.max(impulse, -1),1)) end end,
        GetFowardVector = function (propulsion) return QuaternionUpVector(propulsion.Rotation) end,
        type            = "spinner"
    }

    PropulsionManagers = {}
    if useAllSpinners or useDediSpinners then
        table.insert(PropulsionManagers, RotorManager)
    end
    if useJets then
        table.insert(PropulsionManagers, JetManager)
    end

    PropulsionComponents = {}
    for i=1,table.getn(PropulsionManagers),1 do
        for j=0,PropulsionManagers[i].propulsionCount-1,1 do
            local component             = PropulsionManagers[i].GetComponent(j)
            local globalOrientation     = PropulsionManagers[i].GetFowardVector(component)
            local localOrientation      = VehicleState.normalRotation * globalOrientation
            local horizontalOrientation = VehicleState.projectionRotation * globalOrientation
            local position              = VehicleState.normalRotation * (component.Position - VehicleState.centerOfMass)

            local currentComponent = {
                serial                = j,
                position              = position,
                globalOrientation     = globalOrientation,
                localOrientation      = localOrientation,
                horizontalOrientation = horizontalOrientation,
                sendImpulse           = PropulsionManagers[i].SetImpulse,
                type                  = PropulsionManagers[i].type
            }
            table.insert(PropulsionComponents, currentComponent)
        end
    end
end

function AddComponentImpulse(component, impulse)
    if not component.impulse then
        component.impulse = impulse
    else
        component.impulse = component.impulse + impulse
    end
end

function AddPitchOutput(pitchInput)
    for i=1,table.getn(PropulsionComponents) do
        local component = PropulsionComponents[i]

        if math.abs(component.localOrientation.y)>propulsionAngleTolerance then
            if component.position.z<-0.05 then
                AddComponentImpulse(component, -pitchInput * component.localOrientation.y)
            elseif component.position.z>0.05 then
                AddComponentImpulse(component, pitchInput * component.localOrientation.y)
            end
        end

        if maxPitchDifferential > 0 and math.abs(component.localOrientation.z)>propulsionAngleTolerance and math.abs(component.position.y) > 0.5 then
            if component.position.y<-0.05 then
                AddComponentImpulse(component, maxPitchDifferential * pitchInput * component.localOrientation.z)
            elseif component.position.y>0.05 then
                AddComponentImpulse(component, maxPitchDifferential * -pitchInput * component.localOrientation.z)
            end
        end
    end
end

function AddYawOutput(yawInput)
    for i=1,table.getn(PropulsionComponents) do
        local component = PropulsionComponents[i]

        if math.abs(component.localOrientation.x)>propulsionAngleTolerance then
            if component.position.z<-0.05 then
                AddComponentImpulse(component, -yawInput * component.localOrientation.x)
            elseif component.position.z>0.05 then
                AddComponentImpulse(component, yawInput * component.localOrientation.x)
            end
        end

        if maxYawDifferential > 0 and math.abs(component.localOrientation.z)>propulsionAngleTolerance and math.abs(component.position.x) > 0.5 then
            if component.position.x<-0.05 then
                AddComponentImpulse(component, maxYawDifferential * yawInput * component.localOrientation.z)
            elseif component.position.x>0.05 then
                AddComponentImpulse(component, maxYawDifferential * -yawInput * component.localOrientation.z)
            end
        end
    end
end

function AddRollOutput(rollInput)
    for i=1,table.getn(PropulsionComponents) do
        local component = PropulsionComponents[i]

        if math.abs(component.localOrientation.y)>propulsionAngleTolerance then
            if component.position.x<-0.05 then
                AddComponentImpulse(component, -rollInput * component.localOrientation.y)
            elseif component.position.x>0.05 then
                AddComponentImpulse(component, rollInput * component.localOrientation.y)
            end
        end

        if VehicleState.AIMode ~= "off" and VehicleState.AIMode ~= "on" then
            if math.abs(component.localOrientation.x)>propulsionAngleTolerance then
                if component.position.y<-0.05 then
                    AddComponentImpulse(component, rollInput * component.localOrientation.x)
                elseif component.position.y>0.05 then
                    AddComponentImpulse(component, -rollInput * component.localOrientation.x)
                end
            end
        end        
    end
end

function AddAltitudeOutput(altitudeInput)
    if pitchForAltitude then
        SetPitch(altitudeInput * maxPitchMagnitude)
        return
    end


    for i=1,table.getn(PropulsionComponents) do
        local component = PropulsionComponents[i]
        local magnitude = (component.globalOrientation.y > 0 and 1 or -1)

        if math.abs(component.globalOrientation.y)>propulsionAngleTolerance then
            AddComponentImpulse(component, altitudeInput * magnitude)
        end
    end
end

function AddSlipOutput(slipInput)
    for i=1,table.getn(PropulsionComponents) do
        local component = PropulsionComponents[i]
        local magnitude = (component.horizontalOrientation.x > 0 and 1 or -1)

        if math.abs(component.horizontalOrientation.x)>propulsionAngleTolerance then
            AddComponentImpulse(component, slipInput * magnitude)
        end
    end
end

function AddForwardOutput(forwardInput)
    for i=1,table.getn(PropulsionComponents) do
        local component = PropulsionComponents[i]
        local magnitude = (component.horizontalOrientation.z > 0 and 1 or -1)

        if math.abs(component.horizontalOrientation.z)>propulsionAngleTolerance then
            AddComponentImpulse(component, forwardInput * magnitude)
        end
    end
end

function SendPropulsionImpulses()
    for i=1,table.getn(PropulsionComponents) do
        local component = PropulsionComponents[i]

        if component.impulse ~= nil then
            component.sendImpulse(component.serial, component.impulse)
        else
            if component.type == "spinner" then
                API:SetSpinnerSpeedFactor(component.serial, 1)
            end
        end
    end
end

function SetPitch(targetPitch)
    targetPitch               = math.max(math.min(targetPitch, maxPitchMagnitude), -maxPitchMagnitude)
    pitchInput,pitchPID       = GetPIDOutput(targetPitch, VehicleState.pitch, pitchPID)

    AddPitchOutput(pitchInput)
    return pitchInput
end

function SetPitchWrapper(targetPitch)
    if not pitchForAltitude then
        return SetPitch(targetPitch)
    end
end

function SetRoll(targetRoll)
    targetRoll                = math.max(math.min(targetRoll, maxRoll), -maxRoll)
    rollInput,rollPID         = GetPIDOutput(targetRoll,  VehicleState.roll,  rollPID)

    AddRollOutput(rollInput)
    return rollInput
end

function SetAltitude(targetAltitude)
    local highestTerrainAhead = HighestTerrainAhead()

    targetAltitude            = math.min(math.max(targetAltitude, highestTerrainAhead+minAltitude), maxAltitude)
    altitudeInput,altitudePID = GetPIDOutput(targetAltitude, VehicleState.centerOfMass.y, altitudePID)

    AddAltitudeOutput(altitudeInput)
    return altitudeInput
end

function Turn(targetAzi)
    yawInput,yawPID = GetPIDOutput(0, targetAzi, yawPID)

    AddYawOutput(yawInput)
    return yawInput
end

function SetSlipSpeedForDistance(distance)
    slipSpeed,slipPID = GetPIDOutput(0, distance, slipPID)

    AddSlipOutput(slipSpeed)
    SetRoll(-rollMovementAssist * slipSpeed)
end

function SetForwardSpeedForDistance(distance, usePitch)
    forwardSpeed,distancePID = GetPIDOutput(0, distance, distancePID)

    AddForwardOutput(-forwardSpeed)
    if usePitch then
        SetPitchWrapper(pitchMovementAssist * forwardSpeed)
    end
end

function GetFrontalEvasionData()
    evasionData = {
        slip           = 0,
        altitudeChange = evasiveAltitudeChange * math.sin(VehicleState.currentTime)
    }

    local slipCycleDuration = evasiveRightPeriod + evasiveLeftPeriod
    if slipCycleDuration == 0 then
        return evasionData
    end

    local slipCycleTime = VehicleState.currentTime%slipCycleDuration
    if slipCycleTime < evasiveRightPeriod then
        evasionData.slip = 1
    else
        evasionData.slip = -1
    end
    
    if math.abs(VehicleState.pitch) > 45 then
        evasionData.altitudeChange = 0
    end

    return evasionData
end

function GetOrbitEvasionPitchOutput(altitude)
    if not pitchDown then
        pitchDown = false
    end
    
    if VehicleState.centerOfMass.y > altitude + evasiveAltitudeChange then
        pitchDown = true
    elseif VehicleState.centerOfMass.y < altitude - evasiveAltitudeChange then
        pitchDown = false
    end
    
    if VehicleState.pitch < -maxPitchMagnitude then
        pitchDown = false
    elseif VehicleState.pitch > maxPitchMagnitude then
        pitchDown = true
    end

    if pitchDown then
        return -1
    else
        return 1
    end
end

function HighestTerrainAhead()
    local maxAltitude = 0
    local nextAltitude

    local xStep = VehicleState.velocity.x
    local zStep = VehicleState.velocity.z
    
    for i=-0.5,5,0.25 do
        nextAltitude = API:GetTerrainAltitudeForPosition(VehicleState.centerOfMass.x + xStep*i, VehicleState.centerOfMass.y, VehicleState.centerOfMass.z + zStep*i)
        if nextAltitude>maxAltitude then
            maxAltitude = nextAltitude
        end
    end
    return maxAltitude
end

function GetCombatTarget()
    local target = API:GetTargetPositionInfo(0,0)

    if target.Valid then
        local azimuth
        if target.Azimuth>180 then
            azimuth = target.Azimuth-360
        else
            azimuth = target.Azimuth
        end

        return {
            Elevation      = -math.deg(math.atan((VehicleState.centerOfMass.y-target.AltitudeAboveSeaLevel) / target.GroundDistance)),
            Azimuth        = azimuth,
            GroundDistance = target.GroundDistance,
            Altitude       = target.AltitudeAboveSeaLevel
        }
    else
        return false
    end
end

function GetRepairTarget()
    -- Lower means more important
    local highestPriorityValue = 1000000
    local targetData = false
    local friendlyCount = API:GetFriendlyCount()

    for i=0,friendlyCount-1,1 do
        local friendly = API:GetFriendlyInfo(i)
        local targetCenterOfMass = friendly.CenterOfMass

        local health = friendly.HealthFraction
        if health<=minDamageNoticed and health<1 then
            if not repairClosestFirst then
                if health<highestPriorityValue then
                    targetData = friendly
                    highestPriorityValue = health
                end
            else
                local distance = math.sqrt( (targetCenterOfMass.x-VehicleState.centerOfMass.x)*(targetCenterOfMass.x-VehicleState.centerOfMass.x)
                                           +(targetCenterOfMass.z-VehicleState.centerOfMass.z)*(targetCenterOfMass.z-VehicleState.centerOfMass.z) )
                if distance<highestPriorityValue then
                    targetData = friendly
                    highestPriorityValue = distance
                end
            end
        end
    end

    if targetData then
        return GetDirectionToCoordinates(targetData.CenterOfMass)
    else
        return false
    end
end

function SetSerial()
    local friendlyCount = API:GetFriendlyCount()

    local cloneCount = 0
    for i=0,friendlyCount-1,1 do
        local friendly = API:GetFriendlyInfo(i)

        if friendly.BlueprintName==blueprintName then
            cloneCount = cloneCount+1
        end
    end
    VehicleState.serial = cloneCount
end

function SetTargetPosition(position)
    VehicleState.targetPosition = GetDirectionToCoordinates(position)

    VehicleState.targetPosition.x             = position.x
    VehicleState.targetPosition.y             = position.y
    VehicleState.targetPosition.z             = position.z
    VehicleState.targetPosition.sideDistance  = math.sin(math.rad(VehicleState.targetPosition.Azimuth)) * VehicleState.targetPosition.GroundDistance
    VehicleState.targetPosition.frontDistance = math.cos(math.rad(VehicleState.targetPosition.Azimuth)) * VehicleState.targetPosition.GroundDistance
end

function PitchToTarget()
    if isSatellite then
        SetPitchWrapper(VehicleState.target.Elevation + 90)
    else
        if math.abs(VehicleState.target.Azimuth) > 10 then
            SetPitchWrapper(0)
        else
            SetPitchWrapper(VehicleState.target.Elevation)
        end
    end
end

function Guard()
    SetTargetPosition(VehicleState.flagship.CenterOfMass + VehicleState.flagship.Rotation * API.IdealFleetPosition)
    
    if pitchForAltitude then
        CircleTarget(VehicleState.targetPosition, false)
        return
    end
    
    SetAltitude(VehicleState.targetPosition.y)
    SetAltitudeHydrofoil(VehicleState.targetPosition.y)
    
    if not VehicleState.target then
        Turn(GetVectorAngle(VehicleState.flagship.ForwardVector, VehicleState.forwardVector))
        SetSlipSpeedForDistance(VehicleState.targetPosition.sideDistance)
        SetForwardSpeedForDistance(VehicleState.targetPosition.frontDistance, true)
    else
        PitchToTarget()
        Turn(VehicleState.target.Azimuth)
        SetSlipSpeedForDistance(VehicleState.targetPosition.sideDistance)
        SetForwardSpeedForDistance(VehicleState.targetPosition.frontDistance, false)
    end
end

function Patrol()
    SetTargetPosition(API.Waypoint)

    if pitchForAltitude then
        CircleTarget(VehicleState.targetPosition, false)
        return
    end

    SetAltitude(VehicleState.targetPosition.y)
    SetAltitudeHydrofoil(VehicleState.targetPosition.y)

    if not VehicleState.target then
        if VehicleState.targetPosition.GroundDistance > 200 then
            local slipSpeed = GetSlipSpeed() * 5
            slipOutput,slipPID = GetPIDOutput(0, slipSpeed, slipPID)

            AddSlipOutput(slipOutput)
            SetRoll(-rollMovementAssist * slipOutput)
        
            Turn(VehicleState.targetPosition.Azimuth)
            SetForwardSpeedForDistance(VehicleState.targetPosition.GroundDistance, true)
        else
            SetSlipSpeedForDistance(VehicleState.targetPosition.sideDistance)
            SetForwardSpeedForDistance(VehicleState.targetPosition.frontDistance, true)
        end
    else
        PitchToTarget()
        Turn(VehicleState.target.Azimuth)
        SetSlipSpeedForDistance(VehicleState.targetPosition.sideDistance)
        SetForwardSpeedForDistance(VehicleState.targetPosition.frontDistance, false)
    end
end

function Repair()
    if pitchForAltitude then
        CircleTarget(VehicleState.repairTarget, false)
        return
    end

    local evasion = GetFrontalEvasionData()

    AddSlipOutput(evasion.slip)
    SetRoll(-rollMovementAssist * evasion.slip)

    Turn(VehicleState.repairTarget.Azimuth)    
    SetAltitude(VehicleState.repairTarget.Altitude + VehicleState.adjustedAltitude + evasion.altitudeChange)
    SetForwardSpeedForDistance(VehicleState.repairTarget.GroundDistance - optimalDistance, true)
end

function FightOrPatrol()
    if not VehicleState.target then
        Patrol()
        return
    end
    
    if mode == "orbiter" or mode == "left" or mode == "right" then
        CircleTarget(VehicleState.target, true)
    elseif mode == "attackrun" then
        AttackRun()
    else
        FrontalFight()
    end
end

function FrontalFight()
    local evasion = GetFrontalEvasionData()

    if math.abs(VehicleState.target.Azimuth) < 10 then
        AddSlipOutput(evasion.slip)
        SetRoll(-rollMovementAssist * evasion.slip)
    else
        SetRoll(0)
    end

    if matchAltitude then
        SetAltitude(VehicleState.target.Altitude + evasion.altitudeChange)
    else
        SetAltitude(VehicleState.adjustedAltitude + evasion.altitudeChange)
    end

    PitchToTarget()
    Turn(VehicleState.target.Azimuth)
    SetForwardSpeedForDistance(VehicleState.target.GroundDistance - optimalDistance, false)
end

function CircleTarget(target, combatMode)
    local sign = 1
    if orbiterMode == "left" then
        sign = 1
    elseif orbiterMode == "right" then
        sign = -1
    elseif target.Azimuth < 0 then
        sign = -1
    end

    local optimalAzi = sign * 90 - sign * 45 * math.min(math.max((target.GroundDistance - optimalDistance) / 200, -1), 1)

    if combatMode and math.abs(90 - math.abs(target.Azimuth)) < 20 then
        SetRoll(-sign * target.Elevation)
    else
        SetRoll(0)
    end

    local altitude
    if repairMode then
        altitude = target.Altitude + VehicleState.adjustedAltitude
    elseif not combatMode or matchAltitude then
        altitude = target.Altitude
    else
        altitude = VehicleState.adjustedAltitude
    end

    if combatMode and maxPitchMagnitude > 0  and evasiveAltitudeChange > 0 then
        AddPitchOutput(GetOrbitEvasionPitchOutput(altitude))
    else
        SetPitchWrapper(-pitchMovementAssist)
        SetAltitude(altitude)
    end

    Turn(target.Azimuth - optimalAzi)
    AddForwardOutput(1)
end

function AttackRun()
    local yawOutput
    
    if not running then
        yawOutput = Turn(VehicleState.target.Azimuth)

        if VehicleState.target.GroundDistance < attackRunBreakDistance then
            running    = true
            lastAttack = 0
        end

        if VehicleState.target.GroundDistance < attackRunPitchDistance and VehicleState.centerOfMass.y > minAltitude then
            PitchToTarget()
        else
            if matchAltitude then
                SetAltitude(VehicleState.target.Altitude)
            else
                SetAltitude(VehicleState.adjustedAltitude)
            end
            
            if not pitchForAltitude then
                SetPitch(0)
            end
        end
    else
        lastAttack = lastAttack + 1
    
        if VehicleState.target.Azimuth > 0 then
            yawOutput = Turn(VehicleState.target.Azimuth - 180)
        else
            yawOutput = Turn(VehicleState.target.Azimuth + 180)
        end
        
        if VehicleState.target.GroundDistance > attackRunReengageDistance or lastAttack > attackRunReengageTime * 40 then
            running = false
        end
        
        if matchAltitude then
            SetAltitude(VehicleState.target.Altitude)
        else
            SetAltitude(VehicleState.adjustedAltitude)
        end
        
        if not pitchForAltitude then
            SetPitch(0)
        end
    end

    if attackRunRollToTurn  and math.abs(yawOutput) > 0.2 then
        SetRoll(-yawOutput * maxRoll)
    else
        SetRoll(0)
    end

    AddForwardOutput(1)
end

function Stabilize()
    if pitchForAltitude then
        Patrol()
        return
    end

    SetAltitude(VehicleState.adjustedAltitude)
    SetPitchWrapper(0)
    SetRoll(0)
end

function Idle()
    SetPitchWrapper(0)
    SetRoll(0)
    SetAltitude(VehicleState.adjustedAltitude)
    AddSlipOutput(-GetSlipSpeed() / 10)
    AddForwardOutput(-VehicleState.speed / 10)
end

function Update(I)
    if I:IsDocked() then
        return
    end

    I:TellAiThatWeAreTakingControl()

    Init(I)
    SetupPropulsion(I)

    if useBalloons then
        BalloonCheck()
    end

    if repairMode then
        if VehicleState.AIMode == "combat" then
            if VehicleState.repairTarget then
                Repair()
            else
                if API.IsFlagship then
                    VehicleState.AIMode = "patrol"
                else
                    VehicleState.AIMode = "fleetmove"
                end
            end
        end
    end
        
    if not (repairMode and VehicleState.AIMode == "combat") then
        if VehicleState.AIMode == "off" then
            Idle()
        elseif VehicleState.AIMode == "on" then
            Stabilize()
        elseif VehicleState.AIMode == "combat" then
            FightOrPatrol()
        elseif VehicleState.AIMode == "patrol" then
            Patrol()
        elseif VehicleState.AIMode == "fleetmove" then
            if VehicleState.IsFlagship then
                FightOrPatrol()
            else
                Guard()
            end
        end
    end

    SendPropulsionImpulses()
end

function BalloonCheck()
    if VehicleState.centerOfMass.y < ballonDeployAltitude then
        API:Component_SetBoolLogicAll(0, true)
    elseif VehicleState.centerOfMass.y > ballonReleaseAltitude then
        API:Component_SetBoolLogicAll(0, false)
    end
end

-- ________________________________________________________________________________​​___ Generic helper functions ________________________________________________________________________________​​___


function GetDirectionToCoordinates(position)
    local vectorToPosition = Vector3(position.x-VehicleState.centerOfMass.x, position.y-VehicleState.centerOfMass.y, position.z-VehicleState.centerOfMass.z)
    local direction        = vectorToPosition.normalized

    local aziDot         = direction.x*VehicleState.forwardVector.x + direction.z*VehicleState.forwardVector.z
    local aziCross       = direction.z*VehicleState.forwardVector.x - direction.x*VehicleState.forwardVector.z
    local groundDistance = math.sqrt(vectorToPosition.x*vectorToPosition.x + vectorToPosition.z*vectorToPosition.z)

    return {
        Elevation      = -math.deg(math.atan((VehicleState.centerOfMass.y-position.y) / groundDistance)),
        Azimuth        = math.deg(math.atan2(aziCross, aziDot)),
        Altitude       = position.y,
        GroundDistance = groundDistance
    }
end

function GetVectorAngle(vector1, vector2)
    local aziDot   = vector1.x * vector2.x + vector1.z * vector2.z
    local aziCross = vector1.z * vector2.x - vector1.x * vector2.z
    
    return math.deg(math.atan2(aziCross, aziDot));
end

function InitPID(pidData)
    PID = {}
    PID.Kp            = pidData[1]
    PID.Kd            = pidData[2]
    PID.Ki            = pidData[3]
    PID.OutMax        = pidData[4]
    PID.OutMin        = pidData[5]
    PID.IMax          = pidData[6]
    PID.IMin          = pidData[7]
    PID.integral      = 0
    PID.previousError = 0

    return PID
end

function InitPIDs()
    rollPID     = InitPID(rollPIDData)
    pitchPID    = InitPID(pitchPIDData)
    altitudePID = InitPID(altitudePIDData)
    yawPID      = InitPID(yawPIDData)
    slipPID     = InitPID(slipPIDData)
    distancePID = InitPID(distancePIDData)
end

function GetPIDOutput(SetPoint, ProcessVariable, PID)
    local error     = SetPoint - ProcessVariable
    local timeDelta = 0.025
    local derivative
    local output

    PID.integral = PID.integral + (error*timeDelta) * PID.Ki
    if (PID.integral > PID.IMax) then PID.integral = PID.IMax end
    if (PID.integral < PID.IMin) then PID.integral = PID.IMin end

    derivative = (error - PID.previousError)/timeDelta

    output = PID.Kp*error + PID.Kd*derivative + PID.integral
    if (output > PID.OutMax) then output = PID.OutMax end
    if (output < PID.OutMin) then output = PID.OutMin end

    PID.previousError = error
    return output,PID
end

-- Props to Evil4Zerggin for the function
function QuaternionUpVector(quaternion)
    local x = 2 * (quaternion.x * quaternion.y - quaternion.z * quaternion.w)
    local y = 1-2 * (quaternion.x * quaternion.x + quaternion.z * quaternion.z)
    local z = 2 * (quaternion.y * quaternion.z + quaternion.x * quaternion.w)

    return Vector3(x, y, z).normalized
end

function GetSlipSpeed()
    local velocityHeading = 180/math.pi*math.atan2(VehicleState.velocity.x,VehicleState.velocity.z)
    if velocityHeading<0 then
        velocityHeading = velocityHeading+360
    end
    local slipAngle = velocityHeading - VehicleState.yaw
    local groundSpeedx = VehicleState.velocity.x
    local groundSpeedz = VehicleState.velocity.z
    local groundSpeed = math.sqrt(groundSpeedx^2 + groundSpeedz^2)
    local slipSpeed = groundSpeed*math.sin(slipAngle/180*math.pi)

    return slipSpeed
end


Attached Files
.blueprint   StabilityTest.blueprint (Size: 89.05 KB / Downloads: 522)
.blueprint   Nova PAC satellite by St0rmWyvrn.blueprint (Size: 106.43 KB / Downloads: 274)
.blueprint   OrbiterDummy.blueprint (Size: 241.04 KB / Downloads: 289)
Reply



Messages In This Thread
Hover AI: differential yaw/pitch, attack runs, pitch for altitude - by draba - 2016-01-22, 03:05 AM
RE: Hover gunship AI - by yay101 - 2016-01-22, 04:05 AM
RE: Hover gunship AI - by VodkaBear - 2016-01-22, 02:30 PM
RE: Hover gunship AI - by draba - 2016-01-22, 03:39 PM
RE: Hover gunship AI - by Frosted Mantis - 2016-01-23, 11:46 AM
RE: Hover gunship/repaircraft AI - by draba - 2016-01-25, 02:15 AM
RE: Hover gunship AI - by draba - 2016-01-23, 05:42 PM
RE: Hover gunship AI - by Arpio - 2016-01-23, 06:00 PM
RE: Hover gunship/repaircraft AI - by Arpio - 2016-01-25, 03:27 AM
RE: Hover gunship/repaircraft AI - by nuhll - 2016-01-26, 04:03 AM
RE: Hover gunship/repaircraft AI - by Arpio - 2016-01-26, 05:39 AM
RE: Hover gunship/repaircraft AI - by draba - 2016-01-26, 06:17 AM
RE: Hover gunship/repaircraft AI - by nuhll - 2016-01-26, 11:56 AM
RE: Hover gunship/repaircraft AI - by draba - 2016-01-26, 12:50 PM
RE: Hover gunship/repaircraft AI - by nuhll - 2016-01-26, 12:56 PM
RE: Hover gunship/repaircraft AI - by Ruger - 2016-01-26, 06:41 PM
RE: Hover gunship/repaircraft AI - by draba - 2016-01-26, 07:49 PM
RE: Hover gunship/repaircraft AI - by nuhll - 2016-01-26, 07:52 PM
RE: Hover gunship/repaircraft AI - by draba - 2016-01-26, 07:58 PM
RE: Hover gunship/repaircraft AI - by nuhll - 2016-01-26, 07:59 PM
RE: Hover gunship/repaircraft AI - by draba - 2016-01-26, 10:41 PM
RE: Hover gunship/repaircraft AI - by Ruger - 2016-01-26, 11:04 PM
RE: Hover gunship/repaircraft AI - by draba - 2016-01-27, 07:09 AM
RE: Hover gunship/repaircraft AI - by Ruger - 2016-01-27, 05:20 PM
RE: Hover gunship/repaircraft AI - by draba - 2016-01-27, 06:44 PM
RE: Hover gunship/repaircraft AI - by SynthTwo - 2016-01-28, 03:55 AM

Forum Jump:


Users browsing this thread:
1 Guest(s)