On this page
Integration and Debugging
This page is the “make it actually work in your game” page.
We are going to cover:
- How to drop Whisper into a project cleanly
- Where to put your storylet setup code
- How to debug “why is Whisper returning nothing”
- A practical pattern for verbs when you have a typewriter / reveal effect
Installing Whisper
Whisper comes alongside a free copy of Echo as a local package. The installation steps are:
- Open your GM project (make sure you are on a modern GameMaker version with struct constructors + methods).
- Click Tools > Import Local Package.
- Find the Whisper local package wherever you downloaded it to.
- Select it and wait for GM to open the package.
- Click Add All.
- Click Import.
Whisper sets up its global singletons automatically when the script is loaded, so you do not need to create an object just to “start Whisper”.
That said, you do need a place to register your content (storylets, verbs, insertions). That is the next section.
Where your Whisper setup should live
If you write your storylets inside random object Create events, you will eventually lose track of what exists and why.
A cleaner pattern is:
- One setup script (or one controller object) that runs once.
- All Whisper content registration happens there.
Example (create a function like this with all your Whisper content inside it and then run it once in the Game Start Event or something like that):
/// scr_game_bootstrap.gml (or obj_game_controller Create)
function GameBootstrapWhisper() {
// Register verbs
WhisperVerbAdd("give_gold", function(_ctx, _ev) {
// Your game logic here
_ctx.subject.gold += real(_ev.args[0]);
});
// Register insertions (random text variants)
WhisperInsertAdd("creature", ["rat", "spider", "slime"]);
// Register storylets
WhisperStorylet("sewer_creature")
.TagAdd("sewers")
.SetWeight(10)
.SetCooldown(30)
.SetPredicate(function(_ctx) {
return _ctx.location == "sewers";
})
.TextAdd("A ##creature## scurries past... " + WhisperVerbMarker("give_gold"))
.AddToPool("sewer_events");
}
A couple of notes:
-
WhisperStorylet(id)returns the existing storylet if that id already exists.- That prevents duplicate storylets, which is nice.
- But if you run your bootstrap twice, you can still accidentally add extra text variations/tags twice.
-
If you are doing live-reload style dev workflows,
WhisperResetAll()exists to wipe everything and recreate the singletons. That is more of a dev tool than a normal gameplay tool.
Basic gameplay wiring (pick -> resolve -> display)
Most games follow this loop:
- Build a context (what is happening right now).
- Ask Whisper to pick something.
- Resolve the text.
- Show it in your UI.
- Fire any verbs tied to the text (optional, depending on your UI style).
Minimal example using the pool API:
/// Wherever you want to trigger a story moment
var _ctx = WhisperContextSimple(player, "sewers");
var _storylet = WhisperPoolPickAndFire("sewer_events", _ctx);
if (!is_undefined(_storylet)) {
var _resolved = _storylet.TextResolve(); // returns { text, events }
ShowDialogue(_resolved.text);
// If you are not doing a typewriter effect, you will typically fire
// verbs at "completion" (see typewriter section below for the fancy version).
WhisperVerbRunRange(_resolved.events, -1, string_length(_resolved.text), _ctx);
}
A beginner-friendly way to think about this:
- The context is “what Whisper is allowed to know about the current situation”.
- The storylet is “a little packet of narrative + rules for when it can happen”.
- The resolved text is “the final output after insertions are filled in”.
Runs, run ids, and why they matter
Whisper supports “per run” limits. This is for roguelikes / runs / loops where you might want:
- A storylet that can only happen once per run
- But can happen again on a future run
Whisper tracks a run_id internally.
Useful calls:
WhisperNewRun()-> increments the run id (new run)WhisperGetRunId()-> read current run id (useful for saving)WhisperSetRunId(id)-> restore a run id (useful for loading)
Practical pattern:
- When starting a brand new run: call
WhisperNewRun() - When loading a save: call
WhisperSetRunId(saved_run_id)
This keeps “per run” usage limits behaving the way you expect.
Debugging: “why did nothing match”
This is the most common Whisper bug report:
“WhisperPick returns undefined. But I swear I added storylets.”
Whisper gives you debug helpers that answer that question directly.
1) Turn on debug logging
At the top of the script there is a macro:
#macro WHISPER_DEBUG 0
Set it to 1 while you are developing.
When debug is enabled, Whisper logs extra information via Echo-style functions (for example EchoDebugInfo, EchoDebugWarn).
If you are not using Echo yet, you have two options:
- Add Echo to your project (recommended long-term if you like having nice logs).
- Or stub the functions temporarily:
function EchoDebugInfo(_msg, _tags) { show_debug_message(_msg); }
function EchoDebugWarn(_msg, _tags) { show_debug_message(_msg); }
2) Use WhisperDebugQuery (global) or WhisperPoolDebugQuery (pool)
These return an array of structs describing what happened for each storylet.
Each entry contains:
idok(overall match)can_fire(passes predicate/cooldown/uses rules)- detailed flags like
predicate_ok,cooldown_ok,uses_total_ok,uses_run_ok - tag filter flags like
tags_all_ok,tags_any_ok,tags_not_ok reasons(an array of short strings like"cooldown_active")
Example:
var _ctx = WhisperContextSimple(player, "sewers");
var _report = WhisperPoolDebugQuery("sewer_events", _ctx);
for (var i = 0; i < array_length(_report); i++) {
var _r = _report[i];
if (!_r.ok) {
show_debug_message("Whisper blocked: " + string(_r.id) + " -> " + string(_r.reasons));
}
}
Now you are not guessing. You will see the exact reason list per storylet.
3) Use WhisperDebugMatch for one specific storylet
If you already know which storylet you care about:
var _ctx = WhisperContextSimple(player, "sewers");
var _info = WhisperDebugMatch("sewer_creature", _ctx);
show_debug_message(string(_info.ok));
show_debug_message(string(_info.reasons));
Advanced: verbs with a typewriter / reveal effect
Whisper verbs are stored as “events” inside the resolved result:
var _resolved = _storylet.TextResolve();
// _resolved.text -> final text
// _resolved.events -> event list, with positions and triggers
If you have a typewriter effect, you can fire verbs as the cursor crosses specific positions:
- Track the previous character index (
_from_pos) - Track the new character index (
_to_pos) - Call
WhisperVerbRunRange(events, _from_pos, _to_pos, ctx)
Example sketch:
// When you start showing the line:
_from_pos = -1;
_to_pos = 0;
WhisperVerbRunRange(_resolved.events, _from_pos, _to_pos, _ctx);
// Each time you reveal more characters:
var _next_pos = min(_to_pos + reveal_step, string_length(_resolved.text));
WhisperVerbRunRange(_resolved.events, _to_pos, _next_pos, _ctx);
_to_pos = _next_pos;
// When the line finishes:
WhisperVerbRunRange(_resolved.events, _to_pos, string_length(_resolved.text), _ctx);
Important note (because it will bite you otherwise):
WhisperVerbRunRangedoes not remember what already fired.- If you call it twice with overlapping ranges, you can double-fire verbs.
- Your UI code should make sure
_from_pos->_to_posis always moving forward cleanly.
Quick checklist when Whisper “does nothing”
If WhisperPick* returns undefined, usually one of these is true:
- Your storylet
weightis0or negative (only positive weights are eligible). - Your predicate returned false.
- Cooldown is active.
- Max uses (total or per run) has been reached.
- Your tag filter excludes everything (for example
tags_allrequires tags the storylets do not have). - You are picking from a pool that is empty (you forgot
.AddToPool(...)).
If you are not sure which one: run WhisperDebugQuery / WhisperPoolDebugQuery and look at reasons.