Every game or game engine has at least a few managers under the hood. A manager is a collection of functions and data whose purpose is to “manage” something. You may send HTTP requests through an HttpManager
. You may track the player’s inventory with an InventoryManager
. You may play audio through an AudioManager
. And so on.
How you access a manager is a seemingly mundane decision that can be surprisingly complex and paralysing. Do you pass the manager as a function parameter? Use a global variable? Make a bunch of static functions or a namespace? More elaborate mechanisms? Each option has pros and cons, and personal preference comes into play as well. Changing your mind later can incur significant refactoring overhead.
I’ve been coding games for about a decade now, so I’ve developed some opinions and ideas. I don’t think there’s a “best” or “right” way to do this, but I do currently have a “favorite” method. Below, I’ll review several access options, some pros/cons of each, and finally explain which one I’m liking the most lately.
Singletons
The idea behind the singleton is pretty straightforward: a class contains a static function called Instance()
. The first time you call this function, it creates and stores the class instance in a static variable. Subsequent calls to Instance()
return the previously created instance. You can even make the constructor private, thereby ensuring that the only way to access OR create an instance of the class is by calling Instance()
:
class Manager
{
public:
static Manager& Instance()
{
static Manager manager;
return manager;
}
void PerformAction();
private:
Manager() = default;
};
Then, elsewhere in your code, you can access Manager functions easily:
Manager::Instance().PerformAction();
In this example, I’m using a “static local variable” to store the created instance. Memory for such variables is allocated globally (when the program starts), but the instance is not constructed until that line of code is executed. Subsequent calls to Instance()
just return the existing manager, skipping construction of a new instance.
The singleton approach faciliates “lazy initialization,” which can be either a blessing or a curse. Since the object is only constructed the first time Instance()
is called, it may be possible to delay or entirely avoid constructing the object, thereby making the program launch faster. On the flipside, if you accidentally trigger construction in the middle of gameplay, it can cause a noticeable framerate drop.
A variation of the singleton pattern just provides global access while disallowing multiple instances:
// Manager.h
class Manager
{
public:
static Manager& Instance()
{
return *instance;
}
Manager()
{
assert(instance == nullptr);
instance = this;
}
private:
static Manager* instance;
};
// Manager.cpp
Manager* Manager::instance = nullptr;
In this case, you must be sure that you’ve manually constructed the class instance before you call Instance()
. Attempting to construct a second instance will also trigger an assert, so don’t do that! I use this variation in
my engine project. This variation is also useful when normal singletons are not possible - such as Unity engine Monobehavior subclasses.
Static Variables/Functions
If our intention is to only have one instance, a simple option is to implement the manager purely as static variables and functions:
// Manager.h
class Manager
{
public:
static void PerformAction();
private:
static int sMyInt;
};
// Manager.cpp
int Manager::sMyInt = 0;
void Manager::PerformAction()
{
++sMyInt;
}
Because the functions and data are static, they are associated with the Manager namespace, but not any specific instance of the Manager class. You can then access this elsewhere in your code:
Manager::PerformAction();
Despite the C++ syntax, we are essentially just creating functions/variables that are within a “Manager” namespace. However, we are still able to use C++ access modifiers - in this example, the static variable myInt
is not accessible to outside parties.
One limitation of this approach is that there’s no possibility for inheritance. Also, if you think you might need multiple instances of the object in the future, it’s harder to refactor this into a normal C++ class. Finally, there is no way to define a static constructor/destructor - use separate Init/Shutdown functions if such logic is needed.
One interesting thing about this approach is that it begs the question as to whether a separate manager class is needed at all. For example, let’s say you have these two classes:
class EventManager
{
public:
static Event CreateEvent();
}
class Event
{
private:
std::string mName;
}
You could combine the two and have management functions exist ON the object class itself:
class Event
{
public:
static Event CreateEvent();
private:
std::string mName;
};
If your manager functions are not very complex, this can be a good approach. But I like to keep management functions separate if there are too many of them, or they are very complicated.
Namespaces
Taking the previous approach a little further, you can forego a class entirely and just use a namespace, which makes the syntax a bit simpler:
// Manager.h
namespace Manager
{
void PerformAction();
}
// Manager.cpp
static int myInt = 0;
void Manager::PerformAction()
{
++myInt;
}
You can then access this manager elsewhere like this:
Manager::PerformAction();
This approach is nearly identical to the previous “static” approach, just with different syntax. The variable myInt
is declared as a static global variable in the .cpp file, so it is still technically a “private” variable for the manager. This approach has the same limitations as the “static” approach (no inheritance, no multiple instances, no constructor/destructor).
I generally favor this approach for “utility” type classes where the functions are very self-contained (outside of maybe calling standard library functions). For example, GEngine has a StringUtil implemented as a namespace with various useful string manipulation functions.
Global Variables
You can find plenty of advice online telling you to avoid global variables. And it’s true that your program can quickly devolve into a tangled mess if you get too cozy with the globals.
But here’s the thing: sometimes global variables are OK. Use them cautiously and sparingly - they can be effective.
In this approach, we create a header file for the class which is also responsible for declaring (but not defining) a global variable:
// Manager.h
class Manager
{
public:
void PerformAction();
};
extern Manager gManager;
In C++, you can declare static and global variables in a header file, but you must also define the variables in a cpp file:
// Manager.cpp
Manager gManager;
void Manager::PerformAction()
{
// Code goes here
}
You can then access the manager elsewhere:
gManager.PerformAction();
There is something simple and powerful about directly grabbing the handle to the manager and doing what you’ve got to do - no functions to call, no lookups to perform, no multiple levels of pointer indirection. A project I recently worked on has been using this approach successfully for a decade.
If this makes you feel icky, consider that most alternatives can still be viewed as global variables to some degree. For example, aren’t singletons just a global variable that you hide inside a static function?
Single Global Instance
Let’s say we have 10 or 20 manager classes and we’re feeling a bit squeamish about making them all singletons or global variables. Perhaps we don’t like how decentralized that’d be, and we want to keep things in a single organized place (it’s nice to know at a glance what manager classes exist in your game). Or perhaps we just don’t like singletons and global variables!
One way to get the benefit of global access without spreading it to every class is to choose a single class that will act as the “single global instance” in the program. A “manager of managers” if you will:
class Managers
{
public:
CharacterManager characterManager;
LevelManager levelManager;
HttpManager httpManager;
EventManager eventManager;
// And so on.
};
This class can implement one of the global access methods outlined earlier. As a result, we have just one class that has direct global access mechanisms, but all the contained classes can also be reached fairly easily.
We can then access managers elsewhere like this:
Managers::Instance().characterManager.PerformAction();
If you think that function call is unwieldy and ugly, I agree! One way to alleviate this is by using static class variables that are named effectively:
// Managers.h
class Managers
{
public:
static CharacterManager Characters;
};
// Elsewhere - ahh, much better.
Managers::Characters.PerformAction();
This shortens the function call significantly and conveys only the important information (we are getting a manager, for characters, and performing this particular action).
Service Locator
The key feature of the service locator is that instead of accessing a manager variable directly, we obtain it using a templated “getter” function. The getter function locates the manager in a map/dictionary structure and returns it:
// Managers.h
class Managers
{
public:
template<class T> static void Set(T* instance);
template<class T> static T* Get();
private:
// "ClassType" is a hashable unique identifier for a class.
static std::unordered_map<ClassType, void*> sTypeToInstancePointer;
}
During initialization, you must populate the dictionary:
Managers::Set<MyManager>(new MyManager());
You can then obtain a manager elsewhere in your code like this:
Managers::Get<MyManager>()->PerformAction();
An implementation of this approach can be found here.
The defining characteristic of the service locator is that it’s rather dynamic and generalized. Instead of defining a set number of managers at compile-time, you can specify any number of managers via Set()
and obtain them later via Get()
.
This approach really shines when you use interfaces. For example, if you have an IRenderer
interface that is implemented by different concrete classes for different platforms (RendererD3D, RenderOGL, RendererPS4, etc), you can do something like this to easily swap between them at runtime:
// In game init code:
#if defined(ON_WINDOWS)
Managers::Set<IRenderer>(new RendererD3D());
#else
Managers::Set<IRenderer>(new RendererOGL());
#endif
// Later on in rendering code, not having to worry about platform-specific stuff:
Managers::Get<IRenderer>()->Draw();
There are downsides to this approach. It’s obviously more complicated to implement and use than other methods. It has more likelihood to fail at runtime (what if Get returns a nullptr?). Storing class instances in a map requires some sort of RTTI. And performance is undoubtedly worse just because getting an instance incurs a map lookup and pointer indirection.
In the past, I thought this approach was just the absolute best because it wasn’t much code to write, it was flexible/extendable, and it let you swap implementations using interfaces. Nowadays, I think it has its place, but it feels a bit over-engineered and probably overkill for a lot of scenarios.
Making a Choice
All these options solve the problem…so how do you choose?
Global Access
The methods discussed above boil down to accessing a globally available namespace or variable. Conventional wisdom says global access is a Bad Thing(TM): it increases complexity, it increases coupling between systems, and it makes refactoring more difficult.
The alternative is local access. The most local option is to pass manager references into functions as parameters. The next-most-local option is to store manager references as class member variables. In theory, it’s nice to think about a program that uses no global access; but in practice, this can become tedious.
Rather than never using globals, I think it’s more helpful to be cautious and use it sparingly. Don’t use it for every little variable in your program, but don’t be afraid to use it on occasion and after serious consideration. If ALL global access is bad, you’ll find yourself using increasingly complex alternatives that hide, but probably don’t truly resolve, global access.
Allocation, Construction, and Initialization
These options have various allocation and construction characteristics:
- Allocation: where and when is the memory allocated for the object?
- Construction: when is the object constructed?
Global and static variables (including static local variables) are allocated as the program starts, before main()
executes. This can be beneficial: they don’t eat up limited stack space, and they don’t need to be allocated dynamically.
Global and static class variables are also constructed when the program starts, but with one massive problem: the order of construction is undefined! This means that if there are dependencies between your global/static constructors, you are likely to run into trouble.
One common workaround is to separate construction from initialization. This means constructors do nothing except perhaps set default values for member variables. There is a separate function Init()
that performs initialization, and this function is called during program startup in the correct ordering based on dependencies:
void Engine::Init()
{
// MusicManager depends on AudioManager, and so must be initialized after.
gAudioManager.Init();
gMusicManager.Init();
}
Destruction order is also undefined, so it is useful to also define custom Shutdown()
functions:
void Engine::Shutdown()
{
// Usually, shutdown in opposite order of init.
gMusicManager.Shutdown();
gAudioManager.Shutdown();
}
Note that static local variables do not suffer from this undefined construction order problem - they are constructed when the containing function is executed and their line of code is reached.
Headers
When I teach C++, students are often bewildered at how complex header management can be. When I’m explaining the ins and outs, I am also self-aware at how ridiculous it seems. But it is what it is, so it’s useful to understand it and think about it.
Compilation time can be directly tied to how you include header files. Making a small tweak in a header that is included in hundreds of .cpp files can trigger a very long recompile. On the other hand, if the header is only included in one or two .cpp files, the recompile might be very short.
Additionally, if you include one header in your .cpp file, but that header includes a hundred other headers, your seemingly simple .cpp file is suddenly thousands of lines long once all the includes are considered. This also increases compile times.
In general, I advise only including headers that are immediately relevant to the current file. If you use std::vector
, include <vector>
. If you need to call a function in your audio manager, include AudioManager.h
.
Of the above approaches, the “Single Global Instance” and “Service Locator” patterns can pose a problem here. Let’s say you have your global “manager of managers” class that itself has a dozen managers:
// Managers.h
#include "AudioManager.h"
#include "CharacterManager.h"
// Several dozen other includes here.
class Managers
{
public:
static AudioManager Audio;
static CharacterManager Characters;
// Several dozen other managers here.
};
You want to play a sound effect, so you need to access Managers::Audio
. So you include Managers.h
. But now you’ve inadvertently also included ALL those other headers! This is nearly invisible, but over time, you suddenly have every .cpp file including virtually every header.
The Best Way?
As hinted earlier, I wouldn’t be so bold as to proclaim any method “the best.” However, I can identify the approach that I’m currently quite fond of, and why that is the case.
And here it is: I have found global variables to be quite simple and effective.
It is clearly quite easy to access the manager. Use a global variable with a unique name to avoid naming clashes. Include the header for the manager, and you’ve got access to it.
Such global variables are allocated on program start. Use the Init/Shutdown approach to explicitly define initialization and shutdown orders; this is clearer for understanding how the game works anyway.
The manager is globally accessible, you are still able to use inheritance, and you aren’t limited to only creating a single instance. If you later need a local or secondary instance of the manager, you can easily create it.
You also don’t really have the header problem described above. The header for each manager also declares the manager’s access point. If you need to play an audio file, you just include AudioManager.h
. The class dependencies are quite clear by looking at the headers.
If this choice seems obvious, I must emphasize that my introduction to coding was filled with “globals are evil” tips. Maybe they still are and I’ve just been swayed to the dark side. For certain hyper-complex projects, globals may be extremely problematic. Maybe I’ll look back on this post in the future and think “wow I was dumb.” But for now, they are simple and elegant and do the job well. I can’t argue with the results.