Venus Lua API › Core

Scripting Model

The script file you return, the self you receive, and every callback the engine calls.

authored lua/venus_lua/src/script_behavior.rs

Scripting Model

A gameplay script is a .lua file that returns a table of named functions. The engine loads
it once into a Script asset, and a Behavior runs an instance of it on a
GameObject, calling the functions whose names it recognizes as the object moves
through its lifecycle. Everything a script is — the table it returns, the self it receives, and
the callbacks it may define — is described here.

Two component types run scripts, and their callback sets differ:

The loader reads only the names it knows. Unknown fields are ignored — a misspelled Updte
silently never runs, with no error. Defining a callback the component doesn’t support (a
FixedUpdate on a UI script, say) is the same: harmlessly ignored.

The script file

The chunk must return a table. If it returns anything else — or fails to load/execute — the
attach is rejected and the reason is logged to stderr (did not return a table, Load error,
Exec error); the object simply runs no script.

local M = {}

function M.OnCreate(self) end
function M.Update(self) end

return M

Only functions whose keys match a known callback name (see the catalogs below) are captured, as Lua
registry references; the returned table itself is discarded after the scan. Two consequences:

Each script runs under a sandboxed _ENV: globals you assign land in a per-script table, and
reads fall through to a frozen snapshot of the engine globals. One script cannot clobber another’s
globals, nor the engine’s. This also means the standard library is a reduced setio, os,
debug, and several base globals are absent. See Environment & Sandbox for the
exact surface.

The self context table

Every callback receives, as its first argument, the behavior’s per-instance context table. The
engine populates exactly two fields and reads nothing else back — the rest of the table is yours to
use as per-instance state:

Field Type Meaning
self.object GameObject / UIObject The object this behavior is attached to.
self.behavior integer The raw component handle (rarely needed directly).

YAML-injected params

When a script is attached from a YAML scene layout, the layout’s parameter block is merged onto
self as self.<key> fields, available from OnCreate onward. Values are marshaled by type: whole
numbers → Lua integers, other numbers → floats, booleans → booleans, strings → strings.

# in a scene/prefab layout
- type: script
  script: cube_behavior
  params: { speed: 2.0, hostile: true }
function M.OnCreate(self)
    -- `self.speed` arrives from the layout; default it for code paths that
    -- attach without params (e.g. Behavior.Attach from Lua injects none).
    self.speed = self.speed or 1.0
end

Params are injected only by the YAML installer. Behavior.Attach from Lua creates
the context with object/behavior only — no param merge.

Lifecycle & ordering

The recognized names line up with the engine’s component lifecycle. The ordering
guarantees that trip people up:

add ─▶ (first enable) OnCreate ─▶ OnEnable ─▶ … ─▶ OnDisable ─▶ (re-enable) OnEnable ─▶ … ─▶ OnDestroy*
                                                                              *only if OnCreate fired

Per-frame callbacks (Update, FixedUpdate, LateUpdate, OnPreCull, OnTransformChanged) run
between enable and disable while the behavior is active in hierarchy. Update runs once per rendered
frame; FixedUpdate runs once per fixed physics step (zero or more times per frame); LateUpdate
runs after every Update that frame.

3D callbacks — ScriptBehavior

All names are optional; implement only what you need. self is always the context table.

Lifecycle

Callback Arguments Fires
OnCreate (self) Once, on first enable, before OnEnable.
OnEnable (self) On each activation.
OnDisable (self) On each deactivation.
OnDestroy (self) On teardown — only if OnCreate fired.

Per-frame

Callback Arguments Fires
Update (self) Once per rendered frame.
FixedUpdate (self) Once per fixed physics step.
LateUpdate (self) After all Updates that frame.
OnPreCull (self) Before the camera cull, each frame.
OnTransformChanged (self) When this object’s transform changes.

Pointer / input. Two families, mirroring the engine’s component input-responder callbacks. The
signal family passes only the receiver; the named-source family adds source, the input source
name (a string).

Callback Arguments
OnHoverEnter, OnHoverExit (self)
OnButtonDown, OnButtonUp, OnButtonCancel (self)
OnDrag (self)
OnScrollWheel (self)
OnHoverBegin, OnHoverEnd (self, source)
OnClickDown, OnClickHeld, OnClickRelease (self, source)
OnCancel (self, source)
OnScroll (self, source, amount) — on 3D, amount is always 0.

Character controller (fired by a KinematicController /
DynamicCharacter on the same object). nx, ny, nz are the surface normal.

