Вебинар: Хороший тимлид — не друг и не надсмотрщик. Как найти баланс через 1-to-1 - 28.05
Книга представляет собой сборник размышлений о языке программирования C++, алгоритмах и практиках в контексте разработки игр — о его сильных и слабых сторонах, практических решениях и устоявшихся способах работы. C++ на сегодняшний день остается основным языком в индустрии разработки игр благодаря сочетанию высокой производительности, гибкости и широких возможностей низкоуровневого контроля.

Из всех аспектов разработки игр — это моя любимая тема. За последние годы я столько раз возился с монстрами, что если бы мне давали доллар за каждого, кто застрял в углу, я бы уже купил себе дом. Сначала были одиночки, потом рои, потом группы с координацией через общие BT (Behavior Trees).
И оказалось, что не каждой игре вообще нужен "умный" AI. Да, звучит странно, но в половине случаев достаточно пары простых скриптов, которые по таймеру издают "Ой!" и с криком бегут в стену, и игрок будет доволен. Потому что AI — это не магия, это просто код. Если в вашем движке можно писать if, switch, запускать корутины или ставить таймеры — поздравляю: вы уже поддерживаете AI. Весь "интеллект" — это лишь кучка условий, хитро замаскированных под разум. Но когда хочется "по-взрослому", разработчики обычно берут один или несколько подходов, рассмотренных далее.
...Behavior Trees (BT)
Фактически это иерархия задач: "Если голоден — поешь, если видишь врага — атакуй, если ранен — убегай". BT представляют собой иерархическую структуру принятия решений, организованную в виде дерева из композитных и листовых узлов, где каждый узел возвращает один из трёх статусов: Success, Failure или Running.
Композитные узлы (Selector, Sequence, Parallel) управляют логикой выполнения дочерних узлов, реализуя приоритетную систему задач по принципу "сверху-вниз". Selector может содержать условия "Health < 20% → Flee", "Enemy in sight → Attack", "Hunger > 80% → Eat", где каждая ветка выполняется только при соответствующих условиях. Листовые узлы (Action, Condition) реализуют конкретные действия или проверки состояния. Такая архитектура обеспечивает естественную читаемость кода — дерево буквально отражает логику принятия решений, что делает подход на основе BT популярным в разработке игр и обеспечивает возможность создания и использования визуальных редакторов с hot-reload функциональностью, позволяя геймдизайнерам итерироваться по ИИ без перекомпиляции. Но! BT на 500+ узлов превращается в "лапшу", где никто не понимает, как она работает, даже автор (особенно автор).
...GOAP (Goal-Oriented Action Planning)
"Я хочу убить игрока. Какие действия мне для этого подойдут?" — вот как мыслит AI на GOAP. Не просто идёт по скрипту, но сам составляет план на основе доступных действий и текущего мира. Красиво? Да. Элегантно? Очень. Но писать собственный планировщик — это весьма сложно. Нужно учитывать ресурсы, конфликты, стоимость действий, циклы... GOAP хорош для сложных систем и больших студий, но требует очень много времени и хорошей команды, чтобы всё отладить.
В отличие от жёстко заданных деревьев поведения или конечных автоматов, GOAP использует декларативный подход: разработчик определяет набор доступных действий (Actions) с их предусловиями (Preconditions) и эффектами (Effects), а система планирования динамически находит оптимальную цепочку действий от текущего состояния мира к желаемому. Например, для цели "убить игрока" планировщик может построить план: "взять оружие → подойти к игроку → атаковать", автоматически учитывая, что для атаки требуется оружие, для получения оружия необходимо дойти до склада, а для движения нужно иметь возможность передвигаться, это создаёт более естественное и адаптивное поведение, где NPC может реагировать на изменения окружения, перестраивая планы "на лету".
...FSM (Finite State Machine)
Это классика. Состояния и переходы: стоит — идёт — атакует — убегает. FSM (конечный автомат) представляет собой математическую модель, где объект может находиться в одном из конечного множества состояний, а переходы между состояниями происходят по определённым правилам при наступлении событий или условий. Этот паттерн идеально подходит для описания поведения персонажей и врагов. Игрок может находиться в состояниях "стоит", "идёт", "бежит", "прыгает", "атакует" или "мёртв", а ИИ противника — в состояниях "патрулирует", "преследует", "атакует", "отступает" или "ищет укрытие". Каждое состояние использует специфичную логику: анимации, звуки, физические параметры, доступные действия. Переходы между состояниями контролируются чёткими условиями — нажатие клавиш, столкновения, изменение здоровья, расстояние до цели, что делает поведение предсказуемым и легко отлаживаемым.
...Utility AI
Представьте себе набор действий NPC, где каждое из них доступно для выполнения, если наберётся достаточно условий для его запуска. Система постоянно пересчитывает вес (полезность) доступных действий и выбирает самое подходящее, создавая плавные переходы между поведениями без жёстких условных переходов FSM или сложного планирования GOAP. Но когда у NPC есть 10+ различных действий с функциями полезности, зависящими от десятков параметров, становится практически невозможно предсказать, как он себя поведёт в конкретной ситуации.
Отладка превращается в кошмар ("почему NPC вдруг начал танцевать вместо атаки?"), требует трассировки всех функций полезности и их взаимодействий. Необходимость тонкой настройки весов под разные типы врагов и ситуации будет конфликтовать с локально оптимальными решениями, поэтому Utility-AI-системы часто комбинируют с FSM для высокоуровневых состояний или используют гибридные подходы, где функции работают только внутри определённых контекстов, ограничивая их непредсказуемость.
...экзотика: HTN, ML, Neural Nets, RL
HTN (Hierarchical Task Network) расширяют концепцию GOAP, используя иерархическую декомпозицию задач, где сложные цели разбиваются на подзадачи через систему методов и примитивных действий. Это нейросеть, которая умеет играть в вашу игру за конкретного NPC и создаёт структурированный план с возможностью долгосрочного стратегического мышления.
Reinforcement Learning (RL) и нейросети обещают ещё более впечатляющие результаты — агенты, которые самостоятельно обучаются оптимальным стратегиям через взаимодействие с окружением, адаптируются к стилю игры пользователя и демонстрируют самоорганизующееся (уникальное, emergent) поведение.
Исследовательские работы показывают впечатляющие результаты: AlphaStar в StarCraft II, OpenAI Five в Dota 2, агенты, превосходящие человеческих игроков в сложных стратегических играх.
Эти достижения создают иллюзию готовности технологий для массового применения, но обучение агента для даже относительно простой игры может требовать миллионы прогонов, что в переводе на реальное время означает недели или месяцы непрерывных вычислений на мощных GPU-кластерах. Стоимость обучения одного агента может достигать сотен тысяч долларов, а при изменении геймплея или добавлении новых механик требуется полное переобучение.
Дополнительные проблемы заключаются в непредсказуемости поведения обученных моделей, поэтому отлаживать "чёрный ящик" нейросетей не могут даже ML-инженеры, которые его создали. В результате даже крупные студии предпочитают применять ML-подходы только для специфических задач (процедурная генерация, аналитика игроков), а для основного геймплейного ИИ полагаются на проверенные классические методы с предсказуемым поведением и контролируемыми затратами на разработку.
...pathfinding
Алгоритм A* (A-star) и его производные представляют собой "рабочую лошадку" ИИ в играх, обеспечивая дешёвый поиск пути через граф узлов с помощью эвристической функции. Классический A* использует формулу фактической стоимости перемещения по узлам графа от старта до текущего узла. Разные модификации алгоритма превращают теоретически сложный механизм в практический инструмент, способный обрабатывать сотни запросов навигации за кадр.
Главная проблема — это создание и поддержание дискретного графа навигации и переложение точек пути в пространство игры. A* возвращает последовательность точек, но NPC должен физически перемещаться между ними, учитывая свои габариты, инерцию, препятствия и динамические объекты. Классические проблемы включают: застревание в углах из-за упрощённой геометрии, логические блокировки при попытке пройти через узкие проходы, невозможность обхода движущихся препятствий (других NPC, игрока, динамических объектов), проблемы с вертикальной навигацией (лестницы, платформы, прыжки). Но, несмотря на эти проблемы, A* остаётся самой надёжной навигационной системой, даже если каждая новая карта требует тщательной настройки геометрии и отладки граничных кейсов.
... navmesh
И наконец — основа навигации, навигационная сетка (навмеш) превращает сложную трёхмерную геометрию мира в дискретную сеть узлов и связей, где каждый полигон представляет проходимую область, а рёбра между полигонами — возможные переходы.
Создание качественной навигационной сетки требует баланса между точностью представления и вычислительной эффективностью: слишком детальная сетка создаёт избыточные узлы и замедляет поиск пути, слишком грубая — приводит к неестественным путям и застреванием в углах. Современные инструменты автоматизируют процесс генерации: NavPowe/Recast/Detour (open-source стандарт, используемый в Unity, Unreal Engine) анализирует геометрию уровня и создаёт навмеш на основе параметров агента (радиус, высота, максимальный угол подъёма, высота прыжка), какие-то решения могут предоставлять более продвинутые алгоритмы для сложных сценариев, а проприетарные решения крупных студий оптимизированы под специфические потребности их движков.
Но даже самый совершенный алгоритм генерации не понимает семантики уровня — не знает (алгоритмически довольно сложно понять, это угол здания или край деревянной беседки с решетками), что определённая область предназначена для скрытности, что мост может разрушиться, что лестница требует специальной анимации подъёма. Типичные проблемы включают: создание связей через тонкие стены или окна, некорректное соединение разноуровневых платформ, игнорирование динамических препятствий (двери, лифты, разрушаемые объекты), сломанные графы в сложных областях.
Поэтому современная разработка подразумевает автоматическую генерацию с ручным "допиливанием" уровня level-дизайнером, который размещает специальные маркеры для точек прыжков, укрытий, обхода углов, отмечает области с разными свойствами (скорость движения, стоимость прохода, ограничения по типам юнитов). Без ручной доработки навигации даже самые совершенные ИИ-системы превращаются в беспомощных "котов без лап" — способных принимать умные решения, но неспособных физически их реализовать в игровом мире.
...AI — это не про "умных врагов"
Это про иллюзию разумного поведения, которая живёт в голове игрока. Главное, чтобы враг реагировал, создавал давление, иногда удивлял. А внутри — это всегда if, switch и кусок карты, но игрок этого не знает. И пусть не знает. Потому что хороший AI — это не всегда "умный". AI — это в первую очередь про то, как развлечь игрока, и не дать ему заскучать, а не про интеллект. Пусть NPC тупой — главное, чтобы он вёл себя так, как хочет игрок. Пусть он предсказуем, но так легче запомнить паттерны и тактика от этого только выигрывает. Пусть он не настоящий "ИИ", а просто грамотный скрипт — зато работает стабильно, развлекает игрока и не "тупит". AI — это не про интеллект. Это про иллюзию интеллекта. И если у вас получится заставить игрока поверить, что против него играет кто-то умный — значит, вы всё сделали правильно.
Очень часто для встраиваемой логики используют скриптовые языки вроде Lua, Python, Js, Angel — особенно, если движок написан на C++. Они намного более лояльны к ошибкам во время работы приложения и позволят сэкономить кучу времени. Даже если вы разрабатываете движок с нуля, проще заложить сразу возможность скриптования и учёта возможностей языка, на котором написан сам движок — это снижает накладные расходы и упрощает интеграцию.
Создание движка всё равно требует программирования, здесь не будет коротких и лёгких путей. Какой язык выбрать? Честно, это не критично — лучший язык тот, который вы знаете, опытный разработчик критичные к производительности места сумеет оптимизировать намного лучше на своём языке и попутно избежит проблем новичков. Низкоуровневые языки вроде C++ дают прозрачность в управлении ресурсами, но и требуют качественно лучшего понимания архитектуры и теории разработки.
... это не про производительность и не про fps
Системы скриптинга и конфигурационных файлов служат в первую очередь для увеличения скорости итерации дизайнеров и снижения барьера входа для нетехнических специалистов. Высокоуровневые скриптовые языки типа Lua, Python или собственные предоставляют гибкую песочницу для быстрого прототипирования геймплейной логики, что позволяет дизайнерам экспериментировать с балансом, создавать новые механики и настраивать поведение AI без необходимости пересборки всего проекта и участия программистов.
В контексте низкоуровневых языков каждая модификация требует полного цикла компиляции, линковки и развёртывания, что может занимать от нескольких минут до часов в зависимости от размера кодовой базы. Использование интерпретируемых скриптов и конфигураций JSON/XML создаёт архитектурное разделение между стабильным ядром движка на C++ и изменчивым контентом. Это решение, хотя и вносит небольшие накладные расходы в рантайме (парсинг, интерпретация), кардинально сокращает время разработки и позволяет внедрять A/B тестирование, модификации и патчи без перекомпиляции бинарных файлов игры.
... это про удобство и скорость разработки
Скриптинг обеспечивает принципиально иную модель разработки — цикл "написал-протестировал-исправил" сокращается до секунд благодаря интерпретируемой природе языка и встроенным системам hot-reload. Можно мгновенно изменить алгоритм поиска врага или настроить параметры агрессии, видя результат непосредственно во время игровой сессии. Современные движки предоставляют живые инспекторы переменных, debug-консоли и визуальные средства отладки, что становится важным условием в постоянно меняющихся требованиях дизайнеров. Возможность за один вечер создать и протестировать несколько вариантов поведения AI даёт команде гибкость для экспериментов. В отличие от конвейера C++, где каждое изменение требует времени перекомпиляции от 30 секунд до нескольких минут, скриптовая среда позволяет поддерживать состояние потока разработчика.
... на удивление, это про безопасность
Архитектура песочниц в скриптовых системах предоставляет механизм изоляции, где каждый скрипт выполняется в контролируемом окружении с ограниченным API. Lua VM, например, позволяет создавать отдельные состояния (lua_State) с кастомными таблицами глобальных функций, где можно заблокировать доступ к файловой системе, сетевым операциям или системным вызовам. Это достигается через whitelisting-подходы, где скриптам доступны только явно разрешённые функции типа SpawnActor(), GetPlayerHealth() или PlaySound().
Можно установить максимальное время выполнения скрипта, лимиты на потребление памяти, количество вызовов функций или глубину рекурсии, прерывая выполнение при превышении порогов. В случае ошибок интерпретатор может обработать исключение, вывести ошибку в лог и продолжить работу основного движка без критических сбоев. А нативные модули C++ или плагины DLL работают в том же адресном пространстве, что и основное приложение, где некорректная работа с указателями, переполнение буфера приведут к крашу.
... это про реконфигурабельность проекта в целом
Записанные в исходниках и не поддающиеся изменениям значения в коде C++ создают архитектурный долг, каждое изменение параметров ИИ требует полной пересборки проекта: препроцессинг, компиляция, линковка и развёртывание. Это особенно видно при настройке характеристик вроде множителей урона, весов поиска пути, порогов агрессии или времени перезарядки. Правильный подход предполагает вынос всех настраиваемых значений во внешние конфигурационные файлы (JSON, XML, INI) или таблицы базы данных, что позволяет менять поведение игры без изменения исходного кода. Современные движки предоставляют системы свойств с возможностями рефлексии, где параметры автоматически загружаются из конфигов и могут быть изменены через инструменты инспектора времени выполнения.
Технология перезагрузки скриптов работает через механизм отслеживания файлов и динамической перезагрузки кода, где интерпретатор следит за изменениями в файлах и перезагружает модифицированные модули без остановки основного процесса, что позволяет обновлять логику ИИ, обработчиков событий или игровой механики в реальном времени.
... это дверь для комьюнити
Пользовательский контент требует низкого порога входа и минимальных технических требований для моддеров. Предоставление C++ API означает необходимость распространения SDK с заголовочными файлами, документацией по сборке, поддержку множества конфигураций (Visual Studio, GCC, Clang различных версий). Скриптовые языки решают эту проблему через интерпретируемую модель выполнения, где весь необходимый runtime уже встроен в игру, а для создания контента достаточно текстового редактора и базовых знаний синтаксиса языка.
Архитектура модульности через скриптовый API позволяет создавать чётко определённые интерфейсы взаимодействия, причём API остаётся стабильным даже при изменениях внутренней реализации. В отличие от модов на C++, которые требуют перекомпиляции при каждом обновлении движка и могут вызывать внутренние ошибки, скриптовый контент продолжает работать при условии сохранения API.
... это — всем не угодишь
Lua стал "золотым" стандартом для встраивания в игровые движки благодаря своей простоте и эффективности. Этот язык занимает минимум места, работает очень быстро и интегрируется в C++ буквально за несколько минут. В интернете можно найти огромное количество документации и примеров использования. Отлично подходит как для однопоточных приложений, так и для многопоточной обработки, что делает его универсальным выбором для большинства проектов.
Python обладает богатыми возможностями и удобным синтаксисом, но его интеграция в игровой движок может стать головной болью. Инициализация интерпретатора Python требует значительных усилий и глубокого понимания внутреннего устройства языка. Это серьезно ограничивает возможности многопоточной обработки, из-за чего многие разработчики в итоге переходят на Lua.
JavaScript в виде встраиваемых движков показывает отличные результаты как по скорости работы, так и по потреблению памяти. Современные JS-движки хорошо оптимизированы и могут стать достойной альтернативой традиционным решениям.
Языки вроде AngelScript, Wren, Squirrel и ChaiScript занимают нишевое положение на рынке. По своим возможностям они находятся где-то между простотой C и гибкостью Lua. Если бы эти языки появились до широкого распространения Lua, они могли бы стать ему достойной заменой, но сейчас им сложно конкурировать с уже устоявшимся стандартом.
Не секрет, что все мечтают об этом. Синхронные миры, кооператив, PvP, MMO, или хотя бы чат между игроками. А потом открываешь Wireshark и начинаешь мечтать только об одном — чтобы всё это просто заработало. Начнём с хорошего: возможно, для вашей игры это вообще не нужно. Если делаете оффлайн-игру, пазл, платформер, какой-нибудь "SimTower с жабами", то можно забыть про сеть. Но если вам взбрело в голову "сделать мультиплеер" — готовьтесь. Вы сейчас вступаете на тропу боли, где всё зависит от деталей. Никакой универсальной архитектуры не существует.
В современных языках (C#, Rust, Go) уже будут неплохие абстракции для работы с сокетами. Асинхронные вызовы, удобные корутины и всё такое. В C++ (мои соболезнования), скорее всего, придётся использовать что-то стороннее вроде RakNet/Enet/Unet — библиотеки, хотя и не суперсовременные, но надёжные и зарекомендовавшие себя временем и множеством проектов. Надо быть честным с собой: сокеты — это только начало. Подключиться к серверу — это 1% работы. Дальше начинается самое весёлое. Что вообще такое "сетевой движок"? Хороший вопрос. Я сам им задавался, когда писал первый кооперативный прототип. Ответа так и не нашёл, но получил очень много вопросов по функциональности, перечисленных далее.
...синхронизация состояний
Это основа любой многопользовательской игры, но одновременно и один из самых сложных аспектов сетевого программирования. Каждый игрок видит игровой мир через призму своего клиента, но все эти "миры" должны быть согласованы друг с другом. Проблема заключается в том, что идеальная синхронизация физически невозможна из-за задержек в сети: пока информация о действии одного игрока дойдёт до остальных, пройдёт время, за которое игровая ситуация может кардинально измениться. На практике разработчики используют различные подходы к синхронизации в зависимости от типа игры. В пошаговых стратегиях можно позволить себе ждать подтверждения от всех игроков перед переходом к следующему ходу. В реальном времени приходится идти на компромиссы: либо замедлять игру под самого "медленного" игрока, либо предсказывать действия и потом корректировать ошибки. Третий подход — авторитарный сервер, который принимает решения единолично, но тогда возможности игроков с высоким пингом будут существенно ограничены.
Особенно сложными становятся ситуации с быстро меняющимися состояниями, например, когда два игрока одновременно стреляют друг в друга, или когда несколько игроков пытаются подобрать один и тот же предмет. Система должна определить, кто был первым, учитывая разные задержки соединения каждого игрока. Неправильное решение этой проблемы приводит к "резиновой" игре, когда действия игроков постоянно отменяются и пересчитываются.
...предсказание ввода и откат (rollback netcode)
Прогнозируемый откат (rollback netcode) — технология, которая позволяет играм функционировать плавно даже при высоких задержках сети. Принцип работы основан на том, что игра предсказывает действия других игроков и продолжает симуляцию, не дожидаясь подтверждения. Когда реальные данные всё-таки приходят, система сравнивает их с предсказанием и, если есть расхождения, "откатывает" игровое состояние назад и пересчитывает всё заново с правильными данными.
Техническая реализация rollback требует хранения множественных снимков игрового состояния и способности быстро восстанавливать любой из них. Каждый кадр игра сохраняет полное состояние мира, включая позиции всех объектов, их скорости, состояние анимаций и другие параметры.
При необходимости отката система загружает состояние из нужного момента времени и пересчитывает всё произошедшие с тех пор события. Пользователь при правильной реализации rollback практически не замечает происходящих "под капотом" откатов и пересчётов. Игра остаётся отзывчивой даже при пинге в 100–150 мс, что невозможно при традиционных подходах. Однако разработка такой системы требует переписывания практически всей игровой логики с учётом детерминированности и возможности отката.
...интерполяция, экстраполяция, сглаживание
Сетевые игры постоянно сталкиваются с проблемой неравномерного поступления данных — пакеты приходят с разными интервалами, иногда теряются, иногда приходят не по порядку. Без дополнительной обработки это привело бы к дёрганой, неиграбельной картинке. Интерполяция решает эту проблему, создавая плавные переходы между известными позициями объектов. Когда система знает, что игрок был в точке "A" в момент времени T1, а в точке "Б" в момент T2, она может плавно переместить его между этими точками.
Экстраполяция идёт дальше и пытается предсказать, где будет объект в следующий момент времени, основываясь на его текущей скорости и направлении движения. Правда, чем дальше система экстраполирует, тем больше вероятность ошибки, т. к. игрок может резко изменить направление или вообще остановиться.
...безопасный протокол общения
Безопасность сетевого протокола начинается с базовой валидации всех входящих данных. Каждое сообщение от клиента должно проверяться на корректность: правильный ли формат, не выходят ли значения за допустимые пределы, имеет ли игрок право выполнить запрашиваемое действие. Простое доверие клиентским данным открывает дорогу к множеству эксплойтов, от "телепортации" игрока в любую точку карты до мгновенного получения всех ресурсов в игре.
Авторизация и аутентификация защищают от подключения неавторизованных пользователей и подмены личности. Каждая сессия должна начинаться с процедуры проверки подлинности игрока, обычно через токены или цифровые подписи. Важно регулярно обновлять эти токены и проверять их валидность на каждом критическом действии. Сессионные ключи обеспечивают дополнительный уровень защиты, даже если основные учётные данные скомпрометированы.
...шифрование (параноиками просто так не становятся)
Шифрование в играх — это не паранойя, а суровая необходимость. Даже в казуальных играх передаются данные, которые не должны попасть в чужие руки: очки игроков, внутриигровая валюта, персональная информация. Современные инструменты для анализа сетевого трафика настолько просты в использовании, что любой школьник может перехватить и декодировать незашифрованные пакеты. Шифрование TLS/SSL стало стандартом для защиты данных в пути, но оно добавляет заметную задержку к каждому пакету. Для игр реального времени это может быть критично, поскольку дополнительные 10—20 мс на установку соединения и обработку каждого пакета способны испортить игровой опыт. Поэтому многие разработчики используют облегчённые алгоритмы шифрования или шифруют только критически важные данные, оставляя менее приоритетную информацию открытой.
...репликация объектов
Каждый объект в многопользовательской игре существует в многих экземплярах — на сервере, на клиенте каждого игрока, в нескольких версиях одновременно. Юнит на сервере содержит авторитетную информацию о здоровье, позиции, инвентаре, а клиентская копия может отображать предварительную версию этих данных с учётом предсказания и интерполяции. Системы репликации должны решить, какие свойства объектов синхронизировать, как часто и для кого. Приватная информация вроде содержимого инвентаря должна передаваться только владельцу, а публичная — всем игрокам в зоне видимости. Дельта-компрессия и другие техники оптимизации помогают уменьшить объём передаваемых данных, но усложняют логику синхронизации.
...логирование и отладка
Логирование в сетевых играх — это не просто техническая необходимость, каждое действие игрока, каждое изменение состояния объекта, каждый сетевой пакет должны оставлять след в логах. Проблема в том, что объём этих данных быстро становится огромным, т. к. популярная игра может генерировать гигабайты логов за час. Система должна уметь фильтровать важную информацию и сжимать или удалять менее критичные данные. Структурированное логирование с временными метками и уникальными идентификаторами сессий позволяет восстановить цепочку событий, приведших к багу. Когда игрок жалуется, что его юнит внезапно стал не того типа, разработчик должен иметь возможность проследить всю историю этого объекта: когда он был создан, какие команды получал, как изменялись его свойства. Без детального логирования такие баги превращаются в неразрешимые загадки.
...борьба с читерами
Читеры — это постоянная война без возможности окончательной победы, и разработчики не всегда побеждают. Каждая новая защита порождает новые методы обхода, каждый патч против читов провоцирует разработку более изощрённых инструментов. Проблема усугубляется тем, что код игры выполняется на компьютере игрока, который имеет полный контроль над своей системой. Любые данные, которые попадают на клиент, потенциально могут быть изменены или проанализированы.
Серверная валидация остаётся основной линией защиты, но она не может быть абсолютной без превращения игры в слайдшоу. Античит-системы сканируют память и процессы игрока в поисках известных читерских программ, но это вызывает обоснованные опасения по поводу приватности. Игроки не хотят устанавливать ПО с правами администратора, которое может мониторить всю систему. Античиты часто конфликтуют с легальными программами — от OBS для записи экрана до антивирусов.
Можно сделать игру без физики, без AI, даже без аудио. Но без UI — увы, практически невозможно, мы живём в мире, где игры без UI воспринимаются как "индийская индюшатина". Да и где-то ведь нужно написать "New Game" и "Exit", правильно?
На бумаге всё выглядит просто: ну подумаешь, нужно отрисовать пару кнопок, текстов, может слайдер или чекбокс. Только вот у этих объектов "вдруг" появляется состояние. Каждый элемент интерфейса должен корректно обрабатывать различные взаимодействия с пользователем. Кнопки, поля ввода и другие компоненты должны визуально и функционально реагировать на idle (покой), hover (наведение курсора), pressed (нажатие), focused (фокус) и disabled (отключённое состояние).
Дополнительную сложность вносит управление иерархической структурой интерфейса. Современные UI строятся по принципу вложенности: кнопка может находиться внутри панели, которая, в свою очередь, располагается внутри окна или диалога.
Это окно может быть скрыто до тех пор, пока пользователь не активирует соответствующую вкладку или не выполнит определённое действие. Такая многоуровневая структура требует тщательного управления видимостью, доступностью и обработкой событий на каждом уровне иерархии.
Добавьте сюда различные системы координат, с которыми приходится работать при разработке UI. В отличие от трёхмерной графики, где объекты существуют в едином мировом пространстве (world_space), пользовательские интерфейсы оперируют экранными координатами и координатами внутри родительского элемента. Это означает, что расчёт позиций элементов, обработка кликов и другие операции должны учитывать особенности двумерного экранного пространства и его трансформации.
Универсальность управления — ещё один важный аспект. Пользователи ожидают, что интерфейс будет одинаково хорошо работать с клавиатурой и мышью на настольных компьютерах, с тачскрином на мобильных устройствах, и с игровыми контроллерами на консолях. Каждый тип ввода имеет свои особенности: точность мыши, удобство навигации клавиатурой, естественность жестов на тачскрине и эргономика контроллера.
Наконец, для создания действительно привлекательного пользовательского UI приходится делать визуальную составляющую в виде анимаций и плавных переходов между состояниями, анимированных появлений и исчезновений элементов, визуальных эффектов при взаимодействии — всё это не просто украшения, а важные элементы, делающие интерфейс живым и отзывчивым. И вот вы уже не делаете игру, а пишете UI-фреймворк, который, кстати, не факт, что будет использоваться где-то, кроме конкретно этой игры. И даже если вы возьмёте что-то нормальное — Scaleform, всё же ты не так плох, как изначально казалось — всё равно придётся вкручивать его в свой движок, связывать с системой ввода, событий, шейдерами, текстами и прочим и прочим. UI — это сложная, структурированная, многоуровневая, зависимая от платформы и очень недооценённая часть любого движка. Но вам лучше пройти этот путь хотя бы раз, пусть по минимуму, но хорошо. Не надо пытаться переплюнуть Unity UI, Unreal Slate или HTML.
Говоря о вспомогательных инструментах для работы с движком — как вы уже могли догадаться — не каждый движок или игра нуждается в них. В качестве инструментов, например, могут использоваться простые Python-скрипты. Большинство движков предоставляют "из коробки" полноценные визуальные редакторы, которые выглядят как гибрид IDE (среды разработки) и Blender, с возможностью расставлять объекты на сцене, редактировать материалы и шейдеры, настраивать физику и взаимодействия, просматривать анимации и интерфейс, тестировать всё в режиме реального времени. Но создание полноценного редактора — задача, сопоставимая по сложности с разработкой самого движка. Для небольшого движка делать Unity-подобный редактор не нужно и не стоит, поскольку это будет неэффективно и отнимет уйму времени. Тем не менее, не стоит пренебрегать простыми инструментами, которые пишутся за пару-тройку вечеров, но значительно облегчают жизнь.
...конвертеры уровня
Собственные бинарные форматы данных обеспечивают оптимальную производительность загрузки и потребления памяти через использование структур, совместимых с внутренним представлением движка.
В отличие от текстовых форматов типа JSON или XML, которые требуют парсинга и конвертации типов во время выполнения, бинарные данные можно сразу загрузить в память с минимальными накладными расходами. Это критично для уровней, содержащих тысячи объектов с позициями, трансформациями, материалами и метаданными. Собственный формат также позволяет внедрять там, где нужно, сжатие данных, вычисленные структуры данных или навигационных мешей.
Утилиты конвертации могут интегрироваться с файловой системой, пересобирая игровые ресурсы при изменениях исходных файлов. Разделение между исходными и упакованными ассетами позволяет прозрачно менять форматы хранения без влияния на рабочий процесс контент-команды, и внедрять оптимизации для различных целевых устройств.
...проверка ресурсов
Системы валидации ассетов являют собой важный компонент сборки билда, предотвращающий возможные ошибки и обеспечивающий целостность контента. Валидатор проверяет зависимости, где каждый ассет может ссылаться на другие ресурсы через идентификатор (GUID), пути или собственную систему ссылок. Верификацию материалов на наличие текстур, моделей, анимаций и шейдеров.
Интеграция валидации в CI позволяет выявлять проблемы на этапе сборки ресурсов, до их попадания в финальный билд, генерировать отчёты с указанием конкретных проблем. Это становится важно для больших проектов, где художник может не знать о том, что его изменения сломали зависимости в других частях игры, а тестировщики получают краши без понимания источника проблемы.
Важно помнить: время, потраченное на инструмент, должно экономить вам больше времени в будущем. Не создавайте визуальный редактор ради самого редактора. Но если приходится каждый раз вручную редактировать 200 строк JSON, то простая самописная программа или скрипт сэкономит уйму времени.
К определённому моменту у нас уже есть всё по отдельности: графика, звук, ассеты, физика, UI, может даже AI и скрипты. Но одна важная вещь до сих пор остаётся в стороне: как всё это соединить в нечто работающее? Есть несколько работающих подходов.
...нет архитектуры — тоже архитектура
Многие инди-разработчики, уставшие от сложностей современных библиотек, обращаются к простому библиотечному подходу в разработке. Суть его заключается в создании отдельных независимых подсистем, каждая из которых отвечает за конкретную функциональность. Вы создаёте подсистему gfx для отрисовки графики, audio — для проигрывания звуков, assets — для загрузки данных с диска и input — для обработки пользовательского ввода. Каждая подсистема функционирует изолированно, не имея представления о других частях программы. Вы как разработчик самостоятельно определяете, какие функции вызывать и в каком порядке это делать. Весь процесс находится под вашим непосредственным контролем. Кстати, такой подход был популярен в конце девяностых — начале нулевых годов.
Этот метод имеет ряд преимуществ: он прост в реализации, обладает прозрачностью работы и отлично подходит для небольших проектов. Но успех сильно зависит от личной дисциплины команды, там отсутствуют автоматические менеджеры сцен и другие вспомогательные структуры.
Со временем становится сложно удерживать в голове всю схему вызовов и их причинно-следственные связи. Но поначалу всё работает легко и красиво, для многих этого достаточно. Позже вы, конечно, можете столкнуться с такими багами, как отсутствие уведомлений пользовательского интерфейса об обновлении ассетов или "незапуск" фоновой музыки после загрузки нового уровня. Решение этих проблем — неизбежная плата за отсутствие связей между различными частями программы. А эти связи, по сути, и представляют собой архитектуру приложения, от которой мы изначально пытались уйти.
...godobject — тоже архитектура
Крупные игровые движки вроде Unity, Unreal, Godot, CryEngine, Dagor и других часто выбирают подход, основанный на едином базовом классе. Обычно это класс с названием Object, Entity, GameObject, Actor или подобном, который становится прародителем для всех игровых сущностей. Такой базовый класс обычно содержит виртуальные методы для основных игровых функций:
struct GameObject {
virtual void update(float dt);
virtual void render();
virtual void onEvent(const Event&);
};
Все создаваемые вами игровые элементы — кнопки интерфейса, враги, NPC — наследуются от этого единого класса. Каждый из этих элементов автоматически получает способность обновляться в игровом цикле, отрисовываться на экране и реагировать на различные события. Удобно? Очень. "Из коробки" предоставляется единообразный интерфейс для всех игровых объектов. Централизованная структура обеспечивает возможность легко управлять игровыми сущностями, позволяет реализовать глобальный менеджер объектов.
Главная проблема заключается в чрезмерном использовании наследования — все классы происходят от одного родителя, что противоречит принципу "композиция лучше наследования". Возникают трудности при необходимости реализации множественного поведения для объектов — система становится хрупкой и сложной для расширения. Через пару месяцев GameObject превращается в гигантскую "помойку" с виртуальными методами на 300 строк. Попытки исправить это через компоненты делают только хуже. В итоге начинаешь думать, что это не очень-то и хорошо, но многим такого дизайна хватает, чтобы выпустить игру.
...ECS — модно! Но...
Данная архитектурная модель в последние годы завоевала большую популярность среди инди-разработчиков и энтузиастов. Основная идея заключается в радикальном разделении данных и логики внутри игровой системы — всё строится вокруг трёх ключевых концепций. Entity (сущность) представляет собой просто идентификатор, не содержащий никакой логики или данных самостоятельно. Component (компонент) является контейнером для определённого типа данных, таких как позиция объекта, запас здоровья или параметры звука. System (система) представляет собой функциональный блок, который обрабатывает компоненты по определённым правилам. По раздельности работать не будет, но вместе позволяет исключить традиционную иерархию объектов, заменяя её пакетной обработкой данных.
Например, система физики может одновременно получить доступ ко всем компонентам Transform и Velocity, чтобы обновить координаты всех движущихся объектов за одну операцию. Из преимуществ получаем высокую производительность благодаря эффективному использованию кэша процессора, чёткое разделение ответственности между различными частями системы, и хорошую масштабируемость.
Из минусов — влетаем "по полной" в использование сложной системы, которая часто требует написания значительного объёма шаблонного кода. Без специализированных редакторов код ECS практически нечитаем, без визуальных отладчиков его ещё и не подебажить. Для проектов небольшого масштаба, это будет как пальба из танка по мухам.
А может просто оставить всё как есть? И да, и нет. Движок и всё вокруг него — это не цель, а инструмент. Если делаешь хобби-проект на выходных — не надо строить идеальный фреймворк, ибо получится фреймворк, а когда начинаешь писать фреймворк — разработка игры уходит на второй план. Но также важно, что архитектура — это как скелет игры. Никто его не видит, но если он кривой, то всё будет болеть, гнуться и ломаться при каждом чихе. А когда станет совсем неудобно — не надо бояться всё выкинуть и начать заново. Не бойтесь выбрасывать сломанное, все так делают (правило трёх систем). К тому же GEA всё так же актуален, и лучше пока что не придумали.
Для того чтобы начать разбираться в том, как устроены игры, вам не нужен движок уровня Unreal. Чтобы не стать очередным Unreal "как бы" C++ программистом, который знает, что есть контейнеры, но не понимает, как они устроены внутри, — надо создать свою первую (или вторую) игру без движка, сделать примитивную версию, которая открывает окно, рисует пару спрайтов и простенький менеджер ресурсов, который просто пакует текстуры в .zip. Пусть это будет игра-движок — простой, с ошибками и "велосипедами", но свой, и, что важнее, сделанный с пониманием используемых инструментов. Без Unity, без Godot, без других удобных решений, где вы пропустили 90% того, что задействовано "под капотом".
Зато это позволит повозиться с архитектурой, шаблонами проектирования и разобраться в оптимизации ресурсов. Конечно, такой путь потребует времени, но в итоге вы получите не просто игру, но и понимание "внутренней кухни" и важность таких знаний сложно оценить для дальнейшей карьеры. Здесь закладывается и начинается профессиональный рост, настоящий, без "фантиков". Человек, пришедший в разработку со знанием Unreal, сможет работать только на нём, а программист, знающий, как он устроен изнутри, сможет и в Unreal, и в Unity, и в Godot, и в любой другой движок.
Но... если Unreal Engine, Godot или другие готовые решения кажутся вам удобнее — почему бы и нет? Всё зависит от амбиций, терпения и готовности копать глубже. Но даже если вы в итоге остановитесь на чужом движке, опыт самостоятельной разработки станет хорошим фундаментом для профессионального роста, научив видеть за технической сутью магию игр.
Выбор, как всегда, за вами.
Автор — Сергей Кушниренко
Разработчик с более чем двадцатилетним опытом программирования и создания игр. Выпускник Национального исследовательского университета ИТМО. Начинал карьеру с разработки программного обеспечения для военно-морских тренажеров, навигационных систем и сетевых решений. Последние пятнадцать лет специализируется на разработке игр: в Electronic Arts занимался оптимизацией игр The Sims и SimCity BuildIt, в Gaijin Entertainment руководил переносом игр на платформы Nintendo Switch и Apple TV. Активно участвует в проектах с открытым исходным кодом, включая библиотеку ImSpinner и проект восстановления игры Pharaoh (1999).
0