How I Structure My Game Projects

As a programmer, I spend a lot of time figuring out how things should be named and organized. I want things to have readable, comprehensible names, and to be organized in ways that make sense to me. To that end, when I started out in game development, I wanted to figure out how I should organize the files and directories in my game code.

Surprisingly, I’ve found little in my internet searching that speaks to this topic, nor to how things should be named. It just doesn’t seem to be talked about much, and I think that’s a shame. Sure, in the end, the game’s logic doesn’t care where you place your files, or what you call them, but having an understandable code base makes it easier for you to comprehend what particular files are for, without having to look directly at the code or the places that code gets used, and having easy understanding of your code makes for easier development. To that end, I’m writing a high-level overview of my current project, Sanity Wars Reimagined, which contains my latest iteration of my code’s architecture. I’ll show the high-level directory structure, and explain how I came to name those higher-level directories.

Is it something that you should take as gospel and use for all of your projects? No, because everyone’s code needs are different, which is probably part of the reason why articles and videos on this topic aren’t easy to find. What I intend this article to be is an overview on how I structure things and why I do things the way that I do. Hopefully, someone who is looking for inspiration on how to organize their own code might see things they like and incorporate them, saving them the time it took me to figure those things out through trial and experience.

I’ll be the first to admit that, even though generally I like where things are at now, there are still aspects that I’m not entirely satisfied with, so even though I have guidelines on how I should structure things, if my guides don’t make sense, I’ll break away from them to try something that seems better. If they do prove better than my old ways, I’ll incorporate them into my next project. There’s a few instances of this happening in SWR, and I’ll point them out as I go through my code base.

The Project Directory

Without further ado, here is the top level of my SWR directory:

I like to keep my top level relatively simple. The things that go here (other than Godot and VS Code-related files) represent the highest-level reduction of a collection that I could think of. I don’t like seeing too many files and folders at once, as it makes it harder for me to scan the architecture at a glance, so the more I can reduce things, the better.

Note that I’m using Visual Studio Code. Godot lets you plug in third-party editors to use in place of its own built-in editor, and since I use VSC for my day job (web development), I also use it for my game development. There are also things that an external editor shows you that Godot’s editor hides, like markdown files (which I use for my README files) and JSON files (a file format I work with for some kinds of data).

While I’ll generally avoid going into the specifics of how any particular file or system works, I do want to take a moment to explain one particular file in the project directory, the INIT.tscn file. I have a Godot boilerplate project (which I call “Genesis”) that I clone whenever I want to start a new project. Having boilerplate to start with means I can take things I liked about previous projects and incorporate them in a generalized form, so all future projects I make benefit from what I’ve made in the past. How does the INIT file factor into that? That’s the file I’ve marked as the default scene in the Godot project config, and it reads a value in CORE_CONFIG.gd that tells it which screen file I want it to load first. Note that I said screen, not scene. I’ll get more into what screens are later on.

Why not just change the default screen with each project instead of changing my own config value? To give the short explanation, it gives me a consistent, expected environment for my global scripts to hook into. There might be better ways to do it, but this is what I’ve come up with for now, and it works well enough that I haven’t felt a pressing need to change it. An important aspect of architecting code is that there will be plenty of times you feel something isn’t ideal, but running with a slightly imperfect solution is better than spending a lot of time trying to come up with a perfect solution, only to find later on that future scenarios render that perfect solution imperfect. Iteration makes perfection.

Now that we’ve seen everything at a glance, let’s look at each of the top-level directories in my project.

_debug

As explained in previous tutorial posts (starting with this one), I have a custom system that I use to aid with debugging my code in-game, by providing a comprehensive, extensible way to render debugging information widgets. The _debug directory is where I store the code that makes this system work. Since I believe in keeping debug code as isolated from game logic as I can, I keep the scenes and scripts for my debug code in a separate location as well.

Note that I start the directory name with an underscore. In many programming languages, starting a name with an underscore indicates that this is private to some particular scope or class. That’s roughly the idea I’m trying to communicate by using it here. Nothing in _debug is used to make game logic run, so I want the directory name to help communicate that.

_samples and _tests

_samples is something that I include with my boilerplate that contains sample projects demonstrating some of the game systems I’ve built, so I can mess around with them and see how things should look by default. _tests is where all my experiment code goes. If I’m trying to build a new system, I’ll make test directories and files for it in here so I can experiment with the system in isolation before incorporating it with the rest of the game.

