terminalCode Example
ControlSystem Pattern for Seat/Vehicle Control
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
- Priority System: Uses occupation time to resolve control conflicts
- LinkedGameObjectBuilder: Groups connected objects for unified control
- Input Scoping: ClientInput.PushScope() routes input correctly
- Descendant Traversal: GetComponentsInChildren<IPlayerControllable>() finds all controllables
- Stage-Based: Runs in Stage.StartFixedUpdate for physics compatibility
Was this helpful?