terminalCode Example

ControlSystem: Vehicle seat control with IPlayerControllable

calendar_today May 12, 2026 schedule ~1 min read person PatrickJr verified 50

ControlSystem: Vehicle Seat Control with IPlayerControllable

This example shows the ControlSystem which manages vehicle seat control, sorting seats by occupation time and routing input to IPlayerControllable components.

CSHARP
public class ControlSystem : GameObjectSystem<ControlSystem>
{
    // When each chair first became occupied. Used to sort seats so the earliest occupant is in charge
    private readonly Dictionary<BaseChair, RealTimeSince> _occupiedSince = new();

    public ControlSystem(Scene scene) : base(scene)
    {
        Listen(Stage.StartFixedUpdate, 10, OnTick, "ControlSystem");
    }

    void OnTick()
    {
        var driven = new HashSet<GameObject>();

        foreach (var chair in GetSortedSeats())
        {
            var builder = new LinkedGameObjectBuilder();
            builder.AddConnected(chair.GameObject);

            // Skip if a seat occupied earlier already claimed this
            if (builder.Objects.Any(driven.Contains)) continue;
            driven.UnionWith(builder.Objects);

            RunControl(chair, builder);
        }
    }

    IEnumerable<BaseChair> GetSortedSeats()
    {
        var chairs = Scene.GetAll<BaseChair>();

        foreach (var chair in chairs)
        {
            if (!chair.IsValid() || !chair.IsOccupied)
                _occupiedSince.Remove(chair);
            else
                _occupiedSince.TryAdd(chair, 0);
        }

        return chairs
            .Where(c => c.IsValid() && c.IsOccupied)
            .OrderBy(c => (float)_occupiedSince.GetValueOrDefault(c, default));
    }

    void RunControl(BaseChair chair, LinkedGameObjectBuilder builder)
    {
        var controller = chair.GetOccupant();
        if (!controller.IsValid()) return;

        var player = controller.GetComponent<Player>();
        if (!player.IsValid()) return;

        using var scope = ClientInput.PushScope(player);

        foreach (var o in builder.Objects)
        {
            foreach (var controllable in o.GetComponentsInChildren<IPlayerControllable>())
            {
                if (controllable is null) continue;
                if (!controllable.CanControl(player)) continue;

                controllable.OnControl();
            }
        }
    }
}

Key Features

  • Seat Sorting: Sorts seats by occupation time so earliest occupant has priority
  • LinkedGameObjectBuilder: Finds all connected objects from the seat
  • Conflict Resolution: Skips objects already controlled by earlier-occupied seats
  • ClientInput.PushScope: Routes input from the player to controllable components
  • IPlayerControllable: Interface for components that can be controlled by players
  • Stage.StartFixedUpdate: Runs in the fixed update loop for consistent physics timing

IPlayerControllable Interface

CSHARP
public interface IPlayerControllable
{
    bool CanControl(Player player);
    void OnControl();
}

Usage Example

CSHARP
public class MyVehicle : Component, IPlayerControllable
{
    public bool CanControl(Player player)
    {
        return true; // Allow any player to control
    }

    public void OnControl()
    {
        // Read input and control the vehicle
        var throttle = Input.Float("forward");
        var steering = Input.Float("right");
        
        // Apply to vehicle physics
    }
}

Notes

  • ControlSystem automatically runs in scenes that have BaseChair components
  • Seats are sorted by occupation time to prevent conflicts
  • LinkedGameObjectBuilder finds all connected objects (wheels, thrusters, etc.)
  • ClientInput.PushScope ensures input comes from the seated player
  • Only components implementing IPlayerControllable receive control callbacks
Was this helpful?