Best solution to initialise resources and dependencies in a game engine?

Started by
8 comments, last by Martin Brentnall 5 years, 9 months ago

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:

  1. Resources such as textures, models, and game objects and registered to the engine.
  2. 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).
  3. All loaded resources are initialised with any required resource references.
  4. 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.
  5. 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?

Advertisement
20 minutes ago, Martin Brentnall said:

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?

In contrast to this approach of an all singing all dancing engine the approach I used to use is to pre-bake all the resources required into one or several large files, load them into memory then fixup the data, and create API resources in strict order (if this was even required, sometimes you could use resources 'as is' on consoles).

The task of making sure the right resources are there can then usually be moved from the game itself to the toolchain, which does this as a pre-process while baking the resource files. Of course it may be too much hassle to do this for everything (e.g. analysing scripts to ensure everything they call is present). Mind you if you can do all this as a pre-process it removes a whole world of potential beta testing problems.

Many people somehow get this view that an 'engine' should do everything and contain all the smarts, whereas many of the best designed games and engines put as much of the smarts as possible into the toolchain, and leave the engine itself as a sleek efficient, elegant thing, easy to debug and not full of superfluous code. Imo. :)

That's an interesting idea, but I'm not sure it could be accomplished in my project, since I want to include the editing tool with my game that lets users build new games, and of course that means that resources are not fixed just for my specific game, but can be loaded/unloaded dynamically and configured on demand as a player wishes.  For that reason, I'd like to keep the tool as intuitive as possible for creators.

Also at a deeper level, my engine implements a modular framework, so there's nothing to stop a player from plugging in their own custom DLL that generates all kinds of new resources, and I think it would be difficult to handle these in a generic way, since there's no way for the engine to know what an external module is doing internally with its resources.

Another thing is that different modules may reference resources from each other too, so a game object from one module might use a model or texture generated by a different module.  This is, in fact, already the case, since I already have six different modules for various different purposes.

What I do currently as my preferred way to handle loading is something from all of your approaches. I first keep anything in packages. Those packages are chunked to 64k blocks with padding data so anything can be mapped into memory to ensure multicore resource handling as my engine highly sets on multithreading, task sharing and events.

