Categories
Uncategorized

Save Anywhere

This post is inspired by the current discourse about Pacific Drive‘s save system. It’s a good game and I’m not intending to pick on them. The technical challenges they describe are real, and we’ve all faced them. This is my solution from my solo games.

What Does It Mean to Save Anywhere

“Save anywhere” means the ability to save a game’s progress and restore it to the exact state, or as near as possible. A typical example is a game where a player is allowed to quicksave and quickload at will, perhaps to retry a difficult sequence over and over without consequences. A perfect example is retro console emulator save states, which store the complete memory layout and whatever else they need to load later, with everything exactly as it was, down to the emulated CPU timing. In non-emulated environments, saving the complete state of system memory is not feasible; we, as developers, need to make decisions about what information is important enough to write to disk.

What Do You Save

Everything. Every entity. Every component. Every property. Save everything.

There are various ways to do this—some engines or frameworks or languages provide high-level reflection and serialization, enabling a game developer to simply save the scene/world and get everything within it “for free”. If you’re not working in a space that already does that heavy lifting, this means iterating every entity in your scene and writing all their important properties to disk. This is The Hard Part if you don’t do it early in development; the goal is simple enough, but determining what is an important property is not obvious, and trying to figure out the set of important properties when the game is full of hundreds of entities with tens of thousands of properties is a testing and development nightmare. This is the main reason why games that do not plan for save-anywhere from the start cannot feasibly add it later.

What Do You Not Save

Except!

There are some things that don’t need to be saved. Static entities that are part of an authored level do not need to be saved (unless some part of their state is mutable); when the saved game is loaded, they will be instantiated as a part of that level. Their properties are effectively saved once in the game’s content files, never to be changed.

And other properties may not need to be saved if they can be automatically determined from context. AI behavior states should fall out naturally from all the other state in the game. It’s probably not necessary to remember that enemy X was doing an attack animation at player Y, if you’ve already saved that enemy X is aware of player Y. When the game is loaded, the AI behaviors will fall into place from that information, and enemy X will attack player Y.

When Do You Save

All the time. Constantly.

When to save is partially dependent on the game design, but there is no game design or genre where the player should not have a reliable backup save file in the event of a power outage or Alt+F4.

Save the game when the player clicks Save Game. Save the game when the player quits for any reason. Save the game at regular intervals. Make sure that the player’s progress is never lost.

I’m a big fan of the Gunpoint style of rolling autosaves (for certain types of games, see below). When you save the game at regular intervals, do it in a rotating buffer of save files, so the player can restore to various points in their past, whether they were managing their save files or not.

For most games (see below) don’t autosave the game during combat. Don’t autosave the game when the player is in any dangerous motion (perhaps in midair, or any other unusual move). Don’t autosave during a cutscene unless your game is all cutscenes, and then this probably isn’t the right blog for you.

When Do You Load

This is where the save/load system really depends on the game.

For most games, you should let the player load any save file at any time. Give them a menu of manual saves, autosaves, their most recent quicksave, cloudsaves, friends’ cloudsaves, chapter bookmarks, who knows, who cares? Let them jump to the place in the game they want to go to, at any time, for any reason.

But! Roguelikes are a special case, because you don’t want the player to abuse the save system as a way to cheat death. That doesn’t mean you shouldn’t save the game! Always save! In fact, for roguelikes, you should ignore the rules above about not saving during combat. Don’t let them savescum out of a tough situation! The player might encounter a crash, or need to quit suddenly to go deal with some real life. So always save! But for roguelikes, the player should not have any choices about what to load; it is always the most recent save, so they cannot cheat.

Save Anywhere in Rosa

Finally, the technical bit, specific to my engine.

The Rosa engine uses a pure entity-component framework, where every game entity is of a singular type, composed of virtual components which inherit from a base class. Every entity belongs to the scene. Every component belongs to an entity. Every saved property belongs to a component. So the save process goes:

Basically: iterate the entities in the scene, and iterate the components in each of those entities, and write the important properties of those components.

There is no automatic system here, no reflection of variables that get automatically serialized. When I add a member to a component class, I also add it to the ::Save function. This is The Hard Work.

But, this isn’t a lot of work if you build it from the start! I rarely touch these save/load functions, and when I do, it’s because I’m in the middle of writing a new component, or new functionality on an existing component, and I know what parts need to be serialized. It looks like a lot of work, but it is extremely low maintenance.

