Whether you are a game programmer or a game engine developer, a well-organized architecture is crucial to keep your project maintainable, readable, and safe— especially if you work with a team on a large codebase.
This article aims to present the concept of Dependency Injection, and to show how it can help you to easily structure your game architecture in a robust and flexible way, without any performance overhead.
Using a dummy game application as a practical example, we will start by defining the problem we intend to solve. We will try different strategies, and demonstrate how Dependency Injection emerges as the ideal solution.
We will use C++, the go-to language for high-performance applications, but the ideas discussed are universal, and applicable in other languages.
The code presented in this article is available on GitHub.
Problem Definition
Games are complex applications, consisting of multiple unique systems, such as Rendering or Physics. In a typical game application, systems are initialized on startup, they interact with each other for the lifetime of the application, and are destroyed when the application terminates.
Some systems are specific to the game being developed, while others are more general, and are typically wrapped into a game engine:
In our discussion we will not consider this separation, and refer to any system in the application as a game system.
Let’s now introduce the example that will accompany us for the rest of this article — a dummy 2D game application where two red circles bounce around the screen. Our game consists of just a handful of systems:
State
holds the state of the game: the circle positions and velocities.Simulation
moves circles in theupdate()
method, modifyingState
.Physics
is used bySimulation
inupdate()
to resolve circle collisions.Rendering
readsState
, and draws circles on screen withrender()
.OpenGL
wraps the OpenGL API, required byRendering
torender()
.Loop
runs the game loop, which callsupdate()
and thenrender()
.
Let’s identify the dependencies between our systems:
The design of our application is nicely represented by a graph, where nodes represent systems, and edges represent dependencies. In particular, this is a directed acyclic graph, where there is no circular dependency.
Now that we have a solid theoretical understanding of how to organize our architecture, it is time to introduce the concrete problem at the core of this article: how do we turn this theory into practice?
We wish to find a way to accurately express our theoretical architecture in code, with a focus on clarity, safety, modularity and performance.
Instead of trying to derive our solution from these high-level goals, in the next section we will proceed bottom-up, and dive into implementation. As we try different strategies, we will get a practical understanding of what problems we wish to avoid, and we will define rules that we wish to follow.
Eventually, we will converge on an ideal solution: Dependency Injection.
Further reading:
For simplicity, we ignore the situation where systems implement and depend on interfaces, quite common in real game applications. If you are interested to know more, refer to the appendix on Dependency Inversion after finishing this article.
Problem Solution
Attempt 1: Systems are global variables
Based on the problem definition, we know that game systems must be unique, created at startup, and destroyed at shutdown. These requirements are satisfied by creating a global variable for each system:
// State.h
class State{...}; // class definition
extern State g_state; // global variable declaration
// State.cpp
State g_state{}; // global variable definition
Systems access dependencies directly from global state:
#include <State.h>
Simulation::Simulation()
{
g_state.read();
}
One problem with global variables is that we do not control their construction order. If g_simulation
happens to be constructed before g_state
, the above code causes an uninitialized memory access.
We wish to have a way to guarantee that g_simulation
is constructed after g_state
. And, to avoid the same issue in the destructor, we also wish to guarantee that g_simulation
is destroyed before g_state
.
Let’s formalize our wish in a rule:
Rule 1: Systems must be created after / destroyed before dependencies.
Another problem with global variables is that their access is not limited to constructors — they can be accessed from anywhere:
void Simulation::update()
{
g_state.write();
}
This freedom has a side effect — it becomes difficult to understand the dependencies of a system, because they are not listed in a single place.
Unclear dependencies make it hard to understand the architecture, to refactor, and to make changes. Let’s define a rule to prevent this situation:
Rule 2: Dependencies must be explicit.
The final issue with global variables is the potential to easily introduce circular dependencies between systems:
void Physics::foo()
{
g_rendering.bar();
}
void Rendering::foo()
{
g_physics.bar();
}
To ensure that our codebase remains modular and maintainable, we wish to forbid circular dependencies altogether:
Rule 3: Introducing circular dependencies must be prevented.
Attempt 2: Systems own dependencies
Let’s make systems responsible for managing their own dependencies.
The dependencies are member variables, created in the constructor:
Loop::Loop()
: m_simulation{} // dependency created here
, m_rendering{}
{}
Loop::~Loop(){} // dependency destroyed here
Using constructor recursion, a system creates its entire dependency subgraph. Consequently, constructing the root system Loop
triggers the creation of every system.
We construct our application by creating Loop
on the stack of main()
:
int main()
{
Loop loop{}; // recursively construct every system
}
A problem arises when multiple systems, like Simulation
and Rendering
, depend on the same system, such as State
. Both Simulation
and Rendering
create separate copies of State
, but systems must be unique.
To resolve this, we can create State
within Loop
, the parent system of Simulation
and Rendering
, and have Loop
share State
with their constructors by reference:
Loop::Loop()
: m_state{} // create here
, m_simulation{m_state} // share reference
, m_rendering{m_state} // share reference
{}
This solution patches the problem, but it introduces an extra dependency. We have failed to exactly map our theoretical architecture into code:
There is a fundamental problem with this approach that prevents us from converging on an ideal solution. In order to understand it, we should take a step back, and start thinking in terms of responsibilities.
Systems are burdened with two responsibilities:
Functionality: executing the logic that defines system behavior.
Structure: creating, destroying, organizing, passing dependencies.
The dependency graph should only reflect dependencies that occur due to functionality, such as Simulation
depending on Physics
for collision resolution. However, the need for organizing the application structure introduces additional dependencies, such as Loop
depending on State
.
To resolve this problem, we wish to follow the single responsibility principle, and give systems a single responsibility: functionality.
Rule 4: Systems must not be responsible to structure the application.
Successful attempt: Systems receive dependencies
In this final attempt, the responsibility for structuring the application is moved to main()
, where every system is created in the correct order, and passed as dependency to other systems:
int main()
{
State state{};
Physics physics{};
Simulation simulation {state, physics};
OpenGL openGL{};
Rendering rendering{state, openGL};
Loop loop{simulation, rendering};
} // out of scope: system destructed in reverse order
Systems are no longer responsible for structuring the application. They receive their dependencies through the constructor:
Loop::Loop(Simulation& simulationRef, Rendering& renderingRef)
, m_simulationRef{simulationRef}
, m_renderingRef{renderingRef}
{}
This approach follows all our rules:
Rule 1: Systems must be created after / destroyed before dependencies.
✅ System is outlived by its dependency, as it is created later on the stack.
Rule 2: Dependencies must be explicit.
✅ The signature of the system constructor lists all dependencies.
Rule 3: Introducing circular dependencies must be prevented.
✅ It is impossible to order system creation to cause a cycle.
Rule 4: Systems must not be responsible to structure the application.
✅ Application structure is entirely determined in main().
Success!
We converged on a solution that precisely implements our theoretical architecture, while being simple, safe, modular and efficient.
As promised, this ideal solution is none other than an implementation of Dependency Injection.
Dependency Injection is a programming technique in which an object [..] receives other objects [..] that it depends on.
(Wikipedia)
Further reading:
In applications with a large number of systems, the cost to write and modify the code inmain()
is significant. If you wish to learn of an automated solution to reduce the boilerplate code, refer to the appendix on Injection Container.
Conclusions
I hope this article was able to make you excited about Dependency Injection, and its application in game development.
By adopting Dependency Injection, you will quickly realize how radically it improves your everyday game development experience:
You will easily understand, reason and discuss your architecture.
You will quickly refactor and make changes with peace of mind.
You will never worry about accessing uninitialized dependencies.
I suggest that you try adopting Dependency Injection in your own project, and I believe you will not be able to look back!
Next steps
Check out the accompanying GitHub repository, which contains an implementation of the dummy game application.
Read the appendices on Dependency Inversion and Injection Container.
If you have any questions, or wish to share your thoughts, please feel free to leave a comment, or to contact me on LinkedIn.
Top comments (0)