Разработка MINI HEROES. DevLog 1. Философия Old School и C11
#172 вторник, 27 января 2026 г. вторник, 27 января 2026 г. 45 минут(ы) 4399 слов
Разработка MINI HEROES. DevLog 1: Как я упаковал дух олдскульных стратегий в C и WebAssembly
Привет! В этой серии статей я подробно разберу процесс создания MINI HEROES — пошаговой тактической стратегии на гексагональной карте. Моей целью было не просто сделать игру, а воссоздать дух классики 90-х, используя современный подход к веб-дистрибуции.
Ссылка на онлайн-версию игры ниже:
Философия «Old School» и технический стек
В первой части девлога я расскажу о «движке» игры и о том, почему старые подходы к разработке идеально подходят для современного браузерного гейминга.
Мой технологический фундамент: Я выбрал связку C11 и библиотеку raylib. Это дало мне полный контроль над каждым байтом. Чтобы игра работала в браузере максимально стабильно через WebAssembly, я полностью отказался от динамического выделения памяти. Использование статических массивов исключает утечки и делает производительность предсказуемой: игра всегда точно знает свой объем памяти.
Ключевые фишки реализации:
- Магия гексагонов: Я реализовал систему «островерхих» гексагонов. Моя библиотека берет на себя всё: от перевода клика мыши в координаты сетки до сложного поиска пути (A* и алгоритм Дейкстры) с учетом зон контроля.
- Play-by-Mail 2.0: Я вернул механику обмена сохранениями через файлы. Вы можете просто экспортировать сейв и отправить его другу напрямую. Чтобы всё было честно, я написал детерминированный генератор случайных чисел (XORShift128) — результат любого действия будет идентичен на любом устройстве.
- Послойный рендеринг: Система отрисовки выводит мир поэтапно: ландшафт, сетка, подсветка доступных ходов, юниты с автоматической обводкой контуров и спецэффекты.
- Гибкая логика: С помощью системы
passability_configя могу добавлять новых юнитов (например, летающих или невидимых), просто меняя правила взаимодействия с картой, не затрагивая ядро игры.
В текущей версии v.0.1 я заложил мощный каркас, который позволяет быстро наращивать контент, сохраняя легковесность и тот самый «олдскульный» дух.
Разработка MINI HEROES. Часть 1: Философия «Old School» и технический стек
Привет! В первом выпуске девлога я расскажу, как создавал фундамент MINI HEROES v.0.1. Моей целью было упаковать дух классических стратегий в формат современного WebAssembly.
Технический стек и архитектура
Я выбрал связку C11 и raylib. Это дало мне полный контроль над памятью: в игре практически нет динамического выделения (malloc), всё работает на стабильных статических массивах. Такой подход гарантирует, что в браузере игра не «упадет» из-за утечек и будет летать даже на слабых устройствах.
Ключевые фишки реализации:
- Магия гексагонов: Я реализовал библиотеку для «островерхой» сетки. Система поиска пути использует алгоритмы A* и Дейкстры, учитывая зоны контроля (ZoC) и типы ландшафта.
- Детерминированность: Чтобы игровой процесс был честным, я написал собственный генератор случайных чисел на базе XORShift128. Это позволяет воссоздать ту же последовательность событий при загрузке сохранения.
- Play-by-Mail 2.0: Я отказался от сложных серверов в пользу файлов сохранений. Вы можете просто выгрузить состояние игры и отправить файл другу — прямо как в старые добрые времена.
- Гибкий рендеринг: Моя система отрисовки поддерживает послойный вывод (тайлы, юниты, эффекты) и кастомный движок текста с поддержкой иконок-эмодзи внутри строк.
В версии 0.1 я заложил гибкий каркас: система конфигураций уже позволяет мне легко добавлять в игру летающих юнитов или невидимых лазутчиков, просто меняя правила проходимости в коде.
Почему C11 и Raylib?
Когда речь заходит о разработке под WebAssembly (Wasm), многие выбирают готовые движки (Unity, Godot) или высокоуровневые языки. Я же пошел путем минимализма и выбрал C11 в связке с библиотекой raylib.
Причины выбора:
- Контроль над ресурсами: В C я точно знаю, сколько памяти занимает каждая структура. Это критично для стабильной работы Wasm-модуля в браузере.
- Скорость компиляции: Сборка проекта через Emscripten происходит мгновенно.
- Портируемость: Код, написанный на raylib, легко компилируется как под Windows/Linux, так и под веб, практически без изменений в логике отрисовки.
Архитектура: Никакого динамического хаоса
Одной из главных задач было обеспечить детерминированность игры. Чтобы сохранения работали идеально, а состояние мира было предсказуемым, я применил несколько строгих правил:
- Отказ от динамической памяти: Почти все данные хранятся в статических массивах внутри глобальных структур (например,
game_world). Это исключает утечки памяти и фрагментацию — злейших врагов долгоживущих браузерных сессий. - Header-only подход: Проект организован как набор заголовочных файлов, которые включаются в определенном порядке. Это упрощает сборку и позволяет компилятору лучше оптимизировать код.
- Статическое распределение (Static Allocation): Размеры карты, максимальное количество юнитов и длина путей жестко заданы константами (
MAX_MAP_WIDTH,MAX_UNITS). Это гарантирует, что игра не «упадет» из-за нехватки памяти в разгар сражения.
Play-by-Mail в эпоху Web 3.0
Идея «игр по почте» (Play-by-Mail) кажется архаичной, но именно она легла в основу системы сохранений MINI HEROES. Я реализовал полную сериализацию состояния мира в файл.
Как это работает: Игрок может выгрузить текущее состояние игры в небольшой файл, который содержит всё: позиции юнитов, их здоровье, текущий ход и даже состояние генератора случайных чисел (PRNG). Этот файл можно передать другому игроку, и он продолжит игру именно с того момента, где вы остановились. Никаких баз данных и серверов — только чистые данные.
Разработка MINI HEROES. Часть 2: Математика гексагонов и детерминированный хаос
Если архитектура — это скелет игры, то математика — это её нервная система. В пошаговой стратегии точность вычислений и предсказуемость поведения системы важнее, чем количество кадров в секунду.
1. Гексагональная сетка: Почему «Pointy-Top»?
Я выбрал гексагоны с «острой» вершиной (pointy-top). В отличие от квадратной сетки, гексагоны обеспечивают одинаковое расстояние между центрами всех соседних ячеек, что критично для баланса перемещения.
Техническая реализация:
Для работы с сеткой я использую систему осевых координат (Axial Coordinates: q, r). Это позволяет свести сложные геометрические задачи к простым операциям с целыми числами.
- Преобразование координат: Чтобы понять, на какой гексагон нажал игрок, используется математическая проекция экранных координат (x, y) в дробные осевые координаты с последующим округлением к ближайшему гексагону.
- Хранение: Несмотря на то, что логически сетка гексагональная, в памяти она хранится как двумерный массив. Это требует небольшого смещения при отрисовке четных/нечетных рядов, но значительно упрощает сериализацию данных.
2. XORShift128: Честная случайность
В MINI HEROES реализована система Play-by-Mail. Чтобы один и тот же файл сохранения выдавал идентичные результаты боя на разных устройствах, мне нужен был абсолютно детерминированный генератор случайных чисел (PRNG).
Стандартная функция rand() в C не гарантирует одинаковую последовательность чисел на разных компиляторах или операционных системах. Поэтому я внедрил алгоритм XORShift128.
Преимущества для проекта:
- Компактность: Состояние генератора — это всего четыре 32-битных числа.
- Скорость: Алгоритм использует только побитовые сдвиги и операции XOR, что идеально для Wasm.
- Синхронизация: Состояние PRNG записывается в файл сохранения. Если вы передадите сохранение другу, и он нажмет «Атаковать», результат будет ровно таким же, каким он был бы у вас.
3. Навигация: Дейкстра против A*
В игре используются два алгоритма поиска пути, каждый для своей задачи:
- Алгоритм Дейкстры: Я использую его для расчета «зоны хода». Когда вы выбираете юнита, игра должна мгновенно подсветить все доступные клетки, учитывая стоимость ландшафта (леса — дороже, дороги — дешевле). Дейкстра идеально подходит, так как он ищет кратчайшие пути от одной точки до всех остальных в радиусе действия.
- A* (A-star): Когда цель выбрана, включается A*. Он работает быстрее для нахождения конкретного пути к цели за счет использования эвристики (расстояния до финиша), не проверяя лишние клетки.
4. Конфигурация проходимости (Passability Config)
Чтобы не загромождать код бесконечными проверками if(unit == FLYING && tile == MOUNTAIN), я реализовал систему через функциональные указатели в структурах конфигурации.
Каждый тип ландшафта и каждый тип юнита имеют свои коэффициенты проходимости. Система passability_config позволяет на лету вычислять, может ли юнит пройти сквозь «туман войны», заблокирован ли путь вражеской зоной контроля (ZoC) или может ли он атаковать через препятствие.
Разработка MINI HEROES. Часть 3: Графика, спрайты и «умный» текст
Когда работаешь с чистым C и Raylib, у тебя нет готового инспектора объектов или перетаскивания ассетов мышкой. Каждая иконка, каждый юнит и каждый эффект — это результат работы системы управления ресурсами, которую я спроектировал для максимальной легкости.
1. Система атласов и спрайтов
Чтобы минимизировать количество переключений текстур (Texture Swaps), что критично для производительности в браузере, я использую систему Sprite Collections.
- Коллекции: Все изображения группируются в коллекции (например,
COLLECTION_TERRAIN,COLLECTION_UNITS). - Идентификаторы: Вместо строк-путей к файлам в коде используются перечисления (
enum). Это позволяет избежать ошибок в опечатках и ускоряет доступ. - Рендеринг: Функция
render_spriteпринимает ID спрайта, координаты и параметры трансформации. Она сама знает, из какой части текстурного атласа нужно вырезать нужный фрагмент.
2. Многослойная отрисовка карты (render_tile_map)
Отрисовка гексагональной карты — это не просто вывод картинок в цикле. Чтобы мир выглядел целостным, функция render_tile_map работает по принципу «слоеного пирога»:
- Нижний слой: Базовый ландшафт (трава, песок, вода).
- Слой состояния: Подсветка доступных ходов, путей перемещения и зон атаки.
- Объекты: Декорации и препятствия.
- Юниты: Сами персонажи.
- Верхний слой: Эффекты (частицы, анимации получения урона) и курсор.
3. Динамическая обводка юнитов (Outline)
Одной из «фишек» визуального стиля стала система автоматической обводки. Вместо того чтобы рисовать обводку для каждого кадра анимации вручную, я реализовал генерацию масок.
Когда юнит выделен, игра берет его спрайт и отрисовывает его с небольшим смещением в четыре стороны ярким цветом, а затем накладывает основной спрайт сверху. Это создает четкий тактический контур, который позволяет сразу отличить своего юнита от вражеского или нейтрального, не перегружая экран лишними иконками.
4. Текстовый движок с поддержкой иконок
В пошаговых стратегиях много цифр и характеристик: здоровье, урон, очки действия. Читать сухой текст скучно, поэтому я написал небольшое расширение для текстовой системы Raylib.
Что оно умеет:
- Встроенные иконки: Я использую специальные теги в строках. Например, строка
"{hp} 10 / {ap} 3"автоматически заменяет теги на соответствующие иконки сердца и сапога прямо во время отрисовки. - Автоматический перенос: Система рассчитывает ширину текста и переносит слова, если они не влезают в границы диалогового окна.
- Детерминированный вывод: Текст всегда рендерится пиксель-в-пиксель одинаково, что важно для сохранения визуального стиля при изменении масштаба (Zoom).
5. Камера и интерполяция
Чтобы перемещение по карте было плавным, камера не просто «прыгает» к активному юниту. Я использую линейную интерполяцию (LERP) для всех движений камеры.
Даже если вы играете на старом ноутбуке через браузер, масштабирование колесиком мыши и панорамирование правой кнопкой будут ощущаться мягко, без резких рывков. Это мелочь, которая сильно влияет на «ощущение» качества продукта (Game Feel).
Разработка MINI HEROES. Часть 4: Мозг игры — Предикаты и гибкая конфигурация логики
В пошаговых стратегиях логика часто превращается в «спагетти» из бесконечных условий: может ли этот юнит наступить на эту клетку? Видит ли он того врага? Может ли он захватить это здание?
Чтобы не утонуть в if-else, я спроектировал систему, основанную на предикатах и конфигурационных структурах с функциональными указателями. Это позволяет мне менять правила игры, не переписывая основной движок.
1. Предикаты: Чистый API для сложных проверок
Вместо того чтобы каждый раз проверять флаги внутри структур map_tile или map_unit, я вынес их в набор инлайновых функций-помощников. Это делает код самодокументированным.
Например, проверка «присутствует ли юнит физически на карте» выглядит так:
static inline bool unit_is_on_map(map_unit* u) {
return unit_is_valid(u) && !u->is_outsider && !u->is_transporting;
}
Я сразу вижу, что если юнит сидит в транспорте или еще не «призван» (outsider), то для многих систем поиска он просто не существует. Такие функции как unit_can_fly, tile_is_enemy или transport_can_carry_unit — это кирпичики, из которых строится вся тактика.
2. Паттерн «Стратегия» на функциональных указателях
Сердце логики — структура game_config. Она агрегирует в себе несколько специализированных конфигураций:
- passability_config: Правила движения и блокировок.
- line_of_sight_config: Просчет видимости и стрельбы.
- interaction_config: Что происходит при контакте юнита с целью.
- vision_config: Радиус обзора и обнаружение невидимок.
Каждая такая под-конфигурация состоит из указателей на функции. Например, расчет стоимости хода (tile_cost) или проверка зоны контроля (unit_zoc).
Почему это удобно?
Если я захочу добавить уровень, где гравитация изменена и все ходят иначе, мне не нужно менять код поиска пути. Я просто создам новую passability_config и подставлю её в g_config.
3. Зоны контроля и многоуровневая проходимость
Благодаря системе default_tile_cost, я реализовал поддержку разных типов перемещения: пешком, полет и плавание.
- Летающие юниты игнорируют штрафы ландшафта, если клетка помечена как
flyable. - Зоны контроля (ZoC) работают через функцию
default_unit_zoc. Она учитывает даже скрытые параметры: например, невидимый юнит не создает зону контроля, пока не будет раскрыт (is_revealed).
4. Транспорт и невидимость
В этой версии я заложил мощную базу для продвинутых механик:
- Система транспорта: С помощью
transport_can_carry_typeигра проверяет, может ли, скажем, корабль перевозить пехоту или других летунов. Юниты внутри транспорта получают флагis_transportingи исключаются из обычных циклов отрисовки и поиска. - Стелс-механика: Предикаты
unit_is_hiddenиcan_revealпозволяют реализовать механику «тумана войны» и засад. Юнит может быть физически на карте, но функцияunit_visible_to_playerвернетfalse, если враг его не обнаружил.
5. Универсальное взаимодействие (Interactions)
Для действий я использую два перечисления: interact_type (Близко/Дистанционно) и interact_result.
Когда юнит взаимодействует с объектом или врагом, функция возвращает результат, например INTERACT_RESULT_TARGET_REMOVED. Это позволяет движку автоматически очищать карту или проигрывать нужные анимации, не вникая в детали того, была ли это атака мечом или использование магического свитка.
Разработка MINI HEROES. Часть 5: Единый источник истины и магия сохранений
В современных движках данные часто разбросаны по сотням объектов, скриптов и менеджеров. В моем проекте я придерживаюсь принципа SSOT (Single Source of Truth). Все, что происходит в игре, живет внутри одной-единственной структуры — game_world.
1. Структура game_world: Все яйца в одной корзине
Чтобы реализовать честный «Play-by-Mail» (игра по переписке), мне нужно было гарантировать, что состояние игры на моем компьютере будет бит-в-бит идентичным состоянию на вашем.
Что внутри game_world?
- Карта: Полная сетка тайлов со всеми их свойствами (ландшафт, владельцы, ресурсы).
- Юниты: Статический массив всех героев и существ.
- Генератор (PRNG): Текущее состояние XORShift128. Это критично: если мы сохранимся перед броском кубика, после загрузки результат броска должен остаться прежним.
- Метаданные: Номер текущего хода, активный игрок, история событий.
2. Секрет идеальной сериализации: Никаких указателей!
Главная проблема сохранений в C — это указатели. Если вы сохраните адрес памяти 0x7ffee123, то после перезагрузки по этому адресу может лежать что угодно, но только не ваш юнит.
Мое решение:
Внутри game_world используются только индексы и статические массивы.
- Вместо
map_unit* targetя используюint32_t target_id. - Вместо динамических списков
std::vectorилиmalloc— массивы фиксированного размераMAX_UNITS.
Результат: Структура становится «плоской». Это позволяет использовать самый быстрый способ сохранения в истории программирования:
// Сохранение
fwrite(&g_world, sizeof(game_world), 1, file);
// Загрузка
fread(&g_world, sizeof(game_world), 1, file);
Мы просто берем кусок памяти размером в несколько мегабайт и копируем его на диск как есть.
3. Передача сохранения: От дискеты до Telegram
Поскольку весь мир игры — это один бинарный файл, процесс передачи хода превращается в обычную пересылку документа.
- Экспорт: Игрок нажимает «Save». В браузере (через Emscripten) это триггерит скачивание файла
save.dat. - Передача: Вы отправляете этот файл другу через любой мессенджер или почту.
- Импорт: Друг открывает игру в своем браузере, выбирает «Load» и загружает ваш файл.
Благодаря детерминированному генератору чисел, ваш друг увидит ту же самую ситуацию на карте, с теми же шансами на успех, что были у вас. Это возвращает нас в эпоху классических стратегий, где игроки могли неделями разыгрывать одну партию, обмениваясь файлами.
4. Оптимизация для WebAssembly
В контексте браузера есть нюанс: у нас нет прямого доступа к жесткому диску. Emscripten создает виртуальную файловую систему в оперативной памяти (MEMFS).
Чтобы файл реально попал к пользователю, я использую небольшой JavaScript-мостик, который превращает байты из памяти Wasm в Blob и инициирует загрузку файла через браузер. Это безопасно, быстро и не требует настройки серверов или баз данных.
Что это дает игроку?
- Полная автономность: Вы владеете своими сохранениями.
- Кроссплатформенность: Сохранение с ПК гарантированно откроется на смартфоне в браузере.
- Никаких лагов: Загрузка происходит мгновенно, так как это просто копирование блока памяти.
Разработка MINI HEROES. Часть 6: Инъекция логики и рождение мира
В предыдущих частях я описывал общую архитектуру и предикаты. Но «движок» — это лишь пустая коробка. Чтобы она стала игрой, нужно наполнить её конкретными правилами. В моем коде это происходит через функцию configure_hero_game_logic.
1. Специализация правил (Logic Injection)
Я использую паттерн «Инъекция логики». Вместо того чтобы прописывать поведение героев внутри движка, я передаю в него ссылки на функции, специфичные именно для этого режима игры.
- Тактическое зрение и Маяки: Функция
hero_unit_visionрасширяет стандартный радиус обзора. Если герой стоит на тайле «Маяк» (TILE_LIGHTHOUSE), он получает бонус к видимости. Это создает стратегические точки интереса на карте, за которые стоит бороться. - Боевой контакт: В
hero_interact_unitпрописано простое правило: если актор и цель — враги, то результатом взаимодействия будет удаление цели. Это фундамент для будущей более сложной боевки. - Захват объектов: Самая интересная часть —
hero_interact_tile. Когда герой подходит к интерактивному объекту (например, золотой шахте), срабатываетexec_capture. Захват не только передает объект игроку, но и мгновенно раскрывает туман войны вокруг него и завершает ход героя.
2. Валидация целей: «Дружественный огонь отключен»
Чтобы интерфейс не предлагал игроку абсурдных действий, функция hero_can_be_target фильтрует все клики мышкой.
- Нельзя атаковать своих.
- Нельзя захватить тайл, который не предназначен для этого. Благодаря этому, когда вы ведете героя по карте, движок сам подсвечивает только те действия, которые имеют смысл в контексте текущих правил.
3. Инициализация: Из хаоса в порядок (init_new_game)
Создание новой партии — это сложный процесс, который должен быть абсолютно воспроизводимым (помним про детерминизм!). Вот как это происходит у меня:
- Обнуление и Сид: Мы полностью очищаем структуру мира и инициализируем PRNG переданным сидом (например, на основе текущего времени).
- Генерация ландшафта: Вызывается
generate_world_map, которая создает реки, горы и леса. - Кэширование спрайтов: Чтобы не тратить время в цикле отрисовки, игра заранее вычисляет, какой спрайт соответствует каждому типу тайла (автотайлинг).
- Экономика и Игроки: Каждому игроку выдается стартовый капитал: Золото, Дерево и Руда. Это классический набор ресурсов для развития.
- Армия и Герои: Игра находит подходящие стартовые позиции, создает героев и спавнит им начальные армии.
- Монстры: Чтобы мир не казался пустым, по карте случайным образом распределяются нейтральные отряды монстров.
4. Fog of War: Первый взгляд на мир
В конце инициализации игра проходит по всем юнитам игрока и вызывает reveal_unit_vision. Это критический момент: мир изначально скрыт туманом войны, и только присутствие ваших войск «прожигает» в нем видимые области. Это заставляет игрока исследовать карту, что является одной из основ жанра 4X-стратегий.
5. Подготовка к запуску (state_init_hero_game)
Перед тем как игрок увидит первый кадр, система выполняет «холодный пуск»: загружает ресурсы из директории, инициализирует индексы спрайтов и, наконец, применяет ту самую конфигурацию логики configure_hero_game_logic. Только после этого управление передается главному игровому циклу.
Разработка MINI HEROES. Часть 7: Интерфейс, живой мир и логика транспорта
Когда архитектура и математика гексагонов готовы, встает вопрос: как игрок будет с этим взаимодействовать? В MINI HEROES я реализовал кастомную систему UI и диалогов, а также глубокую механику перевозки юнитов, которая добавляет игре дополнительный слой тактики.
1. Система диалогов и интерактивность
Мир игры наполнен объектами: замки, шахты, маяки, магические башни. Чтобы взаимодействие с ними не было «немым», я внедрил систему динамических диалогов.
- Маппинг объектов: Через массив
g_tile_dialog_keysя связал типы тайлов с ключами локализации. Если юнит наступает на тайл «Золотая шахта», игра автоматически находит ключdialog_gold_mineи подтягивает заголовок и текст на нужном языке (RU/EN). - Состояние диалога: Диалог — это часть структуры мира. Когда он открыт, основной ввод блокируется (
handle_dialog_input), позволяя игроку сосредоточиться на информации. Это классический подход, который гарантирует, что вы случайно не отправите героя в другой конец карты, пока читаете описание хижины ведьмы.
2. Визуальная обратная связь через цвет
В тактике важно считывать состояние поля боя за доли секунды. Я реализовал систему динамической окраски и обводки (outlines):
- Цвета игроков: У каждого игрока свой флаг и цвет армии (
COLOR_PLAYER_0и т.д.). - Состояние юнита: Если юнит уже походил, его спрайт затеняется (
COLOR_UNIT_EXHAUSTED). Невидимые юниты для владельца отображаются полупрозрачными, а для врага — скрыты вовсе. - Умная обводка: Выбранный юнит подсвечивается золотым, а враги получают красную обводку. Это реализовано программно, без отрисовки дополнительных ассетов.
3. Механика транспорта: Корабли и десант
Одной из самых сложных задач стала реализация транспорта (кораблей или караванов). Это не просто юнит, это «контейнер» для других юнитов.
- Посадка и высадка: Я добавил специальные команды
CMD_EMBARKиCMD_DISEMBARK. Посадка происходит автоматически при клике на свой транспорт. Высадка же требует хитрости: я ввел режим Disembark Mode, который активируется зажатием клавиши CTRL или автоматически, когда у транспорта закончились очки хода (MP). - Синхронизация позиций: Пассажиры физически не удаляются с карты, они получают флаг
is_transporting. Чтобы они «не отставали» от корабля, в каждом кадре движения вызываетсяsync_passengers_position, которая привязывает координаты всех пассажиров к координатам транспорта. - UI транспорта: В панели управления отображается статус загрузки (например, «2/5 юнитов»), а игроку выводятся подсказки о том, как высадить десант на берег.
4. Обработка ввода: От клика до команды
Система ввода в MINI HEROES работает по принципу иерархии. Функция state_hero_input опрашивает мышь и клавиатуру, но не меняет состояние мира напрямую, а формирует команду.
- Камера: Правая кнопка мыши — панорамирование, колесико — зум. Это стандарт, к которому привыкли игроки.
- Горячие клавиши: Пробел для конца хода, Tab для переключения между героями, F2/F9 для быстрых сохранений.
- Логика клика: Это самая «умная» часть. Игра анализирует:
- Кликнули на врага? Если мы рядом — атака ближнего боя, если далеко — дальнего.
- Кликнули на пустую клетку? Строим путь и идем.
- Кликнули на свой корабль? Садимся на борт.
5. Подготовка кадра (Render Pipeline)
Перед отрисовкой каждого кадра вызывается prepare_render_map. Она собирает «слоеный пирог» видимых элементов:
- Сначала базовые тайлы и ландшафт.
- Затем highlights — подсветка доступных ходов (синим) и потенциальных целей (красным).
- Превью пути — линия, показывающая, как именно пойдет юнит.
- И, наконец, сами юниты и эффекты.
Такое разделение позволяет интерфейсу быть отзывчивым: как только вы наводите мышь на клетку, игра мгновенно пересчитывает путь и подсвечивает его, используя build_path_preview.
Разработка MINI HEROES. Часть 8: Командный процессор и асинхронное выполнение
Одна из главных трудностей при написании пошаговой стратегии на C — это переход от мгновенного изменения данных к визуальному процессу. Если вы просто измените x и y юнита, он телепортируется. Чтобы этого избежать, я внедрил архитектуру Queue-based Command System (систему очередей команд).
1. Очередь команд: Разделяй и властвуй
Вместо того чтобы выполнять действие сразу при клике, игра создает объект game_command и кладет его в очередь g_cmd_queue.
Зачем это нужно?
- Последовательность: Если за одно действие должно произойти несколько событий (например, юнит подошел и открыл диалог), очередь гарантирует их правильный порядок.
- Асинхронность: Мы можем запустить команду и ждать её завершения. Функция
cmd_executeвозвращаетfalse, если команда требует времени (например, анимация движения), иtrue, если она выполнилась мгновенно.
2. Контроллер движения (Movement Controller)
Команда CMD_MOVE — самая сложная. Она не просто меняет координаты, она активирует movement_controller.
Как это работает:
- Команда
CMD_MOVEпроверяет путь черезfind_path. - Если путь есть, контроллер переходит в состояние
is_moving = true. - Каждый кадр юнит делает шаг, срабатывает таймер задержки (
delay_frames), и только когда юнит доходит до финальной точки, команда считается завершенной (cmd_advance).
3. Обработка сложных действий: Посадка и Высадка
С помощью командного процессора я реализовал специфические механики для транспорта:
- CMD_EMBARK (Посадка): Юнит «исчезает» с карты и привязывается к транспорту. Важный нюанс: после посадки выбор игрока автоматически переключается на транспорт, чтобы можно было сразу плыть дальше.
- CMD_DISEMBARK (Высадка): Здесь актором выступает сам транспорт. Он «выпускает» первого пассажира на соседний гексагон. После этого вызывается
reveal_unit_vision, так как высадившийся юнит приносит с собой «свой» обзор.
4. Взаимодействие и обновление (Refresh)
Команды CMD_CLOSE_INTERACT и CMD_DISTANT_INTERACT используют систему предикатов из предыдущих частей. После любого взаимодействия вызывается магическая функция refresh_after_action.
Что делает Refresh:
- Сбрасывает превью пути.
- Пересчитывает доступную зону хода для текущего юнита (если у него остались очки действия).
- Если очков не осталось — снимает выделение.
Это гарантирует, что пользовательский интерфейс всегда отображает актуальную информацию: если после удара у вас осталось 2 очка хода, вы сразу увидите обновленную синюю зону перемещения.
5. Безопасность и Детерминизм
Вся очередь команд оперирует только ID юнитов (unit_id), а не прямыми указателями. Это критично для стабильности. Если юнит будет уничтожен во время выполнения очереди, функция find_unit_by_id просто вернет NULL, и команда будет безопасно пропущена, не вызывая падения игры (crash).
Что это дает в итоге? Благодаря командному процессору, MINI HEROES ощущается как «взрослая» стратегия. Каждое нажатие кнопки запускает цепочку событий: камера центрируется на герое, проигрывается путь, обновляются ресурсы, открываются диалоги. И всё это — в строгом, предсказуемом порядке.
Разработка MINI HEROES. Часть 9: Магия рендеринга и визуальный фидбек
В пошаговых стратегиях визуализация — это не только красота, но и передача огромного количества информации: куда я могу пойти? Кого могу атаковать? Чья это шахта? Чтобы не перегружать основной цикл, я выделил отрисовку в отдельный конвейер (pipeline) — GAME_HERO_TILE_MAP_RENDER.
1. Подготовка слоев (Multi-pass Rendering)
Рендеринг одного кадра в игре — это процесс наслоения информации. Вместо того чтобы рисовать всё сразу, функция prepare_render_map собирает данные по слоям:
- Слой ландшафта и Туман войны (
render_base_tiles): Игра проверяет флагdiscoveredдля каждого тайла. Если территория не исследована, рисуется «черный гексагон». Если же тайл открыт, накладывается спрайт почвы и, если это захватываемый объект (например, лесопилка), поверх рисуется флаг владельца. - Слой навигации (
render_movement_highlights): Здесь происходит магия поиска путей. На основеpath_mapигра подсвечивает синим те клетки, куда юнит может дойти за этот ход. Особое внимание уделено ZoC (Zone of Control): если юнит входит в зону контроля врага, эти клетки подсвечиваются специфическим цветом, предупреждая игрока об остановке.
2. Динамическая подсветка целей
Функция render_target_highlights — это интеллект интерфейса. Она анализирует выбранного юнита и подсвечивает:
- Красным: Врагов для ближней или дальней атаки.
- Желтым: Нейтральные объекты для захвата.
- Зеленым/Бирюзовым: Свой транспорт для посадки или берег для высадки десанта.
Такой подход позволяет игроку видеть все возможные действия, просто выбрав героя, без необходимости кликать по каждому врагу отдельно.
3. Отрисовка юнитов и индикаторы
Юниты рисуются последними, чтобы они всегда были поверх ландшафта. Но и здесь есть свои хитрости:
- Видимость: Проверка
is_unit_visible_to_playerскрывает вражеских лазутчиков. - Статус: Уставшие юниты (без очков хода) рисуются с наложением темного фильтра.
- Транспортная логика: Если перед нами корабль, игра вызывает
get_transport_passenger_countи рисует маленькую цифру поверх спрайта. Это критически важно: игрок всегда должен знать, пуст ли его транспорт или он везет целую армию.
4. Превью пути и Ховер
Чтобы игрок не совершил ошибку, в игре работает render_path_preview. Когда вы ведете мышкой по карте, игра в реальном времени строит путь от героя до курсора и рисует «пунктирную» дорожку из подсвеченных гексагонов. Это дает стопроцентную уверенность в том, каким маршрутом пойдет герой и сколько очков хода он потратит.
5. Оптимизация: Render Tile Map
Чтобы всё это работало быстро даже в браузере через WebAssembly, я не использую тяжелые объекты. Вместо этого заполняется легковесная структура render_tile_map. Это простой массив данных о том, какой спрайт и какой цвет нужно вывести в конкретной позиции экрана.
Финальный вызов render_tile_map_full просто пробегается по этому массиву и отправляет команды на отрисовку в GPU через Raylib.
Итог версии 0.1:
На данный момент у меня есть полностью рабочее ядро: гексагональная навигация, система захвата ресурсов, боевые столкновения и детерминированные сохранения. Всё это работает в браузере, весит считанные мегабайты и не требует сервера.
Мы получаем чистую, информативную и быструю картинку. Игрок видит границы своих владений, радиус хода своих героев и угрозы от врагов, а туман войны надежно скрывает тайны карты.