Публикация научных статей.
Вход на сайт
E-mail:
Пароль:
Запомнить
Регистрация/
Забыли пароль?

Научные направления

Поделиться:
Разделы: Информационные технологии
Размещена 11.06.2025. Последняя правка: 09.06.2025.
Просмотров - 203

ГЕНЕРАЦИЯ КАРТ ДЛЯ 2Д ИГРЫ С ИСПОЛЬЗОВАНИЕМ СОПРОГРАММ ИЗ С++ 20

Селявин Никита Михайлович

Нет

Московский государственный технический университет радиотехники, электроники и автоматики — Российский технологический университет

Студент

Беляев П.В., кандидат технический наук, доцент кафедры инструментального и прикладного программного обеспечения, Московский государственный технический университет радиотехники, электроники и автоматики — Российский технологический университет


Аннотация:
Рассматривается базовая работа сопрограмм (coroutine), добавленная в С++ 20 стандарта на примере создания генератора клеток карты для 2д игры. Для этого реализуются: класс объекта, возвращаемый сопрограммой, тип-обещание с использованием представленных стандартом объектов ожидания. Созданная реализация сопрограммы используется в заранее созданной заранее игре.


Abstract:
The basic work of C++ 20 standart coroutines is consided on the example of creating map cell for a 2D game. For this, the following are implemented: the coroutine returned object, the promise type with using std awaitable objects. The created implementation of coroutine is used in pre-created game that satisfy minimum requirements.


Ключевые слова:
сопрограмма (корутина); тип-обещание; объект ожидания; приостановка сопрограммы; дескриптор сопрограммы; кадр сопрограммы

Keywords:
coroutine; promise_type; frame; awaitable; coroutine handle; coroutine state; suspend


УДК 004.43

Введение

В статье рассматриваются возможности использования сопрограмм (корутин), введённых в стандарт C++20, на примере генерации клеток карт для 2D-игры. Объясняется базовый механизм работы корутин через реализацию собственных типов promise и awaitable, а также демонстрируется интеграция такого генератора в игровой движок.

Актуальность

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

Цели исследования

  1. Исследовать принципы работы сопрограмм в C++20 и ключевые механизмы co_await, co_yield, co_return.

  2. Разработать и реализовать ленивый генератор клеток карты через собственный promise_type и объекты ожидания.

  3. Интегрировать корутину в простой 2D-движок и оценить её преимущества в управлении генерацией мира.

Задачи исследования

  • Проанализировать интерфейсы требуемых для корутин типов: promise, awaitable, handle.

  • Реализовать класс CellGenerator с методами next_cell() и current_cell(), поддерживающими ленивую выдачу.

  • Продемонстрировать использование генератора в алгоритме случайной генерации плиток в игровом цикле.

Научная новизна

  1. Практическое воплощение пользовательского генератора клеток через сопрограммы C++20: реализован полный цикл создания, приостановки и возобновления корутины для динамической генерации игрового мира.

  2. Разработка адаптированного promise_type awaitable для игрового приложения: подбираются стратегии приостановки (std::suspend_always/std::suspend_never) строго в тех точках, где это критично, что отличает нашу реализацию от типовых примеров в литературе.

Сопрограммы

Сопрограммы — это, тип функций, которые способны приостанавливать и возобновлять свое выполнения с точки остановы. Сопрограммы способны сохранять свое внутреннее состояние.

Впервые термин сопрограммы был введен в 1958 году Мелвином Конвеем. Однако, признание пришло спустя большой промежуток времени. Основное отличие сопрограмм от функций представлена на рисунке 1.

Отличие сопрограмм от функций

Рисунок 1 — Отличие сопрограмм от функций

Если функцию можно лишь вызвать и, по завершении её работы, получить результат, то сопрограмму можно вызвать, получить промежуточный результат, обработать его, пока работа сопрограммы приостановлена, затем продолжить или прервать её выполнение.

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

Обычные потоки управляются системой, тогда как за выполнение сопрограммы отвечает разработчик, она выполняется до тех пор, пока не наступит момент в коде. Сравнение линейного выполнения, сопрограмм и потоков на рисунке 2.


Схема выполнения

 Рисунок 2 — Схема выполнения функций и сопрограмм

Сопрограммы выполняются в рамках одного потока или нескольких потоков. Потоки дают возможность распараллелить задачи, ускорив выполнения, однако, требуют много ресурсов. Корутины не ускоряют работу, но позволяют оптимизировать распределение нагрузки.

Сопрограммы в С++ 20

