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

Blog of sorts to sort out my thoughts

See the navigation on the left.

Non-technical

You can listen to my music on Deezer or Spotify.

Storytelling Engine (STEngine?)

Yeah, I don’t have a better name. Yet.

This is the preliminary outline for a Rust / Bevy based, data-driven ‘engine’ which I hope will be suitable for handling basic visual novel stuff, but also be flexible enough to serve a wide variety of purposes and games, hypothetically even complex CRPG dialog trees and dynamic open world exploration type things.

My thesis right now is that writing the engine will be relatively easy (i.e. there will be no Hard or Cursed1 problems, but not that it’s going to be done in an afternoon), but making an editor which would be easy to learn, pleasant to use, and also sufficiently fool-proof will be the harder part.

Current progress (end of October 2026)

Prototype under construction, nothing demo-able yet.

Demo ETA early September - basic VN text display, sprites, and story branching.

No ETA for editor, file format, import tools from other platforms. Might get an early open source release around end of September 2026 (no editor etc).

Design goals - beginning (prototype Q3/2026)

My goals when I started this are to fit my purpose only, but to be flexible enough in design/architecture to be worth upgrading and making universal enough.

  • no scripting engine required to make branching, conditional player choices or NPC text, etc
  • no DSL required for that either
  • no custom scripting language ever
  • avoid writing BRE as long as possible (failed)
  • Rust, Bevy native, data-driven
  • tremendous performance, no runtime reflection other than what Bevy is doing already
  • offer a default dialogue runner GUI, even if an ugly one (perhaps egui at first)
  • avoid messy string key-value storage for story variables as much as possible
  • coding custom game features with normal Bevy code is good and expected (e.g. custom in-game reward for a story dialog option that’s not just a flag/variable)
  • support for Conditions and Consequences, e.g.
    • hide this dialogue option when energy is low
    • grey out this dialogue option when strength < 6
    • ask this question and get an answer only for the first time
    • show different question and answer every other time
    • allow for random-pool answers
  • misc Consequence features I want to have
    • straightforward dialogue
    • sound cue and music change
    • sprite animation (“john doe bustup enter screen left”)
    • allow for Tracery format sentences
  • everything relevant has custom IDs
  • mapping domain IDs to Entity id with secondary index
  • no storage format at first; Rust only, always-recompile is fine for now
    • compile time correctness if possible
  • make sure design allows for procedurally added story elements

Not in scope (yet):

  • fully featured GUI for everything
  • pretty design
  • voice narration, lip-syncing, progressive text typing
  • easy CRPG dialog trees, although technically possible
  • scripting
  • articy, renpy, yarn spinner, twine import/export of any kind
  • correctness checks for story itself à la https://www.yarnspinner.dev/storysolver

Type of games that should be doable within the first 2 months of development:

  • VNs with branching narrative
  • Stardew Valley type games with a bit richer integration into the game (date, time, relationship, house status or idk)

Design goals - hypothetical mature stage (2 years of work)

  • provides stock egui or bevy_ui implementation, but fully replaceable
  • reasonably easily pluggable into any Bevy game
  • stable and reliable save format for the whole story
    • track discarded IDs so that deleting and adding a character, and then loading a game doesn’t corrupt data
  • stable and reliable save format for the game state
  • 90% feature parity with Ren’Py and Yarn Spinner
  • compatibility or one-way migration tools from Ren’Py and Yarn Spinner
  • incremental sync with Articy exports (i.e. adding/removing/renaming characters won’t break everything)
  • hot reload option in debug builds
  • allow for different pre-defined modalities - DnD, SPECIAL, Scarlet Hollow (Copyright allowing!)
  • have a running, fun, non-theoretical, non-trivial example of procgen dynamic storytelling
    • i.e. a game prototype where player actions can spawn dynamic story elements that might be a little silly and formulaic, but are still better than whatever Skyrim 18th re-release is doing these days
    • no LLMs anywhere in the process
  • implement branched quest lines
  • synthetic benchmark performance:
    • 1000 chapters, 10k characters, 100k story beats,
    • 1M story elements, 5M conditions and 5M consequences
    • 20M story variables (mix of global and per chapter and per character)
    • response time for any given story element <1ms

Type of games that should be doable with this engine

  • any not-fully-detailed-3D CRPG
    • complex, long dialog trees
    • yes: baldur’s gate 2, not yet: baldur’s gate 3
  • even Disco Elysium, Fallout
    • dialogue based on items / equipment
    • dialogue based on past actions / traits (child killer)
    • dialogue based on stats
    • dialogue based on quest status/progress / past quest completion
    • maybe except Disco’s Red Checks, might not be a default feature
  • Scarlet Hollow

