GameArchitect

Musings on Game Engine Design

An Anatomy of Despair: Managers and Contexts

with 5 comments

Many of the design ideas that shaped the Despair Engine were reactions to problems that had arisen in earlier projects that we’d worked on.  One of those problems was the question of how to handle subsystem managers.

Many systems naturally lend themselves to a design that gathers the high-level functionality of the system behind a single interface:  A FileManager, for example, might expose functions for building a file database from packfiles and loose files.  An AudioManager might expose functions for loading and playing sound cues.  A SceneManager might expose functions for loading, moving and rendering models.

Once upon a time, these objects would all have been global variables.  There are a number of problems with using global objects as managers, though, the most critical of which is the uncertainty of static initialization and destruction order.  Your managers start out in a pristine garden of Eden, free of all knowledge of good, evil, and (most importantly) one another.  But as your game gets more and more complicated, your managers are going to develop more dependencies on one another.  To add streaming of sounds to AudioManager, for example, you may need to make it reference FileManager.  To stream asynchronously, you may also need to reference AsyncJobManager.  But at construction time, you can’t be sure that any of those managers exist yet.

This prompts clever people who’ve read Design Patterns to think, Aha!  I’ll make all my managers Meyers singletons!  And they go and write something like

FileManager& GetFileManager()
{ 
    static FileManager theFileManager; 
    return theFileManager;
}

This function will create a FileManager object the first time it’s called and register it for destruction at program exit.  There are still a couple of problems with that, though:

  • First, like all function-local static variables in C++98, theFileManager is inherently unthreadsafe.  If two threads happen to call GetFileManager simultaneously, and theFileManager hasn’t been constructed yet, they’ll both try to construct it.  Wackiness ensues.  This quibble is due to be fixed in C++0x.
  • Imagine what happens if the constructor for AudioManager calls GetFileManager and the constructor for FileManager calls GetAudioManager.  To quote chapter and verse of the ISO C++ Standard, “If control re-enters the declaration (recursively) while the [static] object is being initialized, the behavior is undefined.” (ISO Standard 6.7.4) The compiler can do whatever it wants, and whatever it does is unlikely to be what you want.
  • Although Meyers singletons give you a JIT safe order of construction, they make no promises about their order of destruction.  If AudioManager is destroyed at program exit time, but FileManager wants to call one last function in AudioManager from its destructor, then again you have undefined behavior.

We dealt with those issues in MechAssault 2 by eschewing automatic singletons in favor of explicitly constructed and destroyed singletons.  Every manager-type object had an interface that looked something like 

class FileManager
{
public:  
    static bool CreateInstance();
    static void DestroyInstance();
    static FileManager* GetInstance();
};

This works better than automatic singletons.  It worked well enough that we were able to ship a couple of quite successful games with this approach.  We wrote a high-level function in the game that constructed all the managers in some order and another high-level function that shut them all down again.  This was thread-safe, explicitly ordered and deterministic in its order of destruction.

But as we added more and more library managers, cracks started to show.  The main problem was that although we constructed managers in an explicit order, dependencies between managers were still implicit.  Suppose, for example, that AudioManager is created before FileManager, but that you’re tasked with scanning the filesystem for audio files at start-up time.  That makes AudioManager dependent on FileManager for the first time.  Now AudioManager needs to be created after FileManager.

Changing the order in which managers were constructed was always fraught with peril.  Finding a new working order was a time-consuming process because ordering failures weren’t apparent at compile time.  To catch them, you needed to actually run the game and see what code would crash dereferencing null pointers.  And once a new ordering was found, it needed to be propagated to every application that used the reordered managers.  Day 1 has always placed a strong emphasis on its content creation tools, and most of those tools link with some subset of the game’s core libraries, so a change might need to be duplicated in six or eight tools, all of which would always compile–but possibly fail at runtime.

