Scripting overview

Game logic is written in Lua 5.4, or in TypeScript. A project picks one when it is made. A Lua script is a .lua file that returns a table. A TypeScript script is a .ts file that ends with export = DefineScript({ ... }), and the editor compiles it to Lua: see TypeScript. Every example on these pages has a tab for each language, and the one you pick holds on every page.

If Lua is new to you, start with Learn Lua. To see scripts at work in a whole game, do the tutorial.

Lua
TypeScript
---@class Spinner : Script
local Spinner = {}

-- Values the editor can set per entity.
Spinner.Fields = { Speed = 90 }

function Spinner:OnUpdate(deltaTime)
	self.Entity:Turn(0, self.Speed * deltaTime, 0)
end

return Spinner

In Lua, the first line is for your code editor. It says what self is, so the editor can complete self.Entity. for you. In TypeScript, DefineScript does the same for this. See Code editor setup.

Add it to an entity with Add Component > Scripts in the inspector. There, Add script adds a slot to pick a script file for, and New script writes a new file in Assets/Scripts, named after the entity, and puts it on the entity in one click. One entity can have several scripts, and one script can be on many entities. Each entity gets a copy of its own.

The functions the engine calls

All are optional.

FunctionWhen
OnCreate()When the scene starts, or the entity is made. Every script's OnCreate runs before any OnStart
OnStart()After every OnCreate. Look other entities up here
OnUpdate(deltaTime)Every frame, with the seconds of game time since the last
OnFixedUpdate(step)60 times per second of game time. For physics
OnDestroy()When the entity is destroyed or the scene stops
OnCollisionEnter/Stay/Exit(other, contact)When the entity's body touches another
OnTriggerEnter/Stay/Exit(other, contact)The same, when either body is a trigger

Inside them, self.Entity (this.Entity in TypeScript) is the entity the script is on, and self is that entity's own copy of the script, to keep values in.

Fields

The Fields table lists values the editor shows in the inspector, so each entity can have its own. Read one as self.Speed, or this.Speed in TypeScript.

Lua
TypeScript
Player.Fields = {
	Speed = 5,                                         -- a number
	CanFly = false,                                    -- a tick box
	Greeting = "Hello",                                -- text
	Offset = Vec3(0, 1, 0),                            -- a vector
	JumpSpeed = Field.Number(6, { Min = 0, Max = 15 }),  -- a slider
	Camera = Field.Entity(),                           -- another entity
	Bullet = Field.Prefab(),                           -- a prefab
	JumpSound = Field.Audio(),                         -- a sound
}

A field shows the script's default until it is edited on an entity.

Pointing at entities and assets

Field.Entity() is a box in the inspector that takes another entity of the scene. Pick one from the list, or drag one in from the hierarchy. In the script it is that entity, or nil (undefined in TypeScript) when none was set.

The asset kinds are Field.Prefab(), Field.Material(), Field.Texture(), Field.Audio(), Field.Scene(), Field.Mesh(), Field.Font() and Field.Animator(). Pick from the list, or drag one in from the Assets panel. In the script it is an asset handle, which is empty when none was set: check with handle:IsValid().

Lua
TypeScript
function Player:Shoot()
	local bullet = Scene.Instantiate(self.Bullet, self.Entity.Position)
	Audio.PlayOneShot(self.ShotSound)
end

Use these in place of Scene.Find("Some Name") and Assets.Find("Some/Path"). A name or a path in a script breaks without a word when the entity or the file is renamed. A field keeps pointing at the same thing.

When a group of entities is copied, a field that pointed at one of them points at its copy.

Limits and tips

Field.Number(default, { Min = 0, Max = 20 }) shows a slider. Any Field function takes a table with Tip, which the inspector shows when the mouse rests on the field:

Lua
TypeScript
Reach = Field.Number(2, { Min = 0, Max = 9, Tip = "How far it can hit from" }),
Target = Field.Entity({ Tip = "What to follow" }),

Reading and changing an entity

Lua
TypeScript
self.Entity.Position = Vec3(0, 2, 0)   -- put it somewhere
self.Entity:Move(0, 1, 0)              -- move it from where it is
self.Entity:Turn(0, 90, 0)             -- turn it from how it is turned
self.Entity:LookAt(target)             -- face a point or another entity
Warning

self.Entity.Position.y = 5 (or this.Entity.Position.y = 5) changes a copy and does nothing, because reading Position gives a copy. Use Move, or assign a whole vector.

What scripts cannot do

There is no io, os or dofile. Files are reached through the engine: SaveData for saves, and fields or Assets.Find for assets. require works, for the project's own scripts.

Sharing code between scripts

Put functions that several scripts need in a file of their own. In Lua it returns a table. In TypeScript it exports them:

Lua
TypeScript
-- Assets/Scripts/Utils.lua
local Utils = {}

function Utils.Nearest(entities, point)
	local best, bestDistance
	for _, entity in ipairs(entities) do
		local distance = entity.WorldPosition:Distance(point)
		if not best or distance < bestDistance then
			best, bestDistance = entity, distance
		end
	end
	return best
end

return Utils

Other scripts get that table with require, giving the path from the project's folder. In TypeScript, import it from beside the script:

Lua
TypeScript
local Utils = require("Assets/Scripts/Utils")

function Enemy:OnUpdate()
	local target = Utils.Nearest(Scene.FindByTag("Player"), self.Entity.WorldPosition)
end

The file is run once, and every script that requires it gets the same table. Editing it while the game plays reloads it with the scripts that use it.

Errors

A script error is logged with its file and line, and shows in the editor's console. An error in OnUpdate or OnFixedUpdate switches that script off on that entity until the scene restarts, so a broken script logs once and not every frame.

Using an entity after it was destroyed is an error at the script's line, never a crash. Check entity:IsValid() when unsure.

Hot reload

Save a script while the game plays in the editor and the new code is in use at once. Each running copy keeps its values and gets the new functions.

Next