Static, kinematic, dynamic — forces, velocity, sleep.
Rigidbodies
A rigidbody is the component that hands an entity over to the physics simulation so it can move, fall, slide, and respond to hits. This page explains what that means, when to use each body type, and how to drive motion from Lua in 2D and 3D.
What a rigidbody is
In a game, some things move because you set their position every frame (a menu icon, a cutscene prop). Others move because forces push them around (a crate that falls, a drone that drifts after a collision). A rigidbody is what marks an entity as the second kind: the simulation owns its motion.
A rigidbody does not describe shape. Shape comes from a collider on the same entity. The rigidbody only answers “how does this thing move?” Keeping the two apart means the same body settings work with any shape, and a decorative shape can exist without any body at all.
2D bodies simulate with Box2D and 3D bodies with Jolt. Both step at the project fixed timestep (default 1/60 of a second), which is the tick that runs on_fixed_update. Apply forces and impulses there so results stay identical across runs.
Body types: static, kinematic, dynamic
Every rigidbody picks one of three types. The type is the single most important setting you choose.
| Type | What it does | Use it for |
|---|---|---|
static | Never moves. Infinite mass. Other bodies collide with it and stop. | Walls, floors, level geometry, buildings in Skyforge stations. |
kinematic | Moves only when you move it. It pushes dynamic bodies aside, but nothing pushes it back. | Moving platforms, elevators, scripted doors, hover-trains. |
dynamic | Fully simulated. Gravity, forces, and collisions all move it. | Crates, ragdolls, projectiles, the player when using physics-driven motion. |
A quick rule: if the player script sets the position, the body is kinematic. If the simulation decides where it ends up, the body is dynamic. If it never moves, it is static.
2D rigidbody fields
{
"Rigidbody2D": {
"body_type": "dynamic",
"continuous": true,
"linear_damping": 0.05,
"lock_rotation": true,
"report_contacts": true
}
}
| Field | Meaning | Typical value |
|---|---|---|
body_type | static, kinematic, or dynamic, as described above. | dynamic |
continuous | Continuous collision detection. Fast small bodies are checked along their path instead of only at the end of a step, so they do not tunnel through thin walls. | true for bullets and fast debris |
linear_damping | How quickly straight-line speed bleeds away each step. 0 coasts forever; larger values stop sooner. | 0.05 to 1.0 |
lock_rotation | Prevents the body from spinning. Keeps top-down heroes upright after side impacts. | true for characters |
report_contacts | Extracts contact points into physics_contact events so you can spawn sparks at the exact hit. Collision events fire either way. | true when you need hit positions |
3D rigidbody fields
{
"Rigidbody3D": {
"body_type": "dynamic",
"mass": 12.5,
"continuous": true,
"friction": 0.6,
"restitution": 0.05,
"linear_damping": 0.02,
"solver_velocity_steps": 0,
"solver_position_steps": 0,
"report_contacts": true
}
}
| Field | Meaning | Typical value |
|---|---|---|
body_type | static, kinematic, or dynamic. | dynamic |
mass | How heavy the body is. Heavier bodies shove lighter ones further. | Player 80, crate 15 |
continuous | Continuous collision detection for fast movers. | true for projectiles |
friction | How much the surface resists sliding. Combine with the collider’s own friction for the contact result. | 0.5 |
restitution | Bounciness. 0 lands dead; 1 returns most of the impact speed. | 0 for crates |
linear_damping / angular_damping | How quickly linear or angular motion slows down. | 0.02 / 0.05 |
solver_velocity_steps / solver_position_steps | How much extra solving a step performs. Range 0–128; 0 means the engine default. Raise them only for stacks that jitter. | 0 |
report_contacts | Same job as 2D: fill physics3d_contact with contact points. | true for impact FX |
Forces, impulses, and torque
Three ways exist to push a dynamic body, and they behave differently on purpose.
| Call | Effect | Use it for |
|---|---|---|
add_force | Adds to the force accumulator for this step. A gentle push that fades in as mass resists. | Hover thrusters, wind, continuous acceleration. |
add_impulse | Instantly changes velocity by an amount scaled by mass. Immediate, not gradual. | Jumps, explosion kicks, projectile launch. |
add_torque | Rotational counterpart of a force. | Spin a floating drone wreck. |
Rule of thumb: if the effect should accumulate over time, use a force every fixed step. If it should happen once and be over, use an impulse. In Skyforge, thruster lift is a force applied every step; a jump pad is a single impulse.
Damping and sleep
Damping slows motion over time. Without damping, a crate shoved across a frictionless floor can keep moving. Both 2D and 3D bodies have linear_damping and angular_damping fields.
Sleep is an optimization the simulation applies to bodies that have come to rest. A sleeping body costs almost nothing until something touches it. The catch: a force applied to a sleeping body does nothing until the body is awake. Call set_awake(id, true) before add_force or add_impulse if the body may have settled.
Continuous collision detection
A simulation moves bodies in discrete steps. A bullet that travels 2 meters per step can end up on the far side of a 5 cm wall without any step ever reporting a hit — this is called tunneling. continuous: true makes the engine sweep the body along its path so the wall is hit. Enable it on fast, small bodies (bolts, shrapnel) and leave it off on slow, large ones where it is wasted work.
Moving kinematic bodies
Kinematic bodies do not react to pushes, so you move them yourself. Two helpers exist in 2D, both meant to run inside on_fixed_update.
local Body = require("demi.physics.rigidbody2d")
-- Request a displacement this step; receive the applied displacement.
local moved_x, moved_y = Body.move_and_slide(id, vx * fixed_dt, vy * fixed_dt)
-- Set velocity to reach an absolute 2D target over this step.
Body.move_kinematic(id, 3.5, 4.0, fixed_dt)
| Call | Arguments | Returns |
|---|---|---|
move_and_slide | id, requested displacement motion_x, motion_y | Applied x, y displacement, or nil, nil if unavailable. Clips against static collider bounds. |
move_kinematic | id, target x, y, optional fixed_dt | Boolean. Sets the body's velocity toward the target for this step; the fixed timestep defaults to 1/60, so pass your project's actual step. |
Use move_and_slide for 2D motion that should clip and slide against static collider bounds. Use move_kinematic for rails and platforms following a known target. The 3D version is move_kinematic(id, x, y, z, rx, ry, rz, fixed_dt) with Euler rotation in radians.
Full 2D example
A Skyforge cargo crate that can be shoved, then settles and sleeps.
local Body = require("demi.physics.rigidbody2d")
local Crate = {}
function Crate:on_start()
Body.set_continuous(self.id, true)
Body.set_report_contacts(self.id, true)
end
function Crate:on_fixed_update(fixed_dt)
local vx, vy = Body.get_velocity(self.id)
-- vx, vy → current speed in world units per second (nil per axis when missing)
if vx and vy and math.abs(vx) < 0.05 and math.abs(vy) < 0.05 then
Body.set_awake(self.id, false)
end
end
function Crate:shove()
Body.set_awake(self.id, true)
Body.add_impulse(self.id, 4, 2)
end
return Crate
get_velocity(id)returns two scalars,xandy, in world units per second. Missing bodies reportnilper axis.set_velocity(id, x, y)sets speed directly and returnstruewhen applied.set_velocity_x/set_velocity_ychange one axis and leave the other alone; each returnstruewhen applied.add_impulse(id, x, y)adds an instant kick and returnstruewhen applied.add_force(id, x, y)accumulates force for the current step and returnstruewhen applied.add_torque(id, amount)adds spin;set_angular_velocity(id, w)sets spin directly (radians per second).set_awake(id, bool),set_enabled(id, bool),set_continuous(id, bool), andset_report_contacts(id, bool)toggle runtime flags and returntruewhen applied.
Full 3D example
A Skyforge supply pod that falls, reports impacts, and can be read back for a HUD.
local Body = require("demi.physics.rigidbody3d")
local Pod = {}
function Pod:on_start()
Body.set_continuous(self.id, true)
Body.set_report_contacts(self.id, true)
self.nudged = false
end
function Pod:on_fixed_update(fixed_dt)
local vx, vy, vz = Body.get_velocity(self.id)
-- three scalars, or nil per axis when the body is missing
local state = Body.state(self.id)
-- state → table, or nil when the body is missing:
-- body_type "static" | "kinematic" | "dynamic"
-- mass number
-- use_gravity boolean
-- enabled boolean
-- velocity { x, y, z }
-- angular_velocity { x, y, z }
if state and not self.nudged then
Body.add_impulse(self.id, 0, 6, 0) -- nudge it for a demo
self.nudged = true
end
end
return Pod
get_velocity(id)returnsx, y, zscalars. Any missing axis comes back asnil, so guard before arithmetic.set_velocity(id, x, y, z)sets the linear velocity and returnstruewhen applied.add_force/add_impulse/add_torque(id, x, y, z)push, kick, or spin the body and returntruewhen applied.state(id)returns a table of live simulation values, not the authored file — read it for HUD readouts and gameplay checks.move_kinematic(id, x, y, z, rx, ry, rz, fixed_dt)places a kinematic body over one step, with Euler rotation in radians.
3D validation rejects a moving body without a collider and a moving body parented under another moving body. Give every simulated entity its own collider and keep moving bodies at the root of their local hierarchy.
Choosing what to use
- Level shell and pillars:
static, never touched again. - Elevator the script animates:
kinematicplusmove_kinematicormove_and_slide. - Anything gravity owns:
dynamic, withcontinuousif it can go fast. - Hero that should not spin:
dynamicwithlock_rotation: truein 2D, or use CharacterController3D in 3D.
Filtering who collides with whom is a separate concern — see Physics layers. Reading contacts and hits is covered in Physics events and Queries.