Skip to content

Helpers

trk20 edited this page Aug 8, 2026 · 4 revisions

Overview

This section covers helper classes that provide common functionality for scripting with FtDSharp.

PID Controller

The PID class implements a PID (Proportional-Integral-Derivative) controller for smooth feedback control. Useful for altitude hold, attitude stabilization, speed control, and custom weapon tracking.

Quick Start: Bound PID

The simplest way to use a PID: bind an input and output, then call Update():

public class AltitudeHold
{
    private readonly PID _pid = PID.Bind(
        input: () => Game.MainConstruct.Position.y,
        output: v => Game.MainConstruct.Propulsion.Hover = v,
        setpoint: () => 200f,
        kP: 0.1f, kI: 0.02f, kD: 0.5f
    );

    [OnPhysicsTick]
    public void Update() => _pid.Update(Game.GameDeltaTime);
}

The PID automatically:

  1. Reads the current value from input
  2. Reads the target from setpoint
  3. Computes the PID output
  4. Writes the result to output

Bind Overloads

// Full: input + output + dynamic setpoint
PID.Bind(Func<float> input, Action<float> output, Func<float> setpoint,
         float kP, float kI, float kD,
         float outputMin = -1, float outputMax = 1, float integralLimit = 0);

// Static setpoint (constant target value)
PID.Bind(Func<float> input, Action<float> output, float setpoint, ...);

// Input only (handle output manually)
PID.Bind(Func<float> input, float setpoint, ...);
PID.Bind(Func<float> input, Func<float> setpoint, ...);

Manual PID

For more control, use the constructor directly:

public class ManualPidExample
{
    private readonly PID pid = new(kP: 0.1f, kI: 0.02f, kD: 0.5f, outputMin: -1f, outputMax: 1f, integralLimit: 2f);

    [OnPhysicsTick]
    public void Update()
    {
        float current = Game.MainConstruct.Position.y;
        float target = 200f;
        float output = pid.Update(current, target, Game.GameDeltaTime);
        Game.MainConstruct.Propulsion.Hover = output;
    }
}

Or use pre-calculated error:

float error = target - current;
float output = pid.UpdateWithError(error, deltaTime);

PID Properties

Property Type Description
Kp float Proportional gain (get/set)
Ki float Integral gain (get/set)
Kd float Derivative gain (get/set)
IntegralLimit float Max integral term (0 = unlimited, get/set)
OutputMin / OutputMax float Output clamp range (get/set)
Setpoint float Target value (get/set)
Integral float Current accumulated integral (read-only)
PreviousError float Error from previous update (read-only)
LastOutput float Most recent output value (read-only)
LastError float Most recent error value (read-only)
IsFirstUpdate bool Whether this is the first update (read-only)

Default Values

Parameter Default
Kp 0.05
Ki 250
Kd 0.3
OutputMin -1
OutputMax 1
IntegralLimit 0 (unlimited)
Setpoint 0

Methods

Method Returns Description
Update(float deltaTime) float Uses bound input/output. Throws if no input bound.
Update(float current, float setpoint, float deltaTime) float Explicit values
UpdateWithError(float error, float deltaTime) float Pre-calculated error
Reset() void Clears all state (integral, previous error, first-update flag)
ResetIntegral() void Clears integral only

Example: Multi-Axis Stabilization

public class Stabilizer
{
    private readonly PID altitude = PID.Bind(
        () => Game.MainConstruct.Position.y,
        v => Game.MainConstruct.Propulsion.Hover = v,
        setpoint: () => 200f,
        kP: 0.1f, kI: 0.02f, kD: 0.5f, integralLimit: 2f
    );

    private readonly PID pitch = PID.Bind(
        () => Game.MainConstruct.Pitch,
        v => Game.MainConstruct.Propulsion.Pitch = v
    );

    private readonly PID roll = PID.Bind(
        () => Game.MainConstruct.Roll,
        v => Game.MainConstruct.Propulsion.Roll = v
    );

    [OnPhysicsTick]
    public void Update()
    {
        altitude.Update(Game.GameDeltaTime);
        pitch.Update(Game.GameDeltaTime);
        roll.Update(Game.GameDeltaTime);

        Log($"Alt: {Game.MainConstruct.Position.y:F1}m (error: {altitude.LastError:F1})");
    }
}

WeaponController

The WeaponController coordinates multiple weapons and turrets as a group. Use it to create custom weapon groups that aim and fire together.

Creating a WeaponController

public class CustomGroups
{
    private readonly WeaponController portWeapons;
    private readonly WeaponController starboardWeapons;

    public CustomGroups()
    {
        portWeapons = Weapons.CreateController(
            Weapons.All.Where(w => w.LocalPosition.x < 0));
        starboardWeapons = Weapons.CreateController(
            Weapons.All.Where(w => w.LocalPosition.x > 0));
    }

    [OnPhysicsTick]
    public void Update()
    {
        var target = AI.HighestPriorityMainframe.PrimaryTarget;
        if (target == null) return;

        portWeapons.Track(target);
        if (portWeapons.Controlled.Weapons.Any(w => w.CanFire))
            portWeapons.Fire();

        starboardWeapons.Track(target);
        if (starboardWeapons.Controlled.Weapons.Any(w => w.CanFire))
            starboardWeapons.Fire();
    }
}

WeaponController API

WeaponController implements IWeaponController : IWeaponControl, so it has all the same methods as a single weapon:

Method Description
AimAt(Vector3 position) Aim all controlled weapons at a position
Track(ITargetable target) Track a target with lead calculation (all overloads)
Fire() Fire all ready weapons (FireOptions.Default)
Fire(FireOptions options) Fire with failsafe / AI firing options
TryFireAt(Vector3 position) Aim and fire if on target
TryFireAt(Vector3, FireOptions) Aim and fire with options

Additional members:

Member Type Description
Controlled ControlledItems Access to weapons and turrets in this group
AllKnownTypes bool Whether all weapons are known types that can fire
RebuildHierarchy() void Rebuild if weapons are added/removed from turrets at runtime

ControlledItems provides:

  • .Weapons: IReadOnlyList<IWeapon> (excluding turrets)
  • .Turrets: IReadOnlyList<ITurret>
  • .All: IReadOnlyList<IWeapon> (weapons + turrets)
  • .Count: total count

Clone this wiki locally