One thing about both of these directories is that I specifically filter them out of game exports. These are never intended to be used in my game code, so there’s no reason to include them in official game builds. The reason I don’t do this with _debug as well is because I have to reference the debugging global in my game code to make use of the debugging tools, and I haven’t yet come up with a place to house my debugging global somewhere that makes sense other than in _debug itself. As mentioned before, this is running with a slightly imperfect solution, and if I come up with something that’s better I’ll work it in.

addons

This is what Godot expects you to call the folder which houses third-party engine plugins. I use it for that purpose, of course, but I also use it to house any third-party code (such as GDQuest’s Steering Behaviors AI Framework) that I intend to use exactly as-is in my game code. addons might not be the best name for code serving this latter purpose, but I decided that I preferred to keep all third-party code in a single location instead of using two accurately-named directories, one for proper addons and one for other third-party code. It keeps the top-level architecture simpler, in my opinion.

assets

This is a place where things which are considered resources for other game systems are stored. This includes the obvious in audio and graphics, but also includes scenes that are treated as resources, particles, and shader scripts, among other things. This keeps all those things in one location, and by storing all instances of such things in that one location, I know exactly where to look when I’m looking for anything that I intend to use as some form of content resource.

At the root level of the directory is a README file. This is what I leave for documenting what I intended this directory to be used for, to help future me (and, maybe, future contributors to my game code, should things get to that point) remember what I was thinking. By including this documentation, when I’m considering where to put a new scene or directory, I can refer to a directory’s README file to understand what I intended it to be used for, and see if the new thing I’m making fits that criteria (and if it should fit the criteria and my description says otherwise, I can update the description).

config

This directory is specifically for systems which use some form of scene/node-based configuration, which means they don’t quite fit within the systems directory. This is a new approach I’ve been trying based on past experiences. I’m used to strictly string/file-based configuration files (which is why there is a CORE_CONFIG file at the project level), but making scene-based config files allows for a lot more flexibility in how your configuration works, plus it gives the benefit of using the editor UI to manage your configuration data, which can make for better visual presentation.

Next are the connectors and core directories, but I’m going to skip them for now and come back to them later, as explaining their purpose makes more sense after seeing what the other directories are used for.

effects

This is a directory where I’ve kept scenes that implement a particular effect, such as a 2D trail or shield bubble. Note from the image that there isn’t anything in this directory other than the README file. That’s because I made it part of my boilerplate code, but by default my boilerplate directories don’t include game-specific code.

That said, I’m starting to wonder if this isn’t better served being part of the assets directory. Effects could easily be considered a form of content. This might be something I refactor out in the future.

game_objects

In this directory live all of the objects which get used within the game world, or that augment other game objects. The way I architect this file is the one that is most in flux, since depending on what game I build there could be different ways to classify the game objects. To that end, while I include a default set of directories for the game_objects directory in my boilerplate code, I have the expectation that I’ll likely be making a lot of changes to this architecture.

One subdirectory of game_objects I’ll dig more into is entities. This is where I house any game object that is meant to be rendered visually within the game. Here I store things like the player character, enemies, projectiles, decorative objects, etc. As with the rest of game_objects, the specific architecture of the entities directory is going to adapt whatever structure makes sense for the particular project I’m working on.

There is also a test file, which contains some simple test characters. This predates some of the conventions I’ve come up with for my code, like storing all such test code under _tests, so I plan to refactor this out in the future and move the contents to more appropriate places.

game_world

Here is where I store all scenes that pertain to building the game world. Things like levels, maps, and world areas are included here. Why not make this a part of game_objects, since it could be argued that these are all game objects? I currently think it makes sense to keep things that make up the level formation, such as maps, separate from the objects the populate those worlds. In other words, it’s a personal preference.

The MapManager directory doesn’t really belong here. It’s a system that manages which map is being shown, and although it works with maps, the fact that it’s a system means it would be more accurate to have it be under the systems directory. I’ll probably refactor this at some point.

helpers

These are where files containing generalized helper functions are located. For instance, the MATH helper consists of any functions designed around math calculations that Godot doesn’t provide out of the box. If a helper is system-specific, however, then it would stay with the respective system’s directory instead of being placed here.