Callback Arguments Fires
OnLand (self, nx, ny, nz, impactSpeed) Character became grounded; impactSpeed = downward speed at contact.
OnLeaveGround (self) Character left the ground.
OnWallHit (self, nx, ny, nz) Character hit a wall.
OnHeadHit (self) Character hit an overhead.

Triggers (require a trigger Collider with events enabled). other is the other
GameObject.

Callback Arguments
OnTriggerEnter, OnTriggerStay, OnTriggerExit (self, other)

Collisions (require FixedRigidBody:SetCollisionEvents(true) on this
object). Note the asymmetry — enter/stay hand you a table, exit hands you just the other object:

Callback Arguments
OnCollisionEnter (self, collision)
OnCollisionStay (self, collision) — same table, every fixed step while touching.
OnCollisionExit (self, other) — the other GameObject only, not a table.

The collision table:

Field Type Meaning
gameObject GameObject The other collider’s object.
point Vector3 Contact point.
normal Vector3 Contact normal.
impulse number Normal impulse magnitude.

collision.gameObject for a non-body contact. For voxel-terrain contact — or a body that has
already departed — the other handle is 0, and gameObject is a GameObject userdata wrapping that
null handle: a stale object (its getters return nil, GameObject.Exists is false). It is
a userdata, so it is truthyif collision.gameObject then is always true. Test it with
GameObject.Exists(collision.gameObject) to tell a real body from terrain.

UI callbacks — UIScriptBehavior

The UI component’s set is a strict subset: no FixedUpdate, and none of the physics,
trigger, collision, or signal-family pointer callbacks. self.object is a UIObject.

Callback Arguments Notes
OnCreate, OnEnable, OnDisable, OnDestroy (self) Same lifecycle rules as 3D.
Update, LateUpdate, OnPreCull, OnTransformChanged (self) Per-frame. No FixedUpdate.
OnHoverBegin, OnHoverEnd (self, source) source = input source name (string).
OnClickDown, OnClickHeld, OnClickRelease (self, source)  
OnCancel (self, source)  
OnScroll (self, source, scrollY) scrollY = the UI object’s current scroll offset (real value, unlike 3D).

State-machine sub-tables

Beyond the flat callbacks above, a UI script table may also carry per-state FSM sub-tables
(M.idle = { update = function(self, fsm, data, time, dt) ... end, ... }). These are dispatched by
a finite state machine, which hands itself in as argument 2. That mechanism — the per-state
callback names, their signatures, and the can_transition exception — is documented on its own
pages: FSM (UI), FixedUpdateFSM (3D / fixed step), and
UIFsm (requesting transitions).

Shared vs per-instance state

Because the chunk runs once and all instances share the same function objects, file-scope
locals (and script globals) are shared across every instance
of that script. Per-instance state
must live on self:

local counter = 0          -- SHARED by every instance of this script

local M = {}

function M.OnCreate(self)
    self.phase = counter * 0.1   -- per-instance: read the shared counter, keep our own phase
    counter = counter + 1        -- mutate the shared counter
end

function M.Update(self)
    -- self.phase is this instance's; `counter` is everyone's
end

return M

Reach for a file-scope local when you want one value across all instances (an id allocator, a
shared constant like Vector3.new(0,1,0)); reach for self for anything that must differ per
object. Mixing them up — per-instance data in a file local — is the classic bug: every object stomps
the same variable.

Example

A projectile that spins, reads a YAML-injected speed, and reacts to collisions — exercising the
self table, a param, lifecycle, per-frame Update, and the collision table.

local Y_AXIS = Vector3.new(0, 1, 0)

local M = {}

function M.OnCreate(self)
    self.speed = self.speed or 90.0   -- deg/sec; overridable from the layout's params
    self.hits  = 0
end

function M.Update(self)
    local angle = Time.Time() * self.speed
    self.object:SetLocalRotation(Quaternion.AngleAxis(angle, Y_AXIS))
end

function M.OnCollisionEnter(self, collision)
    self.hits = self.hits + 1
    -- Terrain vs. body: the handle may be stale — test it, don't just `if`.
    local what = GameObject.Exists(collision.gameObject) and "a body" or "terrain"
    print(string.format("hit %s at impulse %.2f (total %d)", what, collision.impulse, self.hits))
end

function M.OnDestroy(self)
    -- Only runs because OnCreate ran. Safe place for teardown.
end

return M