📜 ⬆️ ⬇️

Simple coroutines for C ++ games

Using scenario sequences in games, such as dialogs or video screensavers, is a fairly common practice. But the point is not that these sequences are written in scenario languages ​​(although it happens), but that they follow a certain scenario, like a movie or a play. Of course, unlike films, game scenarios can have many conditional transitions (for example, depending on the question a player chooses, a certain non-player character’s response will follow).



The implementation of such a sequence in the code should be very straightforward - after all, the computer also works according to a peculiar scenario. But in games such scenarios can run for a few minutes, and there, besides this, many other processes are carried out (sound, animation, etc.). In this situation, the most obvious solution would be to put the script on a separate thread of execution. But then there is the risk of a race condition or other nasty bugs related to the execution threads. What to do?
')
One possible solution is to use a finite state machine . But rewriting the script in accordance with the state machine runs the risk of torture, and in this case, the resulting code will be much more difficult to understand.

A simpler solution, which we will focus on today, is to use coroutines . In a nutshell, coroutine is something like a function that supports stopping and continuing execution while maintaining a certain position. Thus, you can execute any part of the subroutine (one line of the script), return to the main thread, and then continue with the coroutine from the previous position. It turns out that the coroutine works much like a thread of execution, but it is run only by a command and readily returns execution to a sequential application.

Coroutines are an integral part of many languages, such as Lua. But if you decide to create a game in pure C ++, the situation is more complicated. Third-party library implementations, which can be found, for example, on boost , are mainly designed to perform several thousand coroutines, which means they must be very lightweight. Of course, with such an emphasis on performance, the ease of use and portability of these libraries suffers.

To work with the scenario sequence of actions in games, as a rule, only one or several coroutines working simultaneously are required. However, in this case, the libraries do not need to be lightweight. To fix this problem, I decided to create a very simple implementation of coroutines, which is essentially a wrapper for std :: thread, but with mechanisms that ensure the transfer of execution from an external thread to an internal one (coroutine); thus, only one thread was executed at a time. Using the appropriate thread of execution, we are in no way limited in what can be done from the thread. This approach also works well in conjunction with many other tools, such as a debugger, which displays all the running threads in their current state. Since the calling thread is paused at the time the coroutine is executed, there is no need to use mutexes or any other ways to synchronize the state of the game.

Here's what I got in the end:

GameUnit camera = ...; GameUnit juliet = ...; GameUnit curtains = ...; cr::CoroutineSet coroutine_set; coroutine_set.start("end_scene", [&](cr::InnerControl& ic){ while (!camera.looking_at(juliet)) { camera.turn_towards(juliet); ic.yield(); // Return to the calling thread } juliet.speak("Romeo, I come! This do I drink to thee."); ic.wait_sec(2.0); // Yield to main thread for the next two seconds auto drink_animation = juliet.animate("drink_poison"); ic.wait_for([&](){ return drink_animation.is_done(); }); auto fall_animation = juliet.animate("fall_to_the_ground");; ic.wait_for([&](){ return fall_animation.is_done(); }); ic.wait_sec(1.0); curtains.animate("drop"); ic.wait_sec(2.0); }); // Game loop: for (;;) { double dt = seconds_since_last_frame(); input(); update(dt); coroutine_set.poll(dt); // Allow coroutines to run for a short while paint(); } 


My coroutine library is available on Github , feel free to use it at your discretion. This is a single .hpp / .cpp pair that depends only on Loguru (my logging library), but you can remove from there what you think is redundant.

Source: https://habr.com/ru/post/308894/


All Articles