screens

This is where I store screens. What’s a screen? It represents whatever collection of systems, game objects, and UI elements the player is currently engaging with. The MainMenuScreen, for example, contains the main menu interactions and visuals. The IntroScreen is where I show the game’s introductory text, TestGameScreen is the screen where actual gameplay for Sanity Wars Reimagined takes place (someday I might remove the “test” prefix because, well, it’s the actual game screen at this point), and the various end screens are where I show the text you see when an end game condition is triggered.

systems

Here reside the various gameplay systems that implement the gameplay for Sanity Wars Reimagined. portals contains the systems logic for making the portals work, while generators contains the base code which is used to control automated spawning of game objects, which is built on for both Eyeball and Tome generation on maps. camera contains the base game objects which implement customized cameras that extend on Godot’s inherent camera nodes.

Didn’t I say game objects belong in the game_objects directory, though? Well, to be more specific, game_objects contains the specific implementations of game objects, whereas any game objects in systems are the foundations on which those specific implementations extend.

Before I move on to ui, I want to go back to one of the directories that we skipped earlier.

connectors

This directory is exclusively for connecting different game systems together. A connector node is a single node that takes two or more other systems nodes as setup arguments, and then applies any connections and custom logic to make those different systems interact with each other. It seems a little convoluted, compared to just building those connections directly into the systems themselves, but, by doing so, I can build individual systems that handle their own logic without directly relying on other systems, and thereby make them easier to reuse with different systems.

Admittedly, this kind of thing isn’t necessarily useful for the specific implementations I’m making for this specific game; the benefit, in theory, will arise from future projects where I want to use certain systems I’ve built for Sanity Wars Reimagined and connect them with the systems of said future projects. I’m still on the fence on whether this abstraction will actually prove useful, but, for now, this is the best solution I’ve come up with to making my systems reusable. If I find a better way to do it in the future, I’ll implement it.

With that out of the way, let’s jump back to

ui

This is both a collection of generalized UI elements that I can build on and the specific implementations of UI elements as shown in my game. Examples of the former include the Sanity gauge and the spell indicator buttons, and an example of the latter is the UI display node which contains the Sanity gauge and each spell indicator being used for the game’s selection of spells.

Given some of the distinctions I’ve made for various game objects, it might seem surprising that I simply lump all the UI elements together. Admittedly, that bothers me a little bit, too, but I haven’t yet thought of a way to cleanly separate the base UI elements and their implementations in a way that makes more sense to me than what I’m doing now. In the meantime, it’s useful for me to know where I store all the UI elements for my game, so they all go here.

Well, not all of them. It’s time now to go back to the other directory I skipped earlier.

core

The core directory, at first glance, looks suspiciously like a microcosm of the project directory, containing directories with names matching the ones I outlined earlier in the article. core is used to contain elements of my code base which I’ve successfully generalized to be reused across any game I make. Therefore, anything housed in core is something which I can safely build specific implementations on top of, without worrying about said things implementing some functionality from a past project.

This is crucial to my approach for making reusable code. Earlier, I mentioned that I make use of Genesis, a boilerplate Godot project. Specifically, the only code that is actually stored in Genesis which gets copied over are the things in this core directory. All the project directories I’ve gone over start out as empty shells, waiting to be filled out with my game-specific implementations that extend from these core systems and files.

What if I need to change how a core system works? In that case, I move the directory/file I need to customize out of core and into its appropriate project-level directory. Why not just modify the things in core directly? It’s out of convention; by enforcing the idea that things in core are never meant to be modified, I can be confident that whatever I build that is on top of a core system or object is something that was designed to work with any project, while something in a project directory, say systems, was designed to specifically work with the current project.

This convention also plays into the scripts I use for managing updating game projects with changes made to the Genesis boilerplate project, such as bug-fixing. Anything in the core directory is something I assume can be straight-up replaced, while anything outside of core needs to receive more manual attention.

That’s All, Folks

That concludes my high-level tour of how I structure my game projects. Again, I’m not saying the way I do things is the best way to organize all game projects; I just wanted to explain how I structure things and the reasons behind those architectural decisions. If you’re looking for ideas on how you should structure your own game projects, I hope my overview helps inspire you to come up with the solutions that work best for you.

No Comments

Post a Comment