Skip to content

First steps

Three small scripts that cover most of the Lua you'll ever need: values, decisions, repetition, and doing something later. Save each into your game's scripts/ folder while the game runs; it runs straight away. Press F1 for the Scripts panel, where print output and any errors show up, with the file and line.

Hello, boxes

Variables, if, a loop, a function and a table, each doing something you can see: a row of boxes in two colours, and a greeting in four languages.

A row of boxes, orange and blue

hello.lua
-- hello.lua: your first script. Variables, if, loops, functions and
-- tables, each doing something you can see. (docs/cookbook/first-steps.md)

-- A variable holds a value. `local` keeps it inside this file.
local count = 8
local size = 0.6

-- A function is a named recipe you can use again.
local function colorFor(i)
  -- if / else: even numbers orange, odd numbers blue.
  if i % 2 == 0 then
    return Vec(0.95, 0.55, 0.2)
  else
    return Vec(0.25, 0.5, 0.95)
  end
end

-- A loop runs its body once for each i from 1 to count.
for i = 1, count do
  physics.box {
    pos = Vec(-4.5 + i, 0.3, 3), -- one metre apart, in a row
    size = Vec(size, size, size),
    color = colorFor(i),
  }
end

-- A table is a list (or a dictionary). # gives a list's length.
local greetings = { "Hello", "Hallo", "Bonjour", "Hola" }
for index, word in ipairs(greetings) do
  print(index .. ": " .. word .. ", world!")
end
print("There are " .. #greetings .. " greetings and " .. count .. " boxes.")

Download hello.lua

  • local makes a variable belong to this file. Without it, it still only belongs to this script (each script has its own globals), but local is faster and says what you mean.
  • Vec(x, y, z) is a position, a size or a colour. y is up; colours go from 0 to 1 (red, green, blue).
  • physics.box { ... } takes a table of named settings. What you leave out gets a sensible default (a 50 cm box that falls).
  • .. joins text; #list is how long a list is; ipairs walks a list in order.

Change count to 20 and save: the old boxes go and the new row appears. That's hot reload: everything a script made is removed before it runs again, so nothing doubles up.

Rain

A timer runs a function later, or again and again. This one drops a ball five times a second at a random spot, keeps a list of them, and removes the oldest so there are never more than 60.

Blue balls raining down and bouncing

rain.lua
-- rain.lua: a timer drops a ball every fifth of a second, somewhere
-- random, and the oldest ones go so there are never more than 60.
-- (docs/cookbook/first-steps.md)

local MAX = 60
local balls = {}  -- a list, oldest first

timer.Create("rain.drop", 0.2, 0, function()   -- 0 repetitions = forever
  local x = math.random() * 16 - 8                -- -8 .. 8
  local z = math.random() * 16 - 8
  local id = physics.sphere {
    pos = Vec(x, 8, z),
    radius = 0.15 + math.random() * 0.15,
    color = Vec(0.3, 0.6 + math.random() * 0.4, 1.0),
    bounce = 0.5,
  }
  table.insert(balls, id)                         -- newest at the end
  if #balls > MAX then
    physics.remove(table.remove(balls, 1))        -- the oldest from the front
  end
end)

-- Stop after 30 seconds: timer.Simple runs once.
timer.Simple(30, function()
  timer.Remove("rain.drop")
  print("The rain stopped.")
end)

Download rain.lua

  • timer.Create(name, seconds, repetitions, fn): 0 repetitions means forever. timer.Simple(seconds, fn) runs once. timer.Remove(name) stops one.
  • math.random() is a number from 0 to 1, so math.random() * 16 - 8 is anywhere from -8 to 8.
  • table.insert(list, x) adds to the end; table.remove(list, 1) takes the first one out (and gives it back, here straight to physics.remove).

A pyramid, and a key to rebuild it

Loops inside loops build in two and three dimensions: layers, rows and columns. G rebuilds the pyramid after you've knocked it over (walk into it, or throw something at it from the input chapter).

A pyramid of crates

pyramid.lua
-- pyramid.lua: loops inside loops build a pyramid of crates, and G
-- rebuilds it after you've knocked it down. (docs/cookbook/first-steps.md)

local SIZE = 0.5
local LAYERS = 6
local crates = {}

local function build()
  for _, id in ipairs(crates) do physics.remove(id) end
  crates = {}
  for layer = 0, LAYERS - 1 do
    local across = LAYERS - layer             -- fewer crates on each layer up
    for x = 0, across - 1 do
      for z = 0, across - 1 do
        local offset = (across - 1) * SIZE / 2  -- centre each layer
        table.insert(crates, physics.box {
          pos = Vec(2.5 + x * SIZE - offset, SIZE / 2 + layer * SIZE, -0.5 + z * SIZE - offset),
          size = Vec(SIZE, SIZE, SIZE) * 0.98,  -- a hair apart, so they settle
          density = 120,
          color = Vec(0.55 + layer * 0.07, 0.4, 0.25),
        })
      end
    end
  end
  print("Built " .. #crates .. " crates.")
end

build()
input.define("pyramid.rebuild", "Rebuild the pyramid", "G")
hook.Add("Think", "pyramid.keys", function()
  if input.pressed("pyramid.rebuild") then build() end
end)

Download pyramid.lua

  • hook.Add("Think", id, fn) runs fn every frame. The id names it, so hot reload can replace it instead of adding a second one.
  • input.define makes a new action with a default key; players can rebind it. More in Input.
  • The crates are made 2% smaller than their spacing so they don't start pressed into each other (which would make them jump apart).

Events you can hook

Think is one of several events. The full list, with what each one passes, is in Scripting in Lua:

Event When
Init once, after every script has loaded and the game has started
Think every frame, with the frame time dt in seconds
Tick 60 times a second exactly (physics-rate logic)
Contact two bodies started touching (used in Moving things)
Break something breakable broke (Physics)
NetMessage a message from another player (Multiplayer)
Shutdown the game is closing

Your own events work the same way: hook.Run("GameOver", score) calls every hook.Add("GameOver", ...) in any script.

Next: make your own controls.