Прості співпрограми для ігор на C++

Використання в іграх сценарних послідовностей дій, таких як діалоги або відеозаставки, є доволі поширеною практикою. Але суть не в тому, що ці послідовності пишуться на сценарних мовами (хоча і таке буває), а в тому, що вони слідують певним сценарієм, як кіно або п'єса. Звичайно, на відміну від фільмів, сценарії ігор можуть мати безліч умовних переходів (наприклад, в залежності від обраного гравцем питання, піде певний відповідь неігрового персонажа).



Реалізація такої послідовності у коді повинна бути досить безпосередній – як-ніяк, комп'ютер теж працює за своєрідним сценарієм. Але в іграх такі сценарії можуть виконуватись на протязі декількох хвилин, а адже там, крім цього, здійснюється багато інших процесів (звук, анімація тощо). У такій ситуації найбільш очевидним рішенням буде помістити сценарій в окремий потік виконання. Але тоді з'являється ризик виникнення стану гонки або інших неприємних помилок, пов'язаних з потоками виконання. Що ж робити?

Одне з можливих рішень – використання кінцевого автомата. Але переписування сценарію у відповідності з кінцевим автоматом ризикує обернутися справжньою мукою, до того ж у цьому випадку результуючий код буде набагато складніше зрозуміти.

Більш простим рішенням, на якому ми сьогодні зупинимося, буде використання сопрограмм. У двох словах, сопрограмма – це щось на кшталт функції, що підтримує зупинку і продовження виконання зі збереженням певного положення. Таким чином, можна виконати яку-небудь частину підпрограми (одного рядка сценарію), повернутися до основного потоку і потім продовжити виконання співпрограми з колишнього положення. Виходить, сопрограмма працює багато в чому як потік виконання, але запускається тільки по команді і з готовністю повертає виконання послідовного додатком.

Співпрограми є невід'ємною частиною багатьох мов, таких як Lua. Але якщо ви вирішили створити гру на чистому C++, справа йде складніше. Сторонні реалізації бібліотек, які можна знайти, наприклад, на boost, в основному призначені для виконання декількох тисяч сопрограмм, а значить, вони повинні бути дуже легковажними. Звичайно, при такому упорі на продуктивність страждає простота використання і переносимість цих бібліотек.

Для роботи зі сценарної послідовністю дій в іграх, як правило, вимагається лише одна або декілька працюючих одночасно сопрограмм. Однак у такому випадку бібліотек зовсім не обов'язково бути легковажним. Щоб виправити цю проблему, я вирішив створити дуже просту реалізацію сопрограмм, яка по суті є обгорткою для std::thread, але з механізмами, що забезпечують передачу виконання від зовнішнього потоку до внутрішнього (сопрограмме); таким чином за раз виконувався тільки один потік. Використовуючи відповідний потік виконання, ми жодним чином не обмежені в тому, що можна робити з потоку. Цей підхід також добре працює у зв'язці з багатьма іншими інструментами на зразок відладчика, що відображає всі запущені потоки виконання в їх поточному стані. Оскільки викликає потік ставиться на паузу в той час, коли виконується сопрограмма, відпадає необхідність використовувати м'ютекси або які-небудь інші способи синхронізації стану гри.

Ось що у мене вийшло в результаті:

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();
}


Моя бібліотека сопрограмм доступна на Github, не соромтеся використовувати її на свій розсуд. Це єдина .hpp/.cpp пара, яка залежить тільки від Loguru (моєї бібліотеки журналювання), але ви можете видалити звідти те, що вважаєте зайвим.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.