Venus Lua API › Physics

FixedRigidBody

Deterministic lockstep rigid body — forces, impulses, materials.

authored lua/venus_lua/src/lua_fixed_rigid_body.rs

FixedRigidBody

A rigid body in the app-owned deterministic FixedXpbdWorld (the lockstep path): box, sphere,
plane, or capsule, driven with conventional rigid-body commands (SetVelocity, AddImpulse,
AddForce, …). Shape and mass are baked once when the body registers (on_enable) — chosen at
attach time via Add / AddBox / AddSphere / AddPlane / AddCapsule, and not mutated live.
Joints between two bodies live on FixedJoint.

Static calls use the type table (FixedRigidBody.AddBox(...)); instance methods use colon syntax
on a handle (rb:AddForce(...)).

Vector arguments come in two flavours. The velocity / force / impulse commands (SetVelocity,
SetAngularVelocity, AddImpulse, AddForce, AddTorque, and the force argument of
AddForceAtPosition) require a native Vector3 — three loose numbers raise a type error. Only the
two world-position overloads, Respawn and the worldPos argument of AddForceAtPosition, accept
either a Vector3 or three loose f64 numbers; the three-number form preserves precision far
from the origin (a Vector3 is f32-backed, ~cm-lossy at range).

Types

FixedRigidBody — an opaque handle (userdata) wrapping a component that holds a generational world
body handle. A handle may be null, stale (its component destroyed), or unregistered
(attached but not yet through on_enable). Every command and query resolves the component and its
world body afresh each call; on a null / stale / unregistered handle, commands no-op and
queries return nil — they never raise. Use IsRegistered() to test readiness.

Creation & Destruction

Each Add* constructor attaches a FixedRigidBody component to gameObject and returns its
handle; the world body is created from the GameObject’s world pose at on_enable, so set the
object’s position before attaching. Returns nil and logs an error if the component cannot be
added (e.g. a null/stale GameObject or an exhausted pool).

FixedRigidBody.Add( GameObject gameObject ) : FixedRigidBody
Attaches a unit-box (half-extent 0.5, mass 1) dynamic body.

FixedRigidBody.AddBox( GameObject gameObject, number hx, number hy, number hz, number mass ) : FixedRigidBody
A box body with half-extents (hx, hy, hz) (all three required). mass is optional (default 1).

FixedRigidBody.AddSphere( GameObject gameObject, number radius, number mass ) : FixedRigidBody
A sphere body of radius (required). mass is optional (default 1).

FixedRigidBody.AddPlane( GameObject gameObject ) : FixedRigidBody
A static infinite ground plane (a half-space; normal = the GameObject’s local +Y). Always
static — it carries no mass and never moves.

FixedRigidBody.AddCapsule( GameObject gameObject, number radius, number halfHeight, number mass ) : FixedRigidBody
A capsule body (axis = local +Y); radius and halfHeight required. mass is optional
(default 1).

FixedRigidBody.Get( GameObject gameObject ) : FixedRigidBody
Returns the FixedRigidBody on gameObject, or nil if it has none. Silent — unlike the
Add* constructors, a miss logs nothing.

FixedRigidBody.Exists( FixedRigidBody body ) : boolean
true if the handle refers to a live component. Safe on any handle (null/stale → false); never
raises.

There is no explicit Destroy. The world body — with its full joint cascade — is torn down
automatically when the GameObject or component is destroyed (on_disable).

Functions

Read-only queries.

rb:Position( ) : Vector3
The body’s current world position. Returns nil if the body is unregistered or the world is
unavailable.

rb:IsRegistered( ) : boolean
true once the world body exists (after on_enable). This is the gate for every command and
query below — before registration they all no-op / return nil.

The two lifecycle getters are the exception: they read the scene component rather than the world
body, so they answer normally on an attached-but-unregistered body.

rb:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. nil (+ a logged
NotFound) on a stale handle. This is intent, not effect: a body whose own flag is enabled but
whose GameObject is deactivated still reports true.

rb:ActiveInHierarchy( ) : boolean or nil
true only when the own flag is enabled and the owning GameObject is active in hierarchy —
i.e. the component is actually running. This is the predicate the engine uses to gate update
dispatch. Returns nil on a stale handle.

Properties (get / set)

Getters return nil on an unregistered / stale handle; setters no-op. Each velocity setter takes a
native Vector3 (not loose numbers).

Velocity

rb:Velocity( ) : Vector3 — current linear velocity, or nil if unregistered.
rb:SetVelocity( Vector3 velocity ) : void

AngularVelocity

rb:AngularVelocity( ) : Vector3 — current angular velocity, or nil if unregistered.
rb:SetAngularVelocity( Vector3 velocity ) : void

