TIMESYNCERS DEV BLOG: HOW WE BUILT A TIME TRAVEL ENGINE
The first version of the TimeSyncers time travel engine stored every single object in the game world in a flat dictionary, keyed by a tuple of entity ID and timestamp. It worked. It was also the worst piece of code I have written in my professional life. A thirty minute playthrough would chew through about six hundred megabytes of RAM and cause the save file to balloon to around forty megs, which is a problem when your game is supposed to ship on handhelds. I killed it on a Tuesday in March and spent the next nine weeks rebuilding the whole thing from scratch.
This post is about what I learned rebuilding it. I want to talk about the actual data model I ended up with, why branching and rewinding are very different problems that people constantly confuse, the UI I built to make timeline changes visible to the player, the save system implications nobody warns you about, and the snapshot math that has been eating my lunch for a year. I also want to talk about what I cut from the design, because the cuts are almost more interesting than what survived.
If you want the philosophical side of TimeSyncers first, I wrote about designing consequences that matter a few months back and that piece is basically the why for what I am about to describe as the how.
The core problem
Time travel in games is not really one problem. It is a small cluster of problems that people lump together because they all involve the word "time," and the genre is haunted by the fact that nobody agrees on what time travel even means mechanically.
You have rewind, which is what Prince of Persia Sands of Time does. The player presses a button and the game plays backward. State that existed a few seconds ago becomes current state. Nothing permanent happens. The rewind is a do-over.
You have loops, which is what Outer Wilds and Majora's Mask do. Time resets to a known good state. The player keeps some things, usually knowledge or items, and loses others. The loop is structural and the world outside the player is deterministic within it.
Then you have actual time travel, which is what I am building. The player goes to a point in the past, does something, and the present changes based on what they did. This is the hardest of the three because it requires the game to track not just state but the causal history of state, and it requires that you have an answer to every branching paradox the player can invent, because players will absolutely try to break it.
TimeSyncers is the third kind. The player moves between eras, their actions in the past ripple into the present, and the present they return to is not necessarily the present they left. This is a nightmare to implement correctly and it is the reason I spent nine weeks rebuilding my engine from scratch.
The data model that actually works
After throwing out the flat dictionary, I landed on something I am calling a timeline tree. It is not my invention. It is basically the same structure that persistent data structures use in functional programming, which I stole wholesale from Clojure's implementation and bent to fit a game engine.
The idea is that the world is not one state. It is a tree of states. Every point where the player makes a change that could diverge a timeline becomes a node. Nodes have parents. A node does not store the whole world. It stores only the diff from its parent. If a chair was red in the parent and blue in the child, the child node contains exactly that fact and nothing else.
This means looking up the current color of the chair is a walk up the tree. Start at the current node. Does it have an opinion about the chair? If yes, use that. If no, check the parent. Repeat until you find an ancestor that has something to say about chairs, or you hit the root, which is the initial world state and always has an opinion about everything.
This sounds slow and it would be, except I cache aggressively. Every node that gets visited more than once in a short window gets its effective state flattened and memoized. The cache gets invalidated when the player makes a change that would affect that subtree. In practice the lookups are close to constant time and the memory savings are enormous because most objects in the world never change across a branch, so most nodes have tiny diffs.
The thing I want to be honest about is that this model took me three tries to land on. Version one was the flat dictionary, which I already described. Version two was a full world snapshot per branch, which solved the lookup problem but blew up the memory in a different way. The timeline tree was version three, and it only clicked after I stopped thinking about the world as a thing you save and started thinking about it as a thing you derive from a history of changes.
Branching versus rewind
These are two different systems in TimeSyncers and I want to draw the distinction sharp because every time I have explained the engine to another developer, this is the part that confuses people.
Rewind is cheap. Rewind is a ring buffer. You record the last thirty seconds of game state at ten hertz and when the player rewinds, you play the buffer back. When they release, you continue from wherever they stopped. Nothing persistent happens. The ring buffer overwrites itself. This is a solved problem and it has been since Braid in 2008.
Branching is not cheap. Branching is when the player does something that creates a new timeline node, which means the game now has to maintain the possibility of a different present. If the player goes back to the 1940s and prevents the lighthouse from being built, the present version of the island has no lighthouse, and every object, script, and NPC that referenced the lighthouse now has to react to that absence.
In TimeSyncers these are two different APIs. There is a rewind system that is purely local and visual. It is what you use when you want to redo a combat encounter or a jump. And there is a branching system that is what happens when you actually travel to another era and change something. The two never touch each other, and that separation was the single biggest architectural decision I made. Conflating them was a temptation because they superficially look similar, and I wasted about a month in the original prototype trying to build one system that did both. Do not do this. They are not the same problem.
Showing the player what changed
This was the hardest UX problem on the project and I am still not sure I have solved it.
The core issue is that in a game where changing the past changes the present, the player needs to understand what changed when they return. If they do not, the whole mechanic feels like it does not exist. They changed something in the 1940s, they come back to the present, and unless the game points at the differences, they will just walk past them assuming everything is the same as they left it.
My first attempt was a notification system. Every time a world state diff got applied to the present, a toast notification would pop up in the corner. "The lighthouse was never built." It worked in the sense that the information was communicated. It failed in every other sense. It felt like a game telling you what to feel instead of letting you notice. Players told me it felt like reading a spoiler of their own actions.
What I ended up with is something I am calling the memory ripple. When the player returns to an era that has been affected by their actions, objects that are different from how they were the last time the player saw them get a faint visual tell for the first few seconds. A subtle shimmer, mostly. Nothing that screams. And NPCs whose lives or dialogue have changed get a small icon above them the first time the player talks to them. The game trusts the player to notice, and rewards noticing. The first time I watched a playtester walk into a town they had transformed and actually gasp because a character they expected to see was gone, I knew the system was doing something right.
The technical implementation is a comparison pass against a last-seen snapshot per player entity. When the player enters a region, the game grabs the current effective state of every object in that region and compares it against what the player last saw. Differences get flagged for the ripple effect. This is fast because of the tree structure. I am only comparing the diffs, not the whole world.
Save system implications
I did not think about saves until about three months into the engine rewrite and that was a huge mistake. Save systems are usually an afterthought. In a time travel game they cannot be.
The problem is that a save is a serialization of the world state. If your world state is a tree with hundreds of nodes, you are serializing the whole tree. You cannot just snapshot the current node because then loading the save would lose every branch the player has created, and some of those branches might be the alternate presents they can still travel between. The save has to preserve the whole structure.
But you also cannot save the raw tree because it is full of engine pointers and runtime state. So you have to build a serialization layer that walks the tree, strips out anything transient, and writes a canonical form that can be reloaded into an engine that might have a slightly different version of the code next patch. This is version tolerance and it is miserable.
I ended up writing a migration system alongside the save system. Every save file has a version header. When the game loads a save from an older version, it runs it through a chain of migration functions that bring it forward one version at a time. This is the same pattern database schemas use and for the same reason. The save files are the canonical record of the player's game and I cannot break them when I patch the engine. So far I have written thirty four migration functions across the development cycle, and I expect to write another ten before ship.
One thing I got right by accident is that the save files are plain JSON under a compression layer. I can open them in a text editor and debug them. This has saved me probably two hundred hours of debugger time. If you are building a save system, make it human readable during development and compress it only at the outer layer. You will thank yourself.
The snapshot math
The question that has eaten the most of my life is how many timeline nodes to keep alive at any given time.
The naive answer is all of them. The player made them, they mean something, throw nothing away. This is what the first prototype did and it is why the save file was forty megs.
The slightly smarter answer is to only keep nodes that the player can still return to. If a branch has been visited and committed, and the player cannot travel back to alter that choice, the branch can be flattened into its parent. This reduces the tree pressure dramatically. In TimeSyncers about seventy percent of nodes end up flattening over the course of a playthrough because most changes are not reversible.
The actually smart answer, which I am still iterating on, is that some nodes are structurally important and some are not. A node where the player made a major choice that has downstream consequences is structurally important. A node that represents the player moving three steps to the left is not. The engine has a heuristic for importance based on how many downstream references a node has, and low importance nodes get folded into their parents aggressively.
I have a budget of about two hundred active nodes at any given time. Beyond that the game starts folding from the oldest, least-referenced leaves first. So far no playtester has noticed, because the folded nodes are by definition the ones that do not matter.
What got cut
I had to kill three things during this rewrite that I am still sad about.
The first was paradox handling. Originally I wanted the game to detect logical paradoxes the player could create and have some sort of reality collapse mechanic. The player kills their own grandfather, the universe resets, something cinematic happens. I spent about three weeks trying to define "paradox" formally and I could not do it. Every heuristic I came up with had false positives or false negatives, and often both. In the end I decided the game would just allow paradoxes and let them exist as uncomfortable truths in the timeline, which is actually more interesting than the collapse idea would have been, but it is not what I set out to build.
The second was multiplayer time travel. I had a design doc for a coop mode where two players could be in different eras affecting each other. The engine can technically support it. The UX cannot. I could not figure out how to communicate to player A that player B had just changed something that invalidated player A's current view of the world without it feeling like a bug. I shelved the feature. Maybe in a sequel. Maybe never.
The third was full timeline visualization. Originally there was going to be a screen where the player could see the entire tree of their timeline and jump to any node. It worked and it looked cool, but in testing it made the game feel like a flowchart puzzle instead of a story. I cut it. The game now only lets you travel to specific pinned moments, which is more restrictive and much better for pacing.
What I would tell past me
If I could go back to the version of me sitting in front of the flat dictionary in March, I would say three things.
Separate rewind from branching before you write a single line of code. They are not the same system. Treating them as the same system will cost you a month.
Write the save system first. Not the save UI, the save format. Decide how a world gets serialized before you decide how the world gets stored in memory, because the serialization constraints will force your hand on the memory model in ways you cannot predict.
And do not build paradox handling. It is a fun idea that eats three weeks and produces nothing shippable. Let the timeline contain the paradoxes. Players find them delightful. I certainly did, the first time a tester triggered one accidentally and told me the game was haunted.
The engine is in a good place now. It runs on the Steam Deck. Saves are under two megs. The tree stays under its node budget. Most importantly, the mechanic actually feels like time travel instead of feeling like a save scrubber with extra steps, which is what the first prototype felt like. If you are building anything in this space and want to compare notes, I am always up for it. The game is not out yet but I am going to keep writing about the guts of it as I go.
LIKED THIS? STAY IN THE LOOP
New posts, game updates, and things you won't find anywhere else.