Like most game engines, Tyr has a number of managers, these managers are typically responsible for loading, saving and manipulating certain objects, they are also (in Tyr at-least) responsible for owning the objects, meaning that they control the lifetime of said objects and are responsible for deletion (to this end, I usually make objects owned by a manager have a private destructor, so that attempting to delete them not through the manager is a compile error).
Now there is a set order, logically that these managers should themselves be created, granted for some of them it doesn’t matter (should the texture manager be created before the shader manager?), whilst for others, it is pretty easy (the Logger, responsible for logging warnings and errors, should probably be first) and if there is an order they should be constructed, they should be destructed in reverse order, otherwise nasty things may occur.
So, I set about trying to come up with a nice way to make such an arrangement happen, this post will document my journey.
First things first, we have some common behaviour, all the managers should have a priority, and throw in Init and Shutdown methods for good measure (these will be called based on the priority) so for that we need a BaseManager (which I was very tempted to call ManagerManager), the base manager doesn’t do much at the moment:
class BaseManager : public NonCopyable
{
public:
//! Constructor
BaseManager(
u8 priority //!< Priority of the manager (lower is higher)
);
//! Destructor
virtual ~BaseManager();
//! Initialise (called in order of priority)
//! \return True on success
virtual bool Init() = 0;
//! Shut-down
//! \return True on success
virtual bool Shutdown() = 0;
//! Get the priority of the manager
//! \return The priority
u8 GetPriority() const;
private:
u8 m_Priority; //!< Priority of this manager, lower higher in list
};
The main point of node is the priority member variable, which is a u8, allowing 255 managers (should be enough for anyone, right?), now the comment for this member talks about a list, and we'll get to that in a moment, but allow be to indulge into a brief tangent on static initialisation in C++.
Lots of people would implement these managers using the Singleton design pattern, indeed it used to be my weapon of choice for such tasks, and it has a lot of merits, especially when you get to the more advanced versions which allow you to control when they are constructed and deleted, however, I grew tired of singletons for a variety of reasons, for one they give you a false feeling of safety (even though it is pretty easy to maliciously, or accidentally create more than one) they can have issues being used in .dlls, and also, typing out Singleton::Instance() (or your equivalent) is a tad verbose and does get tiring after a while.
So now I use global variables straight-up, rather than attempting to hide behind a design pattern to make them go away, global variables are of-course bad, as are macros, and both will be used judiciously here, generally, all the 'bad' things that you 'should never use' end up being useful after a while, you just have to know the risks and decide they're worth it.
So, back to static initialisation (forgive my tangent from my tangent). See all my managers are declared by an extern, something like:
extern TextureManager g_TextureManager;
This is nice and simple but it does mean that the manager object is constructed during static initialisation and thus I have no say in whether this manager is constructed before that manager, which is slightly ok, after-all we made that Init() method so that we could initialise the managers in a different order to the one they were constructed in. The problem, as it turns out, lies with the fact that we have to store references to these managers in a list, and is we want to register managers in their constructor (which we do otherwise we will have to manually update a list of managers and that just sounds horrendous) we need to make sure that the list of managers is constructed before any of the managers themselves.
Lucky for all, all is not undefined in the land of static initialisation, static local variables, declared in a function, will be initialised the first time that function is called. So, to solve our manager-list issue we merely need to define a member function on the BaseManager like so:
ManagerList& BaseManager::GetManagerList()
{
static ManagerList managerList;
return managerList;
}
Then, in the constructor of the BaseManager add:
GetManagerList().push_back(this);
This will make sure that our list of managers is constructed before we attempt to add a manager to it (which is nice, I'm sure you'd agree). So, we have our list, we have our managers that register with said list, sorting the list is a trivial issue, as is calling the Init/Shutdown functions in correct orders, so now we turn our attention to how best to define the priority and thus the order of these managers.
Now, you can just manually define them, that would be ok, for a while, but when you have more than a few managers the relationships get pretty complex and going back and changing numbers is very tedious, no, what we need is a list, a list with the names of the managers in a human readable format, and their position in that list to be their priority. What we need, is an enum.
When this idea hit me I immediately liked the cleanness of it, just define an enum, perhaps in one of the higher-up components of the engine (in the Engine.lib, actually) and all is well, you can re-order the entries in the enum, and all the priority values will change and there will be no errors due to forgetting that one manager. There are, however, two main issues in this technique. Firstly, by making the priority of the manager be defined else-where, we are making that library implicitly rely on another library, which I want to attempt to avoid, however, by marking the priorities as 'extern' I am merely stating that something, somewhere has to define a value for that, if they want to use this library, I personally don't think that is too bad. Secondly, if I am defining this list in a high-level library (and I am) there is a very high-chance that I will create a circular dependency (Engile lib defines the priorities, Engine lib relies on RenderLib to do rendering, RenderLib relies on the priorities defined in EngineLib) however, both Visual Studio and GCC can deal with this (you just have to tell GCC to deal with it using --start-group and --end-group) so that isn't too bad either.
Now all that is left is to make the process of defining these managers and the priority list easier and less prone to errors, and for that, I chose to break-out the macros.
In the BaseManager header, I define 3 macros:
#define TYR_GET_MANAGER_PRIORITY_VAR(name) name##_Priority
#define TYR_DEFINE_MANAGER_PRIORITY_VAR(name) extern u8 TYR_GET_MANAGER_PRIORITY_VAR(name);
#define TYR_MANAGER_CTOR(name) \
TYR_DEFINE_MANAGER_PRIORITY_VAR(name) \
name::name() \
: BaseManager(TYR_GET_MANAGER_PRIORITY_VAR(name))
The first one, simply makes it convenient to get the name of the variable correct everywhere it's used, the second one declares an external variable that is used as the priority, and the final one uses the first two to describe the constructor of a new manager class, which will have the priority assigned to the variable _Priority. Only the final macro is used directly, like so:
TYR_MANAGER_CTOR(TextureManager)
{
}
Which expands to:
extern u8 TextureManager_Priority;
TextureManager::TextureManager()
: BaseManager(TextureManager_Priority)
{
}
Now, in EngineLib, I have a file called ManagerConfig.cpp (it has a corresponding header, but it is empty), which contains the following:
#define TYR_DECLARE_MANAGER_PRIORITY_VAR(name)\
u8 TYR_GET_MANAGER_PRIORITY_VAR(name) = ManagerPriorityList::name;
namespace ManagerPriorityList
{
enum
{
Logger,
TextureManager,
ShaderManager,
SceneManager,
};
};
TYR_DECLARE_MANAGER_PRIORITY_VAR(Logger);
TYR_DECLARE_MANAGER_PRIORITY_VAR(SceneManager);
TYR_DECLARE_MANAGER_PRIORITY_VAR(TextureManager);
TYR_DECLARE_MANAGER_PRIORITY_VAR(ShaderManager);
Another macro, this one defines the values in the format expected by the previously declared extern, this macro assigns the value from a named value in the enum below, and at the very bottom, the list of managers defined using the macro. Note that the order of the managers at the bottom doesn't matter, so I only have to change the order of the enum if I want to change the order of the managers.
I am still slightly annoyed that I have to write the manager name twice in this file, I'm tempted to switch the enum out for some form of static variable that is incremented as part of the macro, but I'm not sure if that would work and it's good enough for now. I hope you enjoyed this unexpected mid-week posting, I'll try to have something more to tell by the weekend, as expected taking a break from tool programming to dive back into the engine proper has boosted my enthusiasm for the project, so progress is picking up somewhat.