Сопрограммы поддерживаются во многих известных языках. В различных средах, они называются по-разному: горутины, волокна, зеленые потоки, стековые корутины.

В 20 стандарте С++ добавили инструменты создания сопрограмм, такие ключевые слова как co_await, co_return, co_yield [1, 2, 3]. Пример работы последнего будет рассмотрен в этой статье.
co_return — приостанавливает и завершает сопрограмму. Похож на return обычных функций.
co_yield — приостанавливает корутину, с запоминанием состояния и возвращает управление вызвавшей стороне.
co_await — приостанавливает корутину, обращаясь к объекту ожидания, и возвращает контроль вызвавщей стороне.
 
co_yield и co_return, по сути, являются синтаксический сахаром, и оберткой над co_await. Можно привести их эквиваленты:
  • co_yield развернется в co_await promise.yuield_value()
  • co_return в co_await promise.return_void() end
  • co_return в co_await promise.return_value() end

Где end — завершение сопрограммы.
co_await в качестве параметра принимает объект обещание (Awaitable), которое определяет поведение корутины в этой точке.

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

Подробности преобразования типов компилятором не рассматриваются в этой статье. Пример преобразования можно увидеть с помощью инструмента cppinsights.io.
Концептуальная модель сопрограммы состоит из трех частей: объект-обещание, дескриптор сопрограммы и кадр, а также объект ожидания, для определения поведения при остовах. Ниже представлены описания и требования к их реализациям.

Объект сопрограммы и дескриптор

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

Дескриптор — промежуточный объект, через который можно возобновлять или полностью прекращать выполнение сопрограамы.

Объект сопрограммы состоит из:

  • promise_type — публичный программно определенный тип объекта-обещания.
  • Может хранить дескриптор сопрограммы типа std::coroutine_handle<promise_type>, который передается от объекта-обещания, для взаимодействия с сопрограммой
  • Опционально методы и свойства нужные для задач. Например, получения сохраненного значения кадра.
 
Объект-обещание
Через этот объект происходит взаимодействие с кадром сопрограммы, видны результаты работы сопрограмм или исключения. Работа с ним происходит из кода сопрограммы. Объект-обещание сохраняет свое состояние в кадре сопрограммы [4].
Объект-обещание должен поддерживать следующий интерфейс:
  • get_return_object() — вызывается при вызове корутины, возвращает объект дескриптор, связанный с текущей сопрограммой;
  • inital_suspend() — вызывается сразу после создания корутины, возвращает объект ожидания;
  • final_suspend() — вызывается перед уничтожением сопрограммы, возвращает объект ожидание;
  • return_value(expr) — вызывается оператором co_return ;
  • return_void() — вызывается оператором co_return;.
  • yuield_value(expr) — вызывается оператором co_yield , возвращает объект ожидание.
 
Объект ожидания
На основе объекта генерируется контроллер ожидания, от которого зависит, будет ли сопрограмма делать паузу в своем выполнении, а также определяет поведение во время останов и возобновлении выполнения.
Объекты ожидания передаются в качестве параметров операторам co_await.
Объект ожидания должен поддерживать следующий интерфейс:
  • await_ready() — определяет, готов ли результат вычислений, если функция возвращает true, результат готов, сопрограмма не приостанавливается и сразу переходит к методу await_resume(). Если false, то приостанавливает выполнение, вызывает await_suspend()
  • await_suspend(handle) — вызывается при остановке сопрограммы и управляет дальнейшим возобновлением работы. Принимает параметром дескриптор сопрограммы. Может иметь один из возвращаемых типов: void — сопрограмма остается приостановленной и возвращает управление; bool — если true, остается приостановленной и возвращает управление, если false, переходит в методу await_resume(); дескриптор другой сопрограммы — продолжает выполнение другой сопрограммы, возвращая выполнение вызвавшему коду.
  • await_resume() — вызывается при возобновлении сопрограммы и устанавливается результат выполнения оператора co_await.
 
В стандарте есть уже реализованные простые объекты ожидания: std::suspend_always — приостанавливает выполнение, std::suspend_never — соответственно, не приостанавливает выполнение.
 
Создание сопрограммы для 2д игры
Для начала имеется 2д игра, в которой персонаж передвигается по клеткам, на которых в будущем с помощью корутин должна генерироваться содержимое. Начальное состояние игры на рисунке 3.

 Каркас 2д игры

Рисунок 3 — Каркас 2д игры

