Нет
Московский государственный технический университет радиотехники, электроники и автоматики — Российский технологический университет
Студент
Беляев П.В., кандидат технический наук, доцент кафедры инструментального и прикладного программного обеспечения, Московский государственный технический университет радиотехники, электроники и автоматики — Российский технологический университет
УДК 004.43
Введение
В статье рассматриваются возможности использования сопрограмм (корутин), введённых в стандарт C++20, на примере генерации клеток карт для 2D-игры. Объясняется базовый механизм работы корутин через реализацию собственных типов promise и awaitable, а также демонстрируется интеграция такого генератора в игровой движок.
Актуальность
С развитием высокопроизводительных игровых приложений растут требования к оптимизации использования ресурсов и распределению нагрузок. Корутинный подход позволяет реализовать ленивую генерацию контента, разгрузить основной поток и улучшить отзывчивость игры без значительных затрат на системные потоки.
Цели исследования
Исследовать принципы работы сопрограмм в C++20 и ключевые механизмы co_await
, co_yield
, co_return
.
Разработать и реализовать ленивый генератор клеток карты через собственный promise_type и объекты ожидания.
Интегрировать корутину в простой 2D-движок и оценить её преимущества в управлении генерацией мира.
Задачи исследования
Проанализировать интерфейсы требуемых для корутин типов: promise, awaitable, handle.
Реализовать класс CellGenerator
с методами next_cell()
и current_cell()
, поддерживающими ленивую выдачу.
Продемонстрировать использование генератора в алгоритме случайной генерации плиток в игровом цикле.
Научная новизна
Практическое воплощение пользовательского генератора клеток через сопрограммы C++20: реализован полный цикл создания, приостановки и возобновления корутины для динамической генерации игрового мира.
Разработка адаптированного promise_type awaitable для игрового приложения: подбираются стратегии приостановки (std::suspend_always
/std::suspend_never
) строго в тех точках, где это критично, что отличает нашу реализацию от типовых примеров в литературе.
Сопрограммы
Сопрограммы — это, тип функций, которые способны приостанавливать и возобновлять свое выполнения с точки остановы. Сопрограммы способны сохранять свое внутреннее состояние.
Впервые термин сопрограммы был введен в 1958 году Мелвином Конвеем. Однако, признание пришло спустя большой промежуток времени. Основное отличие сопрограмм от функций представлена на рисунке 1.
Рисунок 1 — Отличие сопрограмм от функций
Если функцию можно лишь вызвать и, по завершении её работы, получить результат, то сопрограмму можно вызвать, получить промежуточный результат, обработать его, пока работа сопрограммы приостановлена, затем продолжить или прервать её выполнение.
Сопрограммы — это легковесные потоки исполнения кода, которые организуются поверх системных потоков. Они очень похожи на обычные потоки, одна корутины обеспечивают кооперативную многозадачность, но не параллелизм.
Обычные потоки управляются системой, тогда как за выполнение сопрограммы отвечает разработчик, она выполняется до тех пор, пока не наступит момент в коде. Сравнение линейного выполнения, сопрограмм и потоков на рисунке 2.
Рисунок 2 — Схема выполнения функций и сопрограмм
Сопрограммы выполняются в рамках одного потока или нескольких потоков. Потоки дают возможность распараллелить задачи, ускорив выполнения, однако, требуют много ресурсов. Корутины не ускоряют работу, но позволяют оптимизировать распределение нагрузки.
Сопрограммы в С++ 20
Сопрограммы поддерживаются во многих известных языках. В различных средах, они называются по-разному: горутины, волокна, зеленые потоки, стековые корутины.
Где end — завершение сопрограммы.
co_await в качестве параметра принимает объект обещание (Awaitable), которое определяет поведение корутины в этой точке.
На данный момент в стандарте нет полноценных корутин, поэтому задача их реализации ложится на программиста. Процесс создания корутины представляет реализацию типов, удовлетворяющих определенным интерфейсам, которые в дальнейшем будут преобразованы компилятором в код корутин.
Объект сопрограммы и дескриптор
Объект сопрограммы — это, возвращаемый сопрограммой объект, с помощью которого, клиент может управлять ее выполнением, получать результаты вычислений или прекращать выполнение.
Дескриптор — промежуточный объект, через который можно возобновлять или полностью прекращать выполнение сопрограамы.
Объект сопрограммы состоит из:
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
Листинг 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 — готовую реализацию корутин в стандартной библиотеке.
Рецензии:
11.06.2025, 10:14 Огарок Андрей Леонтиевич
Рецензия: Статья содержит результаты исследований, содержащие полезную информацию для разработчиков программного обеспечения. В статье рассмотрены вопросы создания генератора клеток карты для 2д игры с использованием сопрограмм (coroutine) на примере создания генератора клеток карты игры с использованием стандарта С++ 20. Автор статьи изложил апробированные материалы по направлению своих научных исследований. Рекомендую опубликовать статью на сайте и включить в очередной выпуск научного журнала.
Комментарии пользователей:
Оставить комментарий