How to Organize a Unity Project
There’s a lot to learn when it comes to making a game in Unity: complex maths, Unity’s API, the component system, the testing framework… Even though all of these things can be difficult, there’s usually resources and documentation to help in wrapping your head around the concepts and how to use them. Most of the “technical” aspects of Unity are documented and you can find examples of most (if not all) of them.
However, there is one aspect which I’ve found confusing from the very beginning and for which I haven’t found a lot of documentation and clarification (the main reason I’m writing this post): how to structure, architect and organize your Unity project.
Background
Coming from the world of Functional Programming and Backend (I use Haskell in my day-to-day work), I’m used to working with pretty structured and well-defined architectural patterns (ex: MVC). These basic guidelines usually help in structuring your project, avoiding easy pitfalls and guide the overall design of whatever you’re coding. So, when it came to working on my game in Unity, I found it surprising how there wasn’t much in the way of suggestions, examples, frameworks or recommendations about this.
Yes, we all know about the basics of “directories” in Unity: how there are certain folder names that are reserved and “magically” allow you to do some things; but what about “organizing” your code? As I coded my game I progressively started to find that the OOP and Component-based nature of Unity makes it very easy to take your code down a more and more complicated path. You can easily find yourself splitting logic among multiple different Monobehaviours and classes, replicating functionality across your entire code-base and even having issues with the concept of “single source of truth”.
A couple weeks ago, I started working on a new feature (“building”, which generates structures that work as tile modifiers) for my game and I found that I had split logic so much that there was no easy way of modifying the value of a given tile. I decided to leave that aside for a bit and work on refactoring my entire code-base.
In this article I’ll go into details of what architecture I came up with for organizing and designing the code for my game. I’m not saying it’s a silver bullet or that it’s a one-size-fits-all kind of thing; however, I do think that the ideas exposed here might work for others and that the concepts at the core of my pattern are pretty universal, so I hope this helps.
Architectural Patterns
First, let’s review some of the most popular patterns out there and I’ll briefly explain why I decided not to use them as-is.
ECS
The Entity Component System (ECS) follows the principle of composition over inheritance, defining every entity by the components it uses rather than by its’ type hierarchy. Entities are generally thought-of as game objects that consists only of a unique ID, it holds no other data or functionality. Each entity then implements a number of components, which can be understood as “aspects” that the associated entity has; components can be things like “health”, “speed”, “mana”, etc; and hold the data needed to model that aspect (entities with a specific set of components are called archetypes). Systems act on entities with specific components, for example, a movement system might receive all entities with the “speed” component and move them at different rates based on that.
Being honest, I really liked the idea of following the ECS pattern for the game: I find it’s very clear, well defined and easy to understand. It also provides clear boundaries to what goes where and eliminates problems with inheritance/dependency hierarchies while also centralizing and clearly defining a SSoT. However, when deciding to follow this pattern I was faced with two options: either implement my own take on ECS using Unity’s GameObjects/Components (which had already lead me down the path I was finding myself in) or implement Unity’s new ECS system (which would require a pretty sizeable refactor of my entire codebase and/or starting from scratch). I finally decided to create my own pattern, basing it loosely in ECS. This would avoid the need for a massive refactor (or spending loads of time learning the new API) while trying to get some of the benefits of ECS.
MVC
Typically used in Backend engineering, the Model View Controller (MVC) pattern divides code into three categories with well defined boundaries and interactions. Views can be thought of as what a user sees and/or interacts with, interactions with views will trigger calls to controllers. Controllers typically contain the business logic for the software: they receive certain interaction from the views, perform any operations needed based on them and then call models to remove, retrieve or modify data. Models, as already stated, limit themselves to interacting with data, they do not hold any other logic themselves but, changes performed by models can trigger changes in views.
I’m incredibly used to working with this particular pattern and, initially, thought it would fit perfectly into my game. I like the idea of having a centralized place to interact with data and keeping views and logic separate from each other, however, after giving the design a couple of go’s and trying to fit all the different kinds of components into it, I quickly realized that wouldn’t be the case. I spent some time looking through the internet and quickly realized most people arrived to the same conclusion. While there were a lot of people saying to “adapt it” to your needs and/or “not think of it so rigidly”, there weren’t many concrete examples of what those changes looked like (and I did want something more rigid and structured).
MVVM
Being honest, I’m not very familiar with the Model-View-ViewModel (MVVM) pattern but I saw it mentioned as an alternative quite a few times while reading about MVC for game development. In general, it’s seems similar to an MVC pattern, having Models be non-visual classes that encapsulate data, Views which represent what a user sees on screen and ViewModels which work as an in-between (like the “C” in MVC). One of the key differences seems to be the flow of interactions/data: rather than having views be based directly on models/controllers; Views only interact with ViewModels (Views bind to data and execute commands in ViewModels and ViewModels send notifications to Views for updates) and ViewModels interact with Models (ViewModels update the Models and Models send notifications to ViewModels).
While investigating this, I found the same potential pitfalls I already saw with MVC (as well as, again, not finding many examples of game development code).
HAMES
Combining all the options mentioned above, I came up with my own pattern which I call HAMES (pronounced /(heɪmz)/ because I found the definition funny): Handler, Archetype, Model, Entity, System. This pattern is based around the event pattern and tries to keep things as separate as possible, keeping flow of interactions in one direction exclusively and minimizing inter-dependencies (it doesn’t strictly enforce composition over inheritance but it does limit derived classes quite a bit). In the following sections I’ll explain each of the components of the pattern.
Archetypes
An archetype is defined as “a very typical example of a certain person or thing”, as such, archetypes in HAMES are the base classes from which Entities derive. They are generally abstract
classes (though they could easily be interfaces
) and contain the logic/data structure that all entities deriving from them will use. All archetypes provide factories for generating entities of their derived classes. Example:
The
ActionArchetype
is anabstract class
from which all Action Entities derive from. It provides the initialization logic for actions (setting up the executor, end/start position, energy cost, etc… of an action) as well as theActionFactory
which Action Entities will then use.
Entities
Entities are closer to “Components” in ECS than to actual “entities” (however, since in HAMES you never directly interact with objects, I liked the name better). Entities are concrete implementations of archetypes, they contain all logic/data needed by the entity itself. The idea here is that entities are in charge of their own data and provide the API for modifying and updating themselves. Entities can be broken down into two sub-classes: Game and System Entities.
Game Entities
Game Entities are completely self-contained entities. They typically represent concrete things in-game, derive from MonoBehaviour
and should never interact with other models, systems, entities or handlers. Examples of game entities are: units in a strategy game, a car in a racing game, NPCs, etc…
System Entities
System Entities are dynamically created entities that shouldn’t have a representation in-game (don’t need to derive from MonoBehaviour
). They are called System Entities because they can interact with other models directly (they are essentially dynamically generated systems). Example:
The
DivideAction
is a System Entity that derives from theActionArchetype
class and interacts with theSquadModel
,LocationModel
andGridModel
to divide a givenSquadEntity
in two. It also provides an implementation of theActionFactory
for creating new instances of itself.
Models
Models are directly pulled off from the MVC and MVVM patterns. They should follow the single responsibility principle “to the T” and are the data layer of a game and allow for retrieving, removing, updating and adding one specific archetype. They don’t need to derive from MonoBehaviour
and keep a reference/database/state of all entities of the specific archetype. Example:
The
ActionModel
uses theActionFactory
instance implemented by Action Entities to allow for the creation of newActionArchetype
s. It also keeps a list of all Action Entities currently in the game and provides an API for removing, retrieving and updating them.
Systems
Systems make use of models to accomplish a given functionality. They are the “business logic” layer of a game and expose functions in their API to accomplish specific functionalities. Systems should not keep a state (rellying instead in models for all their state needs), should limit their logic (ex: the PhaseSystem is in charge of executing Phases and only that), don’t need to derive from MonoBehaviour
and should limit their interactions to models as much as possible (ie: they should try to avoid calling other systems). Example:
The
PhaseSystem
makes use of theActionModel
to retrieve all Action Entities in the game and perform theirExecute
method.
Handlers
Handlers are pulled straight from the event pattern. They combine multiple systems and - in my current implementation - make use of Unity’s InputSystem
: receiving player inputs, parsing the InputAction.CallBackContext
into the specific arguments required by System’s functions and calling said functions. Again, Handlers should not keep a state and should be divided by the source of the events that trigger them (ex: InputHandler receives events from input devices while the ButtonHandler receives events from UI Buttons). In order to interact with Unity’s InputSystem
they most likely will need to derive from MonoBehaviour
. Example:
The
InputHandler
receives theLeftClickDown
event, pulls the mouse position from theInputAction.CallBackContext
and callsPlanningSystem.SelectSquad
andHighlightSystem.HighlightTilesInRange
to select a given squad (if one exists in the current mouse position) and highlight the tiles reachable by said squad.
Where do GameObjects Fit In?
In HAMES, GameObject
s are seen as the “View” in an MVC or MVVM pattern: they don’t contain any data or logic, they are simply what the user sees and interacts with. GameObjects should essentially be a sprite and a position (both of which can be updated from their associated Game Entities or Models). Keeping GameObjects separate from the data and logic layers allows for clean design, facilitates an easy to follow structure and simplifies coding/game development (since all logic lives in code rather than in the Unity editor).
Example Structure
What follows is an example folder structure for a Unity project that implements HAMES:
> Assets/ > AddressablesAssetsData/ > Library/ > Src/ > Archetypes/ > Entities/ > Game/ > SquadArchetype.cs > StructureArchetype.cs > System/ > ActionArchetype.cs > Enums/ > ActionType.cs > UnitType.cs > Structs/ > ActionStruct.cs > Entities/ > Game/ > SquadEntity.cs > Structures/ > OffensiveStructureEntity.cs > DefensiveStructureEntity.cs > System/ > Actions/ > DivideActionEntity.cs > MovementActionEntity.cs > Handlers/ > InputHandler.cs > ButtonHandler.cs > Models/ > SquadModel.cs > ActionModel.cs > StructureModel.cs > GridModel.cs > TileModel.cs > Objects/ > Squad.prefab > OffensiveStructure.prefab > DefensiveStructure.prefab > Systems/ > PhaseSystem.cs > HighlightSystem.cs > PlanningSystem.cs > StartupSystem.cs > Tests/ > IntegrationTests/ > E2ETests/ > Packages/ > ProjectSettings/ > UserSettings/
That’s it!
I truly hope this was helpful, please let me know what you think of the HAMES pattern in the comments below. I’d love to get your feedback, ideas, suggestions and any issues you think it might have. Thanks for reading!