bubbles
Branching dialogue for Rust games. Write scripts, compile once, drive from any game loop.
No async. No global state. No engine lock-in. Pull events when your game is ready, pick an option, continue. That's the whole API.
Works with Bevy, Godot-rust, Macroquad, or any custom engine. Unity and other native hosts can use bubbles-ffi - a C ABI wrapper with JSON events for P/Invoke.
What a script looks like
title: Start
tags: scene docks outdoor
---
<<declare $gold = 25>>
=> Dockworker: Oi, watch yer step!
=> Dockworker: These crates won't unload themselves, ye know.
=> Dockworker: Smells like low tide and regret out here.
Stumpy: Name's McGee. Stumpy McGee, Harbormaster.
Stumpy: Ye can't sail from Barnacle Bay without a travel permit.
<<jump Options>>
===
The harbour demo continues in examples/harbour/ with an Options node (guarded choices, <<detour MapSeller>>, <<jump Depart>>). Run it with:
Quick start
[]
= "1.0.1"
On crates.io the package is bubbles-dialogue (the name bubbles was already taken). The Rust library is still bubbles, so you write use bubbles::{…} in code.
use ;
Features
| Feature | Description |
|---|---|
| Lines and options | Plain text, Speaker: attribution, -> branches with optional <<if>> guards |
| Variables | Number, Text, Bool; <<declare>>, <<set>>, full expression language |
| Inline interpolation | {$var}, {$gold / 2}, {plural($n, "gem", "gems")} - evaluated before the event arrives |
| Inline markup | [b]text[/b], [color value=red]...[/color], [pause /] - stripped from text, returned as byte-precise MarkupSpans |
| Jumps and detours | <<jump Node>> replaces the call stack; <<detour Node>> / <<return>> push/pop it |
| Conditionals | <<if>> / <<elseif>> / <<else>> / <<endif>> |
| Once blocks | <<once>> content that fires exactly once, with optional <<else>> for every subsequent visit |
| Line groups | => alternatives chosen by the active saliency strategy - no repeated barks |
| Node groups | Multiple nodes sharing a title with when: conditions - great for time-of-day, relationship state |
| Saliency | FirstAvailable, RandomAvailable, BestLeastRecentlyViewed, or your own |
| Host commands | <<play_sound bell>> surfaces as DialogueEvent::Command |
| Built-in functions | visited, visited_count, random, random_range, dice, round, floor, ceil, min, max, abs, clamp, string, int, plural, select |
| Custom functions | Register closures callable from any expression |
| Localisation | LineProvider for #line:id lookup; translated templates can contain {expr} and [markup] |
| Multi-file | compile_many(&[(name, src)]) with duplicate-title detection and cross-file jump validation |
| Bookmarks / save | Runner::snapshot / Runner::restore with RunnerSnapshot (always available); enable serde to persist snapshot and HashMapStorage to disk |
| Line modes | #narration and #debug on lines set DialogueEvent::Line::line_mode for filtering or routing |
| Variable inspection | Runner::all_variables, Runner::variable, Runner::variable_ref; VariableStorage::all_variables (override on custom stores) |
| Option groups | #group:<name> on options for UI constraints (radio buttons, mutually exclusive choices) |
Feature flags
| Flag | Default | |
|---|---|---|
rand |
on | random(), random_range(), dice(), RandomAvailable |
serde |
off | Serialize / Deserialize on Value, HashMapStorage, RunnerSnapshot |
full |
off | Both of the above |
Prior art
Bubbles sits in the same space as Yarn Spinner and Ink. The difference is integration model: Bubbles compiles to a plain Rust struct and runs wherever your binary runs - no binary format, no external runtime, no Unity package. If you want something that drops into a Bevy, Godot-rust, or Macroquad project in an afternoon, that's what this is for.
Learn more
The full guide - language reference, integration walkthrough, localisation, save/load, WebAssembly, and annotated examples - lives at https://viezevingertjes.github.io/bubbles/.
Projects using Bubbles
- SiliSim — browser-first logic-gate course and silicon simulator. Lesson dialogue (speaker lines, hints, branching tutorial copy) is driven by
.bubscripts loaded at runtime in WebAssembly, in the same event loop as the circuit builder.
Contributing
All changes to main go through a pull request (no direct pushes). These status checks must pass before merge:
| Check | Source |
|---|---|
ci |
GitHub Actions (format, clippy, tests, docs, …) |
msrv |
GitHub Actions (MSRV compile) |
The branch must be up to date with main before merge. Conversation threads on the PR must be resolved. Force-push and branch deletion on main are blocked.
See CONTRIBUTING.md for development setup and code quality gates.
License
Licensed under either of Apache-2.0 or MIT at your option.