Loading is basically the same process in reverse, and with conditions to handle different save file versions:

This is why each component serializes a version number, and it is how I make sure that old save files will be compatible with newer code; if the component was serialized with an old version number, it will simply skip loading the related properties and use their default value. Again, this looks messy, but I don’t ever have to think about it! It’s not zero work, but it’s just following a mindless pattern, and it Just Works.

Why Should I Care?

You might think your game design doesn’t need save-anywhere, because it’s meant to be difficult or it’s a short game or whatever. I think those are bad reasons to not do save-anywhere (as I noted above, roguelikes probably need save-anywhere more than any other genre), but if nothing else, consider this: save-anywhere is a huge time saver in testing your game. Anyone who has ever had to replay a 5-minute sequence in a game to get back to the part where a bug can be reproduced should understand this. When you can save/load anywhere, you can reproduce (most) bugs almost instantly, which saves a ton of development time and frustration. Even if your game has no player-facing save system, the ability to restore the game state to repro a bug is immeasurable.

Do it for the players. Do it for QA. Save anywhere.

Categories
Eldritch 2

The Style

Making art is one of my least favorite parts of game development. I’m not good at it, it’s slow (maybe because I’m not good at it), and my best efforts still look subpar (definitely because I’m not good at it). If I were a vampire and could devote a decade or two to immersing myself in art the way I’ve immersed myself in programming and design and music, I could probably git gud. But I’ve got a finite number of years on this rock, and games to make in the meantime.

Five years ago, I started learning Substance Designer. I studied other artists’ graphs and picked up a bunch of neat tricks. I made a template to convert Designer’s material properties into the ones my engine expected, and wrote a custom shader to preview materials in Designer the way they’d look in my engine. I liked the non-destructive workflow, and the logical/mathematical basis for texture creation. But I don’t like being tethered to a proprietary tool. My engine and content pipeline are built on free and open source libraries and tools; and while my old Steam copy of Substance Designer 2019 does still work, it felt like a liability to depend on an app that I can’t run without an internet connection. I stopped using Designer and went back to making textures by hand. But a seed was planted.

I’ve got a Trello board for long-term game and tech ideas—the sort of place I’ll write a card like “Vehicles!” and then sit on it for years until I end up making a game that actually wants or needs vehicles. In December 2022, in the middle of working on Li’l Taffer, fed up with making textures, I wrote a card titled “I should make an offline texture generator using a lot of the standard patterns I’ve used for pixeling stuff.” Wild idea, that would probably never happen. Six months later, I actually started building it.

The fundamental click that helped me jump from a vague idea into writing code was realizing that almost all the textures I was making were built in boxes. I didn’t need to make a texture generator that could do lush organic features; I already lived in the realm of bricks and tiles, and most of what I was doing to make those textures could easily be codified. I drew up a list of use cases that I wanted my texture generator to cover, and 99% of them could be broken down into nested boxes with a few common procedural functions.

So, I started coding! I named the project Piet, inspired by Dutch painter Piet Mondrian’s nested rectangular forms, and I got a proof-of-concept done pretty quickly:

I used to be on Twitter.

And then I got busy with Real Life, and I put Piet (and Eldritch 2) aside for a while. In September 2023, I started working on Schloss der Wölfe, which was my first practical use case for Piet. I added a few features, but struggled to make my proof-of-concept tool deliver the materials I wanted. I made a list of things I needed to improve in the future.

This month, with level generation in the rear view mirror, I’ve finally revisited Piet and checked off all those tasks I’d written last year. The schema is now a proper functional language (i.e., with function calls!), reducing a ton of copy-pasted definitions. I’ve added support for water flow materials and decal materials. And I’ve rewritten my material definitions to be primarily height-based, with other properties like normals and smoothness derived from height.

Screenshot from an early version of Eldritch 2. All materials are generated in Piet.

Why do I do this, when there are existing tools that do this sort of thing better? Maybe I’m just stubborn, or maybe I just enjoy this work. I think it’s the latter. I’m a programmer at heart, and making my little tool with my little domain-specific language… it sparks joy. I’m still bad at art, but at least I’m having fun making it.

Categories
Eldritch Eldritch 2 Slayer Shock

Turtles (All the Way Down)

