Venus Lua API › Physics

KinematicController

Deterministic fixed-point character controller — collide-and-slide.

authored lua/venus_lua/src/lua_kinematic_controller.rs

KinematicController

A deterministic character-controller component: a capsule that sweeps through the world with
collide-and-slide (auto step-up/down, wall sliding, depenetration, walkable-slope grounding). It
mirrors a conventional controller’s API (Move, IsGrounded, CollisionFlags, GroundNormal,
Radius/SetRadius, …) and adds Rotation/SetRotation for an orientation-aware capsule.

The component owns fixed-point (deterministic) authoritative state and derives its f32
transform each frame — so it must be driven from Script.FixedUpdate at the fixed cadence
(Time.FixedDeltaTime()), not from Update. Render interpolation (stair smoothing over risers
and ledge drops) is handled internally; the authoritative state and every determinism hash stay
untouched.

What it collides with — voxel terrain only. Move sweeps the capsule against the voxel
terrain
and, if you enable one, an optional infinite fallback floor (SetGroundPlaneY) —
nothing else. It does not collide with analytic Colliders,
DynamicCharacters, or FixedRigidBody props — those
live in a separate rigid-body world this controller never queries — and two KinematicControllers
pass straight through each other. The controller is likewise invisible to every
Physics query
: it registers no body, so Physics.Raycast / overlaps never return
it. This is the engine’s f32-vs-fixed world split — the controller’s world is the deterministic
voxel collision world, not the rigid-body FixedXpbdWorld that Physics.* and Collider
share. To react to props, cast your own Physics.* ray/shape, or move them with a
DynamicCharacter instead.

Static calls use the type table (KinematicController.Add(go)); instance methods use colon syntax
on a handle (kc:Move(...)).

Types

KinematicController — an opaque handle (userdata) to the controller component on a GameObject.
A handle may be stale (its component or GameObject destroyed); it is safe to keep and pass
around. Passing a value that is not a KinematicController raises a type error, but calling a method
on a stale handle never raises — it returns the documented default (nil, false, 0, or
world-up) or no-ops.

Creation & Destruction

KinematicController.Add( GameObject object ) : KinematicController
Attaches a new controller to object and returns its handle, with default capsule tunables
(radius 0.4, half-height 0.9, step 0.3, skin 0.08, slope 46°, no ground plane). Returns
nil and logs an error if the controller can’t be added (the GameObject is invalid, already has
one, or the pool is exhausted).

KinematicController.Get( GameObject object ) : KinematicController
Returns the controller attached to object, or nil if it has none (no warning).

KinematicController.Exists( KinematicController controller ) : boolean
Returns true if the handle refers to a live controller. Safe on stale handles (returns false).

There is no Destroy binding — a controller is removed when its GameObject is destroyed.

Functions

Read-only queries. On a stale handle each returns a default (nil, or the noted fallback) without
raising.

kc:IsGrounded( ) : boolean
true if the controller is resting on a walkable surface (slope ≤ SlopeLimit). false on a
stale handle.

kc:IsWalkable( ) : boolean
Alias of IsGrounded — the slide controller only reports grounded on walkable surfaces, so the
two are identical.

kc:GroundNormal( ) : Vector3
Unit surface normal of the current ground contact ((0, 1, 0) on flat ground). Returns (0, 0, 0)
when airborne (no ground contact); world up (0, 1, 0) only on a stale handle.

kc:GroundAngle( ) : number
Slope steepness of the ground under the controller, in degrees from vertical (0 = flat, up
to SlopeLimit when grounded). Derived from the ground normal, so it reads 90° while airborne
(the ground normal is zero). 0 on a stale handle.

kc:CollisionFlags( ) : integer
Bitmask (OR) of the COLLISION_* values recording which sides touched during the last Move
(below / sides / above). 0 (COLLISION_NONE) if nothing was hit or on a stale handle. See
Constants.

kc:DidLand( ) : boolean
true if the controller became grounded during the last Move (the EVENT_LANDED transition —
was airborne, now grounded). false on a stale handle.

kc:DidLeaveGround( ) : boolean
true if the controller left the ground during the last Move (the EVENT_LEFT_GROUND
transition). false on a stale handle.

kc:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. nil (+ a logged
NotFound) on a stale handle, so a destroyed controller stays distinguishable from a disabled one.
This is intent, not effect: a controller whose own flag is enabled but whose GameObject is
deactivated still reports true.

kc: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 a stale handle unless noted; setters no-op. Vector3 / Quaternion
arguments must be native math values (not three loose numbers). Config values are quantized to
fixed-point when the next Move runs.

Velocity

kc:Velocity( ) : Vector3 — current velocity (units/second).
kc:SetVelocity( Vector3 velocity ) : void — sets the velocity directly. Move overwrites it
each call, so this is mainly useful for reading (kc:Velocity().y to integrate gravity) rather
than setting.

Position

kc:Position( ) : Vector3 — the authoritative fixed-point position converted to f32 (no
stair-smoothing, no interpolation). During step-up/step-down it can differ from the GameObject’s
rendered transform (gameObject:WorldPosition()), which carries the render-only smoothing.
kc:SetPosition( Vector3 position ) : void — teleports the controller, setting the
fixed-point authoritative position and clearing stair-smoothing. Use for spawns/respawns, not
per-frame motion (use Move).

Rotation

