Tutorial: Authoring Zine-Based Scenarios
A zine is a comic-book-style narrative interface. Characters appear as frames in a dynamic layout that reflows as the cast changes. Downbeat uses the zine as its primary display surface.
This tutorial explains how to design and author content for zine-based scenarios.
Core Concept: Frames, Not Forms
Traditional game UIs organize information in static forms: dialog boxes, status bars, menus.
A zine organizes narrative moments as frames—individual panels that hold a character, a gesture, a line of dialogue, a state change. Frames appear in sequence; their arrival and departure shape the visual rhythm.
Frame 1 Frame 2 Frame 3
[Herald] [Herald] [Herald] [Concierge]
"Welcome to "Come meet "The floor "I'm your
the hall!" our guests!" is open." guide today."
When a frame leaves, the remaining frames reflow to fill space. There is no manual layout work. Layout emerges from the presence and absence of characters.
The Three Layers of a Zine Scenario
All zine content lives on three layers:
| Layer | Author | Controls |
|---|---|---|
| Choreography | Script (Rhai) | Timing, character entry/exit |
| Narrative | Content (YAML) | Character names, dialogue, voice |
| Rendering | Walker | Frames, layout, visual appearance |
Example:
Choreography: concierge enters at frame 300.
Narrative: concierge.yml says "Welcome."
Rendering: Walker draws frame 1 with concierge and "Welcome."
Author choreography only in scripts. Do not hardcode timing in YAML. Author narrative in YAML specs. Do not author layout or styling. Walker owns rendering. Do not author pixel positions or CSS.
Building Blocks: Areas, Creatures, Ren Scenes
Every zine scenario has the same YAML structure:
1. Areas (geography)
An area is where things happen. For downbeat, the entrance area is abstract—it has no visual shape, only a name and capacity.
apiVersion: net.plantange/v1
kind: simulation.area
meta:
title: Entrance Hall
name:
common:
en: entrance
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa2
space:
shape: point # Abstract; no visual footprint
scale: medium
Keep area specs minimal. The zine doesn't care about physics—it cares about character presence.
2. Creatures (cast)
A creature is an NPC. Each creature has:
- identity: name, class, voice traits
- vitality: baseline health and margin parameters
- affordances: what they can do (speak, move, perform)
apiVersion: net.plantange/v1
kind: simulation.npc
meta:
title: The Concierge
name:
common:
en: concierge
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa3
identity:
class: humanoid
species: human
role: guide
vitality:
health: 100.0
margin: 50.0
affordances:
- type: creature.afford.speak
class: dialogue
salience: 0.9
- type: creature.afford.move
class: locomotion
salience: 0.7
For zine scenarios, keep creatures simple. Complex behavior belongs in ren scenes, not in creature definitions.
3. Ren Scenes (narrative)
A ren scene is a cutscene—a moment where characters say things, perform actions, or change state. Ren scenes are YAML files that the runner interprets.
apiVersion: net.plantange/v1
kind: simulation.ren
meta:
title: Entrance Greeting
name:
common:
en: entrance_greeting
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa4
acts:
- number: 1
beats:
- number: 1
speaker: concierge
text:
common:
en: "Welcome to the entrance hall. I'm your guide."
tone: warm
duration_frames: 60
Each beat is a moment. The runner plays beats in sequence. Each beat carries:
- speaker: which character is acting
- text: what they say (or do)
- duration_frames: how long the beat lasts before the next one starts
Choreography: Timing Character Arrivals
Choreography defines when characters enter and leave. It lives in Rhai scripts.
Script Structure
// entrance_choreography.rhai
// Initialize: spawn the herald
let herald_id = world.spawn_creature("herald", "entrance");
// Wait for the herald to settle
yield_for(60); // Let herald appear in the entrance area
// Herald speaks greeting
world.trigger_ren_scene("entrance_greeting");
// Wait for greeting to complete
yield_for(120);
// Enter concierge
let concierge_id = world.spawn_creature("concierge", "entrance");
// Wait for concierge to settle
yield_for(60);
// Concierge welcomes
world.trigger_ren_scene("concierge_welcome");
// Continue the sequence...
Key Patterns
Spawn creatures in order. Each spawn emits a zine frame event. The order of spawns determines frame order.
Use yield_for() for timing. Do not use sleep. Yield returns control to the frame tick; the runner can handle other events while you wait. Use small yield durations (tens of frames, not hundreds).
Trigger ren scenes at right moments. Coordinate character presence with narrative beats. Herald arrives, then speaks. Concierge arrives, then speaks. Never leave a character silent on stage.
Exit creatures at end of arc. When a character's arc is done, despawn them. Walker removes their frame from the zine.
Ren Triggers: Firing Choreography
Choreography scripts are triggered by ren triggers—events that say "play this script now."
apiVersion: net.plantange/v1
kind: simulation.ren_trigger
meta:
title: Start Entrance Choreography
name:
common:
en: start_entrance_choreography
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa5
triggering:
event: world.entrance.initialized # Fires when player joins entrance
action:
type: trigger.action.run_script
script: entrance_choreography.rhai
context:
area: entrance
When the player joins the entrance area, this trigger fires, and the choreography script runs.
Walker Zine Events: What the Panel Receives
You don't author these events—the runner emits them. But understanding them helps you debug choreography.
zine.frame.v1: Character enters
{
"event": "zine.frame.v1",
"frame_id": "herald_001",
"character": "herald",
"name_display": "The Herald",
"sprite_hint": "herald_speaking",
"order": 1,
"timestamp_frame": 300
}
zine.remove.v1: Character exits
{
"event": "zine.remove.v1",
"frame_id": "herald_001"
}
zine.layout.v1: Frames reorder (optional; emergent from adds/removes)
{
"event": "zine.layout.v1",
"frames": ["concierge_001", "ensemble_002", "ensemble_003"]
}
The runner emits these events as choreography plays. Walker subscribes and renders.
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Hardcoded frame count in choreography | Timing breaks if tick rate changes | Use yield_for(N); let the runner tick; don't assume frame count is milliseconds |
| Character speaks without being present | Frame appears after dialogue is done | Spawn character first, then yield, then trigger ren scene |
| Missing ren scene uuid or kind | Parser error on startup | Every ren spec needs apiVersion, kind, meta.uuid |
| Zine frames don't reflow when char exits | Layout stuck, old frames still visible | Despawn the creature; Walker detects absence and removes frame |
| Too many characters at once | Walker zine panel becomes cluttered | Stagger arrivals; let each character breathe; exit old characters before new ones enter |
| Ren scene text is too long | Dialogue overflows frame | Keep beat text short (one or two sentences); add more beats if needed |
Walkthrough: Building the Downbeat Entrance
Let's build a minimal entrance from scratch.
Step 1: Define the area
# area/entrance.yml
apiVersion: net.plantange/v1
kind: simulation.area
meta:
title: Entrance Hall
name:
common:
en: entrance
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa2
Step 2: Define NPCs
# creatures/herald.yml
apiVersion: net.plantange/v1
kind: simulation.npc
meta:
title: The Herald
name:
common:
en: herald
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa3
identity:
class: humanoid
role: announcer
vitality:
health: 100.0
margin: 50.0
# creatures/concierge.yml
apiVersion: net.plantange/v1
kind: simulation.npc
meta:
title: The Concierge
name:
common:
en: concierge
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa4
identity:
class: humanoid
role: guide
vitality:
health: 100.0
margin: 50.0
Step 3: Define ren scenes
# ren/entrance_greeting.yml
apiVersion: net.plantange/v1
kind: simulation.ren
meta:
title: Entrance Greeting
name:
common:
en: entrance_greeting
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa5
acts:
- number: 1
beats:
- number: 1
speaker: herald
text:
common:
en: "Welcome everyone!"
duration_frames: 60
- number: 2
speaker: concierge
text:
common:
en: "I'm your guide for today."
duration_frames: 60
Step 4: Write choreography
# scripts/entrance_choreography.rhai
let herald_id = world.spawn_creature("herald", "entrance");
yield_for(30);
world.trigger_ren_scene("entrance_greeting");
yield_for(120);
let concierge_id = world.spawn_creature("concierge", "entrance");
yield_for(30);
// Cleanup: exit characters
world.despawn_creature(herald_id);
yield_for(30);
world.despawn_creature(concierge_id);
Step 5: Create trigger
# ren_triggers/start_entrance.yml
apiVersion: net.plantange/v1
kind: simulation.ren_trigger
meta:
title: Start Entrance Choreography
name:
common:
en: start_entrance
uuid: 019d8202-e322-76c6-1f0a2b3c4d5e6fa6
triggering:
event: world.entrance.initialized
action:
type: trigger.action.run_script
script: entrance_choreography.rhai
context:
area: entrance
Step 6: Test
cd /home/srussell/src/plantangenet
cargo run -p plant_runner --features server -- \
--load-dir ../plantangenet-core/scenarios/downbeat \
--bind 127.0.0.1:7322
Connect Walker to http://127.0.0.1:7322. You should see:
- Herald appears
- Herald and Concierge both speak
- Both characters disappear
All four frames in the zine panel, in order, with reflow on exit.
Design Philosophy
- Choreography drives narrative. Timing is content.
- Creatures are props. Keep creature specs minimal; behavior lives in ren.
- Ren scenes are beats. Each beat is short, clear, purposeful.
- Zine is comic. Think panels, not UI. Think sequence, not hierarchy.
- Layout is emergent. Do not author positions or coordinates.
Follow these principles and your zine scenarios will feel alive, legible, and extensible.