Methods

Actions that mutate the body. All no-op on an unregistered / stale handle.

rb:AddImpulse( Vector3 impulse ) : void
An instantaneous linear velocity change (v += j · inv_mass). Takes a native Vector3.

rb:AddForce( Vector3 force, string mode ) : void
Applies a force (Unity Rigidbody.AddForce). force is a native Vector3. mode is the optional
force-mode name"Force" | "Impulse" | "VelocityChange" | "Acceleration" (Unity’s
names; omitted or nil"Force"). Any other value raises a Lua error, including a number:
the names are matched exactly, and there is no ForceMode constants table.

rb:AddTorque( Vector3 torque, string mode ) : void
Applies a world-space torque (Unity Rigidbody.AddTorque). torque is a native Vector3. mode
is the optional force-mode name (see AddForce; default "Force").

rb:AddForceAtPosition( Vector3 force, Vector3 worldPos, string mode ) : void
A force plus its induced torque about the application point worldPos. force is a native
Vector3; worldPos may be a Vector3 or three loose f64 numbers (the three-number form
keeps the point precise far from the origin). mode is the optional force-mode name and simply
follows worldPos in either form.

Fixed 2026-07 (was: silent wrong mode). The mode used to fall back to Force on any
unrecognized value, and the global ForceMode.* integer table — a leftover from a retired
binding — coerced through luaL_checkstring to "1"/"2"/"3" and matched nothing. Every
use of those constants silently applied Force, which is wrong by a factor of the fixed step:
Force delivers F·inv_m·dt spread over the next step, Impulse delivers J·inv_m
immediately — so a jump impulse arrived at roughly 1/120 strength, with no diagnostic. The
table is deleted and unrecognized names now raise. Names resolve through
ForceMode::from_yaml_bytes, generated beside the enum in venus-fixed-dynamics, so the
spellings cannot drift from the variants. Note the lowercase form ("impulse") was accepted-
then-ignored before and is now a hard error — spell it as Unity does.

rb:SetRestitution( number e ) : void
Sets bounciness (restitution).

rb:SetFriction( number mu ) : void
Sets the friction coefficient.

rb:SetMaterial( string name ) : void
Resolves a named physics material from the resource registry
(data/core/defs/physics_materials.yaml) and applies its friction, restitution, and combine modes
to the live body, caching it on the component so it survives respawn / re-enable. An unknown name
(or no registry loaded) logs an error and no-ops.

rb:SetCollisionEvents( boolean on ) : void
Opts into OnCollisionEnter / OnCollisionStay / OnCollisionExit on this body’s GameObject
ScriptBehavior. Persists in the authoring params (clones inherit it).

rb:Respawn( Vector3 position ) : void
Teleports to the given world position with zero velocity and identity orientation — a clean
re-drop that resets simulation state (for spawning/respawning, not in-sim motion). Accepts a
Vector3 or three loose f64 numbers; the three-number form keeps the target precise far from
the origin.

Lifecycle

Both act on the component’s enabled flag rather than the world body, so — unlike the commands above
— they are meaningful before registration. Both silently no-op on a stale handle.

rb:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active — and that callback is what creates the world body.

rb:Deactivate( ) : void
Clears the own enabled flag; the disable callback runs only if the component was actually active.

Example

-- A box prop that drops onto the deterministic ground, reports contacts, and
-- re-drops on a timer. (Adapted from data/scripts/fixed_physics_demo.lua.)
function Script.OnCreate(self)
    -- Pose BEFORE Add: on_enable registers the world body from this position.
    -- Three loose f64 numbers keep the drop precise far from the origin.
    self.object:SetWorldPosition(0.0, 9.0, 0.0)
    self.drop = { 0.0, 9.0, 0.0 }

    -- Unit cube → half-extents 0.5, mass 2.
    self.rb = FixedRigidBody.AddBox(self.object, 0.5, 0.5, 0.5, 2.0)
    self.rb:SetMaterial("rubber")      -- a name from data/core/defs/physics_materials.yaml
    self.rb:SetCollisionEvents(true)   -- enable the OnCollision* callbacks
    self.timer = 0.0
end

function Script.OnCollisionEnter(self, other)
    -- Kick straight up on first contact. Mode is the quoted NAME string;
    -- there is no constants table, and an unrecognized name raises.
    self.rb:AddForce(Vector3.new(0, 5, 0), "Impulse")
end

function Script.Update(self)
    self.timer = self.timer + Time.DeltaTime()
    if self.timer >= 3.0 then
        self.timer = 0.0
        -- f64 overload: teleport back to the drop point and fall again.
        self.rb:Respawn(self.drop[1], self.drop[2], self.drop[3])
    end
end