5. Objects and scripts

You now know everything needed to read a game script. This page puts it together: how a table becomes an object with functions of its own, and what each line of a script file is for.

Functions in tables

A table can hold functions, since functions are values:

local door = { open = false }

function door.toggle()
	door.open = not door.open
	print(door.open and "The door opens" or "The door closes")
end

door.toggle()    --> The door opens
door.toggle()    --> The door closes

This works for one door. For two doors the function is stuck, because it names door directly.

self

Give the function the table it should work on, as its first argument. By convention that argument is called self:

local function toggle(self)
	self.open = not self.open
	print(self.name .. (self.open and " opens" or " closes"))
end

local front = { name = "Front door", open = false, toggle = toggle }
local back = { name = "Back door", open = false, toggle = toggle }

front.toggle(front)    --> Front door opens
back.toggle(back)      --> Back door opens
front.toggle(front)    --> Front door closes

One function, any number of doors. Each call says which door it means.

The colon

Writing front.toggle(front) names the table twice. Lua has a shorthand for exactly this:

local front = { name = "Front door", open = false }

function front:toggle()
	self.open = not self.open
	print(self.name .. (self.open and " opens" or " closes"))
end

front:toggle()    --> Front door opens

A colon does two things:

  • In a call, front:toggle() means front.toggle(front). The table before the colon is passed as the first argument.
  • In a definition, function front:toggle() means function front.toggle(self). An argument named self is added for you.

So the rule is short: a dot gets a value out of a table, a colon calls a function on it.

-- file: example
entity.Position          -- a value: dot
entity:Destroy()         -- an action on the entity: colon
self.Speed               -- a value: dot
self.Controller:Jump(6)  -- an action on the controller: colon

Mixing them up is the most common error in game scripts. entity.Destroy() calls the function without telling it which entity, and fails with a message about a bad argument or a nil value.

Reading a script file

Here is a complete script. Every line should now make sense.

-- file: Spinner.lua
local Spinner = {}

Spinner.Fields = {
	Speed = 90,
}

function Spinner:OnStart()
	self.Turns = 0
end

function Spinner:OnUpdate(deltaTime)
	self.Entity:Turn(0, self.Speed * deltaTime, 0)
	self.Turns = self.Turns + self.Speed * deltaTime / 360
end

return Spinner
  • local Spinner = {} makes an empty table. The script is this table.
  • Spinner.Fields = { ... } adds a record to it, listing the values the inspector should show.
  • function Spinner:OnStart() adds a function to the table. The colon gives it a self.
  • return Spinner hands the table to the engine. A script file must end this way.

The engine then does the rest. For each entity that has this script, it makes a fresh table, the instance, that can use every function in Spinner. It calls OnStart once and OnUpdate every frame, passing the instance as self.

So inside these functions:

  • self is this entity's own instance. Ten entities with the script have ten separate self tables.
  • self.Entity is the entity the script is on. The engine puts it there.
  • self.Speed is the field's value for this entity: 90, or whatever was typed in the inspector.
  • self.Turns is a value the script made up for itself in OnStart. Anything you store on self is still there next frame.

A local variable inside OnUpdate is gone when the function returns. A value on self lasts as long as the entity does. That is how a script remembers things.

Your own functions

Add functions of your own the same way, and call them with self::

-- file: Health.lua
local Health = {}

Health.Fields = {
	Max = 100,
}

function Health:OnStart()
	self.Current = self.Max
end

function Health:Damage(amount)
	self.Current = math.max(self.Current - amount, 0)
	if self.Current == 0 then
		self:Die()
	end
end

function Health:Die()
	print(self.Entity.Name .. " is defeated")
	self.Entity:Destroy()
end

return Health

Another script can call these on any entity that has the script:

-- file: example
local health = other:GetScript("Health")
if health then
	health:Damage(25)
end

GetScript gives that entity's instance, or nil when it has no such script, which is why the if is there.

Sharing code between scripts

A file can return a table of helper functions, and other scripts load it with require:

-- file: Assets/Scripts/Dice.lua
local Dice = {}

function Dice.Roll(sides)
	return math.random(1, sides or 6)
end

return Dice
-- file: example
local Dice = require("Assets/Scripts/Dice.lua")

print(Dice.Roll())      -- 1 to 6
print(Dice.Roll(20))    -- 1 to 20

These helpers use a dot, not a colon. Roll does not act on a particular thing, so it needs no self.

What local at the top of a file means

A local outside every function belongs to the file. All the entities using the script share it:

-- file: example
local Enemy = {}

local alive = 0    -- one number, shared by every enemy

function Enemy:OnStart()
	alive = alive + 1
	self.Health = 50    -- one per enemy
end

Use self for what belongs to one entity, and a file-level local for what all of them share.

Try it

  1. Write a Counter table with a count and two functions written with a colon, Add and Report. Call each a few times.
  2. Take the Spinner script above and add a Reverse function that flips the sign of self.Speed.
  3. Read Player.lua from the tutorial. Say for each line whether it uses a dot or a colon, and why.

Next: mistakes everyone makes.