2

I am writing a game in C++, and something I have noticed is that I have many resource files which need to be loaded after a particular point in initialization.

For example, OpenGL textures and VAOs can't be created until there's a current OpenGL context. Once loaded, they will never be modified, so it is most convenient to make them global.

If these objects could be created without a current OpenGL context, this would be trivial to do:

const texture ground_texture = load_texture("ground.png");
const texture robot_texture = load_texture("robot.png");
const model robot_model = load_model("robot.obj");

where texture and model are simple POD structs containing (among other things) OpenGL object IDs.

Instead, I can do this:

texture ground_texture;
texture robot_texture;
model robot_model;

void load_resources() {
    ground_texture = load_texture("ground.png");
    robot_texture = load_texture("robot.png");
    robot_model = load_model("robot.obj");
}

and then call load_resources after the OpenGL context is initialized. But that requires about twice as much code (three times if these globals are also declared in a header file as well) and loses the const modifier.

It could conceivably be produced by a simple script to reduce duplication - then the only downside compared to the original non-working code is that the globals aren't const.

There's also this:

class texture_loader;
class model_loader;
static std::vector<const texture_loader*> tex_loaders;
static std::vector<const model_loader*> model_loaders;

class texture_loader {
    mutable texture tex;
    mutable bool initialized;
    const char *filename;
public:
    texture_loader(const char *filename)
        : initialized(false), filename(filename)
    {
        tex_loaders.push_back(this);
    }

    void load() const {
        tex = load_texture(filename);
        initialized = true;
    }

    operator texture() const {
        assert(initialized);
        return tex;
    }
};
// similarly for model_loader

const texture_loader ground_texture("ground.png");
const texture_loader robot_texture("robot.png");
const model_loader robot_model("robot.obj");

void load_textures() {
    for(texture_loader *t : tex_loaders) t->load();
    for(model_loader *m : model_loaders) m->load();
}

but that seems like quite a lot of complexity for something that is conceptually simple. Also, texture_loader and model_loader (and anything_else_loader) would need to be exposed outside the resource loading module in order for other code to be able to use the globals.

Is this a commonly encountered case? How is it typically solved?

  • 6
    IMHO you are overthinking this. Your second alternative seems to be fine. If you have many more textures/models, you should store them in an array or a hashmap, of course. – Doc Brown Oct 3 '14 at 8:33
  • This can be improved with a Resource Cache / Manager – glampert Oct 3 '14 at 18:02
3

Even if it is true that these things will never change, these textures and models are logically part of a specific part of your program: the 3D environment. They only make sense and will only be used within this context. Thus they should not be globals; they should be declared within the relevant context of your program.

A basic principle is that all variables should have the smallest scope that fits their use case and should be declared as close to where they are used as possible. Even if variables will never change, there is benefit to limiting their scope:

  • It makes the code easier to understand.
  • It makes the code more flexible to change in the future.
  • It aids code reuse (you can take a specific piece of code and use it elsewhere without bringing along a bunch of global variables).
  • I was thinking of global lifetime, not global scope, but you are right. Of course they'll be in a header only included by 3D-environment-related code, and in a namespace. – immibis Oct 3 '14 at 9:40
1

Games typically have common resources that are created once an then stay resident. However, I would advise against exposing such resources as global variables. A better option would be to handle all resource management via a ResourceManager type. The resource manager might be a singleton.

The resource manager should work basically as a cache of resources. Allowing you to request a resource by an identifier, such as the file name, loading the resource transparently if it is not cached or just returning an existing instance. The resource manager might also be able to to pre-load resources once the application starts. It is also the entity responsible for managing the lifetime of resources.

Conceptually, it could look something like this:

class ResourceManager {
public:

    // Preload important stuff.
    // This should probably be called at startup,
    // but after the rendering context is created.
    void preloadResources();

    // Find or load a new resource.
    // If the resource is already available, this is a quick
    // table lookup, else, it is loaded and cached.
    const Texture * findOrLoadTexture(const string & filename);

    // Other methods for other resource types...
    // Or you could have a generic 'Resource' base type.
};

Then in the game logic code, referencing a resource becomes very simple:

void Game::init()
{
    ResourceManager::preloadResources();
}

void Player::init()
{
    robot_texture = ResourceManager::findOrLoadTexture("robot.png");
    robot_model   = ResourceManager::findOrLoadModel("robot.obj");
}

You can get very sophisticated with a ResourceManager, adding a smart cache that can "garbage collect" unused resources. Once you have a solid interface installed, this becomes easy. Also, your would probably not be using raw pointers directly, but smart pointers instead (like std::shared_ptr) to keep correct and efficient track of the lifetime of the resources.

0

Typically, globals and their friendly disguise the singleton are commonly over-used.

However the fact that your data doesn't change once loaded means that, if your process is multi-threaded, once you can be sure they actually are loaded you can have multiple threads access the data at the same.

There are different models that could be used to load the data:

  • All upfront. The simplest model. As you start up you load everything before anything can go. Unfortunately I often find too many apps are guilty of doing this, especially if they are loading something you don't care about. Windows Media Player making you wait while it loads / updates its "library" when all you want to do is play one music file or video that you know the location of is a typical example.

  • All lazy. Nothing is pre-loaded and only loaded the first time it is requested. This works in some cases.

  • A mix: Some pre-loading, others loaded lazy.

  • Pre-loading in a background thread. Items are loaded at the start but the application can run before they are all loaded. If you request something that isn't loaded yet you have to wait for it, though (or have logic to load that item instantly).

In a multi-threaded loading environment you can use futures and once-loading calls to ensure the data is loaded exactly ones.

0

Make an object that loads those textures, and require a constructor parameter for the object that sets up the prerequisite.

class OpenGLContext
{
public: 
    OpenGLContext() { // set up context etc }
}

class TextureRepository
{
public: 
     TextureRepository(OpenGLContext& context) { // set up textures } 
     TextureRepository() = delete;
}

Since the ctor of OpenGLContext sets up the context, and the repo requires a reference to that object, you have a natural way of expressing that dependency, and the compiler will tell you if you forget about it.

This approach can be extended to all kinds of resources with such dependencies.

  • The OpenGL context requires a window... which requires a window class (on Windows) or an X11 display (on *nix). Are you suggesting moving at least half of my initialization code to the constructors of global objects? – immibis Oct 4 '14 at 0:54
  • No. Well, I do believe that initialization code should be in constructors, but it doesn't have to be of global objects. You can pass around the context object after window creation to wherever it is needed/you want. – Wilbert Oct 6 '14 at 8:30
  • Passing the object as param to the ctor just enforces this dependency; it doesn't have to be performed in sequence during start-up. You most likely create the window first and then load your resources at some later point. – Wilbert Oct 6 '14 at 8:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.