With Despair, one of our governing principles has been to make implicit relationships explicit.  As applied to system manager construction, that meant that managers would no longer have static GetInstance functions that could be called whether the manager existed or not.  Instead, each manager takes pointers to other managers as constructor parameters.  As long as you don’t try to pass a manager’s constructor a null pointer (which will assert), any order-of-initialization errors will be compile-time failures.  To make sure that managers are also destroyed in a valid order, we use ref-counted smart pointers to refer from one manager to another.  As long any manager exists, the managers that it requires will also exist.

Our current code looks something like  

class IAudioManager
{
public:
   IAudioManagerPtr Create(const IFileManagerPtr& fileMgr);
};

class AudioManager
{
public:
   explicit AudioManager(const IFileManagerPtr& fileMgr);
};

One problem remains.  Although AudioManager exposes the public interface of the audio library to the rest of the application, there are also private support and utility functions that the audio system should only expose to its own resources.  Furthermore, many of these utility functions will probably need access to lower-level library managers.

This could be solved by having global utility functions that take manager references as parameters and by making every audio resource hold onto pointers to every lower-level manager that it might use.  But that would bloat our objects and add reference-counting overhead.  A more efficient and better-encapsulated solution was to give each library a context object.  As Allan Kelly describes in his paper on the Encapsulated Context pattern:  

A system contains data that must be generally available to divergent parts of the system but we wish to avoid using long parameter lists to functions or globaldata.  Therefore, we place the necessary data in a Context Object and pass this object from function to function.    

In our case, that meant wrapping up all smart pointers to lower-level libraries in a single library context, which would in turn be owned by the library manager and (for maximum safety) by all other objects in the library that need to access a lower-level manager.  Over time, other private library shared state has crept into our library contexts as well.  To maintain a strict ordering, the library manager generally references all the objects that it creates, and they reference the library context, but know nothing about the library manager.

This architecture has generally worked well for us, and has been entirely successful in avoiding the manager order-of-creation and order-of-shutdown issues that plagued earlier games we worked on.  It does have definite costs, however:

  •  A lot of typing.  Library managers know about library resource objects which know about library contexts.  But frequently there’s some functionality that you’d like to be accessible both to objects outside the library and objects inside the library.  In this architecture, there’s nowhere to put that functionality except in the library context, with pass-through functions in the library manager.  Since the library manager is usually hidden behind an abstract interface, you can end up adding function declarations to three headers and implementations to two cpp files before you’re done.
  • Even more typing.  All the lower-level managers held by a library also get passed through a creation function to the library manager’s constructor to the library context’s constructor.  That was a mild annoyance when the engine was small and managers were few, but Despair now comprises over fifty libraries.  Most of those libraries have managers, and there are a handful of high-level library managers that take almost all lower-level library managers as constructor parameters.  Managers that take forty or fifty smart pointers as parameters are hard to create and slow to compile.
  • Reference counting woes.  In principle, every object should strongly reference its library context to maintain a strict hierarchy of ownership and to make sure that nothing is left accessing freed memory at program shutdown time.  In practice, though, this doesn’t work well when objects can be created in multiple threads.  Without interlocked operations, your reference counts will get out of sync and eventually your program will probably crash.  But with interlocked operations, adding references to library contexts becomes much more expensive, and can become a significant part of your object creation costs.  In practice, we’ve ended up using raw pointers to contexts in several libraries where the extra safety wasn’t worth the additional reference cost.

So managers and contexts are far from perfect.  They’re just the best way we’ve found so far to stay safe in an ever-more complex virtual world.

Written by Kyle

September 13th, 2008 at 7:46 pm

5 Responses to 'An Anatomy of Despair: Managers and Contexts'

