[Перевод] Простые сопрограммы для игр на C++

Использование в играх сценарных последовательностей действий, таких как диалоги или видеозаставки, является вполне распространенной практикой. Но суть не в том, что эти последовательности пишутся на сценарных языках (хотя и такое бывает), а в том, что они следуют определенному сценарию, как кино или пьеса. Конечно, в отличие от фильмов, сценарии игр могут иметь множество условных переходов (к примеру, в зависимости от выбранного игроком вопроса, последует определенный ответ неигрового персонажа).

79c97e186f9644489b0f9a08231af0bc.jpg

Реализация такой последовательности в коде должна быть весьма непосредственной — как-никак, компьютер тоже работает по своеобразному сценарию. Но в играх такие сценарии могут выполняться на протяжении нескольких минут, а ведь там, помимо этого, осуществляется много других процессов (звук, анимация и т. д.). В такой ситуации самым очевидным решением будет поместить сценарий в отдельный поток выполнения. Но тогда появляется риск возникновения состояния гонки или других неприятных багов, связанных с потоками выполнения. Что же делать?

Одно из возможных решений — использование конечного автомата. Но переписывание сценария в соответствии с конечным автоматом рискует обернуться сущей пыткой, к тому же в этом случае результирующий код будет гораздо сложнее понять.

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

Сопрограммы являются неотъемлемой частью многих языков, таких как 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)

© Habrahabr.ru