terminalCode Example

ControlSystem Pattern for Seat/Vehicle Control

calendar_today May 3, 2026 schedule ~1 min read person patrickjr verified 50

ControlSystem - Seat/Vehicle Control Management

A GameObjectSystem that manages control transfer between players and seated entities (vehicles, chairs). Handles input routing and priority-based control conflicts.

Implementation

CSHARP
public class ControlSystem : GameObjectSystem<ControlSystem>
{
    // Tracks when each chair first became occupied for priority sorting
    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 these objects
            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;

        // Push the player as the input scope for this control session
        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();
            }
        }
    }
}

IPlayerControllable Interface

CSHARP
public interface IPlayerControllable
{
    /// <summary>
    /// Called every tick while the player is controlling this object
    /// </summary>
    void OnControl();

    /// <summary>
    /// Called when the player starts controlling this object
    /// </summary>
    void OnStartControl();

    /// <summary>
    /// Called when the player stops controlling this object
    /// </summary>
    void OnEndControl();

    /// <summary>
    /// Returns true if this player can control this object
    /// </summary>
    bool CanControl(Player player);
}

Example Implementation

CSHARP
public class Vehicle : Component, IPlayerControllable
{
    [Property] public float MaxSpeed { get; set; } = 1000f;
    [Property] public float TurnSpeed { get; set; } = 45f;

    private Rigidbody _rigidbody;

    protected override void OnAwake()
    {
        _rigidbody = GetComponent<Rigidbody>();
    }

    bool IPlayerControllable.CanControl(Player player) => true;

    void IPlayerControllable.OnStartControl()
    {
        // Enable engine sounds, lights, etc.
    }

    void IPlayerControllable.OnEndControl()
    {
        // Disable engine, apply brakes
    }

    void IPlayerControllable.OnControl()
    {
        // Read input and apply physics
        var throttle = Input.AnalogMove.x;
        var steering = Input.AnalogMove.y;

        _rigidbody.Velocity += WorldRotation.Forward * throttle * MaxSpeed * Time.Delta;
        _rigidbody.AngularVelocity += Vector3.Up * steering * TurnSpeed * Time.Delta;
    }
}

Key Features

Was this helpful?