My procedural level generator has come a long way since Eldritch, and outside of the occasional forum post or summary tweet, I haven’t documented it in any meaningful way since 2015. I’ve made a lot of changes for game jams and abandoned projects between 2017 and 2023, and I’ve recently made even more changes as Eldritch 2 takes a clear shape in my mind. I’m sure there will be more changes along the way, but this is the current state of it.

Prologue: Goals

I’ve used versions of this procedural level generation system in two commercial titles (Eldritch and Slayer Shock) and three recent game jams (NEON STRUCT: Desperation Column, Li’l Taffer, and Schloss der Wölfe). These games each have different goals and different reasons to use proceduralism. Eldritch was a roguelike (or roguelite), and it used procedural levels in the conventional roguelike way, generating new dungeons after each player death. Slayer Shock was a longer campaign game where total failure was a rarer occurrence, so it used procedural levels to create the dozens of missions a player might attempt throughout that campaign. I only expect my game jams to be played once, but they used a mix of bespoke and procedural level design to offload some of the burden of making levels under limited development time constraints.

I have made just as many games and game jams that did not use procedural levels at all. I believe procgen is a tool to use when it supports a game’s design and production needs, and its development should be guided by those needs. When I was invited to speak at GDC about the level generation in Eldritch, I called my talk “Procedural Level Design” rather than “Procedural Level Generation” because every decision I made on that algorithm was a level design decision.

Chapter 1: Eldritch

I spoke about Eldritch‘s procedural levels in length at GDC, and you can watch me be uncomfortable at public speaking or you can make us both happier and just read the slides. Also, Eldritch‘s levels were heavily inspired by Spelunky‘s levels (just extruded into 3D), and I highly recommend reading Darius Kazemi’s excellent interactive guide to Spelunky‘s generator. But a quick summary:

Eldritch‘s generation began by generating a maze on a small 4x4x3 grid, where each grid cell represented a 12x12x8m block of space. After that maze was created, it would randomly open some additional paths to create loops in the maze. Then it would turn that into a real space by populating each cell in that grid with a “room”: an authored 12x12x8m chunk of level design, consisting of voxels and entity spawners. Each room was built to fit a certain configuration of maze directions (north, south, east, west, up, and down), and during generation, a room would be selected at random from any of the authored rooms that could fit a maze cell.

In order to minimize the amount of rooms I had to create and to maximize the apparent variation, each room could be used in any of 8 transformations: its default, or rotated by 90, 180, or 270 degrees, and a mirror image version of any of those. These were extremely simple to do because the rooms were made of voxels, so any rotation or mirroring was an axis swap or negation on the voxel grid indexing.

There was one additional step: “feature rooms” were emplaced at random locations as seeds for maze generation, and to ensure that essential features like an entrance and exit—or random features like shops and bank vaults—would appear in the maze exactly as often as the game required. These were guided by rules that would, for example, place the entrance room on the top floor and the exit on the bottom, or make a shop have a random chance of being open, closed, or entirely absent.

Every room filled exactly one cell on the maze grid. For larger features like the ziggurat at the bottom of World 1 or the outdoor snow field at the start of the Mountains of Madness expansion, I had to build big spaces out of smaller chunks, and emplace them as multiple feature rooms with fixed locations and fixed transforms. It was pretty gross.

My main goal in Eldritch was to create interesting gameplay space; it didn’t matter much if rooms fit together neatly. The Lovecraftian theme supported bizarre, unknowable geometry, so if two rooms didn’t quite align spatially or thematically, that was fine. It worked for the game! And it turned out to be an essential part of the randomness of the game, as I would find out on subsequent projects where I added more constraints.

Chapter 2: Vamp

With Slayer Shock, the modern real-world setting and procedural mission objectives demanded some changes:

  • I wanted spatially coherent procedural levels; no more random crashed-together Lovecraftian spaces
  • I wanted levels to be any size, not limited to a small 3D grid
  • I wanted rooms to be placed according to a critical mission path, not expanded randomly in a maze
  • I wanted rooms to be any size, not limited to one cell in the 3D grid
  • I wanted rooms to be composed of static geo meshes and navmesh, not voxels

The sum of these needs represented a huge delta from Eldritch‘s generator, and I rewrote nearly the entire thing. The maze generator went away entirely, replaced by a portal-based algorithm of placing rooms one by one, wherever they could fit to connect to open portals from existing rooms. Rooms now defined their available connections by portal tags—for example, a cave portal could connect to another cave portal, but not to a forest portal; and caves and forests could be joined by special junction rooms with a cave portal on one side and a forest portal on the other.

