Since my last update, I've primarily been working on a way to load all the assets of a level in an asynchronous manner. Having the assets get loaded into memory (or the GPU for textures) in a separate thread allows the game to display the loading status while the player waits. This obviously isn't terribly important for a game like this, where there will only be a dozen or so assets being loaded. But I wanted to work through it for my own curiosity and for future reference.
When you start a "campaign" (ie. story mode) in The Garden of Eating, or when it loads assets for a new level, the loading screen above will appear for a moment.
This is how I have it working at a high level:
- The game loop keeps track of the current status (or mode) as it pertains to loading the game assets for a level. I use an enum class that progresses from "LOAD_CAMPAIGN" immediately into "LOAD_LEVEL", and then "WAIT_TO_START" where we start accepting user input.
- During the "LOAD_LEVEL" mode, the "update" phase of the game loop queries the asset loader for its status.
- If the asset loading status indicates it's still loading, the mode stays at "LOAD_LEVEL".
- During the "LOAD_LEVEL" mode, the "render" phase of the game loop also queries the asset loader for status information. It receives back additional information about how far along it is, and which file is currently being loaded.
When the main loop (which is running in the main thread) queries the status, it needs to briefly block access to the status variables using a mutex. I used the SFML Thread and Mutex classes for this, but I guess I could've used the C++ STL classes that were introduced in C++ 11.
I'm quite happy with the results at this stage, though I did have one interesting gotcha moment.
I was keeping the list of floor textures and barrier textures each in their own std::unordered_map<int, sf::Texture> structure. At first I had code that looked like so:
void StoryLevelAssetBundle::loadFloorTextureMap(const StoryMapDefn& mapDefn) {
for (auto const& currFloorDefnPair : mapDefn.floorDefnMap) {
this->indicateLoadingFilename(currFloorDefnPair.second.filename);
sf::Texture currFloorTexture;
if (currFloorTexture.loadFromFile(r3::snake::StoryLoaderUtils::resolveImageFilePath(this->campaignFolderName, currFloorDefnPair.second.filename))) {
currFloorTexture.setRepeated(true);
this->floorTextureMap[currFloorDefnPair.first] = currFloorTexture;
this->incrementLoadedAssetCount();
}
else {
this->failedFilenameList.push_back(currFloorDefnPair.second.filename);
}
}
}
The line that copies the texture from the "currFloorTexture" variable into the map was causing a peculiar warning message to show up in the console:
An internal OpenGL call failed in Texture.cpp(98).
Expression:
glFlush()
Error description:
GL_INVALID_OPERATION
The specified operation is not allowed in the current state.
When I first encountered this message, I was just testing the "StoryLevelAssetBundle" class directly -- outside of the game running. So I assumed it was because I didn't have a valid OpenGL context created. But the message persisted once I'd hooked it into the game.
Eliminating the local "currFloorTexture" variable, and instead loading directly into the map, prevented this:
void StoryLevelAssetBundle::loadFloorTextureMap(const StoryMapDefn& mapDefn) {
for (auto const& currFloorDefnPair : mapDefn.floorDefnMap) {
this->indicateLoadingFilename(currFloorDefnPair.second.filename);
this->floorTextureMap[currFloorDefnPair.first] = sf::Texture();
if (this->floorTextureMap[currFloorDefnPair.first].loadFromFile(r3::snake::StoryLoaderUtils::resolveImageFilePath(this->campaignFolderName, currFloorDefnPair.second.filename))) {
this->floorTextureMap[currFloorDefnPair.first].setRepeated(true);
this->incrementLoadedAssetCount();
}
else {
this->failedFilenameList.push_back(currFloorDefnPair.second.filename);
}
}
}
When I looked around for information on this message, I kept coming across the acronym RAII, which stands for Resource Acquisition Is Initialization. This is one of those things that I feel like I understand conceptually, but then when I read articles or people's opinions on how to use it properly in C++, I start to get confused. I'll see people saying that there's no need to ever use "new" and "delete", for example. I still use it, for example, to store a pointer to the current "StoryLevelAssetBundle" above in my game loop. I feel like that class has enough state that I want it to get allocated on the heap, rather than the stack. Am I just a dinosaur who remembers too much what it was like working with the 8086 memory segments and 64kb stack size? Heh...