Consider the following code, that is blocking:
void onNewGame(){ // Load the map loadMap("level1-1.map"); // Start the game startGame();}void loadMap(const std::string& in_filename){ // ... Do extensive work, freezes the screen for couple seconds.}
Lets start by adding the loading code into a thread. Using std::async.#include // This is the include for asyncvoid onNewGame(){ // Load the map, asynchronously std::async(loadMap, "level1-1.map"); // return and continue normal execution... render your animated loading screen on your mainloop}void loadMap(const std::string& in_filename){ // ... Do extensive work}
That was easy?Now we need to notify the game when the loading is complete. By calling "startGame()" in our example above. This is not as easy as it sound, because the loadMap() function is now running on a different thread. You could set an atomic boolean, and check it every frame. But then we will end up with specific cases all over the place.
What we want is to synchronize with our main loop. Consider the following typical mainloop:
void mainLoop(){ while (true) { game.update(); game.draw(); }}
If we were to set a global atomic bool, we would probably put it like this:void mainLoop(){ while (true) { if (mapLoadingDone) { startGame(); mapLoadingDone = false; } game.update(); game.draw(); }}
And mapLoadingDone is a boolean that would be set to true at the end of loadMap().Lets do this a bit more generic. Let's modify our loadMap() first:
void loadMap(const std::string& in_filename){ // ... Do extensive work // Notify that loading is done syncToMainLoop([]() { startGame(); });}
Simple function call, with inline code block. This is called a lambda. In previous C++ or C, you would have to define a function and pass it as pointer.Now lets see the implementation of syncToMainLoop.
#include #include #include std::queue> syncQueue;std::mutex syncMutex;void syncToMainLoop(std::function in_callback){ syncMutex.lock(); syncQueue.push(in_callback); syncMutex.unlock();}
The std::function is the new way of doing function pointers in C++. The queue is used to pile up multiple sync request in case multiple happen during a single frame. The mutex is there to prevent the queue from being accessed in multiple places at the same time.Now we just need to add this to the main loop:
void mainLoop(){ while (true) { // Do the sync requests syncMutex.lock(); while (!syncQueue.empty()) { auto callback = syncQueue.front(); // Get the next in line callback(); // Call it syncQueue.pop(); // Remove it } syncMutex.unlock(); game.update(); game.draw(); }}
Now you can sync back to the main thread during any async loading. Here is the full code of the example:#include #include #include #include std::queue> syncQueue;std::mutex syncMutex;void mainLoop(){ while (true) { // Do the sync requests syncMutex.lock(); while (!syncQueue.empty()) { auto callback = syncQueue.front(); // Get the next in line callback(); // Call it syncQueue.pop(); // Remove it } syncMutex.unlock(); game.update(); game.draw(); }}void syncToMainLoop(std::function in_callback){ syncMutex.lock(); ?syncQueue.push(in_callback); syncMutex.unlock();}void onNewGame(){ // Load the map, asynchronously std::async(loadMap, "level1-1.map"); // return and continue normal execution... render your animated loading screen on your mainloop}void loadMap(const std::string& in_filename){ // ... Do extensive work // Notify that loading is done syncToMainLoop([]() { startGame(); });}