Feature rooms were no longer seeded randomly within a maze grid, but placed in sequence along a non-branching critical path which was laid out before any subsequent room expansion. This was mainly used to place entrances, exits, and junctions between indoor and outdoor areas like caves and forests.

Every region in Slayer Shock had very different requirements, and my codebase got very messy with special case hacks for each. The introduction of so many content-driven rulesets also meant that the algorithm now had the possibility to fail (which was literally impossible in Eldritch as long as there was a complete set of room configurations). And it did fail often, making level generation much slower as it continually restarted with new random seeds until a successful world was found. Worse than that, it meant that viable levels tended to be very similar. I successfully “fixed” the chaos of Eldritch and ended up with something very predictable and bland.

Chapter 3: Loam and Zeta

In 2019, I began developing a side project fantasy dungeon crawler codenamed “Loam”. That project is currently abandoned, though I’m holding onto it because it feels like it could be my magnum opus. But also it being my potential magnum opus is why it’s currently abandoned, because it was extremely overscoped as a thing to work on in my free time.

My primary goal with proceduralism in Loam was to build levels that looked like D&D maps and played like immersive sim levels. I tore out the critical path stuff from Slayer Shock and focused on how rooms were connected within a smaller space. One of the problems that kept coming up was “the door problem”: when two rooms join, where and how do I spawn a door between them? The door spawner has to belong to one room or the other; how do I ensure that there is a door and that there is only one door (i.e., both rooms aren’t trying to spawn at door at their shared boundary)? I eventually decided to use portal tags to enforce placement of small spacer tiles between rooms, and put the doors in those spacers.

I also wanted to minimize failure cases in the level generator. Most failures were due to unclosed room portals: meaning a room was placed with an open exit, but no rooms could fit beyond that exit. I solved that by allowing the generator to conclude its main expansion phase with some portals left unclosed, and then connect those portals via a series of small connective tissue tiles. These tiles are guided by a graph search between unclosed portals, and guarantee closure and connectivity as long as there is a path. The enforced space between rooms provided by the spacer tiles usually affords such a path; and as an extra step to ensure this worked well, I implemented a limited “rewind” of the generator so that spacer tiles which could not be closed would be removed and replaced with connective tiles.

My first game jam to use this algorithm was NEON STRUCT: Desperation Column, but it used an extremely constrained room set so the connective algorithm never kicked in.

Chapter 4: Fray, Lily, and Wolf

Since Eldritch, my level generator had been fundamentally an indoor generator. I faked outdoor space in Slayer Shock (at the cost of huge performance problems), but it was fundamentally designed to connect small convex rooms to other small convex rooms. I wanted to do something bigger.

In 2022, I was working on a side project codenamed “Fray” which could be summarized as “indie Far Cry Star Wars immersive sim”. Yeah, that one was overscoped too. But one of my goals was Far Cry-style outposts: small indoor complexes within a broader outdoor space, that the player can approach from any direction and scope out before attacking. It would require a radical rethinking of not only my level generator but my sector/portal-based renderer as well.

My solution was to use generators within generators. At the outdoor scale, I would generate very large “rooms” on a coarse grid; and then within those rooms, I could generate indoor sublevels comprised of smaller tiles. The most significant constraint—due to the way sublevel and superlevel portals were connected for the renderer—was that a sublevel had to be contained entirely within a superlevel room’s space; I could not use a small sublevel to connect two superlevels. But that worked fine for my Far Cry-esque goals, and I got it working in about a week.

Shortly after that, I got the idea to use room and sector “colorization” to guide room placement, so that I could build levels with sequenced gating: for example, a player must collect the red key and unlock the red door before they can access the red part of the map. I didn’t have a specific goal in mind for this, it just seemed useful and I realized it wouldn’t be too difficult to implement.

I first shipped this version of my level generator in Li’l Taffer. It had no actual outdoor generation, instead using a fixed outdoor space with a single interior generator; but the colorization feature ended up being used to demarcate the public and private space within the mansion the player is tasked to rob.

The broader feature set finally got a shipping use case in Schloss der Wölfe, which is a largely linear experience but used nested generators and region colorization to make indoor/outdoor portaling work for the renderer, to guide placement of entities, and to provide some small variations on replays.

Chapter 5: Rasa and Tofu