Types of games that will be possible only later, with more work and thought

  • JRPG/CRPG with 3D elements, because a lot of work:
    • switching, animating cameras (authoring! editor???)
    • character animations
    • dynamic animations like “X turns head to Y”
    • lip-syncing
    • skipping sentences but keeping animation consistent / idempotent state
    • different lip-syncing for different languages? I don’t even know

Design goals - open source (2-3 years of work)

  • Ideally would have a WASM editor which would allow to create stories and export into a stable+versioned format
  • optional human-readable export format (BSN possibly?)
  • non-cloud browser only storage
  • maybe 3rd party storage integration (google drive .zip upload/download)
  • keep “enterprise” features out of open source because I need to pay rent

Design goals - commercial product (2-5 years of work)

  • wasm editor above, but more powerful?
  • definitely an image editor at least for simple drafts
  • publish stories them into a “cloud” for very cheap (free tier? free play for small chapters? storage caps?)
  • and generate on demand finished games for android/win/linux and maybe if $$ then mac/ios if feasible at all
  • desktop Editor should have powerful integration with Rust codebase, i.e. the ability to track enums and systems and offer these from drop down menus as different Conditions and Consequences
  • tablets with physical keyboards should provide a reasonably productive platform to author stories on.

Secret stretch goals mostly for myself

While not core to the project, I’d like to see if this engine can be actually used to run simulations, the ‘dream’ goal being 12000 NPCs being able to move around and make decision based on their individual situations. Story Elements would not be “show GUI or play sound” but instead just empty containers grouping Conditions and Consequences which would change their relationships, location, resources, etc.

In a way it could ??? replace IAUS for some things? I guess maybe even completely if Conditions return floats, but I don’t think I want to do that in the near future.

Design goals - megalomaniac (5+ years)

  • rival Ren’Py and Yarn Spinner, but without forcing their text formats
  • replace Articy Draft as the industry standard

No that’s it, just two goals, I’m humble like that. /s

Jokes aside, I have a lot of respect for Yarn Spinner and Ren’Py, but I find their text formats ugly and I want to break away from that. (But not to forbid users from using a text-only format.) I have no actual complaints about Articy, I doubt I have a chance of making this into a commercial product, but I want to try anyway because I have a habit of making BREs, and I think the world needs to see a bit more Rust tooling instead of being stuck in 1990s style UIs full of ugly small buttons.

Previous art / To research

  • Twine
  • Yarn Spinner
  • Ren’Py
  • Articy
  • Inkle
  • https://naninovel.com/guide/getting-started
  • Interactive Fiction
  • https://narrat.dev/ js open source https://github.com/liana-p/narrat-engine
  • https://storysynth.org/
  • https://storyflow-editor.com/ 25 euro https://store.steampowered.com/app/4088380/StoryFlow_Editor/
  • Dialogic 2
  • Dialogue Manager 4
  • Arcweave

Business Rule Engines and scripting to research / try / integrate for later maybe

  • Lua, LuaJit, Luau - https://crates.io/crates/mlua
    • 6-10x faster than Rhai
    • seems wasm supported
    • probably my best bet, also more likely to make sense to non-coder story writers
  • Rhai - 10M dl, stable - https://crates.io/crates/rhai
    • slower than V8 JavaScript
    • still good enough for scripting events or animations
    • should be fine unless we need to run hundreds or thousands of short scripts in sync every frame
    • more complex game AI could be handled if it can be ran async / outside of Bevy Schedules.
    • there used to be a very old bevy crate for Rhai: https://crates.io/crates/bevy_rhai
  • ??? Wasmtime ??? https://crates.io/crates/wasmtime
    • idk if it could compile wasm and also run it in wasm??
  • fast, updated, but AI slop: https://github.com/KSD-CO/rust-rule-engine/
  • year old, handful of downloads only: https://crates.io/crates/rusty-rules
  • 250k dl, “microseconds” - https://crates.io/crates/zen-engine

Possible titles

  • STEngine - taken by a 12 years abandoned github project <- most likely will use this
    • StanGin??? StanJinn?
  • ArcBark - taken, dog treat company
  • TaleTrail - taken, AI bs
  • StoryLorry - taken
  • StoryGlory - taken
  • talengine - “ai powerever workforce analysis” :vomit emoji:

  1. Hard problems are things like physics engines, compilers, dealing with Google, optimizing equally for desktop as well as small screens, etc. Cursed problems are usually game design related, which often have no “right” solution, like making a multiplayer game that groups you with strangers that’s resistant to griefers, or making a chat filter that doesn’t censor “assassin”.

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,
  • stores the list of current story beat’s assets actively used (music, backgrounds, characters on screen)
    • since we can “nest” story beats into each other, this will push/pop those onto a stack
  • 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/JRPG, etc).

