Writing gameplay
This page shows the script API for the jobs most gameplay needs. The script page’s API list, in the editor, has the rest.
The two languages call the same API in their own way:
- Luau calls an object’s methods with a colon (
self.entity:name()) and the engine’s shared services with a dot (Log.info,Input.isDown). - AngelScript uses a dot for methods (
self.name()) and::for services (Log::info,Input::isDown). Engine objects are handles:Float3@,Entity@.
A behavior’s life
Section titled “A behavior’s life”A behavior is a class. The engine makes one for each entity it is attached to, and calls its methods when there is something to do. Write only the ones you need:
| Method | Called |
|---|---|
onStart() |
Once, when the entity is in play |
onUpdate(dt) |
Every frame, dt the seconds since the last one |
onFixedUpdate(dt) |
At the physics rate |
onEnable(), onDisable() |
When the behavior is turned on or off |
onDestroy() |
Before the entity goes |
onContactBegin, onContactEnd, onTriggerEnter, onTriggerExit |
On physics contacts (below) |
on<Name>(payload) |
When a message named <Name> arrives (below) |
A Level script gets onStart, onUpdate, onFixedUpdate and onStop. A Game script gets
launch(), update(dt) and exit().
Scripts update before the systems that act on what they set, so a sound started or a
destination set in onUpdate takes effect the same frame.
Moving things
Section titled “Moving things”function Coin:onUpdate(dt) self.time = self.time + dt -- Spin about the up axis and bob up and down. self.entity:setRotation(Quaternion.FromAxisAngle(Float3.new(0, 1, 0), self.time * self.spinSpeed)) local home = self.home self.entity:setPosition(home.x, home.y + math.sin(self.time * 2.5) * 0.2, home.z)endvoid onUpdate(double dt){ m_time += float(dt); // Spin about the up axis and bob up and down. self.setRotation(Quaternion::FromAxisAngle(Float3(0.0f, 1.0f, 0.0f), m_time * spinSpeed)); self.setPosition(Float3(m_home.x, m_home.y + Math::Sin(m_time * 2.5f) * 0.2f, m_home.z));}position(),rotation()andscale()and their setters work in the parent’s space.worldPosition(),worldRotation()andworldScale()read where the entity is in the world.setRotationEuler(x, y, z)takes degrees: pitch, yaw and roll.- In Luau a
Float3is Luau’s own vector type, made withFloat3.new(x, y, z); use Luau’smathlibrary. AngelScript hasMath::Sinand the rest.
Scripts read the actions of the project’s input map, by name, never raw keys. That way a player can rebind them, and a gamepad works too.
if Input.wasPressed("Jump") then self:jump()endlocal crouched = Input.isDown("Sneak")local move = Input.value2D("Move") -- move.x, move.y from -1 to 1if (Input::wasPressed("Jump")){ jump();}bool crouched = Input::isDown("Sneak");Float2 move = Input::value2D("Move"); // move.x, move.y from -1 to 1isDown is true while held, wasPressed and wasReleased on the frame it changed. value
reads a one-axis action. Input.rumble(low, high, seconds) shakes the gamepad.
Finding entities and their components
Section titled “Finding entities and their components”function Coin:onStart() self.player = self.entity.scene:find("Player")end
function Coin:onUpdate(dt) if not self.player:isValid() then return end local light = LightComponent.of(self.entity) -- ...endvoid onStart(){ @m_player = self.scene.find("Player");}
void onUpdate(double dt){ if (!m_player.isValid()) return; LightComponent@ light = LightComponent::of(self); // ...}- The scene’s
find(name)returns the first entity with that name;findByPath("Level/Doors/Front")follows the hierarchy. An entity’sfindChildByName,parent(),firstChild()andnextSibling()walk it. - Check
isValid()before using an entity you found or kept: an entity that was destroyed, or never found, does nothing when called. - A component’s
of(entity)gives its API on that entity:RigidBodyComponent,CharacterComponent,MeshComponent,LightComponent,CameraComponent,AudioSourceComponent,NavAgentComponentand more. Scene-wide services work the same way with the scene:ScenePhysics.of(scene),ScenePrefabs.of(scene). On an entity without the component it isnil(Luau) ornull(AngelScript), so test it before use. If the component is removed while a script still holds it, using it is a script error.
Spawning and destroying
Section titled “Spawning and destroying”-- Copy the prefab's ID with right-click > Copy GUID in the Assets panel.local Pebble = Guid.new("245b459c-4b93-4876-9496-6e4eb3daa145")
function Thief:throw() local p = self.entity:worldPosition() local stone = ScenePrefabs.of(self.entity.scene):spawn(Pebble, Float3.new(p.x, p.y + 0.5, p.z))end
function Pebble:onTriggerEnter(other) self.entity:destroy()end// A property: pick the prefab in the inspector.["asset:Prefab", "The pebble thrown"] Guid@ pebble;
void throwPebble(){ Float3@ p = self.worldPosition(); Entity@ stone = ScenePrefabs::of(self.scene).spawn(pebble, Float3(p.x, p.y + 0.5f, p.z));}
// In the pebble's own behavior:void onTriggerEnter(Entity@ other){ self.destroy();}spawn also takes a rotation and a parent. destroy() removes the entity and its children at
the end of the frame. scene.createEntity(name) makes an empty one.
Messages between scripts
Section titled “Messages between scripts”send delivers a message to the behaviors of one entity: a message named CoinCollected calls
their onCoinCollected method, with the payload if there is one. scene.events.emit does the
same for every script listening, Level and Game scripts included.
-- In the coin:player:send("CoinCollected", 10)self.entity.scene.events:emit("CoinCollected", 10)
-- In the player, or in the Game script:function Player:onCoinCollected(value) self.coins = self.coins + valueend// In the coin:m_player.send("CoinCollected", 10);self.scene.events.emit("CoinCollected", 10);
// In the player, or in the Game script:void onCoinCollected(int value){ m_coins += value;}Messages are delivered after the current update, not during the send.
Physics
Section titled “Physics”Contacts call methods on the behaviors of both entities, each told about the other:
function Crate:onContactBegin(other, point, normal, speed) if speed > 2.0 then Audio.playOneShot3DPath("Audio/Thud", point.x, point.y, point.z) endend
function Pickup:onTriggerEnter(other) other:send("PickedUp")endvoid onContactBegin(Entity@ other, Float3 point, Float3 normal, float speed){ if (speed > 2.0f) { Audio::playOneShot3DPath("Audio/Thud", point.x, point.y, point.z); }}
void onTriggerEnter(Entity@ other){ other.send("PickedUp");}onContactEnd takes the same values, and onTriggerExit(other) fires when something leaves a
trigger.
Push a body with an impulse, and look for what is below or ahead with a ray:
local physics = ScenePhysics.of(self.entity.scene)physics:applyImpulse(stone, Float3.new(0, 5, 0))
local hit = physics:rayCast(self.entity:worldPosition(), Float3.new(0, -1, 0), 2.0)if hit and hit.hit then Log.info("ground at " .. hit.distance)endScenePhysics@ physics = ScenePhysics::of(self.scene);physics.applyImpulse(stone, Float3(0.0f, 5.0f, 0.0f));
RayCastHit@ hit = physics.rayCast(self.worldPosition(), Float3(0.0f, -1.0f, 0.0f), 2.0f);if (hit !is null && hit.hit){ Log::info("ground at " + hit.distance);}A hit has position, normal, distance and entity(). sphereCast and overlapSphere are
there too. A character moves through its CharacterComponent: move, jump, grounded().
local CoinSound = Guid.new("6d963695-67f8-4b7f-9cf4-d234f8034116") -- a sound's ID
Audio.playOneShot(CoinSound, AudioBus.Effects, 0.7)Audio.playCuePath("Audio/Footsteps")Audio.playMusicPath("Audio/Theme", 2.0) -- cross-fade over two secondsGuid kCoinSound = Guid("6d963695-67f8-4b7f-9cf4-d234f8034116"); // a sound's ID
Audio::playOneShot(kCoinSound, AudioBus::Effects, 0.7f);Audio::playCuePath("Audio/Footsteps");Audio::playMusicPath("Audio/Theme", 2.0f); // cross-fade over two secondsA sound plays by its asset ID (playOneShot, playCue, playMusic) or by its path in the
Assets panel (the Path versions). The 3D versions play at a position. The buses are
Master, Effects, Music and UI.
Waiting
Section titled “Waiting”A coroutine runs over several frames, waiting where it needs to:
function Door:onStart() local this = self startCoroutine(function() waitSeconds(1.0) this.entity:setPosition(0, 3, 0) waitUntil(function() return this.open end) Log.info("open") end, self)endPass self as the second argument, so the coroutine stops when the behavior goes.
void onStart(){ startCoroutine(ScriptCoroutine(this.raise));}
void raise(){ wait(1.0f); self.setPosition(0.0f, 3.0f, 0.0f); Coroutine::waitUntil(CoroutinePredicate(this.isOpen)); Log::info("open");}
bool isOpen() { return m_open; }Changing scene
Section titled “Changing scene”run controls the game as a whole. Load a scene by its ID:
local Level2 = Guid.new("c0d1c3a4-58e2-4f3b-9a51-0f6e4f1d2b77")run.loadScene(Level2)Guid kLevel2 = Guid("c0d1c3a4-58e2-4f3b-9a51-0f6e4f1d2b77");run::loadScene(kLevel2);The scene that was playing is removed once the new one is in. loadSceneAsync loads in the
background (check loadComplete), setTimeScale(0) pauses the game, and requestExit() quits.
The HUD and menus
Section titled “The HUD and menus”ui shows UI documents and reaches into them by element name:
local Hud = Guid.new("628098f1-29ea-4b5e-bdea-e6a828da27f5") -- a UI document's ID
ui.push(Hud)ui.findLabel("hud-lives"):setText("x " .. self.lives)ui.findButton("play-btn"):onClick(function() ui.pop() end)Guid kHudDoc = Guid("628098f1-29ea-4b5e-bdea-e6a828da27f5"); // a UI document's ID
ui::push(kHudDoc);ui::findLabel("hud-lives").setText("x " + m_lives);ui::findButton("play-btn").onClick(Action(this.onPlay));push shows a document over what is there, pop and back remove it, and replace swaps it.
Saving progress
Section titled “Saving progress”Save keeps values between runs: a best time, an unlocked level, the game’s options. Values are
ints, floats, booleans or strings, under any key you choose:
if score > Save.getInt("best.score", 0) then Save.setInt("best.score", score) Save.flush()endif (score > Save::getInt("best.score", 0)){ Save::setInt("best.score", score); Save::flush();}The second argument of a get is what it returns when nothing is saved yet. The game writes
what changed when it ends; flush() writes now, so a crash loses nothing. Playing in the editor
keeps its own save, apart from a player’s.
Logging
Section titled “Logging”Log.info, Log.warn and Log.error (Log::info and so on in AngelScript) write to the
Console in the editor and to the log of an exported game.
Time.delta() and Time.now() give the frame time and the time, and Random.value(),
Random.range(a, b) give random numbers.