Recently, I was talking with some of colleagues about the issue with data initialization when booting your game. Mostly the issue with globals, and rules about how to shut them down, order of init/shutdown. I looked back to the different projects I worked on, and realized there are a few ways to approach the problem.
Before I go into the details, its important to notice that systems are singletons. There is only one instance of them in memory at any given time. And there are different ways to manage those. Mostly to initialize/shut them down, access them etc… Here’s a few different techniques that I’ve seen, and I will try to explain their advantages and flaws.
I’ve seen games do different initializations:
First, most naive is to just have a list of globals for your systems, which are auto-initialized when the game boots.
Renderer renderer;
MemoryAllocator memoryAllocator;
void main()
{
while (!quit)
{
DoMainLoop();
renderer.DoStuff();
}
}
This is very simple but its has one huge flaw. It is that you have no control over the order of initialization. Different compilers will generate different initializer lists when it comes to contructing the globals. That means that you can’t have a dependency between systems, which is usually a huge issue. For example, if your renderer needs to allocate memory, the memory system needs to be constructed before the renderer, and using globals, you can’t guarantee in hell it will be the case.
So you would approach the problem another way: You could initialize on first use.
class Renderer
{
static Renderer *renderer;
static Renderer* GetInstance()
{
if (!renderer)
{
renderer = new Renderer;
}
return renderer;
}
};
or
class Renderer
{
static Renderer* GetInstance()
{
// This will initialize the first time we call that function
static Renderer renderer;
return &renderer;
}
};
This somewhat fixes the issue of dependencies. When a system needs to allocate memory, the allocator will “construct” itself on first allocation. But this have a preformance issue. It needs to check on each use if the system is valid or not to decide if it needs to be constructed. Also, while it might work for construction, it won’t work for destructor. So this a technique that should be avoided.
Another approach is to allocate your systems on the heap in the right order (so explicit dependencies):
Renderer *renderer;
MemoryAllocator *memoryAllocator;
void main()
{
memoryAllocator = new MemoryAllocator;
renderer = new Renderer();
while (!quit)
{
DoMainLoop();
}
delete renderer();
delete memoryAllocator();
}
This is now much closer to what we need. We have a defined order of initialization and shutdown and we don’t have any performance issue when accessing those systems. But we still have one problem. The memory allocator has to be initialized first, and it can’t depend on anything.
My prefered way, which I found to always be rock solid is to do something like this:
The initialization/shutdown code snipset would look like this:
void main()
{
Core::Memory::Init();
Graphics::Init();
while (!quit)
{
DoMainLoop();
}
Graphics::Done();
Core::Memory::Done();
}
Where Core and Graphics are namespaces for your different systems and they do their static initialization.
Then, use the placement new/delete for your globals in the Init/Done functions to construct/destruct the system. For example it would look like this:
namespace Graphics
{
char systemMem[sizeof(Renderer)];
Renderer &renderer = reinterpret_cast(systemMem);
void Init()
{
new (&renderer)Renderer;
}
void Done()
{
renderer.~Renderer();
}
}
This has lots of advantages. You have solved the issue where the memory allocator needs to exist first. For example, you could have your logging system be initialized before the memory allocator so that the memory allocator can use it to track down its allocations. Then you still have total control over the order of which your systems are initialized (and shutdown for that matter). You also get the performance of not checking if your system has been initialized or not for each access to it. And finally, a smaller advantage is that your system is guaranteed valid once you went through the initialization list. With a pointer there will always be a doubt.
The one small disadvantage is that you need to check your .bss to see how much memory your systems are using. Its a small annoyance. Some people would prefer to have the smallest .bss section and have everything in their heap. It helps them to have a global view of their memory map. But checking the .map file to track down your .bss memory is not that much of a hassle. And you could add the tracking of those elements in your memory tracking tool.
Another thing related to systems, are the ones created during gameplay. For example how a scene is loaded/unloaded. But I will talk about this in a later article.
Palle wrote:
I’ve always done as such (similar to your preferred approach):
(.h)
class MySystem {
public:
MySystem(MyMemorySystem &); // explicit dependecy on the memory system
// Optional. I prefer explicit dependencies on globals.
[static|friend] MySystem& GetMySystemInstance(); // returns *smInstance
private:
static MySystem* smInstance;
};
(.cpp)
MySystem* MySystem::smInstance = nullptr;
MySystem::MySystem(MyMemorySystem& memory) {
assert(!smInstance); // or similar
smInstance = this;
// …
}
Now just create them on the stack in your main (in any order appropriate) and they’ll
get destroyed when they go out of scope, i.e., no need to pair a call to Init with a
call to Destroy.
// Palle
Link | March 16th, 2011 at 4:22 am
esmolikowski wrote:
The problem with this is that your systems can be fairly large. Putting them on stack can be costly, you end up needing a large stack, which is usually not desirable, specially on consoles. Also, on some platforms, its possible the stack will end not be in the same memory space as the rest of your data, which could cause other issues.
Link | March 24th, 2011 at 6:07 am
dominic wrote:
Hi, pretty interesting article, but I’m not sure I understand why you say that your last example solves the issue where the memory allocator needs to exist first. The allocator still needs to be created before any other system makes us of it. Even in the last example, order of initialization of the systems is important. And it seems that, even in previous examples, adding a logging system before the memory allocator is still possible, for example when the systems are allocated on the heap. They just need to be allocated in the right order.
Maybe I am missing the point or have misunderstood, but other than ensuring system validity, I don’t see much use for using a namespace with an Init and Destroy function. Furthermore, whenever I need access to a system I need to grab it in it’s namespace, which can end up being verbose IMHO.
Cheers,
Dominic.
Link | November 13th, 2012 at 8:43 am