Every object has a custom newtype ID - u64. During early development, it’s hardcoded in Rust code. Maybe stored as RON files. Once 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, esp. once Bevy 0.20 lands (Q3/Q4 2026).

How most of story-related data is handled: All story data is loaded in memory and stored in Bevy ECS. There is no current plan to add i/o streaming; it might come later in theory, as long as we store all IDs, which shouldn’t take more than a few dozen MB RAM even with millions of objects (text lines, dialogue options, conditions, consequences, characters, animations, sound cues, etc).

(Media assets might get loaded or unloaded dynamically, that’s probably easier to do, and saves more RAM than worrying about text content.)

These story objects serve dual purpose of being a story storage, and once they’re activated, gain more Components like Sprite, Transform, Tween, etc, which are then removed once they fill their purpose.

A step by step example

Summary:

  • game triggers Story Beat
  • StoryManager and a few core systems handle logistics
  • if there are Conditions associated, during the Last schedule, queue Elements up for evaluation
  • on the next frame, run the business rule engine during the First schedule
  • in PreUpdate, process results, do init for the newly shows Elements (e.g. spawn sprites)
  • during Update, keep processing them for as long as necessary (e.g. animating a sprite)
  • If conditions fail, jump to another node if one is provided (NextElement)
  • After story Elements complete (animation finishes, player clicks an option), jump to different Element, rinse, repeat

In a lot more words:

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 (hashmap most likely)
    • ?maybe? record it in a list of in-progress events
  • StoryBeat will contain a link to the initial StoryElement

  • we check for the ConditionLink 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 ConditionLinks as Messages, which comprise ConditionEffect and Condition itself
    • Effect is an enum used for grouping, e.g. “is this (dialogue) option visible”, “is this (dialogue) option greyed out”, “do we skip this (story beat)” etc - each Effect one if/then branch, so to speak
      • Since Conditions can apply to any part of Story, I probably won’t bother making sure that every ConditionEffect->target object combination is valid
    • Condition is a singular clause of the imagined “if” statement, i.e. is player health above 50%, is story variable IS_SAD true, etc
  • In Bevy, Story Element entity will have two or more components: mandatory StoryElementId and one or more of different StoryElement structs,

    • e.g. CharacterAnimationElement will move a sprite on screen (e.g. “enter screen left”)
    • in the future, the story engine will provide default canonical implementation of handling each StoryElement
    • but they’ll be each opt-in/out and 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
    • initial set up of Elements is done via a Message, it’s continuation is done via With

  1. Started this 2026-08-17, last update 2026-08-28

  2. Bevy Scene Notation. Introduced in 0.19, will be made a lot more usable in 0.20 when they add a native loader, ETA Q4/2026.

A brief non-API overview of Story Engine data design

These are more in-progress design documents than documentation; besides not being finalized, please keep in mind that most things described here are only for alpha version development; game writers authoring content will not need to know any of these specifics.

StoryManager - Bevy Resource

  • save/load
  • state
  • index for all objects, maps {stuff}Id to Bevy Entity ids
  • log (text log of recent things said by various characters or narrated)
  • history (a list of all player choices already clicked, all text seen, etc)
  • initiates Story Beats (? uncertain and/or one of several ways to start them)
  • heartbeat - every time an element is processed or skipped, increment (persisted u64)
  • possibly more helper functions both for runtime and editor (recursive getters, integrity checks?)

Core systems provided by the engine

  • listen for a message to start a story beat
  • find beats/elements with IsStarting, queue their Condition for evaluation
  • evaluate them
  • collect the results
  • if successful, add IsPlaying
  • if the default egui runner is enabled, listen to that being added and trigger animations etc

Overview of date structures

Content containers: Chapters, StoryBeats, Characters, StoryVars, ChapterVars, CharacterVars

Maybe some small internal enums like PlaybackState

Some small story helper enums like Yes-No-Maybe (IndecisiveBool?), True-False-Unknown (QuantumBool?) etc (helpful for the editor, as a type of story variable). Possibly common dice combinations too. Consider using https://crates.io/crates/gametools for dice and cards.

Most of the story content are Bevy Components and Relationships that aren’t made of any single, heavy enum/struct, but belong to three somewhat overlapping categories:

  • StoryElements (for content),
  • Conditions (for changing the flow, mostly for not showing or skipping Elements), and
  • Consequences (mostly overlapping with StoryElements, but more for changing variables etc).

