ДО ТОГО КАК НАЧНЁШЬ

Как мыслит Unreal Engine

Это не ещё одна тема курса, а компас поверх всех остальных. Двенадцать «Почему», которые движок решает одним и тем же способом мышления снова и снова — если один раз понять сам ход мысли, каждая следующая система движка перестаёт быть новым набором фактов для запоминания и становится предсказуемым следствием того же самого рассуждения.

Правило проекта: меньше магии после каждой темы

У курса нет цели «показать API». Цель — чтобы после каждой главы то, что раньше выглядело магией движка, становилось логичным следствием архитектуры. После главы про Reflection понятно, зачем нужны UCLASS, UPROPERTY и UFUNCTION. После главы про жизненный цикл понятно, почему нельзя делать определённые вещи в конструкторе. После Gameplay Framework очевидно, почему логика разделена между GameMode, PlayerController, PlayerState и Character. Эта страница — не замена этим темам, а их общий знаменатель: одиннадцать разных «почему» ниже на самом деле — вариации одного и того же вопроса, заданного к разным частям движка.

Главная цель обучения

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

Главная цель курса — сделать так, чтобы со временем документация Unreal Engine стала читателю не нужна. Не потому что он всё запомнил, а потому что он научился воспроизводить то рассуждение, которым сама документация была написана.

Почему почти всё в геймплейном коде — UObject?
Потому что «быть UObject» — это не наследование ради наследования, а разовая покупка трёх вещей сразу: видимости для Garbage Collector, видимости для Reflection (значит и для Blueprint/редактора) и единого механизма сериализации. Написать любую из этих трёх систем заново для «облегчённого» типа без UObject — значит решать уже решённую задачу второй раз.
Разбор → UObject: архитектура объекта
Почему почти всё — указатели, а не значения?
UObject — это единица идентичности и владения (через Outer), которую Garbage Collector отслеживает по адресу конкретного объекта в памяти, а не по значению. Скопировать UObject «по значению» означало бы либо разорвать эту идентичность (два разных объекта с одинаковыми данными — не одно и то же для GC и для Reflection), либо тайно всё равно копировать по ссылке — поэтому движок делает эту ссылочность явной через указатель, а не прячет её.
Разбор → Смарт-указатели и GC
Почему есть Garbage Collector?
Граф игровых объектов в реальном времени — не дерево с одним владельцем, а сеть перекрёстных ссылок (актор ссылается на компонент, компонент — на другой актор, тот — обратно), в которой вручную отследить «кто последний перестал ссылаться» практически невозможно без утечек или преждевременного освобождения. GC решает это, отслеживая достижимость от корня, а не подсчёт ссылок вручную.
Разбор → Garbage Collector
Почему есть Reflection?
Редактору, Blueprint и системе сериализации нужно знать о полях и функциях твоего C++-класса в рантайме — то, что сам C++ принципиально не предоставляет (в языке нет рантайм-информации о произвольных полях класса). Unreal Header Tool решает это, генерируя это описание заранее, из макросов вроде UPROPERTY, а не заставляя движок гадать по бинарнику.
Разбор → Reflection
Почему есть Component?
Потому что способности геймплейных сущностей («имеет здоровье», «имеет инвентарь») — это отношение «обладает», а не «является», и наследование, корректно моделирующее «является», при попытке выразить через него «обладает» даёт комбинаторный взрыв классов на каждую новую независимую способность.
Разбор → UActorComponent
Почему Composition, а не наследование?
Потому что композиция единственная из трёх испробованных попыток (наследование, интерфейсы, шаблоны) одновременно даёт независимое включение/выключение способности, физическое хранение состояния и полную видимость Reflection/Blueprint — интерфейсы дают только контракт без состояния, шаблоны невидимы для UHT.
Разбор → UActorComponent
Почему есть Modules?
Кодовая база размера движка не собирается разумное время и не имеет управляемых границ зависимостей как один монолитный бинарник. Модуль — отдельная единица компиляции со своим файлом сборки и явным списком зависимостей от других модулей, что позволяет пересобирать только то, что реально изменилось, и физически не давать одной части движка тянуть за собой то, что ей не нужно (например, редакторный код — в упакованную игру).
Статья скоро
Почему есть Plugins?
Модуль — единица компиляции, но не единица распространения. Плагин упаковывает один или несколько модулей вместе с контентом и конфигом в самостоятельную, включаемую/выключаемую для конкретного проекта фичу — так расширение движка не требует форкать или патчить исходники движка, которые в противном случае разъедутся с каждым следующим обновлением движка.
Статья скоро
Почему есть Subsystem?
Самодельный синглтон/глобальная переменная плохо сочетается с тем, что в Unreal одновременно может существовать несколько «миров» (редактор + Play-In-Editor одновременно, несколько клиентов в одном процессе при тестировании) — глобальному состоянию некуда однозначно «принадлежать». Subsystem — это UObject с автоматическим временем жизни, управляемым движком и привязанным к конкретной, явной границе (Engine/GameInstance/World/LocalPlayer), поэтому он не переживает свою границу и не путается между параллельными мирами.
Статья скоро
Почему есть Gameplay Tags?
Растущий, ветвящийся набор игровых состояний и категорий плохо ложится на C++ enum — новое значение требует перекомпиляции, а иерархические отношения («Status.Debuff.Stun» — это разновидность «Status.Debuff») enum не выражает вообще. FGameplayTag — иерархическая строка с быстрым сравнением, объявляемая как данные, а не перекомпилируемый код.
Разбор → FGameplayTag
Почему есть Data Assets?
Захардкоженные в C++ или Blueprint-константах характеристики контента (урон оружия, вес предмета) не масштабируются на растущий список контента и требуют участия программиста на каждое изменение баланса. UDataAsset — обычный UObject, но предназначенный именно для данных, редактируемых дизайнером без пересборки и без похода к программисту.
Разбор → UDataAsset
Почему есть Primary Assets?
TSoftObjectPtr решает «не грузить всё сразу», но не решает «какие 50 из 3000 DataAsset реально нужны прямо сейчас» — управлять этим по неявным цепочкам мягких ссылок не масштабируется. Primary Asset — явная регистрация конкретного UObject в AssetManager под стабильным идентификатором (тип + имя), не зависящим от пути к файлу, что даёт единый, централизованно управляемый каталог загрузки контента.
Разбор → UAssetManager
💡 Мысли Senior

Когда встречаю незнакомую систему движка впервые, я не иду читать её API целиком. Я сначала спрашиваю: какую из этих двенадцати проблем она решает — и для кого она играет роль UObject, Reflection, GC или Component в этой конкретной задаче? В девяти случаях из десяти ответ находится быстрее, чем документация по самой системе.