Appearance
Physics: rigid bodies & forces
Galatrix runs a real rigid-body simulation (Rapier) in every runtime — the editor play-test, standalone builds, and the multiplayer server all step the same physics. This guide is the scripting side: how to make things fall, push, bounce, and — the part everyone gets wrong first — how to move a body correctly from a script.
New to scripts? Read Scripting basics first. Physics code lives in specific lifecycle hooks, and putting it in the wrong one is the single most common mistake (covered below).
What is this.entity.id?
this.entity is the GameObject the script is attached to (Unity's this.gameObject), and this.entity.id is its unique handle. The physics.* functions take that id string, not the object — so you always pass something.id. To act on a different object, get its id from this.game.findEntity('Name')?.id, a collision's other.id, or an Inspector reference field (this.target.id). findEntity returns null when nothing matches — guard it.
The one rule: physics owns dynamic bodies
Give an entity a RigidBody component and choose a type:
| Type | Who moves it | Use for |
|---|---|---|
| static | nobody — it never moves, but it still collides | floors, walls, terrain, fixed props |
| dynamic | the simulation — gravity, forces, collisions | crates, balls, ragdolls, debris |
| kinematic | you (script or animation) — it shoves dynamic bodies but ignores forces itself | moving platforms, doors, elevators |
(A walking player usually uses a Character Controller instead — a different component with its own guide: Scripting your own player.)
Here's the rule that trips everyone up:
On a dynamic body, the simulation owns the transform
Writing entity.position = … or entity.rotation = … on a dynamic body is silently overwritten by the next physics step. To move it you talk to physics — forces, velocities, or a teleport (all below).
On static, kinematic, and script-moved entities the opposite holds: you write the transform directly and the collider follows. (The same split governs rotation — see Rotations → Physics owns dynamic bodies.)
Moving a body: forces vs impulses vs velocity
Three ways to move a dynamic body, from gentlest to bluntest:
Force — a continuous push. Apply it every physics step and it builds up like a rocket; stop applying and it stops. Measured in Newtons, scaled by the body's mass.
js
onFixedUpdate() {
// steady lift while held — a jetpack
if (this.input.getKey('space')) this.physics.applyForce(this.entity.id, [0, 200, 0])
}Impulse — an instant kick: one call, one change in velocity. A jump, a hit, a launch.
js
this.physics.applyImpulse(this.entity.id, [0, 8, 0]) // pop straight up, onceVelocity — set the speed directly, ignoring mass and momentum. Precise but overrides the sim; reach for it when you want tight arcade control. Read-modify-write to change one axis and let gravity keep working:
js
const v = this.physics.getVelocity(this.entity.id)
this.physics.setVelocity(this.entity.id, [5, v[1], v[2]]) // drive X, keep the body's own fallEach has a spin counterpart — applyTorque / applyTorqueImpulse / setAngularVelocity (radians/s) — and an at-a-point variant, applyForceAtPoint / applyImpulseAtPoint, that pushes at a world point instead of the centre of mass, so it also tilts and spins the body. (That off-centre push is the primitive a scripted vehicle's suspension is built on — a spring force at each wheel contact IS the suspension. See Vehicles.)
Forces belong in onFixedUpdate
onUpdate runs once per rendered frame — 60, 144, however many the machine manages — so a force applied there hits a different number of times per second on every device. onFixedUpdate runs exactly once per physics step (20 Hz), everywhere. Rule of thumb: if you're calling physics, you're in onFixedUpdate.
Unity style: the RigidBody handle
Everything above addresses a body by id — physics.setVelocity(this.entity.id, v). Unity instead hangs the same operations off the Rigidbody component, and Galatrix accepts that spelling too:
js
const rb = this.entity.getComponent('rigidBody')
rb.velocity = [0, 10, 0] // same as physics.setVelocity(this.entity.id, [0, 10, 0])
const speed = rb.velocity.y // reads the running bodyThe two are the same call — the handle just already knows which entity it belongs to. Use whichever reads better; physics.* is still the way to touch a body that isn't the one you're holding.
What the handle carries:
| Member | Notes |
|---|---|
velocity, linearVelocity | get + set. Both names work — Unity 6 renamed velocity to linearVelocity |
angularVelocity | get + set, radians/sec (same unit as Unity) |
worldCenterOfMass | read-only |
AddForce(f, mode?) | see ForceMode below |
AddForceAtPosition(f, pos, mode?) | pushes off-centre, so it tilts and spins |
AddTorque(t, mode?) | spin |
GetPointVelocity(worldPoint) | how fast a point on the body is really moving |
AddRelativeForce(f, mode?), AddRelativeTorque(t, mode?) | the vector is in the body's local axes |
GetRelativePointVelocity(localPoint) | the local-space twin of GetPointVelocity |
AddExplosionForce(force, pos, radius, upwards?, mode?) | see below |
friction, restitution | get + set, live on the collider |
ccd, collisionDetectionMode | continuous collision — see Fast bodies |
linearDamping, angularDamping | drag, live |
gravityScale, useGravity | 1 normal, 0 weightless, negative floats up |
mass | exact target mass, independent of collider size/density |
freezePosition, freezeRotation | Unity RigidbodyConstraints; a bare true freezes all three axes |
isKinematic, bodyType, type | swap dynamic / kinematic / static mid-play |
detectCollisions | colliders off, body still falling — no-clip / ghost / phase-through |
position, rotation, MovePosition(p), MoveRotation(r), Move(p, r?) | move the real body — see below |
Sleep(), WakeUp(), IsSleeping() | see Sleeping bodies |
Every method also has a camelCase twin — addForce, getPointVelocity, wakeUp — so both house styles work.
AddExplosionForce acts on this body only (a common misreading is that it hits everything nearby — to blast a crowd, loop over physics.overlapSphere). Force falls off linearly to zero at the rim, bodies outside the radius get nothing, and upwardsModifier sinks the blast origin by that many units so debris lifts instead of skidding.
position and MovePosition zero the velocity
All the pose members go through warpBody, which is the right way to move a body — but unlike Unity's rb.position, it also clears velocity. That's usually what you want for a respawn or teleport. To move a body and keep its momentum, set velocity again afterwards.
ForceMode
AddForce and AddTorque take Unity's ForceMode as an optional second argument:
js
rb.AddForce([0, 500, 0]) // ForceMode.Force — continuous, call every step
rb.AddForce([0, 8, 0], ForceMode.Impulse) // instant kick, scaled by mass
rb.AddForce([0, 8, 0], ForceMode.VelocityChange) // instant, IGNORES mass — +8 m/s whatever it weighs
rb.AddForce([0, 20, 0], ForceMode.Acceleration) // continuous, ignores massVelocityChange and Acceleration are the mass-independent pair: a 1 kg crate and a 5 kg crate given the same VelocityChange end up at the same speed.
Torque ignores the mass-independent modes
AddTorque(t, ForceMode.VelocityChange) needs the body's inertia tensor, which no runtime exposes. On torque, VelocityChange behaves as Impulse and Acceleration as Force.
Tuning a body mid-play
Authored rigidBody fields are baked into the physics body when it is built, and nothing re-applies them afterwards. Writing one at runtime used to change the JSON and leave the simulation exactly as it was — setting gravityScale = 0 mid-fall didn't slow the fall by a single unit. Now they're wired to real runtime verbs, so they mean what they say:
js
const rb = this.entity.getComponent('rigidBody')
rb.linearDamping = 4 // drag, live
rb.angularDamping = 2
rb.gravityScale = 0 // weightless; negative floats it up
rb.useGravity = false // the on/off face of the same thing
rb.mass = 50 // exact, whatever the collider's size or density
rb.freezeRotation = true // all three axes, Unity-style
rb.freezePosition = [true, false, true] // or per-axis
rb.isKinematic = true // solver stops moving it — drive it with MovePosition
rb.detectCollisions = false // keeps falling, but phases through everything
rb.friction = 0 // ice
rb.restitution = 0.9 // bouncy
rb.ccd = truedetectCollisions vs isKinematic
They sound similar and do opposite halves. detectCollisions = false leaves the body falling but stops it touching anything (a ghost). isKinematic = true stops the body moving but it still shoves things (a platform). Turning a body's collisions off does not freeze it.
Each has a namespace twin (physics.setDamping, setGravityScale, setMass, setFreeze, setBodyType…) if you'd rather address a body by id.
isKinematic is how a ragdoll wakes up
Flipping a body to kinematic freezes it under script control; flipping it back to dynamic hands it to the solver with no leftover momentum. That's the switch behind ragdoll activation, a carried prop going inert, and freezing an enemy mid-charge.
Five fields still throw rather than pretend, because rapier has no runtime equivalent: startAsleep (use rb.Sleep() / rb.WakeUp()), collisionGroups (casts take a layerMask argument), frictionCombine, restitutionCombine, and autoCollider. The error names the field and the alternative. To change one, author it in the Inspector or replace the whole component:
js
this.addComponent('rigidBody', { type: 'dynamic', mass: 2, frictionCombine: 'min' })Writing a read-only member (rb.worldCenterOfMass = …) throws too, rather than quietly storing a dead field.
freezePosition / freezeRotation read back what you wrote
Rapier can set the axis locks but not report them, so those two getters return the last value written through the handle (which is also mirrored into the component, so multiplayer clients see it). Every other live member reads straight off the body.
Cross-entity works too
The handle binds the id of the entity it came from, not the script's own: game.findEntity('Crate').getComponent('rigidBody').velocity = [0, 5, 0].
Teleporting: warpBody
To reposition a dynamic body — a respawn, a reset — don't fight the sim with entity.position. warpBody moves the actual body and zeroes its velocity:
js
this.physics.warpBody(this.entity.id, [0, 5, 0]) // drop it at a spawn point
this.physics.warpBody(this.entity.id, [0, 5, 0], [0, 90, 0]) // …and face 90° (3 values = Euler degrees)Sleeping bodies
A body at rest eventually sleeps — it stops simulating (free performance) until a contact or force disturbs it. You can drive that directly, like Unity's Rigidbody.Sleep/WakeUp/IsSleeping:
js
this.physics.sleep(this.entity.id) // stop simulating now
this.physics.wakeUp(this.entity.id) // resume
if (this.physics.isSleeping(this.entity.id)) { /* it has settled */ }To spawn a pile of props that stays frozen until the player touches it, check Start Asleep on the RigidBody component — the bodies sleep from the first frame and wake on the first contact or impulse.
Fast bodies: continuous collision (CCD)
The simulation advances in fixed 20 Hz steps. Between two steps a body just teleports from its old position to its new one — so a body moving faster than its own thickness per step can jump clean over a thin wall and never register the hit. Classic case: a bullet, a launched prop, a fast car.
The fix is continuous collision detection (CCD) — instead of testing only the end position, the engine sweeps the body's whole path that step and stops it at the first surface. Turn it on where you need it:
Authored: tick Continuous Collision (CCD) on the entity's RigidBody component (the
rigidBody.ccdfield). Best for anything born fast — projectiles, vehicles at speed.At runtime, like Unity's
Rigidbody.collisionDetectionMode:jsthis.physics.setCcd(bulletId, true) // this body now can't tunnel if (this.physics.isCcdEnabled(bulletId)) { /* … */ }A common pattern is to enable it on a projectile the moment you spawn/launch it:
jsconst b = this.game.spawnEntity('Bullet', 'sphere', muzzle) this.physics.setCcd(b.id, true) this.physics.setVelocity(b.id, [dir[0] * 80, dir[1] * 80, dir[2] * 80])
CCD costs a little CPU per body, so it's off by default — reserve it for the handful of bodies that actually move fast, not every crate. (The 20 Hz step itself is fixed on purpose: it keeps the editor, standalone, and multiplayer server in lock-step so physics is deterministic everywhere. CCD, not a smaller timestep, is the right remedy for tunnelling.)
For almost every map the default is enough. If you have several extremely fast bodies that must resolve against each other in a single step, raise CCD Substeps in Game Settings ▸ Physics (project-wide, 1–8, default 1). Each substep re-runs the swept CCD pass, so higher is more accurate but more CPU — leave it at 1 unless you actually see fast bodies passing through each other (a single body vs. the world is already handled at 1).
Surface material: friction & bounce
Every collider carries two surface properties, exactly like a Unity Physic Material:
- Friction — grip.
0is ice; higher is stickier. Default0.5. - Restitution — bounciness,
0–1.0is a dead thud;1is a superball. Default0.
For a fixed value, set friction / restitution in the RigidBody component's Surface group in the Inspector — they bake in when the map loads, and they matter on static bodies too (an ice floor is static).
To change it live from a script — flip a floor to ice mid-game, make a power-up ball extra bouncy:
js
this.physics.setFriction(this.entity.id, 0) // this surface is now ice
this.physics.setFriction(this.entity.id, 3) // …now grippy
this.physics.setRestitution(this.entity.id, 0.9) // …and bouncy
const f = this.physics.getFriction(this.entity.id)One friction coefficient
The engine uses a single friction value per collider — there's no separate static vs dynamic friction like Unity's Physic Material. Friction and bounce are the two coefficients; the combine rule below decides how they mix when two surfaces touch.
When two bodies touch, their two frictions (and restitutions) combine — by default they average, so friction 0 against a 0.5 crate still leaves ~0.25 grip. That's what the combine rule is for (Unity's frictionCombine/bounceCombine, the Friction Combine / Bounce Combine dropdowns in the Surface group): average (default), min, multiply, or max — when the two surfaces disagree, the higher-priority rule wins (max > multiply > min > average).
- True ice: friction
0+ combineminon the floor → slippery against anything that steps on it. - Trampoline: restitution
1+ combinemax→ bouncy no matter how dead the thing that lands is.
Gravity
The world pulls down at -50 on Y by default — deliberately strong, for snappy Roblox-style jumps rather than floaty ones. Don't hard-code that number; a map can change it, so read it:
js
const g = this.physics.getGravity() // e.g. [0, -50, 0] — reflects THIS map's setting
this.physics.setGravity([0, -20, 0]) // low-gravity moon levelPer body, the RigidBody's gravityScale multiplies world gravity: 1 is normal, 0 floats (weightless), 2 is heavy, a negative value falls up. Air resistance is linearDamping (a feather drifts down with high damping); spin resistance is angularDamping.
Colliders & collision events
The Collider component gives an entity its physical shape — box, sphere, capsule, convex, or trimesh. It has two modes:
- Solid (default) — bodies bounce off it, and contacts fire
onCollisionEnter/onCollisionStay/onCollisionExit. - Trigger (
isTrigger, Unity's sensor) — no physical response; overlaps fireonTriggerEnter/onTriggerStay/onTriggerExitinstead. This is how you build pickups, checkpoints, and damage zones.
js
class Target extends Behaviour {
onCollisionEnter(other) { // something physically hit me
if (other.compareTag('Bullet')) this.hp -= 10
}
onTriggerEnter(other) { // something entered my sensor volume
if (other.compareTag('Player')) this.game.findEntity('Door').setActive(false)
}
}other is an EntityRef — compareTag is the Unity-style tag check (the player is tagged 'Player'). For the common "a player stepped in this zone" case there's also onPlayerEnter(player) / onPlayerLeave(player). For a worked example — a trigger pad that rewards the player who steps on it — see Scripting basics.
Sensing the world: raycasts & overlaps
Ask the physics world what's out there — line-of-sight, ground checks, shooting, proximity:
js
// clear line of sight from me to the player?
const from = this.entity.position
const target = this.game.findEntity('Player').position
const dir = [target[0] - from[0], target[1] - from[1], target[2] - from[2]]
const hit = this.physics.raycast(from, dir, Math.hypot(dir[0], dir[1], dir[2]))
if (hit && hit.entityName === 'Player') this.shoot() // nothing in between → fireraycast returns the nearest hit — a RaycastHit of { entityId, entityName, point, normal, distance } — or null. The family:
| Query | Returns |
|---|---|
raycast(origin, dir, maxDist?, layerMask?, ignoreEntities?) | the nearest hit, or null |
raycastAll(origin, dir, maxDist?, layerMask?, ignoreEntities?) | every hit along the ray, nearest-first |
sphereCast(origin, radius, dir, maxDist?, layerMask?, ignoreEntities?) | the first entity a swept sphere would touch — the thick ray |
overlapSphere(center, radius, layerMask?) | entities within a radius — an explosion's blast list |
overlapBox(center, halfExtents, orientation?, layerMask?) | entities inside a box |
The two trailing options on the ray/sweep queries — layerMask (which collision layers to see) and ignoreEntities (specific entity ids to pass through) — are both optional; skip them, or pass -1 for layerMask to mean "all layers", when you only want the one after it.
sphereCast is Unity's Physics.SphereCast and the reliable ground check: a thin ray slips through a crack or off the edge of a ledge your character is half-standing on; a swept sphere the size of the body doesn't. Its distance is how far the sphere's center travelled before touching; point/normal are on the surface it hit.
Notes that save debugging time:
A ray starting at an entity's own
positioncan hit its own collider first. The clean fix is theignoreEntitieslist —this.physics.raycast(from, dir, 20, -1, [this.entity.id])passes straight through you (see below). You can also offset the origin past yourself — e.g. start a ground-check just below the feet:[x, y - 0.6, z]casting[0, -1, 0].sphereCastreportsdistance: 0when the sphere already overlaps a collider atorigin(it's a shape sweep — a shape starting inside geometry contacts at once), and the returned hit setsoverlapping: trueso you can detect exactly that case instead of guessing why the distance is 0:tsconst hit = this.physics.sphereCast(origin, 0.4, [0, -1, 0], 5) if (hit && hit.overlapping) { /* already touching it — distance 0 is contact, not a gap */ } else if (hit) { /* hit.distance is the real gap to the surface */ } else { /* nothing within maxDistance */ }So a
sphereCastthat begins touching the thing you're measuring is contact-at-start (overlapping), and itsdistanceis a real gap only when the cast starts clear of everything (overlappingis falsy). For a true gap-to-surface from inside contact, start the sphere back from the surface (offsetoriginalong-direction) or use a thinraycast, whosedistanceis the straightorigin → pointlength. (overlappingis asphereCast-only field — rapier returnsdistance 0on contact-at-start no matter what, so this flag is the honest way to tell it apart from a genuine zero gap.)The optional layerMask limits which collision layers a query sees:
1 << 3hits only layer 3; omit (or-1) for all layers.ignoreEntitiesis the LAST argument, afterlayerMask—sphereCast(origin, radius, dir, maxDist, layerMask, ignoreEntities). To skip only yourself, passundefinedfor the mask:sphereCast(o, r, dir, 5, undefined, [this.entity.id]). Putting the id list in thelayerMaskslot is the classic slip; the engine now detects an array there, treats it asignoreEntities, and warns in the console — but write it in the right slot and the warning goes away.The optional
ignoreEntitiesarray (trailing arg onraycast,raycastAll,sphereCast) lists entity ids the cast passes straight through, as if they weren't there — so the hit you get back is the first thing that isn't on the list:js// A hitscan shot from the shooter that never hits the shooter or the gun it's holding: const hit = this.physics.raycast(muzzle, aimDir, 100, -1, [this.entity.id, this.gunId]) if (hit) this.game.findEntity(hit.entityId)?.send('damage', 25)Where
layerMaskfilters broad categories (all Enemies, all Terrain…),ignoreEntitiesskips a few specific objects — most often yourself ([this.entity.id]) or the thing you're carrying/riding. They combine: a collider is skipped ifignoreEntitiesnames it orlayerMaskexcludes its layer. Unity has no such parameter (it uses layers + filtering the results in code); this is a convenience on top oflayerMask. Pass entity ids (e.g.this.entity.id, or anentityIdfrom another hit) — not names.Aim with a direction the entity already gives you —
this.entity.forwardis where it points (see Rotations).To see a ray while tuning it,
Debug.DrawRay(origin, dir)draws it in the editor viewport and in published builds.
Recipes
Explosion — everything nearby gets kicked away from the blast, harder the closer it is:
js
explode(center, radius, power) {
for (const e of this.physics.overlapSphere(center, radius)) {
const p = e.position
const d = [p[0] - center[0], p[1] - center[1] + 1, p[2] - center[2]] // +1 biases the kick upward
const len = Math.hypot(d[0], d[1], d[2]) || 1
const falloff = Math.max(0, 1 - len / radius) * power
this.physics.applyImpulse(e.id, [d[0] / len * falloff, d[1] / len * falloff, d[2] / len * falloff])
}
}Ice-patch trap — a trigger zone that makes the floor slick while a player is on it, then restores grip:
js
class IcePatch extends Behaviour {
floor = null // an entity-reference field, set in the Inspector
onPlayerEnter() { this.physics.setFriction(this.floor.id, 0) }
onPlayerLeave() { this.physics.setFriction(this.floor.id, 0.5) }
}Grounded double-jump — sweep a sphere down to check for floor before allowing a jump (a thick check never misses the edge of a platform the way a thin ray does):
js
class Jumper extends Behaviour {
jumps = 0
onFixedUpdate() {
const p = this.entity.position
const grounded = !!this.physics.sphereCast([p[0], p[1], p[2]], 0.4, [0, -1, 0], 0.75)
if (grounded) this.jumps = 0
if (this.input.getKeyDown('space') && this.jumps < 2) {
this.physics.applyImpulse(this.entity.id, [0, 9, 0])
this.jumps++
}
}
}Editor & multiplayer
Everything here behaves identically in the editor play-test and a standalone build. In multiplayer the simulation is server-authoritative: forces, velocities, friction, and gravity are applied on the server and the result streams to every client. So physics.* calls only affect bodies the server actually simulates — a purely cosmetic client-side entity, or a character-controller-driven player, won't respond to them. (Character-controller players are the deliberate, client-authoritative exception — see Scripting your own player.)
See also
- Scripting basics — the
onUpdate/onFixedUpdatelifecycle this code runs in. - Rotations — orienting and spinning bodies through physics.
- Vehicles & driving — a full controller built on
applyForceAtPoint. physicsin the Script API reference — every method, with signatures.