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: 512)
.blueprint   Nova PAC satellite by St0rmWyvrn.blueprint (Size: 106.43 KB / Downloads: 267)
.blueprint   OrbiterDummy.blueprint (Size: 241.04 KB / Downloads: 279)
Reply

#2
Awesome.
Reply

#3
Surely will be pain in the ass for slow battleships without rails/sabot guns with good prediction.
Reply

#4
Against small ones staying out over 1500 meters cannons are pretty bad,
even a 1000 m/s sabot will mostly miss even against the striker in the video(and it isn't that small/dodging too well).
IR missiles are useless, a portion of the rest can't even reach 1500 with any accuracy and some will be intercepted.

Lasers that can overwhelm the batteries and lua thumpers are an absolute pain in the backside though.
Reply

#5
I was wondering if it was possible to add the ability to point to another craft using only yaw AND continue to stabilize.

Additionally to not point at a ship on the y axis but still on the x axis, if this isn't a good description Ill screenshot some stuff.

I feel it would be useful for craft that stay high above their enemy but don't want to pitch wildly.

sorry I keep pressing for features but I think this is some great work you've done.
Reply

#6
They are simple changes, I'll have some time on monday so I'll add them and the repair behaviour.
No worries, I wouldn't tinker with lua if I didn't enjoy it; it's actually really nice to get feedback and ideas for improvement.
Reply

#7
umm... could you make this type of movement work with this craft? would you like to look at this?


Attached Files
.blueprint   cycliccopterconcept.blueprint (Size: 27.96 KB / Downloads: 157)
People has asked what's the easiest way to kill a squirrel, most will say laser. But for me the easiest way is pressing Esc and choose "destroy enemy vehicles" Big Grin
Reply

#8
Updated the code and the blueprints with the new changes.

(2016-01-23, 11:46 AM)Frosted Mantis Wrote: I was wondering if it was possible to add the ability to point to another craft using only yaw AND continue to stabilize.

Additionally to not point at a ship on the y axis but still on the x axis, if this isn't a good description Ill screenshot some stuff.

Forgot to ask, are these 2 separate things? Kinda unintuitive, but the game uses y as the vertical axis(I think everything unity based does this).

(2016-01-23, 06:00 PM)Arpio Wrote: umm... could you make this type of movement work with this craft? would you like to look at this?

Yep, it's doable. I've attached a halfway working blueprint, it just goes in the 4 main directions for 20 seconds and everything is hardcoded for now. Dunno about tilt rotor helicopters, while they certainly can work they are just not very practical. Gravity is low so to have a big enough forward component in your rotor's lift you have to tilt it and pitch/roll in very high angles, but then it's really hard not to flip and you won't be very aerodynamic.

Will try to get something workable up a bit later.


Attached Files
.blueprint   working_cycliccopterconcept.blueprint (Size: 41.4 KB / Downloads: 125)
Reply

#9
Woah this is cool
People has asked what's the easiest way to kill a squirrel, most will say laser. But for me the easiest way is pressing Esc and choose "destroy enemy vehicles" Big Grin
Reply

#10
Nah it's one singular thing, just so you don't have to pitch/roll wildly on craft meant to stay level all the time. I explained that pretty badly Tongue
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)