Loops Across Ticks
Before this: Index and Value, Waiting
A for loop finishes inside one tick, so it cannot wait. To repeat a step
with a pause between each, you build the loop from await and a buffered
emit instead. This lesson fades a lamp up like a sunrise, one small step
every quarter of a second.
Contents
button Held -> chip pressed
chip lamp -> light Color
chip lit -> light Enabled
chip text -> decal Text
Code
const STEPS = 20
@left in pressed: exec
var step: int = 0
var running: bool = false
let t = (step.Value as float) / (STEPS as float)
@right out lamp = Color(1.0, 0.8, 0.5) * t
@right out lit = step.Value > 0
@right out text = "${step.Value * 5}%"
on pressed {
if running {
return
}
running = true
step = 0
let ramp: exec
buffer emit ramp
await ramp
if step < STEPS {
step += 1
buffer(0.25s) emit ramp
} else {
running = false
}
}
let ramp: execis a local signal, andawait rampwaits for it, as in Waiting.buffer emit rampfiresrampone tick later. By then the handler has reachedawait rampand is waiting, so it wakes up.buffer(0.25s) emit rampfiresrampagain a quarter of a second later. The handler goes back to theawaitand runs theifagain. That jump back is the loop.- When
stepreachesSTEPS, theelseruns instead, nothing firesrampagain, and the loop ends. runningstops a second press from starting a second loop on top of the first.returnends the handler early, so a press during the sunrise does nothing.tgoes from 0 to 1 asstepgoes from 0 to 20, and the lamp’s color is the warm white timest.litturns the light on from the first step.
How long does the sunrise take? What happens if you press the button again halfway through?
Run
- Compile the chip and paste it in-game. Place a Button, a Point Light and a brick with a Text Decal. Turn off the light’s Use Brick Color. While it is on, the light ignores the wired color.
- Wire the Button’s Held to
pressed,lampto the Point Light’s Color,litto its Enabled, andtextto the Text Decal’s Text. - Press the button. The lamp warms up over five seconds and the sign counts to 100%. Presses during the sunrise are ignored.
Under the Hood
Open the chip. buffer emit ramp is a Buffer (Ticks) gate set to one
tick, and buffer(0.25s) emit ramp is a Buffer (Seconds) set to 0.25.
Both feed a Union, which merges the two into one exec. The await is a
hidden Var that remembers it is waiting, read by a Get Variable
and a Branch. Follow the exec from the Buffer (Seconds) through the Union
and the if, and you are back at the Buffer (Seconds). It is a loop of wires
with a buffer in it, like the latch in Buffers.
A for loop cannot do this. Its iterations all run in one tick, and the
compiler refuses an await inside one:
const STEPS = 20
@left in pressed: exec
var step: int = 0
on pressed {
for i in 0..STEPS {
step = i
await Sleep(_, delay = 0.25)
}
}
Error: [WS079] a loop body runs synchronously within one tick, so it cannot suspend. Move the `await` outside the loop, or use a back-edge loop (`emit` + `await`) if you need work spread across ticks.
The emit that starts the loop must be buffered as well. A plain emit ramp
fires before the await on the next line is waiting, so nothing hears it:
const STEPS = 20
@left in pressed: exec
var step: int = 0
@right out text = "${step.Value * 5}%"
on pressed {
step = 0
let ramp: exec
emit ramp
await ramp
if step < STEPS {
step += 1
buffer(0.25s) emit ramp
}
}
WARN [WS053] `emit ramp` fires in this tick, before the following `await ramp` arms - so the await never resumes from it and the chain parks forever. Use `buffer emit ramp`, which lands the next tick, after the await is armed
The loop’s result
A for loop has a result too. let run = for ... names it, and after the
loop run.iterations says how many iterations ran:
@left in roll: exec
var rolls: int[]
var shown: string = "No rolls yet"
@right out text = shown.Value
on roll {
rolls.push(Random(1, 6))
var total: int = 0
let run = for r in rolls {
total += r
}
if run.aborted {
shown = "Cut short after ${run.iterations} rolls"
} else {
shown = "${run.iterations} rolls, total ${total}"
}
}
run.aborted is true when the game cut the loop off, for running too many
iterations in one tick. A break does not count as cut off. These are the
loop chip’s Iterations Executed and Aborted outputs.
Modify
- Make it a sunset too. Add a second button and a second handler that runs
the same loop downward:
stepstarts atSTEPSand goes down by one each pass while it is above 0. Give it its own signal, and sharerunning, so neither starts while the other runs. - Change the pace.
buffer(0.1s)is faster.buffer(3) emit rampwaits three ticks instead of a time in seconds. - Make the steps visible. Instead of a color, show a bar on the sign, built
with a
forloop as in Counting Loops, redrawn on every pass of the slow loop. The fast loop runs inside the slow one.
Make
Make a light show. A const array holds six colors. A button plays them on a
Point Light, one every half second, then turns the light off. A second press
during the show does nothing.
button Held -> chip play
chip lamp -> light Color
chip lit -> light Enabled
Good to Know
- A loop can sit inside an
if(for exampleif !running { ... }around the whole loop) when its signal,let ramp: exec, is declared inside that sameif. If the signal is declared outside it, the loop would run once, and the compiler warnsWS118at theawait. The code usesreturninstead, which keeps the loop at the top of the handler. - A
buffer emitends its path through the handler. Code after theifthat holds it runs only on the path that did not loop, which here is once, when the loop ends. A line after abuffer emitin the same block never runs, and the compiler warnsWS058there. - Keep loop counters like
stepoutside the handler. They have to keep their value from one pass to the next. - Use a
forloop for work that must finish now, and a buffered loop for work that should take time.
See Loops under emit and
Loop result in the reference.
Next: Dice.