Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture of the storytelling engine

This is a preliminary design as of now.1

In short: StoryManager is the Bevy Resource which:

  • loads and saves user data (game save),
  • and story data (game content),
  • keeps a secondary index of every story related object,
  • and stores the current progress.

Everything else is a Bevy Component. This allows gradual development of new features without having to refactor a large unwieldy struct/enum/system.

Some objects might reference assets (character thumbnail, bustup, audio). Likely just a String with the file path, and not another layer of abstraction, although that might change to offer either - String for small games with limited assets (most VNs, JRPG), indirect reference for complex games that need to manage assets in different complex ways (3D CRPG, etc).

Every object has a custom newtype ID - u64. In alpha, it’s hardcoded in Rust code. When editor is available, it will be created and maintained by the editor and unique to the story, i.e. once assigned ID will never be reused, to prevent save corruption on game update. Future format will likely be BSN2.

A step by step example

The work is starting with Bevy’s First schedule (i.e. before PreUpdate, SceneTransition, Update, PostUpdate) so that things can be evaluated in the same frame by the time Update schedule starts.

The game must keep track of conditions necessary to trigger a story somehow. The exact membrane doesn’t have a clear API yet, and the current idea is that

  • A) for small projects, this can be somewhat hardcoded, which is my use case now, so game system can just say “trigger story beat 123” (using ResMut<StoryManager>)
  • B) still for small projects, there will be Triggers, e.g. a game system sends a Trigger Message like “Meeting Character John Doe”, potentially validate other conditions (is in location X, etc) and the handler will convert that into Story Beat Id
    • the game would do that in the previous frame during Update or PostUpdate
    • in First, the StoryManager checks if there are any Triggers
    • if there are any Conditions associated with it, add them to the queue by sending them as Messages
    • which will be processed in PreUpdate as described below
  • C) As the project matures, and as Bevy gets a first party editor, there will be a plugin for the editor that will link game events to Story Beats (main container for small chunks of story).

Once either of the above finishes, we continue during First schedule:

  • a Story Beat Id will be started by StoryManager.

    • StoryManager will set something like current_story_beat_id = 1
    • StoryManager will record this to the history of visited nodes (as In Progress)
    • (later, after finishing, it will also update it as finished)
  • StoryBeat will contain a link to the initial StoryElement

  • then we check for Conditions associated with this StoryElement (1:n connection, the same conditions can be reused later)

    • Because we’ve started in the First Schedule, have time to evaluate things
    • Send Conditions as Messages, which will have Effect and Type
    • Effect is enum, eg. “is this dialogue option visible”, “is this dialogue option greyed out”, “do we skip this story beat” etc
    • Type (todo name) is the condition itself, i.e. is player health above 50%
  • In Bevy, Story Element entity will have components StoryElementId, and one of different StoryElement structs,

    • e.g. CharacterAnimationElement will move a sprite on screen (enter stage left)
    • in the future, the story engine will provide default canonical implementation of handling each StoryElement
    • but they’ll be each fully programmable to suit game’s GUI
    • one handler can handle as many different aspects as it’s practical, but often just 1
    • they’re just Bevy systems

  1. Time of writing: 2026-08-18

  2. Bevy Scene Notation