A Fact-Based Story Design

TL;DR — Use a single source of truth for tracking the player’s journey across the game and make it easy to query!

Ian Thomas
4 min readOct 27, 2024

In a linear game experience it’s very easy to figure out when and where to update information about the world. As you start level 3, you have definitely encountered goblins in the cutscene at the end of level 2, so you can put goblins into the game’s codex by unlocking the text about them. Maybe you’ll use a function like ShowCodexItem(Goblins), or however you think that should work.

During level 3, the player meets orcs. ShowCodexItem(Orcs). Job done!

Ah, wait — the orc encounter is optional. But that’s fine, you only call ShowCodexItem(Orcs) once you’ve actually seen the orcs.

Then, in level 4, the player meets SuperOrcs, and ideally, that codex should say something like, “Big goblins, bigger than the regular orcs.” But wait — what if the player skipped the original orc encounter? Should we unlock a different codex entry, or vary the text, or otherwise what do we do here? We’ll have to think about that.

We also want the game’s dialogue to be different when the player meets the super-orcs, depending on whether they met the original orcs. Okay, let’s add a bool to the mix for the dialogue system — setting DialogueSystem_WeMetOrcs to true when we meet the orcs, right next to ShowCodexItem(Orcs). In the SuperOrcs encounter we can test that value to decide which dialogue to play. “What are these beasts, I’ve never seen anything like them before?” vs. “These look like orcs, but more super!” (Game writing is serious stuff.)

There’s also some gameplay code that someone has added for the objectives system, which updates the player’s objective. ObjectiveDone_MeetOrcs() needs to be called. We can do it at the end of the first orc encounter, shortly after ShowCodexItem(Orcs) and DialogueSystem_WeMetOrcs=true, and…

Suddenly we’ve got information on the orc encounter scattered across three systems, tracked in slightly different ways, all with dependencies and overlaps. It’s fragile, bug-prone, hard to save, load, or test thoroughly.

With a mostly linear story this might be manageable. But let’s say we’ve got an open world. Or perhaps a detective story or puzzle game where clues can be found in any order, affecting conversations and codex entries based on what the player has discovered. We want dialogue that changes based on story progress, codex entries that unlock dynamically, achievements that track multiple encounters, and maybe even hint systems that adapt to the player’s experience.

Enter the fact-based system. Rather than scattering this info across various isolated state trackers, you keep one centralised source of truth. The idea is simple: store all meaningful events or “facts” in a single, queryable system, and everything else reacts to that.

It’s a very simple idea and has been around since the early days of CRPGs, but it’s amazing how often in the rush of development it gets overlooked as different departments all implement quick fixes in isolation.

In its simplest form our fact-based system could just be a set of flags, and might look like this:

Add_Fact(MetGoblins)
Add_Fact(MetOrcs)
Add_Fact(MetSuperOrcs)

And we could test fact X using Has_Fact(x).

With a quick change to our existing setup, our codex system could unlock the Goblins entry if Has_Fact(MetGoblins) is true. The SuperOrcs codex would appear if Has_Fact(MetSuperOrcs) is true, but might show slightly different text depending on whether Has_Fact(MetOrcs) is true.

We could make our dialogue system branch depending on whether Has_Fact(MetOrcs) is true when we first meeting the SuperOrcs. Our objective system could complete the MeetOrcs objective if the fact Has_Fact(MetOrcs) is true. Our achievement tracker could check for Has_Fact(MetGoblins) && Has_Fact(MetOrcs) && Has_Fact(MetSuperOrcs) to determine if all monster types were encountered before giving a trophy.

With everything in one place, loading, saving, and testing game states becomes much easier, and getting a dump of “what state is our game story in” is easier.

This idea could work beyond simple flags, too — think of it as centralised state management system that all of your other systems can talk to. You can provide an easy-to-use interface for your developers that abstracts away more complicated queries. So, for example, you could provide adaptive calls like Has_Fact(HealthIsLow) or Has_Fact(IsInSneakMode).

None of this is a new concept, but it’s easy to go for a more tangled solution during implementation when team members are rushing to solve issues in isolation. Starting with this shape of design in mind could make development much simpler and save plenty of headaches.

--

--

Ian Thomas
Ian Thomas

Written by Ian Thomas

Ian is narrative director, coder, and writer of video games, films, larp, books, live events, and VR/AR experiences. Find him on Bluesky or LinkedIn.

No responses yet