Venus Lua API › Physics
FixedRigidBody
Deterministic lockstep rigid body — forces, impulses, materials.
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
Forceon any
unrecognized value, and the globalForceMode.*integer table — a leftover from a retired
binding — coerced throughluaL_checkstringto"1"/"2"/"3"and matched nothing. Every
use of those constants silently appliedForce, which is wrong by a factor of the fixed step:
ForcedeliversF·inv_m·dtspread over the next step,ImpulsedeliversJ·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 invenus-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