Named values: the numbers a machine runs on
local car = entity.find("car")
car.values.set("rpm", 3200)
car.values.set("brake", true)
car.values.set("spin", vector.create(0, 12.5, 0))
print(car.values.get("rpm")) --> 3200
The values global takes the same calls with the entity first, which is the
form for code that holds ids rather than proxies:
values.set(car, "rpm", 3200) -- or values.set(car.id, ...)
values.get(car, "rpm")
values.list(car) --> { rpm = 3200, brake = true, spin = ... }
values.remove(car, "rpm") -- or values.set(car, "rpm", nil)
A name takes the kind of its first value and keeps it: writing a boolean to a
name that holds a number raises, and remove frees the name for another kind.
What a value is, next to a component field
A component field is authored state: saved with the scene, edited in the
inspector, and, when it is Sync, replicated on every write. A named value
is live runtime state: never saved, never shown as a field, and local to this
engine unless it is declared to replicate. A gauge reading, an engine's rpm, a
wheel's slip or a light's on/off switch written every frame belongs in a value;
keeping it on a Sync field sends every write to every peer.
A value lives as long as its entity: it goes when the entity leaves the world. Each mode starts with none: entering play or going back to edit empties every value, and a subscription stands across the change and is told its value went.
A write is a store: about the cost of a table write plus a call, with nothing queued and nothing sent. Every reader sees it on its next read, whichever of them it is.
Declaring a value, and replicating it
declare states what a name is before anything writes it: its type, the
default it starts at, and whether it replicates.
car.values.declare("throttle", { type = "number", default = 0, replicate = 10 })
car.values.declare("headlights", { default = false, replicate = 2 })
car.values.declare("gear", { type = "number" }) -- kept on this engine
replicate is a rate in Hz (up to 60). The peer that may write the entity
(its owner, or any peer while no peer owns it) sends the value to every other
peer at most that often while it changes, and always sends the newest value,
so a throttle written every frame at 60 fps and declared at 10 Hz reaches the
others ten times a second and ends on the value it settled at. The entity has
to be synced (entity.spawnSynced, setSynced(true)): an entity no other peer
shares has no one to receive it. A value declared before that, or in the frame
the entity spawns, is sent as it stands once the entity is synced, and so is
every replicated value of an entity this peer takes over, so a headlights
declared once and never written again still reaches every peer. On the other
peers the value is an ordinary value, written by remote; a peer that does
not own the entity cannot send it.
A value with no replicate rate is never put on the wire, however often it
changes. values.stats().replication counts what was sent and received.
Following a value: subscriptions
local conn = car.values.subscribe("gear", function(gear)
print("now in gear", gear)
end)
values.subscribe(car, nil, function(value, name) end) -- every value of car
values.onAnyChange(function(value, name, entityId) end) -- every value anywhere
conn:Disconnect()
A callback runs once per frame in which the value changed, whoever changed it
(a script, an engine system or another peer), with the value as it stands at
that point; a value that went arrives as nil. A subscription a component
makes ends with the component. Each VM hears the changes its own
subscriptions follow: a job on a worker following a value hears it, and the
engine thread's subscriptions to the same value hear it too. onAnyChange with values.list is what a
recorder of a session's values follows: the list for where every value starts,
each change after it as it happens.
A material that reads a value
A material binds any of its shader's scalar, vector, colour or bool
properties to a value in its values: block, and the property then draws the
value of the entity wearing the material:
# brake_lamp.material/mat.yaml
shader: "@builtin::shaders.pbr"
colors:
base_color: { r: 0.3, g: 0.02, b: 0.02, a: 1 }
emissive: { r: 1, g: 0, b: 0, a: 1 }
floats:
emissive_intensity: 0
values:
emissive_intensity: { value: "brake", scale: 3 }
The lamp glows at intensity 3 while its entity's brake value is true and at
0 while it is false: scale and offset map the value as value * scale + offset, a boolean reads as 1 or 0, and a number fills every channel of a
colour or vector property. An entity holds the value itself, or an ancestor
holds it: a lamp mesh under the car reads the car's brake, so one value on
the root drives every part of the machine. An entity that holds no such value
(nor any ancestor of it) draws the material's own value. The read happens on
the draw, so a thousand lamps wearing one material each light from their own
car with no script and no material per car.
A runtime material binds the same way: renderer.material.create({ shader = ..., values = { emissive_intensity = { value = "brake", scale = 3 } } }, key),
or asset.create("material", ..., { values = ... }). A binding names a
property the shader declares; a texture slot takes no value. A material binds
at most 32 properties.
An audio voice that follows a value
Audio takes pitchValue and volumeValue, each the name of a value: the
voice plays at pitch times that value and volume times that value, read
from the source's entity or its nearest ancestor every frame.
car.component.add("Audio", { clip = engineLoop, looping = true, pitch = 0.5, pitchValue = "rpmRatio" })
car.values.set("rpmRatio", rpm / 1000) -- the engine note rises with it
A UI widget bound to a value
values.bind(entity, name, opts) stands in a widget's text, title,
header, placeholder, value, min, max, visible, enabled or
checked, and the widget reads the value every frame with no updateScreen:
ui.registerScreen("dash", { type = "vertical", children = {
{ type = "label", props = { text = values.bind(car, "rpm", { format = "%d rpm" }) } },
{ type = "slider", props = { min = 0, max = 1, value = values.bind(car, "throttle") } },
{ type = "label", props = { text = "BRAKE", visible = values.bind(car, "brake") } },
} })
format is a string.format pattern with one conversion (%d, %.1f, %s,
%x, %g); without one a number reads as it is. %d and %x take the whole
part of any number, so "%d rpm" shows 3215 for 3215.7. scale and offset map a
number first. fallback is what the widget shows while the entity holds no
such value (empty text, 0 or false when absent).
Values the engine writes
Engine systems publish their own state as values, under a prefix naming the
system, on the entity the state belongs to. A WheelCollider writes, every
physics tick, on its wheel entity:
| Name | Kind | What it is |
|---|---|---|
wheel.compression | number | suspension compression |
wheel.angularVelocity | number | spin, radians per second |
wheel.skid | number | how hard the tyre slides |
wheel.slipRatio | number | longitudinal slip |
wheel.slipAngle | number | slip angle, radians |
wheel.roadSpeed | number | contact speed along the road, m/s |
wheel.load | number | suspension load, newtons |
wheel.forwardForce | number | drive or brake force, newtons |
wheel.sideForce | number | cornering force, newtons |
wheel.grounded | boolean | whether the wheel touches ground |
So a tyre-squeal voice on the wheel takes volumeValue = "wheel.skid", and a
gauge binds wheel.roadSpeed, with no script reading the wheel.
Seeing every value
values.describe(entity) lists an entity's values with their type, last
writer (script, engine, remote), revision and replication rate, and
values.entities() names every entity holding one. The values toolbox is
the same view as tools (zero values list, zero values describe car,
zero values set car brake true), and the files /zero/runtime/values and
/zero/runtime/values/<entityId> hold it as JSON, read again on each read.