Repository navigation
Callback Functions
A callback is a function you define in a script and the engine calls at the right moment. Every callback is optional: define only the ones you need. The names are reserved, so you can't use them for public fields.
| Callback | Entity scripts | Sector scripts | When |
|---|---|---|---|
Start() |
✓ | ✓ | Once, the first time the script is active |
Update() |
✓ | ✓ | Every frame |
FixedUpdate() |
✓ | ✓ | On a fixed 60 Hz step |
OnEnable() |
✓ | ✓ | When the script becomes active |
OnDisable() |
✓ | ✓ | When the script stops being active |
OnDestroy() |
✓ | ✓ | When the script is torn down |
OnCollisionEnter(other) |
✓ | Another solid collider starts touching this entity | |
OnCollision(other) |
✓ | Every frame while they touch | |
OnCollisionExit(other) |
✓ | They stop touching | |
OnTriggerEnter(other) |
✓ | A collider starts overlapping a trigger | |
OnTrigger(other) |
✓ | Every frame while they overlap | |
OnTriggerExit(other) |
✓ | They stop overlapping | |
OnSectorChange(sector) |
✓ | This entity moved into a different sector | |
OnEntityEnter(entity) |
✓ | An entity entered this sector | |
OnEntityExit(entity) |
✓ | An entity left this sector | |
| Flipbook frame events | ✓ | A Flipbook reached a frame with an event function |
If a callback is defined on the wrong kind of script (for example OnEntityEnter on an entity
script), it is silently ignored.
If a callback raises an error, the message is printed in red in the in-game console and written to the log. The script keeps running, and the callback is called again next time as normal.
Most callbacks only run while a script is active:
-
Entity scripts are active when the script's own Enabled box is ticked and its entity
is enabled (
entity.enabled). - Sector scripts are active when the script's Enabled box is ticked. Sectors can't be disabled.
Scripts can switch both flags at run time: entity.enabled = false on an entity, and
behaviour.enabled = false on a script reference (see Script).
Disabling the entity doesn't change each script's own flag, so re-enabling the entity brings back
exactly the scripts that were enabled before.
function Start() endRuns once per script instance, the first time the script is active.
- At level start, every script in the level is loaded before any
Startruns. So insideStartevery other script already exists and all public fields hold their inspector values. It's the right place to look things up (Game.FindEntity,GetScript) and to set up state. - Entity scripts start before sector scripts.
- A script that is inactive when the level starts doesn't run
Startthen. It runsStartthe first frame it becomes active, right afterOnEnable. - Code at the very top of the file (outside any function) runs even earlier, when the script is
loaded, and before the inspector values are applied. Public fields still hold their inline
defaults there. Put setup code in
Start.
local player
function Start()
player = Game.FindEntity("Player")
if player == nil then Debug.LogWarning(entity.name .. ": no Player in the level") end
endfunction Update() endRuns every frame while the script is active. GameTime.deltaTime is the length of the last
frame in seconds. Multiply anything that changes over time by it.
public number speed = 20
function Update()
entity.transform:AddPosition(Vector3(0, 0, speed * GameTime.deltaTime))
endfunction FixedUpdate() endRuns on a fixed step of 1/60 s, independent of the frame rate. GameTime.fixedDeltaTime is
the step length.
- The engine adds each frame's time to a counter and runs
FixedUpdateonce for every full 1/60 s in it. A 30 fps frame runs it twice; a 144 fps frame often runs it zero times. - It runs after every script's
Updatein the same frame. - At most 5 steps run per frame, so a long stall doesn't cause a burst of hundreds of calls.
- The engine's physics does not use this fixed step yet. It still steps once per frame.
FixedUpdateis best for logic that must tick at a steady rate, like counters, AI decisions or anything that should behave identically at any frame rate.
function OnEnable() end
function OnDisable() end-
OnEnableruns when the script becomes active: at level start (just beforeStart), and whenever it becomes active again later. -
OnDisableruns when the script stops being active, because its own Enabled flag or its entity'senabledwas switched off. - The switch is noticed when the engine next reaches that script in the
Updatepass: later in the same frame if that script hasn't run yet, otherwise next frame. It never happens in the middle of your own call.
-- A light that only hums while its entity is enabled.
function OnEnable()
if entity.audioSource ~= nil then entity.audioSource.looping = true end
end
function OnDisable()
if entity.audioSource ~= nil then entity.audioSource.looping = false end
endfunction OnDestroy() endRuns once when a script instance is torn down:
- its entity was destroyed with
entity:Destroy(); - the script was removed from its entity or sector, or the sector was deleted;
- the level stops (leaving play mode).
OnDestroy only runs if Start ran first. A script that was never active has nothing to tear
down, so it gets no OnDestroy.
entity:Destroy() is deferred. The entity keeps working normally until the end of the
current frame, then every script on it gets OnDestroy, then it is removed.
function OnCollisionEnter(other) end
function OnCollision(other) end
function OnCollisionExit(other) end| Parameter | Type | |
|---|---|---|
other |
Entity |
The entity this one collided with. |
Called on entity scripts when this entity's Collider and another entity's collider are pushed apart by physics.
-
OnCollisionEnterruns on the first frame of contact. -
OnCollisionruns every frame the two are in contact, including the first frame. -
OnCollisionExitruns on the first frame they are no longer in contact. - Both entities get the call, each receiving the other as
other. - They fire at the end of the frame, after all movement and physics.
What counts as a collision:
- Both colliders are Sphere colliders, active, and not triggers. Box colliders don't collide yet.
- At least one of the two has a non-static Rigidbody. Two static things never collide with each other.
- Walls, floors and ceilings are not entities, so they never cause these callbacks.
- Two bodies resting against each other keep a tiny overlap, so
OnCollisionkeeps firing while they touch, even when nothing moves.
If other is destroyed while touching, the next frame calls OnCollisionExit with an entity
whose isValid is false. Check other.isValid before using it in OnCollisionExit.
-- Scripts/BouncePad.lua: launch whatever bumps into this entity straight up.
-- Give the pad a Sphere Collider (not a trigger) and no Rigidbody.
public number launchSpeed = 180
function OnCollisionEnter(other)
local body = other.rigidbody
if body == nil then return end
local v = body.velocity
body.velocity = Vector3(v.x, launchSpeed, v.z)
endPushing the player sideways works, but only briefly: the built-in Player Controller pulls the player's horizontal velocity back toward what the movement keys ask for, at its Acceleration / Deceleration rate. Upward velocity is kept, so launches and jump pads work.
function OnTriggerEnter(other) end
function OnTrigger(other) end
function OnTriggerExit(other) end| Parameter | Type | |
|---|---|---|
other |
Entity |
The entity overlapping the trigger, or the trigger itself when this script is on the other entity. |
A trigger is a Collider with Is Trigger ticked. Nothing collides with it: things pass straight through, and the engine reports the overlap instead.
-
OnTriggerEnterruns on the first frame of overlap,OnTriggerevery frame of overlap (including the first), andOnTriggerExiton the first frame they no longer overlap. - Both entities get the call. The script on the trigger learns who came in, and the script on the visitor learns which trigger it entered.
- A trigger does not need a Rigidbody. A static trigger volume is the usual setup.
- Two overlapping triggers call each other too.
- A pair involving a trigger only ever calls the
OnTrigger*callbacks, neverOnCollision*. - Only Sphere colliders are checked. The trigger's entity must be inside a sector.
- They fire at the end of the frame, just after the collision callbacks.
-- Scripts/Checkpoint.lua: a trigger that saves where the player last was.
function OnTriggerEnter(other)
if not other.hasPlayerController then return end
Global.checkpoint = entity.transform.position
Debug.Print("Checkpoint reached")
end-- Scripts/HealingPool.lua: heal whoever stands in it, a bit every frame.
public number healPerSecond = 10
function OnTrigger(other)
local health = other:GetScript("Health")
if health.isValid then
health.health = math.min(health.maxHealth, health.health + healPerSecond * GameTime.deltaTime)
end
endfunction OnSectorChange(sector) end| Parameter | Type | |
|---|---|---|
sector |
Sector or nil
|
The sector the entity is in now, or nil if it left the map. |
Called on entity scripts when their entity ends up in a different sector.
- It fires at the end of the frame the entity crossed over, right after the sectors'
OnEntityExit/OnEntityEnter. - The sector the entity starts the level in does not count as a change.
- Which sector an entity is "in" works exactly like
entity:GetSector(): only the(x, z)position matters, and the innermost sector wins. See Sector.
-- Scripts/Footsteps.lua: change footstep sound by floor type.
function OnSectorChange(sector)
local audio = entity.audioSource
if audio == nil or sector == nil then return end
-- Paths are inside Assets, with the extension.
if sector:HasTag("water") then audio.soundFileName = "Sounds/Footsteps/splash.wav"
elseif sector:HasTag("metal") then audio.soundFileName = "Sounds/Footsteps/clank.wav"
else audio.soundFileName = "Sounds/Footsteps/step.wav" end
endfunction OnEntityEnter(entity) end
function OnEntityExit(entity) end| Parameter | Type | |
|---|---|---|
entity |
Entity |
The entity that came in or went out. |
Called on sector scripts when an entity crosses into or out of their sector.
- They fire once, at the end of the frame the entity crossed the boundary.
- Entities already inside when the level starts don't trigger
OnEntityEnter. Usesector:GetEntities()inStartif you need them. - Moving from one sector to another fires the old sector's
OnEntityExitfirst, then the new sector'sOnEntityEnter. - An entity that is destroyed inside, or leaves the map, also fires
OnEntityExit. After a destroy,entity.isValidisfalse. - The parameter is named
entityhere, which hides the script's ownentityglobal inside the function. On a sector script that global is an empty placeholder anyway.
-- Scripts/TrapRoom.lua (sector script): lock the door behind the player
-- and open it once every enemy in the room is gone.
public Sector door = nil
function OnEntityEnter(e)
if e.hasPlayerController and door ~= nil then door:MoveCeilingToFloor(1, 200) end
end
function OnEntityExit(e)
if door ~= nil and sector:CountEntities("enemy") == 0 then door:MoveFloorToCeiling(1, 60) end
endfunction Footstep(frameName) end -- any name you type into the frame| Parameter | Type | |
|---|---|---|
frameName |
string | The name of the frame that was reached. |
Unlike the callbacks above, the function's name is up to you: it's whatever you type into a frame's Event function box in the flipbook editor. When a Flipbook reaches that frame, the function is called on every enabled script on the entity that defines it.
- It fires each time playback reaches the frame, including every time a loop comes back to it,
and for frames skipped over in a slow frame.
SetFramedoesn't fire it. - The callback names in the table above can't be used as event functions.
- If no script on the entity defines it, a warning is logged once.
| # | Step | Callbacks |
|---|---|---|
| 1 | Scripts | For each script in turn: OnEnable/Start or OnDisable if its active state changed, then Update. Then all FixedUpdate steps. |
| 2 | Sector movement | Floor/ceiling moves and light fades advance. |
| 3 | Flipbooks | Every Flipbook advances and updates its sprite, then frame events fire. |
| 4 | Player controller | WASD movement, jumping, mouse look. |
| 5 | Physics | Gravity, velocity, collisions. |
| 6 | Sector membership | Every moved entity is assigned to the sector it's in now. |
| 7 | Sector events |
OnEntityExit, OnEntityEnter (sector scripts), then OnSectorChange (entity scripts). |
| 8 | Contact events |
OnCollisionExit, OnCollisionEnter, OnCollision, then OnTriggerExit, OnTriggerEnter, OnTrigger. |
| 9 | Destroys | Entities queued with Destroy() run OnDestroy and are removed. |
In step 1, entity scripts run before sector scripts. Something a script does in
Update (moving an entity, starting a door) is already reflected in the events at the end of the
same frame.