DEMIENGINEDOCUMENTATION Everything is free
Physics · Game systems

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.

TypeWhat it doesUse it for
staticNever moves. Infinite mass. Other bodies collide with it and stop.Walls, floors, level geometry, buildings in Skyforge stations.
kinematicMoves only when you move it. It pushes dynamic bodies aside, but nothing pushes it back.Moving platforms, elevators, scripted doors, hover-trains.
dynamicFully 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
  }
}
FieldMeaningTypical value
body_typestatic, kinematic, or dynamic, as described above.dynamic
continuousContinuous 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_dampingHow quickly straight-line speed bleeds away each step. 0 coasts forever; larger values stop sooner.0.05 to 1.0
lock_rotationPrevents the body from spinning. Keeps top-down heroes upright after side impacts.true for characters
report_contactsExtracts 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
  }
}
FieldMeaningTypical value
body_typestatic, kinematic, or dynamic.dynamic
massHow heavy the body is. Heavier bodies shove lighter ones further.Player 80, crate 15
continuousContinuous collision detection for fast movers.true for projectiles
frictionHow much the surface resists sliding. Combine with the collider’s own friction for the contact result.0.5
restitutionBounciness. 0 lands dead; 1 returns most of the impact speed.0 for crates
linear_damping / angular_dampingHow quickly linear or angular motion slows down.0.02 / 0.05
solver_velocity_steps / solver_position_stepsHow much extra solving a step performs. Range 0–128; 0 means the engine default. Raise them only for stacks that jitter.0
report_contactsSame 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.

CallEffectUse it for
add_forceAdds to the force accumulator for this step. A gentle push that fades in as mass resists.Hover thrusters, wind, continuous acceleration.
add_impulseInstantly changes velocity by an amount scaled by mass. Immediate, not gradual.Jumps, explosion kicks, projectile launch.
add_torqueRotational 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)
CallArgumentsReturns
move_and_slideid, requested displacement motion_x, motion_yApplied x, y displacement, or nil, nil if unavailable. Clips against static collider bounds.
move_kinematicid, target x, y, optional fixed_dtBoolean. 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, x and y, in world units per second. Missing bodies report nil per axis.
  • set_velocity(id, x, y) sets speed directly and returns true when applied.
  • set_velocity_x / set_velocity_y change one axis and leave the other alone; each returns true when applied.
  • add_impulse(id, x, y) adds an instant kick and returns true when applied.
  • add_force(id, x, y) accumulates force for the current step and returns true when 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), and set_report_contacts(id, bool) toggle runtime flags and return true when 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) returns x, y, z scalars. Any missing axis comes back as nil, so guard before arithmetic.
  • set_velocity(id, x, y, z) sets the linear velocity and returns true when applied.
  • add_force / add_impulse / add_torque(id, x, y, z) push, kick, or spin the body and return true when 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: kinematic plus move_kinematic or move_and_slide.
  • Anything gravity owns: dynamic, with continuous if it can go fast.
  • Hero that should not spin: dynamic with lock_rotation: true in 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.