Bodies & Forces
Movement is split into simple linear and angular velocity integration and optional rigid-body force integration. An entity can move with Position and Velocity or rotate with Rotation and AngularVelocity, while dynamic bodies can additionally receive gravity, forces, torque, damping, and collision response.
Body types
Section titled “Body types”Set RigidBody.type with a BodyType value:
| Type | Gravity, forces, and torque | Collision response | Typical use |
|---|---|---|---|
BodyType.Dynamic | Applied | Moves according to inverse mass and inertia | Players, projectiles, movable props |
BodyType.Kinematic | Ignored | Treated as infinite mass | Scripted platforms and obstacles |
BodyType.Static | Ignored | Treated as infinite mass | Walls and level geometry |
Velocity integration is independent of body type. Any entity with Velocity and Position moves each fixed step, and any entity with AngularVelocity and Rotation rotates each fixed step, including kinematic and static bodies. Omit velocity components from static geometry.
Components
Section titled “Components”RigidBody
Section titled “RigidBody”| Field | Type | Default | Description |
|---|---|---|---|
type | BodyType | BodyType.Dynamic | Determines whether the body has finite inverse mass |
mass | number | 1 | Body mass used by force integration |
inverseMass | number | 1 | Cached inverse mass; falls back to 1 / mass when not positive |
inertia | number | 1 | Rotational inertia used by torque integration |
inverseInertia | number | 1 | Cached inverse inertia; falls back to 1 / inertia when not positive |
restitution | number | 0 | Bounciness used by collision impulses |
friction | number | 0 | Reserved field; not currently applied by the resolver |
linearDamping | number | 0 | Per-second damping applied after force integration |
angularDamping | number | 0 | Per-second damping applied after torque integration |
Dynamic bodies with mass <= 0 have zero effective inverse mass, and bodies with inertia <= 0 have zero effective inverse inertia. Keep mass and inverseMass, and inertia and inverseInertia, consistent when changing them:
import { Define } from '@shell/ecs';import { BodyType, RigidBody } from '@shell/physics';
const BouncyBodyPrefab = Define.prefab().with(RigidBody, { type: BodyType.Dynamic, mass: 2, inverseMass: 0.5, inertia: 4, inverseInertia: 0.25, restitution: 0.25, linearDamping: 1, angularDamping: 0.5,});Velocity
Section titled “Velocity”Velocity stores movement in world units per second:
const MovingPrefab = Define.prefab().with(Velocity, { x: 120, y: 0, z: 0,});Each fixed step updates position using position += velocity * deltaSeconds.
AngularVelocity
Section titled “AngularVelocity”AngularVelocity.z stores rotation around the 2D axis in radians per second:
const RotatingPrefab = Define.prefab().with(AngularVelocity, { z: Math.PI,});Each fixed step updates Rotation.z using rotation.z += angularVelocity.z * deltaSeconds.
Force accumulates force for the current fixed step:
const ForceDrivenPrefab = Define.prefab().with(Force);The physics pipeline clears Force.x and Force.y at the start of every fixed step. Add persistent effects again each step rather than setting them once.
Torque
Section titled “Torque”Torque.z accumulates scalar torque around the 2D axis for the current fixed step:
const TorqueDrivenPrefab = Define.prefab().with(Torque);The physics pipeline clears Torque.z at the start of every fixed step. Add persistent torque again each step rather than setting it once.
Dynamic body
Section titled “Dynamic body”A force- and torque-driven body needs Position, Rotation, Velocity, AngularVelocity, RigidBody, Force, and Torque. Add a collider when it should also participate in collision detection:
import { Define } from '@shell/ecs';import { CircleCollider, DynamicBody, RigidBody } from '@shell/physics';import { Position } from '@shell/transform';
const DynamicBodyPrefab = Define.prefab() .with(DynamicBody) .with(Position, { x: 0, y: 0, z: 0 }) .with(RigidBody, { mass: 2, inverseMass: 0.5, inertia: 4, inverseInertia: 0.25 }) .with(CircleCollider, { radius: 12 });
const body = world.get('commands').spawn(DynamicBodyPrefab).root;Gravity is applied only to entities that have both RigidBody and Force. Force integration additionally requires Velocity.
Queued forces and impulses
Section titled “Queued forces and impulses”CorePlugin binds the physicsForces resource as a PhysicsForces instance. The aggregate PhysicsPlugin installs CorePlugin, so it exposes the same resource:
const physicsForces = world.get('physicsForces');
physicsForces.applyForce(body, { x: 600, y: 0 });physicsForces.applyImpulse(body, { x: 0, y: -200 });physicsForces.applyTorque(body, 30);physicsForces.applyAngularImpulse(body, Math.PI / 2);All four methods require RigidBody; only dynamic bodies are affected when calls are drained:
| Method | Effect when drained | Additional required component |
|---|---|---|
applyForce(entity, { x, y }) | Adds to Force.x and Force.y | Force |
applyImpulse(entity, { x, y }) | Adds a one-time mass-scaled change to Velocity.x and Velocity.y | Velocity |
applyTorque(entity, torqueNumber) | Adds to Torque.z | Torque |
applyAngularImpulse(entity, impulseNumber) | Adds a one-time inertia-scaled change to AngularVelocity.z | AngularVelocity |
The helpers assert at call time that the target has the required components. Vector components and scalar values must be finite. Static and kinematic targets accept calls but are ignored during the drain. Calls are queued and drained once at the next fixed integration stage, after all gameplay force producers in PhysicsSet.Forces and before force integration. A call made after that drain waits for the following fixed integration stage. Stale or despawned queued targets are skipped.
Static body
Section titled “Static body”Static level geometry does not need velocity, force, or torque components:
import { Define } from '@shell/ecs';import { BoxCollider, StaticBody } from '@shell/physics';import { Position } from '@shell/transform';
const WallPrefab = Define.prefab() .with(StaticBody) .with(Position, { x: 400, y: 200, z: 0 }) .with(BoxCollider, { width: 64, height: 32 });
const wall = world.get('commands').spawn(WallPrefab).root;Apply forces in a system
Section titled “Apply forces in a system”Put gameplay force systems in PhysicsSet.Forces. This set runs after force and torque clearing and before the PhysicsForces queue is drained and force integration begins. Calls made from these systems affect the same fixed step. Gravity also runs in this set, so use .after(GravitySystem) only when a system specifically depends on gravity having already been added.
import { Define, Schedule } from '@shell/ecs';import { Force, PhysicsSet, RigidBody } from '@shell/physics';
export const ApplyThrustSystem = Define.system() .inSet(PhysicsSet.Forces) .queries({ bodies: Define.query({ data: [RigidBody, Force] }), }) .update(({ queries, resources }) => { const physicsForces = resources.get('physicsForces');
queries.bodies.forEach((entity) => { physicsForces.applyForce(entity, { x: 600, y: 0 }); }); });Register the system in Schedule.FixedUpdate, where force integration runs:
world.registerSystem(Schedule.FixedUpdate, ApplyThrustSystem);For an instantaneous velocity change, queue an impulse instead of applying force:
world.get('physicsForces').applyImpulse(entity, { x: 0, y: -300 });Direct Mut(Force) or Mut(Torque) writes remain available for low-level, batch-oriented systems,
but physicsForces is the recommended gameplay API because it also provides mass-scaled impulses,
inertia-scaled angular impulses, input validation, and stale-entity protection.
Change gravity at runtime
Section titled “Change gravity at runtime”The configured gravity is available through the physicsGravity resource:
const gravity = world.get('physicsGravity');gravity.x = 0;gravity.y = 200;Gravity is acceleration. The gravity system multiplies it by body mass before adding it to Force, so all dynamic bodies receive the same acceleration regardless of mass.
System ordering
Section titled “System ordering”| System | Schedule | Set | Description |
|---|---|---|---|
ClearForcesSystem | FixedUpdate | Before PhysicsSet.Forces | Resets accumulated force and torque |
GravitySystem | FixedUpdate | PhysicsSet.Forces | Adds gravity to dynamic bodies |
| Gameplay force producers | FixedUpdate | PhysicsSet.Forces | Add per-step force and torque |
PhysicsForces queue drain | FixedUpdate | After PhysicsSet.Forces | Applies each queued call once |
ApplyForcesSystem | FixedUpdate | PhysicsSet.IntegrateForces | Updates linear and angular velocity and applies damping |
ApplyVelocitySystem | FixedPostUpdate | PhysicsSet.IntegrateVelocity | Updates position and Rotation.z from velocity |