kc:Rotation( ) : Quaternion — current orientation.
kc:SetRotation( Quaternion rotation ) : void — sets the orientation. The capsule mover is
orientation-aware, so this turns the body (e.g. to face the movement direction).

Radius

kc:Radius( ) : number — capsule radius (default 0.4).
kc:SetRadius( number radius ) : void

Height

kc:Height( ) : number — capsule half-height, center to cap-center (default 0.9). Named
Height for convention, but the value is the half-height.
kc:SetHeight( number halfHeight ) : void — sets the half-height.

SlopeLimit

kc:SlopeLimit( ) : number — max walkable slope in degrees (default 46). Steeper
surfaces are treated as walls and do not ground the controller.
kc:SetSlopeLimit( number degrees ) : void

StepHeight

kc:StepHeight( ) : number — max step height, both directions (default 0.3): auto
step-up over risers up to this height, and step-down snap when descending ledges/slopes.
kc:SetStepHeight( number height ) : void

SkinWidth

kc:SkinWidth( ) : number — contact skin gap held back from surfaces to prevent sticking
(default 0.08).
kc:SetSkinWidth( number width ) : void

GroundPlaneY

kc:GroundPlaneY( ) : number or nil — world y of the optional infinite fallback floor, or
nil if none is set (the default). nil also on a stale handle.
kc:SetGroundPlaneY( number y ) : void — enables an infinite walkable floor at world y,
layered under the voxel terrain (terrain wins where it exists). Catches the character in open
space so it can’t fall through the world. There is no un-set binding — it can only be enabled or
moved.

Methods

kc:Move( Vector3 velocity, number dt ) : void
The core per-step drive. Sets the controller’s velocity to velocity (units/second, with gravity
already folded in by the caller — the controller applies none), then advances it by dt seconds
against the deterministic collision world using collide-and-slide (sweep, step-up/down, wall slide,
depenetration). Refreshes grounded state, ground normal, collision flags, and the land /
leave-ground events. No-op on a stale handle. With no collision world set it still updates the
stored velocity but skips the collide-and-slide step
(position/grounded/flags unchanged). Call once per fixed
step from Script.FixedUpdate with dt = Time.FixedDeltaTime().

kc:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active. Silently no-ops on a stale handle.

kc:Deactivate( ) : void
Clears the own enabled flag; the disable callback runs only if the component was actually active.
Silently no-ops on a stale handle.

Constants

Enum fields on the global KinematicController table (e.g. KinematicController.COLLISION_BELOW)
— not on instances.

Collision flags. CollisionFlags() returns a bitmask (bitwise OR) of these; test a side with
bitwise AND (flags & KinematicController.COLLISION_SIDES ~= 0):

Constant Value Meaning
COLLISION_NONE 0 Nothing touched during the last Move.
COLLISION_BELOW 1 Touched a walkable surface below (ground).
COLLISION_SIDES 2 Touched a wall to the side.
COLLISION_ABOVE 4 Touched a ceiling above.

Events. One-step ground transitions, surfaced by the DidLand() / DidLeaveGround() query
methods (you rarely read these constants directly):

Constant Value Meaning
EVENT_NONE 0 No transition this step.
EVENT_LANDED 1 Became grounded this step — read via DidLand().
EVENT_LEFT_GROUND 2 Left the ground this step — read via DidLeaveGround().

Example

-- A deterministic WASD character. Adapted from data/scripts/kinematic_player.lua.
-- Driven from FixedUpdate at the fixed cadence — the controller's state is
-- fixed-point and lockstep-deterministic; render smoothing is handled internally.
local Script = {}

local SPEED, GRAVITY, JUMP = 5.0, 20.0, 8.0

function Script.OnCreate(self)
    KinematicController.Add(self.object)
    local kc = KinematicController.Get(self.object)
    kc:SetStepHeight(0.6)    -- climb 0.5-unit risers
    kc:SetGroundPlaneY(0.0)  -- fallback floor so it can't fall through empty space
end

function Script.FixedUpdate(self)
    local kc = KinematicController.Get(self.object)
    if kc == nil then return end
    local dt = Time.FixedDeltaTime()
    if dt <= 0 then return end

    -- Horizontal input (world axes; forward is -Z).
    local ix, iz = 0, 0
    if Input.Key("move_forward") then iz = iz + 1 end
    if Input.Key("move_back")    then iz = iz - 1 end
    if Input.Key("move_right")   then ix = ix + 1 end
    if Input.Key("move_left")    then ix = ix - 1 end
    local move = Vector3.new(ix, 0, -iz)
    if move:sqrMagnitude() > 1 then move = Vector3.Normalize(move) end
    move = move * SPEED

    -- Script-driven gravity (the controller applies none).
    local vy
    if kc:IsGrounded() then
        vy = -2.0                                -- gentle stick to follow slopes/steps
        if Input.KeyDown("jump") then vy = JUMP end
    else
        vy = kc:Velocity().y - GRAVITY * dt
    end

    kc:Move(Vector3.new(move.x, vy, move.z), dt)

    -- Face the movement direction (orientation-aware capsule).
    if move:sqrMagnitude() > 0.01 then
        local yaw = math.deg(math.atan(move.x, move.z))
        kc:SetRotation(Quaternion.AngleAxis(yaw, Vector3.new(0, 1, 0)))
    end
end

return Script