terminalCode Example
Sandbox: ControlSystem — vehicle seat and contraption control with IPlayerControllable
ControlSystem — Vehicle Seat and Contraption Control
ControlSystem is a GameObjectSystem that drives IPlayerControllable components on contraptions. When a player sits in a BaseChair, the system walks the connected contraption graph and calls OnControl() on every IPlayerControllable in the contraption.CSHARP
public class ControlSystem : GameObjectSystem<ControlSystem>
{
// Tracks when each seat was first occupied — used to sort seats
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>();
// Process seats in order of occupancy (earliest first)
foreach ( var chair in GetSortedSeats() )
{
var builder = new LinkedGameObjectBuilder();
builder.AddConnected( chair.GameObject );
// Skip if an earlier-occupied seat already claimed this contraption
if ( builder.Objects.Any( driven.Contains ) ) continue;
driven.UnionWith( builder.Objects );
RunControl( chair, builder );
}
}
void RunControl( BaseChair chair, LinkedGameObjectBuilder builder )
{
var controller = chair.GetOccupant();
if ( !controller.IsValid() ) return;
var player = controller.GetComponent<Player>();
if ( !player.IsValid() ) return;
// Push a ClientInput scope so weapons know which player is controlling them
using var scope = ClientInput.PushScope( player );
foreach ( var go in builder.Objects )
{
foreach ( var controllable in go.GetComponentsInChildren<IPlayerControllable>() )
{
if ( controllable is null ) continue;
if ( !controllable.CanControl( player ) ) continue;
controllable.OnControl();
}
}
}
}IPlayerControllable interface
CSHARP
public interface IPlayerControllable
{
// Return false to prevent this player from controlling this component
public bool CanControl( Player player ) => true;
public void OnStartControl() { }
public void OnEndControl() { }
public void OnControl(); // Called every fixed update tick while seated
}ClientInput scope
CSHARP
// ClientInput.PushScope sets the "current player" context for the duration of the control tick
// Weapons read ClientInput.Current to know if they're being controlled from a seat
public static IDisposable PushScope( Player player )
{
var previousState = _currentState;
_currentState = new State( player?.Network?.Owner, player );
return DisposeAction.Create( () => _currentState = previousState );
}
public static Player Current => _currentState.player;Implementing IPlayerControllable
CSHARP
// Example: Dynamite entity — explodes when Activate input is pressed from a seat
public class DynamiteEntity : Component, IPlayerControllable
{
[Property, Sync, ClientEditable]
public ClientInput Activate { get; set; }
void IPlayerControllable.OnControl()
{
if ( Activate.Pressed() )
Explode();
}
void IPlayerControllable.OnStartControl() { }
void IPlayerControllable.OnEndControl() { }
}
// Example: ThrusterEntity — applies force while input is held
public class ThrusterEntity : Component, IPlayerControllable
{
[Property, Sync, ClientEditable] public ClientInput Activate { get; set; }
[Property, Sync, ClientEditable] public ClientInput Reverse { get; set; }
void IPlayerControllable.OnControl()
{
var forward = Activate.Down() ? 1f : 0f;
var backward = Reverse.Down() ? -1f : 0f;
CurrentThrust = forward + backward;
}
}Key points
- LinkedGameObjectBuilder.AddConnected walks the constraint/joint graph — all welded/constrained objects are treated as one contraption
- Seats are sorted by _occupiedSince — the player who sat down first controls the contraption
- ClientInput.PushScope is a stack — nested scopes restore the previous state on dispose
- [ClientEditable] on ClientInput properties lets players remap seat controls in the UI
- CanControl(player) returning false prevents a specific player from controlling a component (e.g. ownership checks)
Was this helpful?