Shortly after I released Schloss der Wölfe, I started another game jam—something akin to The Unfinished Swan or Scanner Sombre where the player starts effectively blind and uses a tool to identify the space around them. I ended up bailing on that to focus on Eldritch 2 instead, but in the short time I was working on it, I added “furnish rooms” to my generator.

Furnish rooms function almost identically to sublevel generators (i.e., the indoor parts of an outdoor room), but they do not create new sectors and portals for the renderer. Instead, they just add all their geo to their containing room. This gave me a way to build most of a room by hand but then let the whitespace be filled in randomly.

I bailed on that game jam and returned to Eldritch 2. The last big thing I needed to address to make all of these years of changes work for this game was a problem that had been lingering since Slayer Shock. Ever since I moved from voxels to mesh-based rooms, I’d needed to make sure that navmesh edges fit together precisely so I could stitch them up after rooms were placed. This prohibited me from doing the sort of chaotic crashed-together spaces of Eldritch, because every edge of every room needed to conform to a consistent navmesh signature. I waffled on this for a while, but ultimately decided that the accidental chaos of Eldritch‘s worlds was something I needed to now intentionally repeat. I rewrote some big chunks of my navmesh stitching and pathfinding code, and now my generator will stitch together any overlaps in coincident navmesh edges, to ensure that AIs can navigate between rooms as long as there is any actual space to do so.

I also finally reintroduced two Eldritch-era parts of the generator: mirrored rooms (a whole separate blog post in and of itself because of all the new requirements of mirroring meshes and navmesh, through multiple stacked transforms) and feature rooms, which are now emplaced at two stages: first as seed rooms before the map is “colorized” for gating; and then in a second pass, for feature rooms which may depend on that region colorization.

All of this leads to the current state of Eldritch 2‘s level generation. Levels are formed by a series of nested generators (outdoor overworlds with indoor dungeons, both with rooms optionally populated by random furnish rooms); with optional sequence gating, either within the overworld or within any dungeon; and each nested level seeded parametrically to guide placement of special features like shops and bank vaults. Room placement can still fail at any step depending on the authored room content; but as long as I populate all the common cases and keep the constraints reasonable, it rarely does. And the generator produces interesting levels in under 200ms, keeping load times to a minimum.

Categories
Eldritch 2

What Eldritch 2 Will Not Be

Since I’ve mentioned multiplayer, I feel some disclaimers might be in order:

Eldritch 2 will not be a live service game. The networked elements will be run on Steam and funded by the 30% revenue share that Valve takes anyway. It will always be fully playable offline.

Eldritch 2 will not have microtransactions or DLC. It will be a one-time premium purchase, and any subsequent additions will be gratis.

Eldritch 2 will not be a worse experience if you choose to play offline. The networked elements are additive, but it is and will always be fundamentally a single-player game.

Categories
Slayer Shock

The One About Slayer Shock

It’s a delicate balancing act being transparent in game development. I’ve seen friends who had the best intentions speak openly about what went wrong in their games, only to have their words turned against them in the streamlined narrative that their game was a failure and they were a failure for making it. It taught me to be guarded. Mainly for that reason, I’ve never said much about Slayer Shock (Steam user reviews: 55%). It was a dud, and nothing I could say would change that. You can’t mea culpa a game into success. But it’s been 7 years now, I’m not going to damage its sales or my reputation by talking about it. I’ve spent a lot of time reflecting on what happened there, I’ve internalized the lessons and made conscious positive course corrections on subsequent projects. I think I actually have something to say about it now.

Vampires in Nebraska

It was 2015. I was wrapping up NEON STRUCT (Steam user reviews: 87%) and thinking about what my third indie game would be. After spending a year feeling out of my element in level design and writing, I was eager to return to the familiar territory of procedural levels and systems-driven gameplay. The central concept owed a lot to an early version of XCOM (the ill-fated 2K project which eventually shipped as 2K Marin’s The Bureau: XCOM Declassified): a strategy-level campaign game combined with a first-person stealthy shooty game. Then I wrapped it in a Buffy the Vampire Slayer skin and sprinkled on some of my own experience growing up in Nebraska, and that was Slayer Shock. Patrol and protect the town, research the vampires, find and eliminate the vampire leaders. The elevator pitch still works for me, I just wish I had made the game I envisioned.

Sometimes Real Life Happens

