Timers & watchers#
Some of the best moments in an adventure game happen on a delay: the player rings a doorbell, wanders off, and ten seconds later someone upstairs mutters "the doorbell!" and comes down to answer, wherever the player happens to be by then. Ignitor builds this out of two small pieces, timers and watchers, and this guide shows you how to wire them into a loop that works every time, not just the first time.
The three pieces, in plain words#
- A timer is a kitchen timer. You wind it up with a name and a number of seconds
(
STARTTIMER:doorbell_timer|10), walk away, and it goes off on its own. Nothing happens when it goes off: it just sits there, rung, until something checks on it. - A watcher is a tripwire. It watches a condition every frame ("has
doorbell_timergone off?") and the moment the condition is true it fires its effects, once. After firing, it's disarmed: it will never fire again until something re-arms it (or until its condition goes false, if you set it to fire every time). - A behaviour is a routine a character runs on their own (walk here, open that, wander this hallway), no player input involved.
Chain the three and you get the doorbell scene: the click starts the timer, the timer trips the watcher, the watcher launches the reaction.
The loop, drawn out#
This is the actual setup from our Maniac Mansion tribute demo: Weird Ed, upstairs in his room, reacting to the front-door bell:
The example, piece by piece#
Three editors, one piece each:
1. The doorbell hotspot (Room Editor): the use reaction plays a chime and winds
up the timer. That's all the hotspot knows; it has no idea Ed exists.
SOUND:assets/audio/sfx/doorbell_chime.ogg
STARTTIMER:doorbell_timer|10
2. The watcher (Puzzle Editor, π Global Watchers tab): global, because the player has probably wandered into another room by the time the timer goes off, and a global watcher listens everywhere.
id: weird_ed_reacts_to_doorbell
when: timerExpired Β· doorbell_timer
do: CLEARTIMER:doorbell_timer
CUTSCENE:weird_ed_hears_doorbell
3. The cutscene (Cutscene Editor, π Global bucket): Ed reacts, walks to his door, and hands control back. Then his behaviour takes over: a routine that marches him downstairs, room by room, to answer the front door. The final step is the one that makes the whole thing repeatable:
SWITCHCHAR:weird_ed
SAY:weird_ed|"Ah! The doorbell!"
WAIT:1
SAY:weird_ed|"Maybe that's my package!"
WALKNPCTO:weird_ed|1348|358
SWITCHCHAR:{_prevChar}
BEHAVIOUR:weird_ed|weird_ed_notices_doorbell
CLEARWATCHER:weird_ed_reacts_to_doorbell
Ring the bell again after Ed settles back in, and the same ten-second fuse burns down to the same reaction. Every lap of the loop resets itself.
The two golden rules#
Both of these bit us while building the demo, so they're earned wisdom:
An expired timer stays "expired" until you clear it. When it goes off, the timer doesn't vanish: it sits in its rung state, and a
timerExpiredcondition keeps answering yes forever. MakeCLEARTIMERthe first effect of whatever watcher listens for it, or that watcher's condition is permanently true.
A watcher can't re-arm itself from its own effects. The engine marks a watcher as fired after running its effect list, so a
CLEARWATCHERtargeting itself inside its owndoun-marks something that isn't marked yet: a no-op, and the watcher stays dead. Put theCLEARWATCHERat the end of the cutscene (or effect chain) the watcher launched instead. Nice side effect: while the reaction is still playing, the watcher stays disarmed, so mashing the doorbell can't stack five Eds.
A watcher that fires every time#
Firing once is the default, and for a story beat it's what you want. When a watcher should react every time its condition comes true (a bar that shows whenever the cursor reaches the edge, a lamp that switches on whenever the player steps into a zone), set Fires to Every time it comes true on its card in the Puzzle Editor.
It still fires once when its condition turns true, and then stays quiet while the condition
holds, so it never repeats every frame. What changes is the re-arming: the moment the
condition goes false again, the watcher re-arms by itself, and the next time it comes true it
fires again. No CLEARWATCHER needed.
Two things to keep in mind:
- It re-arms by looking at the condition, not at what it launched. If the watcher starts a
cutscene and the condition comes true again while it's still playing, it fires again. That's
why the doorbell above keeps Once and its
CLEARWATCHERat the end of the cutscene: Ed has to finish before the bell counts again. - A
timerExpiredwatcher still needs itsCLEARTIMER. An expired timer keeps the condition true (rule one), so the watcher would never see it go false and never re-arm.
A timer as a cooldown gate#
A timer isn't only a fuse for a watcher; you can also ask whether it's still running and use that as a gate. Two conditions read the same timer from opposite ends:
timerExpired: true only after the timer has rung (and stays true until youCLEARTIMERit, per the first golden rule above).timerRunning: true while the timer is still counting down, and false the instant it runs out, or before it ever starts.
That turns a cooldown into a one-liner. Start a timer as the "cooling" window, then guard the
action on timerRunning:
- While
timerRunningβ do nothing (or say "not yet"). The window is still open. - Otherwise β do the thing, and start the timer again. That re-opens the window and blocks a repeat until it burns down.
No companion flag, no re-arming watcher: the timer itself is the memory of "done recently".
Reach for timerExpired when you want a delayed one-shot (the doorbell); reach for timerRunning
when you want to suppress something during a window: a button that can't be spammed, an NPC
that won't repeat a line for a while.
A watcher that follows the cursor#
Some interface only shows up when the player reaches for it: a verb bar that appears when the cursor touches the bottom of the screen, a panel that drops in from the top, an exit arrow at the side. Two conditions read where the cursor is right now:
cursorAtEdge: true while the cursor sits on one edge of the screen (top, bottom, left or right), inside a strip a few pixels thick: 4 unless you give it another thickness. Going past the edge counts too, so the black bars around the picture, when the window doesn't match your game's shape, still count as that edge.cursorInRect: true while the cursor is inside a rectangle: the x and y of its top-left corner, then its width and height. Those are screen pixels, not room pixels, so the camera never moves the rectangle.
The mouse and a controller's stick move the same cursor, so both conditions work with either. A touchscreen has no cursor resting anywhere, so there both stay false: give touch players another way in, like a button.
"Show it on the edge, hide it when the cursor leaves" takes two watchers, both set to fire every time:
id: bar_show
when: cursorAtEdge Β· bottom
fires: Every time it comes true
do: SHOWGUI
id: bar_hide
when: NOT cursorAtEdge Β· bottom Β· 60
fires: Every time it comes true
do: HIDEGUI
Two details make it work:
- Each watcher re-arms on its own.
bar_showre-arms as soon as the cursor leaves the edge, andbar_hideas soon as it's back inside the strip, so the pair keeps working however many times the player comes and goes. - The hide strip is thicker than the show strip.
bar_hidewaits for the cursor to leave the bottom 60 pixels, not just the 4-pixel edge, so the bar stays put while the player moves across it. Set that number to your bar's height.
If you built this recipe before Fires existed, each watcher re-armed the other with a
CLEARWATCHER at the end of its effects. That version keeps working as it is.
When the game starts the cursor isn't on the edge yet, so bar_hide fires right away and the
bar starts out hidden, which is what you want. SHOWGUI and HIDEGUI show and hide every
on-screen GUI at once.
For the main bar itself there's a built-in option that also slides it and keeps it up while an item is in hand: Slide in from the edge in the GUI Editor. Reach for the watcher recipe when it's something else that should appear: a panel, an exit arrow, a hint.
When something doesn't fire#
- "My watcher only works once." It fired and disarmed itself. That's the default.
If it should fire every time its condition comes true, set Fires to Every time it
comes true. If it should wait until its reaction is over, keep Once and add a
CLEARWATCHER:<id>at the end of the reaction it triggers (see rule two). - "My reaction repeats every frame." The watcher is being re-armed while its
condition is still true: almost always a
timerExpiredwith noCLEARTIMER. Clear the timer in the watcher's own effects (see rule one). - "The second ring does nothing." The timer is starting again (
STARTTIMERalways rewinds it); it's the watcher that's still disarmed. Check that theCLEARWATCHERstep actually runs: it must sit on a path the reaction always reaches, like the last step of the cutscene. - "The timer never goes off at all." Check that its
STARTTIMERactually carries a duration. Seconds starts blank and nothing forces you to fill it in, so it's easy to wire a timer with a name and no number. ASTARTTIMERwith no duration is ignored: no timer starts, sotimerExpirednever becomes true and nothing downstream ever runs.
Where to go next#
- Puzzle Editor: where watchers and global watchers are authored, with the full condition catalog.
- Cutscene Editor: authoring the step lists that reactions like this one launch.
- Effect tokens: the reference table for
STARTTIMER,CLEARTIMER,CLEARWATCHER,BEHAVIOUR, and everything else the effects widget offers.