terminalCode Example
Sandbox: ThrusterEntity and WheelEntity — contraption propulsion with ClientInput bindings
ThrusterEntity and WheelEntity — Contraption Propulsion Components
Both ThrusterEntity and WheelEntity implement IPlayerControllable and use ClientInput properties so players can bind them to keys from a vehicle seat.
ThrusterEntity
CSHARP
[Alias( "thruster" )]
public class ThrusterEntity : Component, IPlayerControllable
{
[Property, Range( 0, 1 ), ClientEditable]
public float Power { get; set; } = 0.5f;
[Property, ClientEditable]
public bool Invert { get; set; } = false;
[Property, Sync, ClientEditable]
public ClientInput Activate { get; set; }
[Property, Sync, ClientEditable]
public ClientInput Reverse { get; set; }
// Current thrust output, -1 to 1 (synced for effects)
[Sync] public float CurrentThrust { get; private set; }
void IPlayerControllable.OnControl()
{
var forward = Activate.Down() ? 1f : 0f;
var backward = Reverse.Down() ? -1f : 0f;
CurrentThrust = (forward + backward) * (Invert ? -1f : 1f);
}
protected override void OnFixedUpdate()
{
if ( IsProxy ) return;
if ( CurrentThrust == 0 ) return;
var rb = GetComponentInParent<Rigidbody>( true );
if ( !rb.IsValid() ) return;
var force = WorldRotation.Up * CurrentThrust * Power * rb.Mass * 800f;
rb.ApplyForceAt( WorldPosition, force );
}
}ThrusterTool — placement
CSHARP
[Rpc.Host]
public void SpawnThruster( SelectionPoint point, GameObject thrusterPrefab, Transform tx, bool noWeld )
{
var go = thrusterPrefab.GetScene().Clone();
go.Tags.Add( "removable" );
go.Tags.Add( "constraint" );
go.WorldTransform = tx;
if ( !noWeld && !point.GameObject.Tags.Contains( "world" ) )
{
var thruster = go.GetComponent<ThrusterEntity>();
// Attach with a rigid FixedJoint
var joint = thruster.AddComponent<FixedJoint>();
joint.Attachment = Joint.AttachmentMode.LocalFrames;
joint.LocalFrame2 = point.GameObject.WorldTransform.WithScale( 1 ).ToLocal( tx );
joint.LocalFrame1 = new Transform();
joint.AngularFrequency = 0;
joint.LinearFrequency = 0;
joint.Body = point.GameObject;
joint.EnableCollision = false;
}
go.NetworkSpawn( true, null );
Track( go );
}WheelEntity
CSHARP
public class WheelEntity : Component, IPlayerControllable
{
[Property, ClientEditable] public bool Reversed { get; set; } = false;
[Property, Range( 0, 1 ), ClientEditable] public float Power { get; set; } = 0.5f;
[Property, Sync, ClientEditable] public ClientInput Accelerate { get; set; }
[Property, Sync, ClientEditable] public ClientInput Brake { get; set; }
[Property, Sync, ClientEditable] public ClientInput SteerLeft { get; set; }
[Property, Sync, ClientEditable] public ClientInput SteerRight { get; set; }
void IPlayerControllable.OnControl()
{
var joint = GetComponentInChildren<WheelJoint>( true );
if ( !joint.IsValid() ) return;
var accel = Accelerate.Down() ? 1f : 0f;
var brake = Brake.Down() ? 1f : 0f;
var steer = (SteerRight.Down() ? 1f : 0f) - (SteerLeft.Down() ? 1f : 0f);
var dir = Reversed ? -1f : 1f;
joint.MotorTorque = accel * Power * 500000f * dir;
joint.BrakeTorque = brake * 500000f;
if ( steer != 0 )
{
joint.EnableSteering = true;
joint.TargetSteeringAngle = 30 * steer;
}
else
{
joint.TargetSteeringAngle = 0;
}
}
}Key points
- [ClientEditable] on ClientInput properties lets players remap inputs in the UI without host involvement
- Thrusters apply force at their world position via Rigidbody.ApplyForceAt — this creates torque on the parent body
- Power is a 0–1 multiplier; actual force is Power Mass 800f for thrusters
- Wheels use WheelJoint.MotorTorque and BrakeTorque — max values are 500,000 Nm
- WheelJoint.SteeringLimits = new Vector2(-45, 45) — max steering angle is ±45°
- Both entities tag their GameObjects with "constraint" so the limits system can count them
Was this helpful?