Chapter, StoryBeat

Chapters are just a logical grouping, and don’t have any functionality of their own.

StoryBeat represents a chunk of the story, a logical group that doesn’t hold story content directly. It can comprise one dialog box, a thought bubble, a long conversation, or the whole game.

TODO: Might contain a link to some “macro” for a scene setup, which will likely just be a collection of Elements (background change, music change, sound cue, character sprites animating in)

StoryBeat contains a link to the first Element to be evaluated. When a Beat is played, that Element is marked with IsStarting marker Component, and its Conditions (if any) are evaluated.

If there are no conditions, or display conditions pass, emit a message with the Element and mark it is in progress.

StoryBeat doesn’t control the narrative pace, it’s up to the game-story engine integration to mark story element as completed and move on to the next one.

Nesting

StoryBeats can be nested, with a stack-like behavior or pushing and popping all visual elements to be resumed later, after the “interjected” StoryBeat finishes.

Following nodes are Components on StoryBeats and StoryElements. There is no implicit array of elements that follow each other with a custom breaking conditions or GoTo. Instead, most Elements contain StoryElementId that follows. If it’s missing, and there’s nothing in the queue, StoryBeat ends and gets cleaned up.

The following Components affect the flow:

  • NextElementOnceOnly (top priority)
  • NextElement (high priority, erases flavor queue)
  • FlavorElement (used in player choices, parent node contains NextElement, and each of player choices contains a FlavorElement and other optional consequences; after the queue is exhausted, NextElement is called, which can be overridden)
  • PostFlavorElement (same as above, just put at the end of queue)
  • NextElementRandom (different from others, contains array of ElementIds that will be picked randomly, if they pass their Conditions)
  • NextStoryBeat (deletes any Element Queues, cleans up the scene, starts the new beat)
  • InterjectStoryBeat (pushes all displayed data onto a stack, and starts the new story beat right away)

StoryElement summary

Something like “dialogue box” or “sound cue” or “one player dialog option”.

These are separate Components instead of a single Component/Enum, for a few reasons:

(1) cleaner code, since different types will have very different ways of handling them, and (2) much easier extensibility, since adding very different types of content just means defining a new component and system to handle that component when it’s being played (3) for many use cases, easier to parallelize a playing few elements at once

Parallel animations etc are not currently in “Alpha 1” scope, there will be Sequencer for that later.

Tracking state

Elements themselves will have tags (marker Components):

  • Seen - marked after finishing animation/etc. Do not mark unclicked menu options just because you’ve displayed them.
  • SeenInPreviousGame - ??? todo, something like after completing a game or a chapter, Seen components will be copied to this one, for fast-replay of branching narratives
  • IsPlaying - sound, music, video, animation currently playing
  • IsFinishedPlaying - animation and visual elements no longer animating, but still on screen
  • IsLooping - TBD, perhaps mark things as looping after IsPlaying finishes once? Not sure if I want to turn it into an enum. Perhaps more likely just keep AnimationState that will have a is_looping bool property, but limit the components to just IsPlaying and IsFinishedPlaying.

Possibly these, but undercooked:

  • disable player control while running
  • disable skip
  • hide UI
  • encourage autosave

Misc quick haxx

  • AnswerRoundRobin - a list of IDs that will rotate, and perhaps ?? update the NextElementId component on each click. Used for getting 2+ answers to the same question, unlike the ShowOnlyOnce component.
  • QuestionAndAnswerRoundRobin??? - TBD - loop both questions and answers in sync. Used for monkey island-esque “do you sell X?” gags, or for encouraging a character to tell more bit by bit.
    • could also just be a “macro” in the editor, and be just a regular chain in the data or idk

Example StoryElements

  • /// when several things need to happen at the same time; continue after all finish StoryMolecule(Vec),
  • /// VN style sentence, potentially with a character name and portrait (or bustup later) Sentence(Sentence),
  • /// Window with a message and an OK button Modal(String),
  • /// Toast that will appear for a while (and stack) in the corner and then disappear (animate in/out preferably) TimedToast(String),
  • /// Toast that will appear in the corner (and stack) and stay there until dismissed WaitingToast(String),
  • // also add PriorityModal? Interrupting? Queued? Ignorable? Summarizable?
  • /// will automatically display “…” and wait for a click, VN style Pause,
  • CharacterAnim,
  • BgChange,
  • MusicChange,
  • De/ActivateBeatAudioFilter - turns on audio filter on main channel that can be deactivated later, or deactivates at a change of a story beat (incl. interjection)
  • De/ActivateBeatVideoFilter - adds a filter/shader (e.g. retro, B&W, for flashbacks)
  • De/ActivateBeatVideoFrame - adds frame (for flashbacks)
  • De/ActivateStickyAudioFilter - turns on a sticky global audio filter that does not go away even after ending story beats. Stays in save. Deactivates in main menu.
  • De/ActivateStickyVideoFilter
  • De/ActivateStickyVideoFrame
  • SoundCue,
  • ActionReward,
  • Choice,
  • /// stop the parent Beat now and skip to a different beat GoTo(StoryBeatId),
  • /// insert the following story beat here, and then result the current beat Interject(StoryBeatId),
  • /// finish the current beat and go to the following beat QueueAtEnd(StoryBeatId),