Сейчас персонаж бегает по пустому миру. Реализуем классы клетки и сопрограммы генератора.
Листинг 1 — Cell.cpp. Клетка просто загружает текстуру в зависимости от выбранного типа.

Cell::Cell(CellType type) {

    bool is_load = false;

    switch (type) {

        case CellType::sand: {

            is_load = texture.loadFromFile(RESOURCE_PATH + "sand.png");

            break;

        } case CellType::grass: {

            is_load = texture.loadFromFile(RESOURCE_PATH + "grass.png");

            break;

        }case CellType::water: {

            is_load = texture.loadFromFile(RESOURCE_PATH + "water.png");

            break;

        } case CellType::rock: {

            is_load = texture.loadFromFile(RESOURCE_PATH + "rock.png");

            break;

    }}

    if (!is_load) {

        throw std::runtime_error("[ERROR] Failed to load cell texture");

    }

    setTexture(texture);

}

Листинг 2 — Класс CellGenerator в CellGenerator.h

#include

#include "Cell.h"

class CellGenerator {

public:

    class promise_type {

        friend class CellGenerator;

    public:

        CellGenerator get_return_object();

        std::suspend_always initial_suspend();

        std::suspend_never  final_suspend() noexcept;

        void unhandled_exception();

        std::suspend_always yield_value(std::shared_ptr cell);

        std::suspend_always return_void();

    private:

        std::shared_ptr current_cell;

    };

    using handle_type = std::coroutine_handle<CellGenerator::promise_type>;

 

    explicit CellGenerator(handle_type handle);

    CellGenerator(const CellGenerator&) = delete;

    CellGenerator(CellGenerator&&);

    CellGenerator& operator=(const CellGenerator&) = delete;

    CellGenerator& operator=(CellGenerator&&);

    ~CellGenerator();

    std::shared_ptr current_cell();

    std::shared_ptr next_cell();

private:

    handle_type handle;

};

 

CellGenerator в содержет себе определение promise_type, конструктор по умолчанию, которые принимает дескриптор кадра. Объект поддерживает  семантику перемещения, чтобы только один мог объект мог управлять одним кадром. Метод next_cell позволяет продолжить выполнение корутины, чтобы получить новое значение, current_cell возвращает текущее значение не продолжая выполнение.

Листинг 3 — Реализация promise_type в CellGenerator.cpp.

CellGenerator CellGenerator::promise_type::get_return_object() {

    return CellGenerator{CellGenerator::handle_type::from_promise(*this)};

}

std::suspend_always CellGenerator::promise_type::initial_suspend() {

    return {};

}

std::suspend_never CellGenerator::promise_type::final_suspend() noexcept {

    return {};

}

void CellGenerator::promise_type::unhandled_exception() {

    std::cout << "[ERROR] Coroutine exceptionn";

}

std::suspend_always

    CellGenerator::promise_type::yield_value(std::shared_ptr cell) {

    current_cell = cell;

    return {};

}

std::suspend_always CellGenerator::promise_type::return_void() {

    return {};

}

Генератор клеток является ленивым генератором, запускается, только по запросу новой клетки, поэтому метод inital_suspend возвращает std::suspend_always, чтобы корутина сразу приостанавливалась. При завершении останова не нужна, поэтому final_suspend возвращает std::suspend_never. Планируется использовать корутину с ключевым словом co_yield, поэтому реализуется метод yuield_value, который также приостанавливает выполнение корутины и сохраняет новое значение в кадре.

Листинг 5 — Реализация CellGenerator в CellGenerator.cpp

CellGenerator::CellGenerator(handle_type handle) : handle(handle) {}
CellGenerator::CellGenerator(CellGenerator&& other) : handle(other.handle) {
    other.handle = nullptr;
}
CellGenerator& CellGenerator::operator=(CellGenerator&& other) {
    handle       = other.handle;
    other.handle = nullptr;
    return *this;
}
CellGenerator::~CellGenerator() {
    if (handle) {
        handle.destroy();
    }
}
std::shared_ptr CellGenerator::current_cell() {
    return handle.promise().current_cell;
}
std::shared_ptr CellGenerator::next_cell() {
    if (!handle.done()) {
        handle.resume();
    }
    return handle.promise().current_cell;
}

В конструкторе инициализирует дескриптор. Метод next_cell продолжает выполнение корутины с помощью вызова handle.resume() и возвращает новую сохраненную в обещании клетку. current_cell не продолжает выполнение в отличие от next_cell.
Теперь демонстрируется использование созданной корутины в самой игре (листинги 6-7). В классе игры создается метод с алгоритмом бесконечной случайной генерации клеток разной поверхности. Возвращаемый тип метода — объект корутины.

