Veduta

Scenes

A scene is the starting state of a level: a camera, a light, a background colour and the entities that exist when it loads. It lives in assets/scenes/<name>.vscene.

The game starts in the project's default_scene (main unless veduta.json says otherwise). scene.load(name) switches to another one, and scene.name() tells which is loaded.

{
  "veduta": "scene/1",
  "camera": { "type": "perspective", "fov_deg": 60, "position": [0, 5, 10], "look_at": [0, 0, 0] },
  "light": { "direction": [-0.4, -1, -0.3], "color": "#ffffff", "ambient": "#404040" },
  "background": "#202830",
  "entities": [
    { "name": "ground", "kind": "static", "model": "ground", "material": "grass" },
    { "name": "player", "kind": "player", "model": "hero", "material": "hero",
      "rotation_deg": [0, 180, 0], "tags": ["player"] },
    { "name": "hat", "kind": "static", "model": "hat", "parent": "player",
      "position": [0, 1.8, 0] },
    { "name": "exit", "kind": "static", "position": [0, 0, -9],
      "hitbox": [[-1, 0, -0.5], [1, 2, 0.5]], "tags": ["exit"] }
  ]
}

Units and axes

Camera

FieldDefaultMeaning
type"perspective"perspective or orthographic
fov_deg60vertical field of view, perspective only
sizerequired for orthographicthe visible height in units; the width follows the 4:3 panel
near, far0.1, 200clip distances
positionrequiredwhere the camera is
look_atrequiredthe point it looks at

An orthographic camera looking down −Z is a 2D view, see 2D Games. Scripts change the camera while the game runs, see Camera.

Light

One directional light plus an ambient colour. direction is where the light travels ([0, -1, 0] shines straight down). Materials marked unlit ignore it, as every sprite should. Leave light out for the defaults shown above.

Entities

FieldDefaultMeaning
namerequiredunique in the scene; scenarios and scene.find use it
kindrequiredstatic for scenery, or a kind defined in kinds
model, materialnoneasset names; without a model the entity is invisible
position[0, 0, 0]
rotation_deg[0, 0, 0]
scale[1, 1, 1]no component may be 0; a negative one mirrors
tags[]labels for scripts, invariants and tests
parentnoneanother entity's name: this one's transform is then relative to it
visibletrue
hitboxnone[[minx, miny, minz], [maxx, maxy, maxz]] in the entity's own space: replaces the model's bounds for collisions
layer0draw order, −1000 to 1000
frame0the frame of a sprite sheet material

Ids are given in file order when the scene loads: the first entity is 1. Entities spawned later take the following ids.

An entity with a hitbox and no model is an invisible trigger zone:

kinds.player = {
  update = function(e)
    if #e:overlapping("exit") > 0 then
      trace("level_done", {})
      scene.load("level2")
    end
  end,
}

A scene per screen

Scenes are cheap. A title screen, each level, a game-over screen can each be a scene, with game.update deciding when to switch and game.draw drawing the text of each:

function game.update()
  local s = scene.name()
  if s == "title" and input.pressed("a") then
    scene.load("level1")
  elseif s == "gameover" and input.pressed("a") then
    scene.load("title")
  end
end

Remember that scene.load resets entities but not Lua variables: reset the score and the lives yourself when a new game starts.

A load can fade:

kinds.door = {
  on_touch = function(e, other)
    if other:has_tag("hero") then
      scene.load(e.state.to or "level2", {fade = 0.4})             -- to black and back
    end
  end,
}

With fade (seconds) the frame fades to color (default black; a white color is a flash), the scene changes in the middle, and it fades back in; the buttons read as nothing meanwhile. A load also cancels the timers, tweens and tasks (but those made with keep, and the task that called it), the cooldowns, the camera's follow and shake and the particles. For games with a title, levels and a pause menu, screens keep each part's code in its own place.

Queries

FunctionReturns
scene.nearest(x, y [, tag [, max]])the live entity (with tag) nearest (x, y), within max, and its distance; or nil
scene.within(x, y, radius [, tag])the live entities (with tag) within radius, nearest first
scene.raycast(x, y, dx, dy, distance [, tag])the first entity or map cell tagged tag (default "solid") a ray from (x, y) along (dx, dy) meets within distance: {x, y, distance, nx, ny, entity}, with cell = {x, y} instead of entity for a map cell; or nil

All work in the x, y plane. nx, ny is the normal of the side hit: ny == 1 is a floor.

kinds.guard = {
  update = function(e)
    local hero, d = scene.nearest(e.x, e.y, "hero", 8)
    if hero then
      local hit = scene.raycast(e.x, e.y, hero.x - e.x, hero.y - e.y, d)
      e.state.sees_hero = hit == nil            -- no solid wall in between
    end
    for _, coin in ipairs(scene.within(e.x, e.y, 2, "coin")) do
      coin:despawn()                            -- the guard pockets coins nearby
    end
  end,
}

Editing a scene while it shows

Lay a level out with its scene file open beside the simulator. Every save reloads the game in place: it restarts in the scene it was in (not the project's default), so a moved platform or a new enemy shows a second later, without playing back to the level. The same goes for a texture, a material or a hud written in game.draw. A file that does not compile, or a script that fails, leaves the last frame on the screen with the error over it until a save fixes it; F9 restarts from the start.

To open the simulator directly in a scene, in VS Code add a debug configuration with the snippet Veduta: Play a scene ("scene": "level3" in .vscode/launch.json) and press F5, or in a terminal:

veduta sim --scene level3

The run starts at tick 0 in that scene and game.init runs again, so a game whose init loads its title scene comes back to the title: load the first scene from a scene file's entities or from game.update instead, or make init respect scene.name().

The complete format, with every error message, is in the scene reference.

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/.