"Flexible" Game Design and Code Modularity
Some thoughts on my game design philosophy and tips for generalist indie game programmers wearing multiple hats
I got a question on Twitter in a thread asking about coding patterns used in games:
I always wondered how other devs reuse code, do you have like a personal library of plug and play stuff, do you focus on having code be modular, etc?
Before this post gets technical, I need to talk about game design a bit!
“Flexibility” as Game Design Philosophy
For what I write about reusing code to make sense I need to outline my values as a game designer a bit:
I like games that have a sense of the game designer messing around and being willing to let anything happen - and yet - keeping that to a < 10-20 hour experience. Hydlide 2, Undertale, Silver Case, and Anodyne 1/2 are great examples of this. In Hydlide 2, what you think is a bush ends up being a password gate to the final dungeon. Undertale’s UI elements might become relevant for different battles in surprising ways. Anodyne 1 and its swap ability has a loose definition of ‘what is a wall’, and Anodyne 2 completely throws away your sense of world-continuity by the 4 hour mark. Silver Case is an experimental visual novel that plays with different narration styles and levels of understandability, even throwing in some 3D exploration.
That is I don’t really like to design games that are ‘loop-based’, in the sense that I can predict what hour 100 of the game will be like after hour 1. (A prime example of this is Harvest Moon - although you may encounter new events, you’ll still be maintaining a farm or gathering resources at hour 100). I don’t like designing overly skill-based games like competitive FPSs, and what I find appealing about something like Dark Souls is not the roleplaying, builds, or janky combat, but all the funny little enemy layouts and different shaped levels the game has.
I like to make games that feel ‘flexible’ in the sense that maybe a tree is a tree at one point, maybe it talks to you later, maybe it changes into a lamppost if you touch it, maybe it walks around, maybe it attacks you… that kind of thing.
I see this definition of ‘flexibility’ as distinct from simulation-style design, like deep roleplaying where your choices have consequences, or being able to design your town/house in Animal Crossing. I would try to explain but I have trouble putting it into words, but there’s a kind of regularity to the decision making in those games that feels too loop-y to me.
Consider my take on puzzle games: my style is a lot more along the ONYX Linking in Sephonie - not even quite a puzzle, almost turn-based action/strategy - more focused on ways of ‘expressing’ a creature through the game board. Unfortunately language doesn’t have an accurate way of describing this type of gameplay, so I had to go with puzzle, but…
See 26:33, a Linking Puzzle from Sephonie where you can only place the pieces near the moving circular things. Normally in the game you can place pieces anywhere, but for this one I wanted to force you to view the board differently by strictly limiting where they can be placed. It creates a sense of ‘floating on an ocean,’ with only these shifting circular things allowing you to place pieces near them. What I care about with these ‘puzzles’ is the board feeling like a different space, with a different way of navigating and placing pieces on it. Which, ultimately was done in service of trying to abstractly express all these little creatures you meet in the game.
The goal in these is more of a loose strategy/optimization puzzle - it’s about filling that bar at the top, not by finding a correct solution.
Back to Modularity
I mention all the game design things because I think my style of programming should be in service of those goals:
Single-player 2D or 3D games, best with controller
Developed by small teams with usually 1 programmer
Has “flexibility” as defined above
Not based around complex tech problems (e.g. the rendering/collision algorithms for minecraft, optimization for bullet hell games, keeping all things working well in a simulation game like factorio, being moddable)
I’m not really giving advice outside of that.
My major works are Anodyne 1+2, Even the Ocean, Sephonie, All Our Asias and Angeline Era. These are projects that take anywhere from 1 to 3ish years to complete.
Here’s a quick rundown of the categories of code I have to write and my sense for how modular they are between games:
Very modular: Math, Audio, Sound Effects, Text Storage, Helper Libraries
Modular (with some refactoring): Input, Saving, VFX Playback, Shaders, Lighting, Event Authoring Tools, Level Design Tools
Slightly modular: Menus, Title Screen, Camera Controllers, Player Controllers, Event Playback, Scene Management, Doors, Dialogue Boxes
Not modular: Enemies, Bosses, Entities that Interact with Slightly Modular scripts (E.g.: Sparkable Anodyne 2’s NPCs, Sephonie Gripshrooms, Treasure Chests), Random one-off triggers/etc
Over the course of a game I find that the further we go into production, the more I’m working on “slightly” and “not” modular things, as work focuses more on player-facing things. However, every now and then something in the “Very” or “Modular” category requires me to adjust/add to it.
Survival Coding: Prioritizing Speed
Generally speaking I find speed to be very important as a programmer under my circumstances. While it’s important to have maintainability, it’s also important not to overengineer. If it works it works! We make games in genres that are relatively unpopular. Despite our prowess we barely make money compared to what we could get working at Google or a big studio - there’s only a handful of years we’ve broken $100k USD - and the majority of those years were because we were fortunate to get platform support (Humble Bundles, Epic Games, Steam Features,) which is waning in recent years.
So my view on coding is one of ‘survival’ - spend as little time as possible to get the games out quickly. We can aim for marketability and platform support, but with the kinds of games we make - those meant to remain historically relevant and exist beyond the orbit of popular tropes or compulsive gameplay loops, rather than become a meme of the week - we’re ultimately a historical anomaly of a studio that shouldn’t really exist, so we can’t rely on support coming our way too often.
Anyways…
Focus on good coding infrastructure only when it truly matters: tool creation you expect to update/modify over a year or two. An enemy parent class whose decisions will be used by every enemy in the game.
Even then, it’s good to keep things kind of loose - it’s possible to program yourself into a corner where you can’t add an edge case you need later. If stuff is kind of loose then you can more easily add in weird one-offs later, like the Sephonie Linking Puzzle above. Or turn enemies into strange variants… or hack stuff into your cutscene engine… etc.
Let’s discuss the above categories of modularity.
Not modular
Enemies, Bosses, Entities that Interact with Slightly Modular scripts (E.g.: Sparkable Ano2 NPCs, Sephonie Gripshrooms, Treasure Chests), Random one-off triggers/etc
How this helps me in practice is that the "less modular" something is - the less I worry about it being well thought-out with respect to future games.
You might also notice that the Not Modular stuff is the most directly player-facing: it's what a player most directly interacts with. Going back to looseness, if this code isn’t too ‘strict’, you could set how strong a bouncer entity would bounce the player, even to an unexpected extent, but it could also be something like being able to set a flag to make a certain enemy run in circles instead of back and forth. Maybe one enemy talks to you if you hit it in a certain way. Etc.
Since you probably won't use this code in the next game, and the size of the files is pretty small, then do just the bare minimum you need to make stuff work and get your game design across.
This is a bit different if you are going to be making a sequel with the same mechanics, but I can't speak much to that. Although, we do plan to use Angeline Era’s engine/combat in future games, so mainly what I’ve been doing is to try avoiding enemy scripts from accessing the player/other systems in too much detail (preferring go-between classes or function calls to do stuff.)
Anyways, it's worth thinking about potential ways to use inheritance here. If you're going to have to code like 50+ enemies (like Angeline Era,) then you should try to construct a parent component (mine's called EnemyLogic) that handles stuff like respawning, death effects, collision logic, overlap checking, so you don't have to write it for each enemy. Will there be edge cases? Yes, but as long as you still keep this parent class kinda loose you should be OK.
How you choose to use inheritance differs from game to game, but the solution should always be based on tangible facts about your game. For example, Angeline Era's EnemyLogic parent component was done after we knew how levels would be screen-based, how player taking/giving damage worked, the knockback system. Even then, I had to hack in edge cases (for different weapon collisions/rules), but it's turned out fine.
People might argue about inheritance/OOP, but it’s worked for me for these types of games.
Slightly-modular
Menus, Title Screen, Camera Controllers, Player Controllers, Event Playback, Scene Management, Doors, Dialogue Boxes
These are things where you will definitely be able to re-use parts of it from game to game (e.g. Angeline Era was built on Sephonie's movement/camera,) however, these kinds of things are often very tied to the 'not modular' stuff, and so when moving it to another game you will need to do a bit of cleaning to remove the old linkages to the previous game.
Overall, with these types of code, I do put more thought into organization - but I also accept that some of it may be scrapped in the next game. Theoretically could you architect things to be very modular, but it’s impossible to predict what your next game will be like so there’s a point in which you’re burning time on making the code look good but function the same.
A few concrete examples of this...
The logic of choosing options in a pause menu is modular. But the things that happen when you select an option might be game-specific, or they might not! For example, changing the volume never differs from game to game for me, nor does something like "disabling shadows". But options like "easy mode" will differ, etc.
Event playback in All Our Asias/Ano2/Sephonie/AngEra use the same systems refined over time. However, the playback engine of course contains game-specific one-offs that need to be removed from the next game (e.g. Anodyne 2 does a lot of weird things with moving the player around for cutscenes, based ongoing from 2D to 3D, etc)
The feel of a camera/player controller can differ from game to game, however, there are some bits that could be re-used (e.g. rotating the camera, types of collision polish in 3D.) If you keep your code sort of neat, you can re-use bits and pieces of this in future games - annoying rotation math, etc.
Doors often interact with the Scene Management system, and while the skeleton of this process is consistent from game to game, it might interact with the player, or cutscenes, in different ways
Modular (with some refactoring)
Input, Saving, VFX Playback, Shaders, Lighting, Event Authoring Tools, Level Design Tools
From here out I try to be more careful about what other scripts these systems interact with. If one of these systems DOES need to interact a lot with a non-modular system, can I write it in a way that the non-modular code does the interaction? Etc. At this stage it’s good to think about “how can I prevent this system from having to know about the less modular systems?”
I'll just go through one by one.
Input - I'll have to change what the input is from game to game (e.g. a game might have a shoot button, another might not), but if you keep all the stuff that polls the keyboard/controller in one script, then you can just copy paste that to a new game and basically have input working well. I also keep functions like "replacing tags with button sprites" in dialogue here, or internal keyboard remapping logic here.
Saving - This contains the code that creates/loads save files - I like to keep all the data I want to save as statics in this file, so that other scripts can modify the data and the Saving data doesn't need to know about most of the less-modular code. For example, the player’s saved position is here, but it’s fetched by passing the player transform into a function call (I think) rather than searching for the player directly or something.
VFX Playback/Shaders/Lighting: These tend to interact with Unity's internal systems - Mesh Renderers, Materials, Lights - so if you're careful you can do something like make a lighting-change trigger, or material-emission-manipulation effect, dither effects, carry over from game to game.
Event Authoring Tools: This is something like RPG Maker's event-maker, where you can type in line-by-line what you want to happen. My tool saves the event data to a format that is then readable by the event engine - so the tool doesn't need to 'know' about the game at all, for the most part (Although for some usability things I do make my event editor know about the current open scene.).
For example, if I want to change the scene, my event editor might just write a line of text to a file: "Change Scene|FOREST1". Then in the game, the event engine would 'give meaning' to this line of text, and actually do the scene-changing.
Level Design Tools: My only experience with this is the 3D Autocuber Level Design tool I made for Angeline Era. In some sense you can think about this analogously to the event-authoring tools: The purpose of the level design tool is only to create mesh colliders, meshes, and other game objects, so while there will be some non-modularity (e.g. if the level design tool needs to know how many doors are in a level or something), a lot of these tools can be built to be mostly general-purpose. If I had to write a tool that did a lot of game-specific entity manipulation, I’d probably write that as a separate tool rather than fit it into the Autocuber.
(The autocuber)::
Very modular
Math, Audio, Sound Effects, Text Storage, Helper Libraries
This is more straightforward - it’s stuff that doesn’t really change from game to game.
Helper Libraries/Math
I like to keep a file called "HF" - Helper Functions. This stores common math operations or transform operations, like...
Rotating the XZ components of a Vector3 by some amount
Rotating a transform to face something, by a certain rate
Easing functions
Randomizing a Vec2/Vec3 in some way
Reducing a Vec3 to zero, at a certain rate
Checking if a point is in a box
Seeing if a point is near the ground
etc...
These are nice to have on hand because they're pretty bug-prone to make from scratch each time. The only problem is sometimes you might forget if you have a function or not. I feel like it could be useful to look into documentation-generating scripts, but my games never get big enough to where I really wish I had such a thing.
When writing these it's good to think about the most easy-to-use way to structure it for yourself in the future - it's easy to have too many parameters or confusingly named things, and then you waste time trying to remember how to use the helper function...
Not Modular Helper Libraries
I actually end up putting a lot of game-specific stuff in these files as well out of laziness! While not ideal, in practice it’s probably fine to do this (e.g. a weird helper function that three enemies use - it’s awkward to put the function in one of the enemy’s files, so into the helper library it goes…)
Audio/SFX Playback
Audio/SFX playback will always be tied to the scene hierarchy because of using AudioSources, but other than that my code for these are pretty game-agnostic - other scripts call the Audio manager script to start/stop songs, play SFX, etc. Keeping these in one file is nice because you can build up useful things like "stopping a song by name," "fading a song out," or "panning sfx to different channels if a bunch of the same one play at once," etc.
This probably applies less for games with more complex dynamic audio.
Text Storage
I have tools that convert a .txt format for dialogue into a engine-parsable .xml file. The game then can turn the XML file into a dictionary for looking up strings, which helps with localization.
In Conclusion
Coding style is very personal, but it is something that is also very socially informed. Under what circumstances are you programming a game? What expectations are you meeting? These will all inform how you should proceed. But overall, my rule is to not worry too much. It’s hard to write code that is truly so bad it breaks your entire game. Just don’t build your own engine…