Листинг 6 — Использование корутины в классе игры в Game.cpp

CellGenerator Game::cell_generator() {
    std::random_device           rd;
    std::mt19937                 gen(rd());
    std::discrete_distribution<> d({70, 10, 10, 10});
    std::shared_ptr ptr_cell = nullptr;
    while (true) {
        switch (d(gen)) {
            case 0: {
                ptr_cell = std::make_shared(Cell::CellType::sand);
                break;
            } case 1: {
                ptr_cell = std::make_shared(Cell::CellType::grass);
                break;
            } case 2: {
                ptr_cell = std::make_shared(Cell::CellType::rock);
                break;
            } case 3: {
                ptr_cell = std::make_shared(Cell::CellType::water);
                break;
            } default: {
                break;
        }}
        co_yield ptr_cell;
    }
}

 

Алгоритм этой функции будет выполнятся до оператора co_yield. Дальше сопрограмма приостанавливается, новая клетка передается в promise_type, где сохраняется и передает управление обратно вызвавшей стороне (в данном случае классу Game);

Листинг 7 — Вызов next_cell() у генератора в методе Game::loadChunk. Game.cpp

void Game::loadChunk(Point center, int rad) {

    for (int y = std::max(center.y - rad, 0);

         y < std::min(center.y + rad, (int)MAP_SIZE);

         y++) {

        for (int x = std::max(center.x - rad, 0);

             x < std::min(center.x + rad, (int)MAP_SIZE);

             x++) {

            Point p(x, y);

            if (inMapBound(p) && !isFilledCell(p)) {

                map[mapInd(p)] = cell_gen.next_cell();

}}}}

 

Каждый раз, когда нужна новая клетка, вызывается cell_gen.next_cell().

В игре добавлен функционал для наглядности, генерировать новые клетки по нажатию клавиши. Вот как теперь выглядит генерация карты (рисунок 4). Текстуры взяты из репозитория github:qudodup/pastel-tiles [5].

Генераций клеток

Рисунок 4 — Генерируемый клетки

Весь проект находится на ресурсе GitHub: https://github.com/Fume5678/coro_game_example

Заключение

Создан класс CellGenerator, поддерживающий ленивую выдачу объектов Cell по запросу: метод next_cell() возобновляет корутину и возвращает следующую клетку, а current_cell() позволяет получить текущее значение без возобновления.

Отстутствует многопоточность, однако мир генерируется одновременно с движением игрока.

Сопрограммы мощный и удобный в использовании инструмент, позволяющий создавать эффективный код для многозадачнасти и ассинхронности. В С++ 20 представлены базовый инструментал для построения своих корутин, который был рассмотрен в данной статье. Стандарт C++23, утвержденный в 2023 году, привнес значительные улучшения в язык, включая std::generator — готовую реализацию корутин в стандартной библиотеке.

Библиографический список:

1. Cppreference, раздел официальной документации С++ по сопрограммам [Электронный ресурс]. — Режим доступа: https://en.cppreference.com/w/cpp/language/coroutines (дата обращения 23.05.2025)
2. Rainer Grimm, Concurrency with Modern C++, версия публикации 2020. - 550 c.
3. C++ lectures at MIPT (in Russian). Lecture 10. Coroutines, part 1 [Электронный ресурс]. — Режим доступа: https://www.youtube.com/watch?v=R_gZQJC-uv0 (дата обращения 23.05.2025)
4. Подробнее о корутинах С++, Хабр [Электронный ресурс]. — режим доступа: https://habr.com/ru/company/piter/blog/491996/ (дата обращение 23.05.2025)
5. Репозиторий игровых ресурсов взятый для демонстрации [Электронный ресурс]. — Режим доступа: https://github.com/qubodup/pastel-tiles (дата обращения 23.05.2025)




Рецензии:

11.06.2025, 10:14 Огарок Андрей Леонтиевич
Рецензия: Статья содержит результаты исследований, содержащие полезную информацию для разработчиков программного обеспечения. В статье рассмотрены вопросы создания генератора клеток карты для 2д игры с использованием сопрограмм (coroutine) на примере создания генератора клеток карты игры с использованием стандарта С++ 20. Автор статьи изложил апробированные материалы по направлению своих научных исследований. Рекомендую опубликовать статью на сайте и включить в очередной выпуск научного журнала.



Комментарии пользователей:

Оставить комментарий


 
 

Вверх