The resource manager system handles a list of the packages and headers contained in them to determine where to find certain resource. When a resource is requested, the system starts a loading task that may return immediatly if there is already a resource with the same ID (an FNV32 hash of it's path inside a package) or enters loading loop. Loading loop will first determine if the section the resource stays is cached or else do a memory mapping to the chunk(s) the resource is put into. Resource is then processed by sub-systems like Texture handler or Mesh loader, dependencies are again requested by the manager system. If a resource is disposed, it will either be removed entirely or kept for reference.

The outer code will not be disturbed by resource loading. Instead a resource handle struct is returned that is some kind of smart pointer keeping reference to the resource by an integer index into the memory pool. This way it is easy for the resource manager system to reorder loaded resources during runtime and also to change the target resource. This is happening when the resource is first load into the manager system, while doing so, the resource may be already displayed so there is a build-in dummy resource attached to the handle first and swapped for the real one when finished loading. This also helps developers to see if they referenced anything correctly (if not, they see a purple Texture or purple cube Mesh).

I know this sounds a bit old-school but it works :D

I do your #1 and #4/5.

Loading consists of a parsing step and a link resolution step. Parsing happens for all assets in any order / in parallel (as the data from disk becomes available). 

Resolving happens in a sorted order per resource type (all textures in parallel, then all materials, etc). Each resource type is fixed in what other resource types it can possibly link to, so resources that link to no other types specify a resolve-priority of 0. Resources that DO link to other types specify a resolve-priority of the maximum of all their dependent types plus 1.

A tangential note - I keep in-memory and on-disk formats identical in a lot of cases, so the parsing/deserializing step can be empty/nothing for many types. 

One other thing is that your description of #5 bugs me slightly -- there should be very strong separation between static/reusable asset data (such as a 3D mesh, a skeleton, a texture) and instances of that data (a 3D object in the world). Don't mix up or blur the lines between these. Assets must be initialised before they are instanced (which is obvious when you type it out, but needs to be stated for emphasis). You should at least have a two phase loading system that can delay the instantiation of an asset until after is has been loaded. 

Some of the reviewed techniques could be made to work better.

Cyclic resource dependencies can be probably avoided by adding more resources and/or resource types: for example if two otherwise self-contained game levels have mutual references because the player can navigate from one to the other, you can instead have named "gates" in the levels, with no reference to the other side, and a separate level navigation graph resources that is loaded later and specifies which gates are connected.

What you call resource proxies are not really proxies, because they stop being useful as soon as the referenced resource is available and they actually start behaving like a proxy. They are placeholders, and they should simply disappear after resource loading by replacing references to them with references to the actual resource.

Sorting resources means sorting in which order they are loaded, not necessarily the layout of data files. You can invest on building and possibly storing an index of your data files. Maintaining a queue of resources that need to be loaded seems an easy enough job for a computer, you just need to specify dependencies in an easy to build resource index. You might be able to script resource unloading (when the player passes some point of no return, a certain set of resources won't be needed any more) and/or resource caching (when the player is busy in a limited environment and few immediately useful resources need to be loaded, load in advance some of the resources that might be needed later).

Omae Wa Mou Shindeiru

3 hours ago, Hodgman said:

One other thing is that your description of #5 bugs me slightly -- there should be very strong separation between static/reusable asset data (such as a 3D mesh, a skeleton, a texture) and instances of that data (a 3D object in the world). Don't mix up or blur the lines between these. Assets must be initialised before they are instanced (which is obvious when you type it out, but needs to be stated for emphasis). You should at least have a two phase loading system that can delay the instantiation of an asset until after is has been loaded. 

This is a very good point.  I do already instance many of my resource types, but not everything.  For example, textures and colours are not currently instanced, but I can certainly see the benefits of making everything instanced.

 

 

3 hours ago, LorenzoGatti said:

What you call resource proxies are not really proxies, because they stop being useful as soon as the referenced resource is available and they actually start behaving like a proxy. They are placeholders, and they should simply disappear after resource loading by replacing references to them with references to the actual resource. 

This is an ideal situation, but how would it work technically?

If my object has just received a pointer to a place holder resource, then as soon as the real one is loaded, the pointer would become invalid because it still points to a place holder that was destroyed:


IModel* mModel = resources->getModel(node.getAttribute("model"));

// ... some time later

mModel->render(); // If the place holder is destroyed, this will crash.

I could solve this by having a pointer to the pointer to the resource, so when the place-holder is replaced by the real thing, the pointer that my pointer points to now points to the real thing, but it looks super ugly:


IModel** mModel = resources->getModel(node.getAttribute("model"));

// ... some time later

(*mModel)->render(); // This always works, but it's ugly!

 

 

3 hours ago, LorenzoGatti said:

You might be able to script resource unloading (when the player passes some point of no return, a certain set of resources won't be needed any more) and/or resource caching (when the player is busy in a limited environment and few immediately useful resources need to be loaded, load in advance some of the resources that might be needed later). 

Fortunately my current game isn't big or complex enough to require dynamic loading and unloading of resources during actual gameplay; I just load everything at the beginning and keep everything in memory until the game ends.  I guess this could change in the future though.

 

Anyway generally speaking, it's great to see a lot of suggestions and ideas here, but I'm still not really clear on what advantages are over my current approach?

Another thing:  Many seem to assume that all resources will be loaded from files on disk, but many of my resources are generated programmatically, and I'm not sure if this makes a difference?

As a simple example, my game defines a palette of 16 colours, and all of the game object textures are generated using colours selected from this palette.  The design is intended to make it easy for a creator to change the appearance of the game by selecting different colours for the textures and/or editing the palette.

Of course, a creator can also just load .png's (etc.) and use those as textures too if he prefers, but this ability is only used for some static parts of the HUD in my game.

2 hours ago, Martin Brentnall said:

This is an ideal situation, but how would it work technically?

Simply take a pointer struct instead of plain pointers so you could have logic behind, even if that means that a pointer will increase in size. I use something like this for my ResourceHandle class


template<typename T> struct ResourceHandle
{
    public:
        inline ResourceHandle(uint32 index, ResourceManager* manager) : index(index), manager(manager)
        { }
        inline ~ResourceHandle()
        {
            if(index) manager->Release(this); //decrease reference count for this resource
            manager = 0;
            index = 0;
        }
  
        inline T* operator ->() const
        {
            return static_cast<T*>(manager->Get(index)); 
        }
        inline T* operator ->()
        {
            return static_cast<T*>(manager->Get(index)); 
        }
        inline const const T& operator *() const
        {
            return *static_cast<T*>(manager->Get(index)); 
        }
        inline const T& operator *()
        {
            return *static_cast<T*>(manager->Get(index)); 
        }
  
    private:
        uint32 index;
        ResourceManager* manager;
}

The instance of manager could then atomic lock the resource internal to do movements or replacements before it is returned. I use an integer instead of pointer here because the integer + manager::dataIndex is the pointer and it is safe for a memory resize of the manager class (forexample when increasing cache)

2 hours ago, Martin Brentnall said:

Many seem to assume that all resources will be loaded from files on disk, but many of my resources are generated programmatically, and I'm not sure if this makes a difference?

The difference is memory usage/performance when accessing the resource, thats it. YOu need to ensure that the resource is known at runtime and another neck breaker could be something like networking. What happens if the package aborts, a connection is closed or corrupted. Anything else behaves equal I think

Good idea.  I tend to overlook the possibilities afforded by operator overloading in C++, since my background (and job) is Java.  C++ is just a hobby for me.

Actually I think a lot of my habits from Java are probably evident in my C++ code, but that's an entirely separate issue. :)

This topic is closed to new replies.

Advertisement