Subscribe to comments with RSS or TrackBack to 'An Anatomy of Despair: Managers and Contexts'.

  1. As usual, very interesting to read.
    Thanks for sharing Kyle !

    Guillaume

    17 Sep 08 at 12:58 pm

  2. There’s an idea that I always wanted to try but have never gotten the chance to.

    The problem with singletons is that you can never make a system independent of other systems if they are pulling each other’s singletons into themselves left and right. How about if each system declares an interface – file system, memory allocation, and the like – and then an outside system injects the implementation of those interfaces into the various subsystems.

    The disadvantages are, as I see them:

    * Virtual call overhead (and lack of inlining etc)
    * Lots of boilerplate code to make your actual file system (eg) implementation adhere to the various subsystems’ file system interfaces.

    The advantages are:

    * Total decoupling of systems, leading to…
    * Reuseability of the systems since other clients can now cherry-pick the systems they want without getting the whole code base with it.

    Tomas

    9 Oct 08 at 3:49 pm

  3. Very nice post, I really enjoy those insights in professional game development, thanks a lot for sharing.

    I have encountered some of the issues you present in the development of the Phoenix engine* for my uni course.
    Here’s my take at solving the singleton hell:
    Andrei Alexandrescu describes in his book Modern C++ Design a complex singleton template based on policy classes.

    Those policies allow a lot of customization of the behavior of the singleton; one of those policies being the singletonWithLongevity.

    Essentially each singleton class specify a static function that returns its longevity as an unsigned int in its class declaration body.

    The policy uses that number when it builds a list of singletons to sort them by order of destruction.
    It then registers each of them with atexit.
    Singleton instantiation is JIT.

    When one needs to know if they can access a singleton from another singleton, it’s a simple matter of looking up in the header file for the getLongevity method and comparing the numbers.

    Now for the icing on the cake, by using Alexandrescu’s static_check (or compile time assertion) and the static longevity number, checking and changing the order of destruction of the singletons becomes a breeze :)

    Of course the whole thing is thread-safe thanks to the threading policy.

    * For the story, the name comes from the fact that every semester we pretty much rebuild the engine from “scratch” but keeping the good elements from the previous design.
    Like the Phoenix, it never really dies :)

    Julien

    30 Oct 09 at 7:29 am

  4. Hit my head with a club if I’m wrong, but you’re advocating:

    * the use of managers
    * grouping manager instances into a structure

    Now, I might be a wrong doer, but that’s a bit too much for me.

    * managers are low cohesion, high coupling classes that are typically very stable and concrete. (OK ; simplified reading gives: a manager is made of several parts that have little in common (low cohesion), involve a lot of dependencies (high coupling), are difficult to modify because they are used everywhere (very stable), and cannot be extended because they use no abstraction (concrete)). That’s a 0 out of 4 in the realm of ‘do the correct design’. Most managers can be implemented as a factory, a collection and possibly a cache. That’s 3 parts, and that should be 3 classes. The behavior of the factory, the collection and the cache should be modifiable, so that even if they are coupled, they are not coupled to other part of the software. There is no such thing as a manager.

    * library context – wait, what’s that? As I read your description, it appears to me that it’s a God class.

    I kind of agree with you: neither managers nor contexts are perfect. I would go farther than you in your conclusion: I bet that if you’d stayed far away from both managers and contexts, you’d be safer in the ever-more complex virtual world. I would love to see a dependency graph of Despair. OK, maybe not (frankly, 50 libraries, most of them with a manager?)

    BTW, I really love your blog. It’s just that post… Sorry.

    Emmanuel Deloget

    15 Jul 10 at 4:11 pm

  5. ———————————————
    We dealt with those issues in MechAssault 2 by eschewing automatic singletons in favor of explicitly constructed and destroyed singletons. Every manager-type object had an interface that looked something like

    class FileManager
    {
    public:
    static bool CreateInstance();
    static void DestroyInstance();
    static FileManager* GetInstance();
    };
    ————————————————

    But why using Singlenton? Why all methods can’t be static? Why this can’t be like that:

    class FileManager
    {
    public:
    static bool CreateInstance();
    static void DestroyInstance();

    static File* LoadFile(const std::string &name);
    static void ExampleMethod();
    };

    Szefoski

    25 Jul 11 at 12:03 am

Leave a Reply