The Design is the Implementation
Reset Hard is in active development; more details will be incoming throughout September. Interested in learning more about Reset Hard? Want to follow up about something in this article? Want to get notified when demos are available? Visit the site, or come chat with me directly on Discord!
Tactical Time Travel
Reset Hard is a competitive time-traveling arena shooter about laying traps, outsmarting your friends, and learning how to do impossible things.
The gameplay is based around that amazing feeling you get when a new idea or concept clicks and you start thinking about all of the crazy things you can do with it. Time travel in Reset Hard is like a toy, and I'm constantly encouraging the player to break down the game's rules and reapply them in creative ways.
In competitive multiplayer, being able to creatively combine mechanics or experiment with tools in novel ways lends everything an amazing sense of tension, because your opponents have access to the same tools you do, and they can put together dynamic strategies and traps that you might not be able to anticipate.
But in order to encourage players to be creative, I need to have a system that supports their creativity. In order to make players feel like anything is possible, I need a time-traveling framework where anything is genuinely possible. I can't have sections of the game where players are supposed to deconstruct and think deeply about mechanics alongside sections where I tell them not to look too closely at the details of how something works.
The game needs to be totally consistent.
That consistency dictates that there must never be a moment in Reset Hard where you see something unexpected or unintuitive and I tell you not to think about it. Quite literally everything you see in the game is a mechanic.
How do that?
By minimizing the number of abstractions between gameplay and implementation code, I've been able to build a set of gameplay mechanics that I know are extremely robust. There is no distinction for me between time travel in gameplay and time travel in code. What this means is that I don't design tricks or implement shortcuts to make it look like something is happening; instead I just make it happen.
This has a profound effect on the way that I talk about mechanics. When I explain how time travel works I have an extra sense of authority. I never worry that I've forgotten about some kind of edge case or paradox in the game mechanics. When I talk about mechanics, I don't use phrases like 'in-universe' because as far as I'm concerned, Reset Hard doesn't have pretend mechanics or separate in-universe explanations for anything.
Unlike a game like Pokemon, or Animal Crossing, or Amnesia, where my goal would be to use hidden mechanics to create depth and to allow a player to share an illusion with me, Reset Hard is an internally consistent simulation. The game mechanics are facts.
This means that the puzzles and tutorials inside Reset Hard really boil down to me teaching you how my code works and us exploring the consequences of that code together. And as long as I stay within that realm, for the most part I don't really have to worry much about mechanical 'plot holes.'
Theory vs Practice
I've presented this so far as a very theoretical, esoteric idea, but it's really quite practical.
The explanation of time in Reset Hard is that individual 'frames' of time each contain perfect copies of the world. When a player moves through time what's really happening is that they are sequentially becoming aware of the whatever the next frame is. The world of Reset Hard is entirely deterministic. If a player moves backwards in time, all that they're really doing is shifting their focus to a previous frame.
Representing this in code is thus relatively straightforward. Time is a bounded array. Within each 'frame' of time I store an entire copy of a serialized game state. I don't anything fancy with computing deltas. I don't do anything complicated like rerunning the simulation from a keyframe.
var timeline = {
length : 30 * 60 * 10, //30 fps 10 minutes
frames : [{ /*json serialized initial frame */ }],
waves : [{
frame: 0,
}],
};
function update() {
_.each(waves, function (wave) {
var world = frames[wave.frame];
var next_world = world_update(world);
frames[wave.frame++] = next_world;
});
}
Levels in Reset Hard are at most maybe 10 minutes in length, and the memory requirements for 10 minutes of 30fps game logic is trivial. If memory does become a problem in the future, or if I just want to make the game more efficient on lower-end computers, I'll switch to a more efficient data compression format for world states. I won't change the implementation to do something clever like add partial world states or record keyboard input.
You'll notice above that I use 'waves' to determine which frames to update and overwrite. These also appear as game mechanics - there are a set of game rules determining when a wave gets created, what it's velocity is, how waves react to colliding with each other, and so on. Again, this is just me exposing how the code works.
"But wait", I hear you asking. "If the world is deterministic, how does the player overwrite the past? Surely there's some kind of implementation trickery going on there."
Not so!
var timeline = {
length : 30 * 60 * 10,
frames : [{}],
waves : [{ frame: 0 }]
};
var meta = {
player : {
focus : 0
}
};
Player controllers exist outside of time. They use the same system to interact with the world that I use internally for puzzles. The player focus determines which frame will be shown to the player; and the player's input is projected onto the focused frame so they can direct their avatar's actions.
This even extends to the physical human sitting in the chair in front of the computer.
How are the players actions transmitted to the game? The player exists in a state that I call 'meta-meta-time.' Their actions are projected via a keyboard and an Operating System into meta-time (the game's global state). This is why no one in the game bats an eye when from their perspective someone new just happens to exist one day. The player exists in meta-meta-time, the player's avatar is projected into meta-time, and then their physical presence is projected from meta-time onto the regular timeline.
Of course at this point I'm starting to stray away from the practical stuff that I was talking about above. But I bring this up as an example of just how far I'm willing to go to avoid hand-waving or hiding narrative inconsistencies with reality. When the story of Reset Hard deviates from the player's physical reality, I alter the story. I don't pretend that reality changed.
Overcomplication
From a pure engineering perspective, a secondary benefit of all of this is that the implementation actually becomes a great deal simpler for me to reason about. I'm building Reset Hard as a one person dev team, so I have a lot of stuff on my plate: marketing, artwork, design, programming, community management, music composition and sound work, and so on.
When I'm coding the game, it is highly advantageous for me to be able to keep one canonical view of my game in my head at a time. If the mechanics of Reset Hard were based around clever illusions, I would need to remember roughly twice as much about how the game worked: both the mechanics as related to the player and the tricks and illusions I was using in code to support those mechanics.
When I talk to people about Reset Hard, often they comment that an internally consistent, robust system for multiplayer time travel must be extremely complicated. It's not. In the game's current state, there are maybe 300 lines of code devoted to making time travel work. I expect that by the end of development that might rise to maybe 600-800 lines of code.
Reset Hard does not have a complicated codebase.
And I can be confident about how that codebase will evolve because I already know how it's going to be structured. It's going to be structured to mirror the game mechanics; and I know roughly speaking how most of the core mechanics are going to work. Of course things will evolve; I'll make decisions based on what feels good or what adds additional layers of strategy. But I know that short of changing something totally fundamental to the game, I won't need to do many large-scale rewrites.
A word of caution
It's possible to take this philosophy too far. When I first started building an update system for entities within the world, I experimented with using events and special methods to pass around information.
//Loop through entities, attach events
entityA.on('x', function (properties) {
entityB.storeProperties(properties); //maybe I need these later
});
entityA.on('x-end', function (properties) {
entityB.removeProperties(properties); //But I shouldn't have them now
});
//I need to be very careful about update order
//A needs to be updated before B so the events will trigger.
//Also if A ever misses an event, the world enters a broken state...
entityA.update();
I did this because I was convinced it didn't make sense for every entity to be able to access the entire world state. After all, I planned to hide information from the player. Why should the internal mechanics be different?
There are a couple of reasons why this was a bad idea:
- I didn’t actually know what all of the central gameplay mechanics would be around hidden information. I was building an implementation before I built a design.
- I could start with a simpler implementation (letting every entity access the entire world) and later on do something like filter that world state if I really needed to.
- An implementation that matched gameplay at this point would be significantly more complicated than one that didn’t.
- And finally, when I designed levels and talked to players about them, I didn’t think in these terms. So I wasn’t getting any of the benefits I talked about above.
Instead, I now allow any entity to access any property or object in the entire world during its update loop. I still use events, but only when it makes sense to do so; only when using them allows me to express game logic in the same way that I would express a level's logic on a whiteboard.
If in the future I do come up with a solid, in-game set of mechanics for when certain entities are accessible, I very well may come back and redo the implementation. But I won't change the code until I know what the design will be.
Wrapping up
The approach I outline above is good for system-driven games; games where you want players to have a deep understanding of how mechanics are structured. It may not be a good approach for every game, and it may be difficult to do if you are accustomed to third-party frameworks or engines that force you to embrace a specific paradigm when coding.
Even in those scenarios though, you should take time to think about whether a development pattern matches the design you want to create. You should avoid thinking about your game's architecture as a completely separate concern from its design.
This article could be a great deal longer, but I'll leave you with a quote by Manasoba Tanaka, the lead animator of The Last Guardian. When discussing the animation process for Guardian's wind, the interviewer expressed surprise that Tanaka would come up with many of the algorithms himself. Wasn't that a programmer's job?
Tanaka responded: "When you ask a programmer to design the algorithm itself, there are repeated confirmations and adjustments until you reach a satisfactory end result. That takes time and can get you distracted from what you originally wanted to do, so sometimes I design the algorithm myself here when I can, and ask them to implement it using the calculations. We try to convey non ambiguous ideas and make sure that the final implementation is as close as possible to the design of algorithm."
Similarly, when designing a game, you should be conscious of the immense difficulty in getting disparate parts (music, art, programming) to align in a way where every part reinforces the whole. Where possible, I look for ways to decrease the amount of conflict or fighting between these systems, and to allow them to instead work together to reinforce each other.