the symptom, in your words

"Rigidbody component API"

✓ verified on 26.08.05
lane Writing gameplayposted updated

▸ SYMPTOM

  • You need a pushable crate / physics prop and don't know which members the Rigidbody component actually exposes.
  • ApplyForce does nothing: wrong force scale or missing collider setup.
  • A prop flies off or barely budges when the player walks into it.

▸ CAUSE

The Rigidbody component's public surface isn't fully enumerated in the wiki. Some members (like MassOverride) are set-only, others (like PhysicsBody) are nullable, and force units are engine-inches-based, all easy to get wrong without a verified reference.

▸ FIX

Verified Rigidbody members:

snippet
// Core properties
rb.Gravity          // bool - enable/disable gravity
rb.MassOverride     // float (kg) - SET-ONLY override
rb.Mass             // float (kg) - READ
rb.Velocity         // Vector3 - read/write
rb.MotionEnabled    // bool

// Physics body - typed PhysicsBody? but populated on the creation tick (26.08.05)
rb.PhysicsBody      // → .MassCenter, .AutoSleep (SET-ONLY)
rb.Sleeping         // setter per-body, GETTER reads scene-wide (see below)
rb.SleepThreshold   // float

Force API:

snippet
rb.ApplyForce(force);               // whole-step force
rb.ApplyForceAt(position, force);   // force at world point
rb.ApplyImpulseAt(position, impulse);

Dynamic prop recipe:

A pushable physics prop = ModelRenderer + non-static BoxCollider + Rigidbody:

snippet
var go = new GameObject();
var renderer = go.Components.Create<ModelRenderer>();
renderer.Model = Model.Load("models/dev/box.vmdl");

var col = go.Components.Create<BoxCollider>();
col.Static = false;  // MUST be non-static

var rb = go.Components.Create<Rigidbody>();
rb.Gravity = true;
rb.MassOverride = 10f; // kg

Push-on-contact spring (drag toward hold point):

snippet
var offset = holdPoint - body.MassCenter;
var force = offset * springK * body.Mass;
// Cap magnitude so heavy props resist a single puller
force = force.ClampLength(maxForce);
body.ApplyForce(force);

Force units are engine-based (kg·units/s²). To convert from SI Newtons, multiply by the meters-to-units constant (39.37). Light props (MassOverride small) fly; heavy ones barely move. That's F = ma working correctly.

AutoSleep is on PhysicsBody (set-only), and the creation-tick null trap is gone

AutoSleep is not a member on Rigidbody itself. The real path is Rigidbody.PhysicsBody.AutoSleep. Two things to know as of 26.08.05:

  1. PhysicsBody is now populated on the creation tick. On builds ≤ 26.07.22 it read null on the same tick Components.Create<Rigidbody>() returned, so you had to defer the AutoSleep write by a tick. That trap is fixed on 26.08.05: PhysicsBody reads non-null immediately (confirmed in edit mode and in a live editor play session, across two fresh scenes; true even for a bare Rigidbody with no collider). You can set it right away.
  2. AutoSleep is set-only. Reading PhysicsBody.AutoSleep no longer compiles (CS0154). You can only assign it.
snippet
var rb = go.Components.Create<Rigidbody>();

// 26.08.05: rb.PhysicsBody is already non-null here.
rb.PhysicsBody.AutoSleep = false;   // OK - set-only, don't try to read it back

Evidence is an in-editor play session (EditorScene.Play), not a shipped/networked game session. Behavior in a fully networked build may differ.

Perf corollary: s&box rigidbodies auto-sleep by default. A worst-case count of free/dynamic bodies must be split into sleeping vs. awake. A scene full of settled dynamic props is far cheaper than a naive "N rigidbodies" count assumes, and a perf budget written against the naive count overstates the cost.

Rigidbody.Sleeping: the setter is per-body, the getter reads scene-wide

Rigidbody.Sleeping's setter lands per-body: waking one crate wakes that one crate. But its getter reads scene-wide, not per-instance. Waking a single crate out of 12 scattered, non-touching crates made a census that read Rigidbody.Sleeping on every crate report all 12 as awake for 1–2 seconds, then all 12 asleep together, reproduced twice from clean boots.

A sleep census that needs genuine per-instance truth cannot call the component getter naively. Read a different per-body signal (velocity, or PhysicsBody state directly) if you must distinguish individual sleep state rather than a scene-wide aggregate.

▸ WHY IT WORKS

The Rigidbody wraps a PhysicsBody managed by the engine's physics simulation. Setting Static = false on the collider is what makes the body dynamic (the default is static/kinematic). MassOverride as a set-only property means you can't read back what you set. Use .Mass for reads. Force applied per-step accumulates naturally with the fixed-tick simulation.

Verified on engine 26.08.05: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Engine audit (26.08.05): the null-on-creation-tick trap is fixed. Rigidbody.PhysicsBody now reads non-null on the same tick Components.Create<Rigidbody>() returns (confirmed edit mode + editor play session), so AutoSleep can be set immediately. PhysicsBody.AutoSleep is now set-only (reading it fails to compile, CS0154). Verified surface also carries .SleepThreshold.
  • Added (26.07.22): Rigidbody.Sleeping's setter is per-body but its getter reads scene-wide. A per-instance sleep census reads every body as awake/asleep together for 1-2s. Use velocity or PhysicsBody state directly for genuine per-body sleep state.
  • Added (26.07.22): AutoSleep is not a member on Rigidbody itself. It lives on Rigidbody.PhysicsBody, which reads null on the same tick Components.Create<Rigidbody>() returns. Set it one tick later and read it back. Perf corollary: bodies auto-sleep by default, so split a worst-case body count into sleeping vs awake.

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord