Everything that changes in a world changes in one of three ways:
| Mechanism | What it is | When it acts |
|---|---|---|
| Subscription (this page) | A value defined from other values: derive(path, expression). | Whenever an input changes. |
| Event | on(trigger, action): when something happens, do something once. | When the trigger fires. |
| Animator | A process that takes a value from here to there over time, built and then started. | Every tick until it ends or is stopped. |
A subscription works like a cell in a spreadsheet. Say once "the light's strength is the bulb's glow times 0.45 candela per nit, times the gain", and whenever the glow or the gain changes, the light follows by itself; no line anywhere "updates" it. The subscription subscribes to everything its expression mentions, the clock included, and the engine keeps it current.
None of the three has to be written beside the field it changes. Each names its target by path, so a statement can sit with the field, with the design, with the event, or in a world-level section, wherever reads best. Where they sit is a matter of arrangement, not meaning.
How you write it
derive(targetPath, expression)
The target is a field, named by path (fields.html, parts.html). The expression may name any value the author can address: a field of this part (glow), another part's (self.parent.lantern[13].glow), a material's (materials.lanternBulb.glow), what another part answers (land.height(here)), the clock (now(world), now(sim)).
// The world's lamp level, and what follows it field lampLevel: fraction = 0, within: 0 ..= 1, mutable: true derive(materials.lanternBulb.glow, lampLevel * bulbBrightness.at(eveningHour(hour(now(world))))) derive(materials.lanternGlass.glow, materials.lanternBulb.glow * 0.09) derive(materials.lanternHead.glow, materials.lanternBulb.glow * 0.0015) derive(materials.room.glow, materials.lanternBulb.glow * 0.4) // In the lantern: two events (in, out) become one value a formula can read derive(near, occupied(post, by: me)) derive(glass.tint, gain) derive(bulb.tint, gain) derive(light.intensity, materials.lanternBulb.glow * cdPerNit * gain) // The balloon: riding is never stored, only launched field launched: moment = never, mutable: true, lives: server derive(riding, now(sim) - launched < sim(170 s))
In this example the bulb's glow is about 2,800 nits at dusk (17:28), falling to 1.3 nits from 20:00: the lamp level times a brightness that follows the hour, read from the world's curve bulbBrightness, eight evening hours blended on a log scale (a morning hour mirrored about noon). The light gives cdPerNit, 0.45 candela per nit. The balloon is riding for 170 s after its last launch.
A block as the expression
Where a value takes several steps to say, the expression may be a block: local names, then a last line, written without a semicolon, whose value is the block's (blocks.html). In Cone's form, the balloon's burner, which fires in bursts whose rhythm depends on where the ride is:
derive(flame.firing, {
let t = now(sim);
let landed = launched + ride.length();
let reachedSummit = launched + ride.timeAt(along: course.pointNamed(summit)) // about 70 s into the ride
let quiet = landed - sim(45 s);
let burst = if launched == never {
pulse(from: start(sim), every: sim(4 s), for: sim(3 s), at: t)
} else if t >= landed {
pulse(from: landed, every: sim(4 s), for: sim(3 s), at: t)
} else if t < reachedSummit {
pulse(from: launched, every: sim(5 s), for: sim(3.2 s), at: t)
} else if t < quiet {
pulse(from: reachedSummit, every: sim(20 s), for: sim(1.5 s), at: t)
} else {
until(pulse(from: reachedSummit, every: sim(20 s), for: sim(1.5 s), at: quiet), quiet)
};
burst.contains(t)
})
derive(roar.gain, if flame.firing { 1.5 } else { 0 })
On the ground: 3 s on in every 4; climbing to the summit: 3.2 s in every 5; after it, 1.5 s in every 20; nothing in the last 45 s. pulse, until and span are defined on Blocks. summit is a point of the course and reachedSummit a moment; the notation refuses a local name that hides a name its own definition reads.
What it means
It defines the value outright
- A derived value is never written by
setor by an animator. Nothing else may write it. To animate something a formula defines, animate one of its inputs: to end the ride early, the content would changelaunched, notriding. - It is recomputed when an input changes. How often, and whether a small change is worth sending, is the engine's business, not the content's.
- A subscription to the clock is a value over time with no animator. Between 18:00 and 20:00 the bulb dims from about 800 to 1.3 nits with nothing running but a formula.
- There is no trigger. A subscription has no
alwaysand nowhile; it simply is.
A value block may not act
The block inside a derive is a pure calculation. It may not set, start, spawn, send, say or play, because the engine recalculates a value when and as often as it chooses (every frame, only when an input changes, on the server, on each visitor's machine), so an act inside one would happen an unknown number of times in unknown places. Observations that change nothing in the world, such as a line written to a debug log, are allowed. This discipline is what makes recalculation safe, and so makes the spreadsheet model possible at all.
Calculate, then kick something off
To act when a calculated value changes, write two lines: the calculation as a subscription, and a rule that watches it with became. The rule fires once, at the moment the value becomes true, however often the value is recalculated (events.html). An illustration:
derive(home, launched != never && !riding) on(became(home), play(sound.chime, at: basket)) // illustration
Several effects on one value: the author's formula
Every value is one value. When several things affect one thing, each effect gets its own value, driven by its own animator or subscription, and the value seen is a subscription over them that the author writes. The lantern's light is already three effects multiplied: the lamp level, the hour's brightness and the nearness gain. An illustration with two more:
field flicker: number = 0, mutable: true // illustration: its own looping animator
field flash : number = 0, mutable: true // illustration: up and back in a fraction of a second
derive(light.intensity, clamp(materials.lanternBulb.glow * 0.45 candelaPerNit * gain
+ flicker + flash, within: 0 ..= maxIntensity))
The author decides whether effects add, multiply or are clamped, and has to: an animated value and a bound value acting together can never combine by a rule nobody wrote. What it costs: an effect added later needs its own named value and a change to the formula. What it buys: one value per name; state that is plain values; no engine policy hidden in combine modes.
Poses blend the same way. A wave over a walk is a formula over two poses, using the pose library's blending, and the wave's fade in and out is an animator on its weight:
derive(body.pose, add(blend(gait.pose, hailPose, weight: wave,
mask: bones([rightUpperArm, rightForearm, rightHand, neck, chest])),
spellPose))
Build-time and live: one mechanism
A build-time subscription is one whose inputs stop changing once the world is built: a placement at (p.x, land.height(p), p.z), the brook's level from the land. A live one keeps following its inputs: the light from the glow. Both are written with derive. A world states what depends on what; when the engines recompute is their business.
Deriving a kind, a product or an image
The target may be more than a number. A part's kind and its solid can be written as subscriptions, and a live picture can be wired the same way:
derive(pane.kind, if open { portal(link, shows: labyrinth) } else { mirror }) // the looking-glass
derive(house.solid, if open { difference(house.shape, notch(...)) } else { house.shape })
derive(sign.image, gateCamera.picture) // illustration
Whether a field may change a part's kind or product this way, or a part may change type, or two parts are shown one at a time, is open.
When it runs, and where
Each tick, in order: events are handled; animators are stepped and write their values; then the subscriptions whose inputs changed are recomputed, in dependency order; then physics steps. Where a subscription runs follows from what it reads: one that reads a server-only field runs on the server; one whose inputs every machine has costs nothing on the network, since every machine can work it out. To begin with, the server runs the world's logic and sends results.
How it connects
- Events:
becamewatches a derived value. - Animators: the cells formulas read.
- Blocks: value blocks and acting blocks.
- Fields: the targets.
- Images: a live image from a source, wired by subscription.
- Distribution: where a subscription runs.
Open questions
- A loop of subscriptions: the brook's level reads the land's height, and the land is carved by the brook.
- Relevance to each view: "faces within 40 m" is neither client, server nor both.
- Which observations a value block may make. A debug log, yes; a counter or a timing that a later rule could read, no longer an observation.
- Derivation as a relation of its own or a consequence of expressions that mention other parts. The notation now writes it explicitly as
derive.