Through all of NEON STRUCT‘s development (2014-2015) and into Slayer Shock (2015-2016), I was in the middle of an extended separation and divorce. I shared custody of a toddler. I had no family nearby, and I had somewhat alienated myself from my former work friends after quitting and going indie. I felt alone, and I felt under immense pressure, and I didn’t handle it well. I drank a lot, and I created new problems for myself.

That’s not an excuse for how Slayer Shock turned out, but it is the truth. I still believe there was a great idea at the heart of that game, and maybe a healthier version of me could have made it work. But I was at an all-time low and my heart wasn’t in it.

The Scope Monster

Or maybe I just got too big for my britches. After the modest success of Eldritch and the warm reception to NEON STRUCT, I was starting to feel like I could do no wrong. I had great ideas and I could execute on them. I made games faster than anyone. Could I make a stealth FPS with a strategy campaign layer, built for endless expansion and designed to be played for 100 hours? Of course I could! If anyone could do that, it was me.

And that was Big Mistake #1. I arrived at the idea of endless expansion and a 100-hour play time in a few ways. The first—and more legitimate—reason was because of what happened with Eldritch: the game was successful, and I wanted to add more, but I’d painted myself into a corner with its rigid quest structure. The second reason was a more toxic one: I craved success in all its forms, and to me that meant people playing my game for hundreds of hours the way they did with The Binding of Isaac or other roguelikes.

And this is Big Lesson #1. There are only two ways to make a game that people play for 100 hours. Either you make a game that is So Good that people want to play it over and over, or you exploit players’ addictive tendencies. These aren’t mutually exclusive, but I’d rather not do the latter at all. And I didn’t want to do that in 2016 either, so I accidentally set myself up to make a 100-hour game purely on the strength of its content. Oops.

Big Lesson #2 is: if your game relies on future expansions to be good, then it’s not good now. I built a game infrastructure in which I could endlessly layer in additional content on multiple axes. New levels? Yes! New level twists? Yes! New enemy types? Yes! New weapon types? New ammo types? New research methods? Yes, yes, yes! I’ve got a framework for that! I spent so much time building neat little systems without realizing the cost of populating those systems with interesting content. I shipped Slayer Shock in what I considered to be a finished form, but with big plans to grow it from there. Another way to describe that might be “Early Access”. And then I ran out of money and those plans never happened.

Art Is Hard

After making two games with voxel-based worlds, and especially after hearing Eldritch described so many times as looking like a Minecraft mod, I wanted to step up my art game. No more voxels, I was going all in on mesh geo and dynamic lights and physically-based lighting and materials. I had a vision in my head of the style I wanted, and if you’re familiar with Warhammer 40,000: Boltgun, it was a lot like that; but I’m not a good artist and I extremely missed the mark.

As I was nearing the game’s release, I was talking with my brother about the game, and he said something to the effect of: “Eldritch was simple, but it had a style.” And he was right, but it was far too late for me to course-correct. I guess Big Lesson #3 is to know your strengths and weaknesses. I have put in tens of thousands of hours to be a world-class programmer, a great designer, and a pretty good composer. I could do the same for visual arts, but I’m running out of years here. So if I’m not good at art, maybe a better option than making bad art is making the most out of simple art.

The Grind

The core loop of Slayer Shock isn’t bad per se: sneak around an environment, dispatch some vampires, grab some goodies, and get back home. For one or two missions, it works pretty well. But it quickly gets tiresome.

Big Mistake #2 was my assumption that if replaying levels in a roguelike is fun then replaying levels in a campaign-based game would be equally fun. But there’s a psychological element I hadn’t accounted for. When you replay a level in a roguelike, you know you’re playing it again, from the start, and you’re challenging yourself to do better this time. Roguelikes trade player character progress for player progress. Your character doesn’t get better over time, you do. But if the game takes away permadeath, and you keep playing variations of the same level over and over in pursuit of some unclear finish line, it doesn’t feel like progress. It’s a grind.

I hoped to offset this sameness with level twists, but (see Big Lesson #2), I didn’t have enough of those to make a meaningful difference. And maybe I could have changed the player build model so that players didn’t grow into and then stick with a single loadout for the whole game; but none of that really addresses the psychological element of the game structure. Big Lesson #4 (which is a corollary to Big Lesson #1): if your game asks players to replay content—even if that content is randomized—you sure as heck need to make sure they have a good reason to care.

What Went Right

I shipped a game, and that is a miracle, because every game that ships is a miracle.