Veduta

How a Game Runs

The three callbacks

main.lua runs once when the game starts. It defines functions in the global game table, which the engine calls:

function game.init()    -- once, after the first scene is loaded
end

function game.update()  -- once per tick, before the entities
end

function game.draw()    -- once per frame shown, after the scene: the HUD
end

All three are optional. The scene does not exist yet while main.lua itself runs: call scene.* functions from game.init on.

Ticks

The game advances in ticks, 20 per second by default. engine.tick counts them: 0 in game.init, then 1, 2, 3… engine.dt is the length of one tick in seconds (0.05). Move things by speed * engine.dt and their speed stays in units per second.

Each tick runs in this order:

  1. game.update() (while at most one screen is on the stack), then the top screen's update.
  2. The update of every live entity's kind, in id order. An entity spawned during the tick starts updating on the next one.
  3. Bodies move and particles age; then timers, tweens and tasks run, in the order they were made, and the camera follows and shakes.
  4. Entities despawned during the tick are removed; positions and bounds are recomputed.
  5. Collisions are found (each new overlap calls the kinds' on_touch), invariants are checked, and the tick is recorded in the trace.

While game.pause() holds or a screen is pushed over the game, steps 2 and 3 (but the timers of the screen on top) and on_touch wait.

The player draws one frame per tick: the scene, then game.draw() over it, then the screens' draw, bottom first, and dialogs and toasts.

Kinds

An entity's kind names its behaviour. A kind is a table in the global kinds:

kinds.coin = {
  init = function(e)     -- when the entity is loaded or spawned
    e.state.value = 1
  end,
  update = function(e)   -- once per tick, after game.update
    e:set_rotation(0, engine.tick * 4.5, 0)
  end,
}

Both functions are optional, as are on_touch(e, other) and on_despawn(e) (see Entities). static, camera and light are built-in kinds with no behaviour. A scene that names a kind no script defines fails to load, with a message naming it. Every entity of a kind shares the same functions; what differs from one entity to another goes in e.state (see Entities).

Modules

require("name") runs name.lua once and returns what it returned; later calls return the same value. Dots are folders relative to main.lua: require("enemies.bat") loads enemies/bat.lua.

local bat = {}

function bat.update(e)
  e:move(e.state.dir * 3 * engine.dt, 0, 0)
end

return bat
local bat = require("enemies.bat")

kinds.bat = { update = bat.update }

Determinism

Given the same seed and the same buttons at the same ticks, a game does exactly the same thing on every run and every machine: the PC, the test runner and the console. That is what makes scenarios reliable. The Lua runtime keeps it so:

The Lua dialect

Scripts are Lua 5.4, run by a Lua virtual machine written in Go inside the engine. The differences:

Lua 5.4Veduta
string, table, math, utf8, coroutinethe same (a coroutine may even yield inside a table.sort comparison)
printwrites to the tool's error output (the Debug Console in VS Code)
pairs orderorder of insertion
math.randomthe run's seeded generator
io, os, debug, load, dofilenot available
string.pack, string.unpack, string.dumpnot available
gotonot available

Errors and endless loops

A Lua error (a call on nil, a bad argument, error("...")) in any callback or kind stops the run and reports the script's file, line and traceback: in the Debug Console when the game runs from VS Code, in the test's result when a scenario hits it. A syntax error is reported earlier, in the Problems panel, when the file is saved.

Setting an entity field that does not exist (e.speed = 3) is an error too: your own values go in e.state.

A single callback that runs more than 20 million steps (loop iterations and calls) without returning is stopped as an endless loop.

Switching scenes

scene.load(name) replaces the whole scene, as a reset: every entity is removed, the scene file's entities are loaded again with fresh ids, and their kinds' init run. Lua variables are not reset: a script resets its own (score, lives) when it loads a level, as in Your First Game. game.init does not run again.

Veduta v2 (Lua games, release candidates). This wiki is built from wiki/ in the engine's repository, where its examples are tested, and published with every engine release on https://veduta.roomve.it/docs/.