Log inGet started

Named values: the numbers a machine runs on

Updated October 26, 2026
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:

NameKindWhat it is
wheel.compressionnumbersuspension compression
wheel.angularVelocitynumberspin, radians per second
wheel.skidnumberhow hard the tyre slides
wheel.slipRationumberlongitudinal slip
wheel.slipAnglenumberslip angle, radians
wheel.roadSpeednumbercontact speed along the road, m/s
wheel.loadnumbersuspension load, newtons
wheel.forwardForcenumberdrive or brake force, newtons
wheel.sideForcenumbercornering force, newtons
wheel.groundedbooleanwhether 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.