Where is the sandbox in your code base?

Where is the sandbox in your code base?

I was watching Casey Muratori and the Primeagen the other day, and they were talking about the high-level architectural design of the core data inside of Primeagen's game. What Casey says sounds counter-intuitive for anyone from a traditional OOP software background. He suggests not committing to an organization of data of different game entities around different objects (as in OOP objects). The data for rendering game entities shouldn't be cordoned off from the data for the physics of game entities.

Crazy talk. But he's right. I think there's a bit of nuance that got glossed over in the discussion that makes this the right architectural call.

A fundamental fact in game dev is that you don't know what's fun until you try it. Plenty of prototypical board games seem fun on paper, but it turned out not to be fun when you actively try out the rule set in play. Here's an exercise: take any game you know well and change one constraint. It could be the countdown timer for the level, the number of lives, or the type of weapons. Whether you get more fun or less fun depends on the constraint, but there exist small constraint changes that can completely alter a game, or even break it completely. [1]

What makes games fun is not just the individual entities and the game mechanics. It's how they all interact to create a system of interlocking parts. They all interact together in different ways to create interesting consequences and effects. Go, Blokkus, Spleunky, and Breath of the Wild are all examples of this on full display: From simple parts and composition comes a host of interesting consequences for solving problems. That means as a game designer you need to continually experiment with the entities and elements of the game until you get a system that has both simple, legible parts, and their compositions yield interesting effects. These two design requirements are often at odds.

If you don't know what will make a game fun until you play with it, how do you make a fun game? You iterate, trial and error, and hill-climb to fun. [2] And to do that, you need to create a conducive environment for experimentation.

That is what Casey is advocating. Because you don't yet know the solution or even the shape of the problem, it doesn't make sense to lock yourself into an ontology before you understand it. If you lock yourself in before understanding, you're drawing boundaries that must be overcome and worked around, creating more work for yourself. And this inevitably happens because you drew the boundaries wrong. After all, you drew them before understanding the problem space! It's unlikely you drew it correctly on the first try.

The fundamental assumption of 90's style OOP (not Smalltalk style OOP) is that if one can model the problem as an ontology, a hierarchy of categories of objects, then it takes you most of the way to solving the problem. I think this is one of the most misguided principles and assumptions in software. It's led the entire field and industry astray for the last 30 years. Luckily, we've slowly moved past it, as almost no one is espousing proper class hierarchies.

When ontologies solve a problem, it works quite well. But empirically, it often does not, because it often is not the right tool for thinking about, framing, understanding the nuances of, and solving a problem. [3] Worse, once you pick an ontology, it's significantly harder to undo as you use it more. And because 90's style OOP forces you to pick an ontology before you fully understand the problem space, it's never what you want later on in project development. So we spend more time working around our misguided choices earlier on, and management wonders why we've slowed down the pace of shipping features.

Hence, there needs to be somewhere in your codebase right underneath the domain level of your game that lets you experiment quickly with the different combinatorics to yield interesting, yet understandable gameplay.

Enjoying what you're reading? Subscribe for more

So at least in game dev, they've moved away from a collection of instantiated objects from a class hierarchy to an entity component system. Entity component systems are a simplified key/value store of components–a bin of Lego blocks–you use to compose game entities. You use the components to mix and match the behaviors of entities in the game and the interactions between those entities. So instead of a Monster class, you might put together a position component, a sprite component, an AI component, a health component, and a weapon component to represent a monster. The monster as an entity doesn't exist or is grouped anywhere in the system, except perhaps, as an alias. This ability to mix and match behaviors allows you to experiment with gameplay possibilities.

For example, if you decided that the camera will always follow the player in your top-down shoot-em-up adventure, that's a reasonable initial assumption. Every game in this genre you've played had such a thing, and you've never seen any different.

But games stand out by being differentiated with unique ideas. What if you had the idea that you want to add a remote-controlled missile as a new weapon? Now, the camera needs to follow the missile instead of the player.

If you locked yourself into "only players have the cameras" on them, you've just created a lot of work for yourself. Either to break the ontology and recategorize, or never have the ontology in the first place. That's why people spout "composition over inheritance" after decades of experimentation with 90's style OOP and realizing that certain aspects just do not work well.

While I've focused on games thus far, I think this idea of a sandbox is broadly applicable to software. It's just that only the games industry has acknowledged it explicitly. As software is more expensive to write than to buy, it gets written if an off-the-shelf solution wouldn't solve the problem. By definition, we only write software when solving a new problem; when we don't fully understand the problem.

You want a place you consider a sandbox in your code to explore the consequences of the system you're trying to model but don't yet fully understand its nature. Though it seems counter to the idea of a separation of concerns, I think it's correct to have a single struct that mixes all the states relevant to both game state and rendering state. Until you understand what you're building, you shouldn't lock yourself into a structure you might need to undo later.

But it's not right for that experimental play to spread through all parts of the code base. The nature of the analogy of a sandbox is that what's inside of the boundary is fair game for play, but outside of it, there needs to be discipline and clarity. And most of the time, we delegate this to the framework and libraries.

But I don't think it's enough. Many application devs think frameworks and libraries will solve all their problems, and if they only draw between the lines, they'll be fine. Inevitably, they will come across some aspect of their problem not covered by the framework, and if they don't have practice moving good experimental solutions from inside the sandbox to a place outside of the sandbox, they will suffer. Or more likely, complain on Twitter that they have to fight against the framework.

When we've settled on a particular solution or design, it needs to be shaped up and graduated from the sandbox to life outside of the sandbox with good judgment.[4] Without that discipline, the experimental production code remains in a state of arrested development. If your entire codebase is in a sandbox, it'll become harder to build upon. A sandbox cannot support its own weight, nor anything else on top of it–it's built out of sand.

What part of the code base are you working on? Are you inside of the sandbox? Or the outside? When is it time to transition something outside of the sandbox? How do you communicate it to management? There are all good questions to stop and ask yourself as you write code to serve the end-user and your future self.


[1] If you're curious about what sort of things can break games, this Extra Credit episode is a good intro to the concept of Power Creep.

[2] When you can't express the problem in a closed form, you're required to break out numerical methods and iterate like Newton's Method to the solution.

[3] Functional programming isn't a panacea either. It models problems as data transformations, but the underlying assumption is that the data model is fixed. We know this is not the case given we have schema migrations.

[4] As software developers, we appear to be bimodal when it comes to refactoring. Some of us do it too early, hence the blog posts about how you should never make abstractions until you see three examples. Some of us do it too late (or never) hence the blog posts about technical debt.