Decisions, PlayerChoice, ChoiceOption

ChoiceNode is a (Story Element) Component that’s the root, optionally with text like “where do you want to go” Choice is a singular option - a Component of a separate entity, which also has PlayerChoices/ChoiceInsideGroup are the relationship types between the two.

Consequences

Consequences are technically StoryElements, but are more focused on “invisible” things - changing variables, affecting game state, changing the world, history, relationships. ConsequenceOf is the relationship type to ChoiceOption. Examples:

  • RunSystemCached (enum of manually mapped enum options to unparametrized bevy systems)
  • SetVar
  • SetChapterVar
  • SetCharacterVar
  • Adjust…Var
  • Delete…Var

(low prio idea) More complex animations combos

Sequencer will be available for chaining a bunch of Elements together, with either sequential timing (x after x), or discreet values (animate x in, wait 200ms, after that do X and Y in parallel).

If it’s easy to use, sequencer might be also used to set up scenes (Element bundles of sorts).

TODO: figure out if this can be also used for staggered typing, and/or for captions during cutscenes

Condition Effects

By default, an element is shown. Element can have conditions applied to it, which are child entities of one of the following relationship sub-types:

  • IsShownIf - if conditions pass, show elements. No children = show. This is the default condition type.
  • IsGreyedOutIf - applicable to player choices and buttons - if conditions pass, grey out
  • IsHiddenIf - opposite of IsShownIf. Has priority over IsShownIf.

These can apply to a StoryElement directly, or to Choices/Buttons.

There’s also a special Component (not a relationship)

  • ShowOnlyOnce - after viewing the story element, replace this component with Hidden

Other marker Components:

  • Hidden - automatically skipped, takes precedence over IsShown and IsHidden
  • Seen - marked as already visited (maybe with visit timestamp, story heartbeat id, idk)
  • WasSeenInPreviousGame ??

Player choices

One type of a StoryElement is PlayerChoice. They all are checked for conditions, and if all of them are not shown or greyed out, skip to the next StoryElement.

Condition

Like Elements, not actually a single struct/enum, but a collection of different Components, e.g. ChapterVar(untyped string name, operator enum(compare value or values))

I will try to use Traits not to create overly verbose API for each separate type (ChapterVarBool, ChapterVarInteger, ChapterVarFloat etc).

Conditions are evaluated with logical AND within their group (e.g. targets of IsShown). LogicalOrCondition(Vec<impl Condition>) is a helper Condition to allow OR.

(low prio idea) StoryEnums

(Much) Later, in the editor, users will be able to define Enums which will become Rust Enums with all their properties, including Default, and able to contain other data (probably with limitations, e.g. must implement PartialEq, Copy and/or Clone).

These will make it easier / more reliable to check custom story conditions instead of confusion prone magical numbers “player_mood == 6” or typo prone magical strings like “player_mood == ‘hangry’”.

(low prio idea) ElementModifier

just a dumb idea - maybe create a list of common vfx to apply, e.g. reverb, blur, b&w

this would have 0 or more Conditions of its own

????????? SceneOverrides?

idk like. thinking like P5X has overrides for UI style when P3 characters are on screen maybe SceneSetup or whatever could hold info about current StoryBeat maybe StoryBeat additional components. think about it

Or maybe this could be like. A Bundle/bsn preset that uses some default, but a different constructor function will produce different UI style?

Currently just spitballing ideas

This is more of a to-do list of things to figure out, and stuff to keep in mind while developing a prototype and setting the design

User side

let them eat cake

Developer side

Stuff that developer can bake into the final game hopefully with config changes, and not rewrites.

Controls

Do some research, play a few VNs, offer default-ish Ren’Py-ish experience/controls.

Offer also more modern defaults. Should be one config line.

Everything needs to be mappable either way, even with default egui/bevyui runner.

Fast forwarding

  • speed between slides
  • ignoring pause elements y/n
  • skip music y/n
  • limit of sounds played at once
  • audio fade speed