Technical Deep Dives
Ludonarrative Resonance
When approaching this project, it was important to me to use the medium of video game storytelling to its full potential -- otherwise, why not write a book?
Video games are still a nascent art form and are the medium that gives its audience the most agency.
This allows for a connection with the audience emotionally in a way no other form can.
One way it does this is through ludonarrative resonance.
Ludonarrative resonance is when the gameplay mechanics and themes of the story resonate with each other,
elevating the whole to a new level.
Within Sunset High, the main theme I explore is obsession. This begs the question: how do we achieve
ludonarrative resonance with the theme of obsession in mind? This was done through a couple of mechanics
and design decisions:
-
Limited Play Space
By setting the game almost entirely within a classroom, the player had to entirely focus on the
classroom to solve the mystery. We reward an attention to detail, which is not too far from obsession.
-
Time Loop
By making the game operate under a time loop, the player had to focus on what was in front of them
again and again. This could be considered a negative gameplay loop because players generally do not
like seeing their progress reset.
-
Expansive Decision Tree
By giving the player an expansive decision tree, where small changes lead to different causal events,
we allow the player to feel as if their decisions matter.
-
Rewind / Fast Forward
These mechanics allow for ease of traversal through the aforementioned decision tree. They act as
positive feedback mechanisms, empowering the player to quickly and easily hop from node to node.
Rewind allows exploration of branches very quickly, while Fast Forward allows players to identify
key nodes in the tree and move to them efficiently.
Together, these mechanics and design decisions combined make the player obsessive, which allows our
emotional storytelling climax to hit the audience in a way entirely unique to the medium of video games.
The Dialogue Pipeline
In a branching narrative game containing over 80,000 words and dozens of interwoven objectives,
managing dialogue flow, localization, and quest logic presented a serious technical challenge.
Solving this challenge was tantamount.
It starts with Twine, which is an open source branching narrative editor. From here, we have to get it
into the game. The base .twee
format did not give us the flexibility to fire in-game events,
so instead we expanded a JSON library to parse the Twine files as needed to create a JSON structure that
could be consumed by our custom dialogue engine in game.
The plugin can be seen here:
https://github.com/ErraticUnicorn/twine-to-json-to-godot
Within Godot, we create a conversation data model which is a tree with two types of nodes. A dialogue node,
and a reply node, where replies lead to other dialogue nodes. These trees allow localization out of the box
and the firing of various in game events.
Finally, these conversations live within Objectives, which give us the skeleton of the game -- our quest system.
Our quests rely on Directed Acyclic Graphs to hold and chain objectives together.
Directed Acyclic Graph (DAG) structure used for the quest system
To edit these quickly and efficiently, I designed a custom Godot plugin to visually edit these DAGs as can be
seen below, which shows a sidequest in our game.
Custom Godot plugin for visually editing quest DAGs
This system enabled rapid iteration on branching narratives, seamless localization, and dynamic in-game
event triggering -- all essential to a narrative-driven title like Sunset High.
Rewind System
Allowing the player to rewind anything, everywhere, all at once, as far back as they want, can easily get out of hand.
Maintaining performance and controlling memory usage were essential for making this mechanic viable.
To solve this, I designed a system where all game state is serialized into lightweight snapshots using primitive types,
stored in one of two ways:
-
Pollable Snapshots: Every engine tick, we poll the object's state. If it's changed, a new snapshot is created
and added to storage. If it's unchanged, we increment a frame_count within the latest snapshot. This is ideal for
frequently changing objects, like player position.
-
Event-Based Snapshots: Objects with infrequent state changes (like objectives) emit an event to create
a snapshot when their state updates. On every tick, their latest snapshot's frame_count is incremented instead of polling.
At rewind time, we maintain an array of snapshots per object, each snapshot containing the relevant serialized state
and how many frames it persisted for. Moving backward through time means decrementing frame_count values, and when a
count reaches zero, popping to the previous snapshot and applying its state.
This hybrid approach allowed us to balance flexibility with performance, delivering a seamless rewind mechanic while
maintaining a consistent 60 frames per second, even across large, dynamic scenes.