Разработка MINI HEROES. DevLog 1. Философия Old School и C11

#172  вторник, 27 января 2026 г.  вторник, 27 января 2026 г.  45 минут(ы)  4399 слов

Разработка MINI HEROES. DevLog 1: Как я упаковал дух олдскульных стратегий в C и WebAssembly

Привет! В этой серии статей я подробно разберу процесс создания MINI HEROES — пошаговой тактической стратегии на гексагональной карте. Моей целью было не просто сделать игру, а воссоздать дух классики 90-х, используя современный подход к веб-дистрибуции.

Ссылка на онлайн-версию игры ниже:

Играть в MINI HEROES

Философия «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.

Причины выбора:

  1. Контроль над ресурсами: В C я точно знаю, сколько памяти занимает каждая структура. Это критично для стабильной работы Wasm-модуля в браузере.
  2. Скорость компиляции: Сборка проекта через Emscripten происходит мгновенно.
  3. Портируемость: Код, написанный на 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*

В игре используются два алгоритма поиска пути, каждый для своей задачи:

  1. Алгоритм Дейкстры: Я использую его для расчета «зоны хода». Когда вы выбираете юнита, игра должна мгновенно подсветить все доступные клетки, учитывая стоимость ландшафта (леса — дороже, дороги — дешевле). Дейкстра идеально подходит, так как он ищет кратчайшие пути от одной точки до всех остальных в радиусе действия.
  2. 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 работает по принципу «слоеного пирога»:

  1. Нижний слой: Базовый ландшафт (трава, песок, вода).
  2. Слой состояния: Подсветка доступных ходов, путей перемещения и зон атаки.
  3. Объекты: Декорации и препятствия.
  4. Юниты: Сами персонажи.
  5. Верхний слой: Эффекты (частицы, анимации получения урона) и курсор.

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

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

  1. Экспорт: Игрок нажимает «Save». В браузере (через Emscripten) это триггерит скачивание файла save.dat.
  2. Передача: Вы отправляете этот файл другу через любой мессенджер или почту.
  3. Импорт: Друг открывает игру в своем браузере, выбирает «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)

Создание новой партии — это сложный процесс, который должен быть абсолютно воспроизводимым (помним про детерминизм!). Вот как это происходит у меня:

  1. Обнуление и Сид: Мы полностью очищаем структуру мира и инициализируем PRNG переданным сидом (например, на основе текущего времени).
  2. Генерация ландшафта: Вызывается generate_world_map, которая создает реки, горы и леса.
  3. Кэширование спрайтов: Чтобы не тратить время в цикле отрисовки, игра заранее вычисляет, какой спрайт соответствует каждому типу тайла (автотайлинг).
  4. Экономика и Игроки: Каждому игроку выдается стартовый капитал: Золото, Дерево и Руда. Это классический набор ресурсов для развития.
  5. Армия и Герои: Игра находит подходящие стартовые позиции, создает героев и спавнит им начальные армии.
  6. Монстры: Чтобы мир не казался пустым, по карте случайным образом распределяются нейтральные отряды монстров.

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 опрашивает мышь и клавиатуру, но не меняет состояние мира напрямую, а формирует команду.

  1. Камера: Правая кнопка мыши — панорамирование, колесико — зум. Это стандарт, к которому привыкли игроки.
  2. Горячие клавиши: Пробел для конца хода, Tab для переключения между героями, F2/F9 для быстрых сохранений.
  3. Логика клика: Это самая «умная» часть. Игра анализирует:
  4. Кликнули на врага? Если мы рядом — атака ближнего боя, если далеко — дальнего.
  5. Кликнули на пустую клетку? Строим путь и идем.
  6. Кликнули на свой корабль? Садимся на борт.

5. Подготовка кадра (Render Pipeline)

Перед отрисовкой каждого кадра вызывается prepare_render_map. Она собирает «слоеный пирог» видимых элементов:

  1. Сначала базовые тайлы и ландшафт.
  2. Затем highlights — подсветка доступных ходов (синим) и потенциальных целей (красным).
  3. Превью пути — линия, показывающая, как именно пойдет юнит.
  4. И, наконец, сами юниты и эффекты.

Такое разделение позволяет интерфейсу быть отзывчивым: как только вы наводите мышь на клетку, игра мгновенно пересчитывает путь и подсвечивает его, используя 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.

Как это работает:

  1. Команда CMD_MOVE проверяет путь через find_path.
  2. Если путь есть, контроллер переходит в состояние is_moving = true.
  3. Каждый кадр юнит делает шаг, срабатывает таймер задержки (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 собирает данные по слоям:

  1. Слой ландшафта и Туман войны (render_base_tiles): Игра проверяет флаг discovered для каждого тайла. Если территория не исследована, рисуется «черный гексагон». Если же тайл открыт, накладывается спрайт почвы и, если это захватываемый объект (например, лесопилка), поверх рисуется флаг владельца.
  2. Слой навигации (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:

На данный момент у меня есть полностью рабочее ядро: гексагональная навигация, система захвата ресурсов, боевые столкновения и детерминированные сохранения. Всё это работает в браузере, весит считанные мегабайты и не требует сервера.

Мы получаем чистую, информативную и быструю картинку. Игрок видит границы своих владений, радиус хода своих героев и угрозы от врагов, а туман войны надежно скрывает тайны карты.