A game engine loads a game, and a game contains many resources (textures, models, sounds, scripts, game objects, etc.), and each resource may be dependent on other resources (e.g. a game object might require a model and a script). So let's say resource B requires resource A. How can we be certain that resource A is available at the moment resource B is loaded?
Here are the possible solutions I've tried:
1. Only allow resource types to be dependent one-way.
A game object may be dependent on a model, but a model may not be dependent on a game object. If this is strictly enforced, then we can always load the models first, so we know all the models are available once we start loading the game objects that might use them.
Maybe this is all it takes for a lot of games; maybe my game just has weird requirements or a strange technical design, but I've ended up with a lot of interdependency between various resource types that make this approach impossible.
2. Sort resources in order of dependency.
If resources are sorted so that a resource is only saved after its dependencies are saved, then when the project is loaded, the dependencies of a resource are always loaded before the resource that needs them.
This can be a pain to implement. Aside from that, in practice I wrote a lot of my game project file by hand because my tools weren't yet developed, so I constantly had to manually sort resources. I'm also not a fan of requiring order in data files unless it serves a functional purpose (e.g. to define Z-order sorting of game objects in a 2D game).
3. Use "proxy" resources.
If a resource isn't available yet, a "proxy" implementation of that resource is returned, and a reference to the real thing is added to the proxy object once the missing resource becomes available.
I started doing this when I got tired of manually sorting resources as my game grew, but I hated having a lot of "proxy" classes strewn across my engine. I also doubt that constantly calling through proxy objects does wonders for performance either.
4. Repeating initialisation.
This idea was to have the initialisation function on each resource return false to report lack of resource availability, then the initialisation loop would just repeat until every resource initialisation returned true.
It worked, but I didn't really this solution, since repeating initialisation actions in some resources could possibly lead to bugs or unintended effects if the repetition wasn't anticipated, which made it feel very error prone.
5. Multi-phase initialisation
Resources are loaded in multiple phases, e.g: All resources are created in the first phase, then all resources are initialised in the second phase.
This is my current approach. It sounds very simple, but I've found it somewhat more complicated in practice. There are actually five phases in my current engine:
- Resources such as textures, models, and game objects and registered to the engine.
- Game object instances (e.g. player objects) are created and registered to the engine. This is a separate step because the context in which game object instances exist is dependent on a world resource that must be known before the world contents can be parsed. The game object instances are also regarded as resources (mostly to be used by event scripts).
- All loaded resources are initialised with any required resource references.
- The actual game content is loaded such as terrain, pick-ups, enemies, player, etc.. By this point, all types of game object, instances, and other resources have been fully initialised.
- Any initialisation requiring OpenGL is performed (e.g. textures, models, etc.). In order to enable the screen to continue rendering while the previous phases are performed (which may take several seconds), the loading is performed on a second thread. But since OpenGL functions can only be called on the main thread, these operations must be deferred until this phase.
So the order of resources no longer matters, proxy classes aren't required, and we have full flexibility to allow any resource type to reference any other resource type.
The downside is that each relevant phase must be implemented for each resource, and perhaps this isn't very intuitive for an unfamiliar developer (the engine is intended for open source distribution eventually, so I think the API design is quite important). I guess the use of multiple phases also makes the loading time slightly longer than the first three solutions, but I haven't actually measured a difference.
Anyway, I original defined interface functions to be called for each phase, but in the interest of simplicity and brevity, I've settled on a solution that uses std::function callbacks, which looks something like this (extreme simplification):
/**
* A texture resource that is procedurally generated using two colour resources.
*/
class MyTexture:public ITexture {
private:
// Colours used by the texture.
IColour* cColourA;
IColour* cColourB;
GLuint cTextureId;
// etc.
public:
MyTexture(const DOMNode& node, IProjectRegistry* registry) {
// Phase 1: Make "this" object available to the project as a named texture.
registry->registerTexture(node.getAttribute("name"), this);
// Phase 2: Only applicable to world and game object resources.
// Phase 3: Callback to initialise the texture with named colour resources
registry->initReferences([this, &node](IResources* resources) {
cColourA = resources->getColour(node.getAttribute("colourA"));
cColourB = resources->getColour(node.getAttribute("colourB"));
});
// Phase 4: Only applicable to world and game object resources.
// Phase 5: Callback to make sure OpenGL stuff happens in the main thread.
registry->initMainThread([this]() {
// Do OpenGL stuff (allocate texture, render-to-texture, etc.)
});
}
/***********************\
* Implements ITexture *
\***********************/
void set() {
glBindTexture(GL_TEXTURE_2D, cTextureId);
}
};
Actually, I don't normally include the "phase" comments above, since I find it clear enough without them.
/**
* A texture resource that is procedurally generated using two colour resources.
*/
class MyTexture:public ITexture {
private:
// Colours used by the texture.
IColour* cColourA;
IColour* cColourB;
GLuint cTextureId;
// etc.
public:
MyTexture(const DOMNode& node, IProjectRegistry* registry) {
registry->registerTexture(node.getAttribute("name"), this);
registry->initReferences([this, &node](IResources* resources) {
cColourA = resources->getColour(node.getAttribute("colourA"));
cColourB = resources->getColour(node.getAttribute("colourB"));
});
registry->initMainThread([this]() {
// Do OpenGL stuff (allocate texture, render-to-texture, etc.)
});
}
/***********************\
* Implements ITexture *
\***********************/
void set() {
glBindTexture(GL_TEXTURE_2D, cTextureId);
}
};
So based on my current experience, I'm pretty satisfied with this. But I still wonder if there are better or standard ways to do this? Is this a common problem? How does your game (engine) manage resource initialisation? Do you restrict resource dependencies?
Also, is this a good approach with regards to API design of a game engine intended for open source distribution?