Skip to content

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:

  1. Herald appears
  2. Herald and Concierge both speak
  3. Both characters disappear

All four frames in the zine panel, in order, with reflow on exit.


Design Philosophy

  1. Choreography drives narrative. Timing is content.
  2. Creatures are props. Keep creature specs minimal; behavior lives in ren.
  3. Ren scenes are beats. Each beat is short, clear, purposeful.
  4. Zine is comic. Think panels, not UI. Think sequence, not hierarchy.
  5. Layout is emergent. Do not author positions or coordinates.

Follow these principles and your zine scenarios will feel alive, legible, and extensible.