Entities
Everything in a game is an entity: the hero, a wall, a coin, an invisible trigger, the level builder. An entity has a position, rotation and scale, an optional model and material (what it looks like), tags, a kind (what it does) and a state (its own values).
Entities come from the scene file when a scene loads, or from scene.spawn
while the game runs. A Lua value of an entity is always the same value, so a == b tells
whether two variables hold the same entity.
Fields
| Field | Access | Meaning |
|---|---|---|
id | read | a number, 1, 2, 3… in load and spawn order |
name | read | unique in the scene |
kind | read | the name of its behaviour |
alive | read | false once despawned |
x, y, z | read/write | position, relative to the parent if it has one |
visible | read/write | drawn or not; an invisible entity still updates and collides |
model, material | read/write | asset names, or nil |
layer | read/write | the first key of the draw order (see 2D Games) |
frame | read/write | the frame of a sprite sheet its material cuts into a grid (see 2D Games) |
parent | read/write | the parent entity or nil; set it to an entity, an entity's name or nil |
hitbox | read/write | {{min x, y, z}, {max x, y, z}} in the entity's own space, or nil |
anim, anim_done | read/write, read | the clip it plays (see 2D Games); true once a clip that does not loop has ended |
color | read/write | a tint multiplying what it draws ("#40a0ff", 0x40a0ff, nil for none), read back as 0xrrggbb; "#rrggbbaa" sets alpha too |
alpha | read/write | 0 (invisible) to 1 (default); below 1 it blends over what is behind |
body | read/write | a velocity the engine moves each tick, or nil: see Bodies |
state | read/write | a table of your own values |
Setting any other field is an error. Changing frame every few ticks is how a sprite is
animated, when its material is a sprite sheet:
kinds.torch = {
update = function(e)
e.frame = engine.tick // 4 % 3 -- frames 0, 1, 2, a new one every 4 ticks
end,
}
Methods
| Method | Does |
|---|---|
e:position() | returns x, y, z |
e:set_position(x, y, z) | |
e:move(dx, dy, dz) | adds to the position |
e:world_position() | x, y, z in the world, with the parents' transforms applied |
e:rotation(), e:set_rotation(x, y, z) | Euler angles in degrees |
e:scale(), e:set_scale(x, y, z) | |
e:has_tag(t), e:add_tag(t), e:remove_tag(t), e:tags() | tags |
e:overlapping([tag]) | the live entities whose bounds overlap this one's, in id order; only those with tag when given |
e:bounds() | min x, y, z, max x, y, z in the world, or nil for an entity with no model and no hitbox |
e:children() | the live entities whose parent is this one, in id order |
e:despawn() | removes the entity, and its children, at the end of the tick; its kind's on_despawn runs first |
e:play(clip), e:clips() | starts a clip from its first frame; the clips it can play |
e:move_and_slide(dx, dy [, solid [, oneway]]) | moves in the x, y plane, stopped by entities and map cells tagged solid (default "solid") and sliding along them; oneway ones (default "oneway") stop it only falling onto their top. Returns stopped along x, along y |
e:move_toward(x, y, speed [, solid]), e:move_toward(other, speed [, solid]) | one tick's step at speed metres a second toward a point or an entity, sliding against solid when given; true once there |
e:distance_to(x, y), e:distance_to(other) | the distance in the x, y plane |
e:face(dx) | turns a sprite toward the side dx points to by mirroring its x scale; 0 keeps it |
e:flash([color [, seconds]]) | paints it color (white) for seconds (0.1): a hit |
e:cooldown(name, seconds) | true, and starts the wait, when the cooldown name is over; false while it runs |
Rotations are Euler angles in degrees, applied roll about Z first, then pitch about X, then yaw about Y (R = Ry · Rx · Rz), as in scene files.
Kind callbacks
Besides init and update, a kind may have two callbacks the engine calls on events:
kinds.coin = {
on_touch = function(e, other) -- e and other started to overlap (end of the tick)
if other:has_tag("hero") then e:despawn() end
end,
on_despawn = function(e) -- inside e:despawn(), before it goes
particles.burst{x = e.x, y = e.y, color = "#ffd84a"}
end,
}
on_touch runs once per new overlap, for each of the two whose kind has it, when the
trace records the collision event; it waits while the game is paused. Keep checking
e:overlapping for things that last (standing in lava).
Bodies
e.body makes the engine move the entity each tick, after the kinds, as
e:move_and_slide does:
kinds.crate = {
init = function(e)
e.body = {gravity = 30, max_fall = 15} -- falls and lands on "solid"
end,
update = function(e)
if e.body.on_ground and #e:overlapping("spring") > 0 then
e.body.vy = 16 -- metres a second, up
end
end,
}
| Body field | Meaning |
|---|---|
vx, vy | velocity, metres a second (+y up) |
gravity, max_fall | pulls vy down (metres a second²); the fastest fall |
solid | the tag that stops it ("solid"; false for none) |
oneway | the tag of platforms it lands on from above ("oneway"; false for none) |
on_ground, on_ceiling, on_wall | set by the engine: what it hit this tick (on_wall −1 left, 1 right, 0 none) |
A side it hits zeroes that velocity. e.body = nil stops it. See
Game Toolkit for a platformer and a top-down hero.
State
e.state holds the values that belong to one entity: its health, its direction, a timer.
Entities of a Lua kind start with an empty table.
kinds.enemy = {
init = function(e)
e.state.health = 3
e.state.dir = 1
end,
update = function(e)
for _, shot in ipairs(e:overlapping("shot")) do
shot:despawn()
e.state.health = e.state.health - 1
end
if e.state.health <= 0 then
trace("enemy_down", {name = e.name})
e:despawn()
end
end,
}
The trace records the state with every tick as state.<key>, so a scenario can
check it: {"entity": "boss", "path": "state.health", "op": "==", "value": 0}. Keep it to
numbers, strings, booleans and tables of them: other values (functions, entities) are
recorded as a description only.
Spawning
local coin = scene.spawn{
kind = "coin",
name = "coin_7",
model = "quad",
material = "coin",
position = {3, -2, 1},
rotation = {0, 0, 45},
scale = {0.5, 0.5, 1},
tags = {"coin", "pickup"},
visible = true,
layer = 0,
frame = 0,
parent = nil, -- an entity or an entity's name
hitbox = {{-0.3, -0.3, -0.5}, {0.3, 0.3, 0.5}},
state = {value = 5},
}
Every field is optional. kind defaults to static (no behaviour). Without a name the
entity is named after its kind and id (coin_12); a name already taken gets #id added.
state is merged into the state the entity starts with. The new entity's kind init runs
at once; its update starts on the next tick.
Spawning is allowed from game.init, game.update and any kind's init or update,
including while a scene loads, which is how Your First Game builds its
level from a text map.
Parents
An entity with a parent moves, turns and scales with it: its position, rotation and scale are relative to the parent. A sword in the hero's hand, a party member's shadow, a turret on a tank:
local hero = scene.find("hero")
local sword = scene.spawn{name = "sword", model = "sword", material = "steel",
parent = hero, position = {0.4, 0.2, 0.1}}
-- later: drop it where it is
local x, y, z = sword:world_position()
sword.parent = nil
sword:set_position(x, y, z)
Setting parent keeps the entity's local position, so re-parenting moves it unless you
set the position again, as above. A parent that would make a cycle (an entity under its
own child) is an error. Despawning a parent despawns its children.
Prefabs
A prefab (assets/prefabs/<name>.vprefab) is a group of entities placed as one: a
house with its door, a camp, a room. Worlds place them; scripts can too:
local camp = scene.spawn_prefab("camp", 12, 0, -4, 90, "camp_north")
camp.fire.state.lit = true -- each entity under its name in the prefab
for _, e in ipairs(camp) do -- and all of them in prefab order
e:add_tag("camp")
end
scene.spawn_prefab(name, x, y, z [, rotation [, prefix]]) puts the corner of the
prefab's footprint at (x, y, z), turns it by 0, 90, 180 or 270 degrees about +Y, names its
entities <prefix>_<entity> (the prefix defaults to the prefab's name) and keeps the
parents the prefab gives. The prefab format is in the
prefab reference.
Finding entities
| Function | Returns |
|---|---|
scene.find(name) | the entity, or nil |
scene.tagged(tag) | a list of the live entities with that tag, in id order |
scene.entities() | every live entity, in id order |
scene.nearest(x, y [, tag [, max]]) | the nearest live entity (with tag, within max) and its distance, or nil |
scene.within(x, y, radius [, tag]) | the live entities within radius, nearest first |
scene.raycast(x, y, dx, dy, distance [, tag]) | the first entity or map cell tagged tag ("solid") along a ray, or nil (see Scenes) |
Lists are ordinary Lua tables, safe to keep for the tick. Across ticks, check e.alive
before using an entity you kept: a despawned entity stays a valid value, but it is no longer
in the scene.
Collisions
e:overlapping(tag) compares axis-aligned bounding boxes: the model's bounds, or the
entity's hitbox when it has one. It uses the bounds computed at the end of
the previous tick, so an entity moved this tick is seen where it was.
- Boxes that only touch do not overlap.
- A sprite made from the
quadmodel is only 0.02 units deep: two sprites overlap only when their z are close. Keep actors that must touch at the same z, or give them hitboxes with depth. See 2D Games. - For walls and floors, use
e:move_and_slideor a body: entities and map cells taggedsolidstop them before they overlap, which is exact.
The engine also records a collision event in the trace when two entities start
overlapping (two static entities never do), which scenarios can count, and calls the
kinds' [on_touch](#kind-callbacks) then.