UObject и рефлексия — что UHT генерирует и почему это меняет всё
UObject в Unreal — это не обычный C++ класс. Это вход в рефлексионную систему движка. Когда вы пишете UCLASS(), UPROPERTY(), UFUNCTION(), эти макросы — не просто хинты. Они инструкции для Unreal Header Tool (UHT), отдельной программы, которая запускается перед обычным C++ компилятором, парсит ваши заголовки и генерирует код: таблицы свойств, фабрики, обёртки для Blueprint, маркеры для GC, хелперы сериализации. Этот сгенерированный код — то, что превращает обычный класс в полноценный гражданин движка.
Из-за этого UObject-классы получают всё, что считается «магией» Unreal: автоматическую сборку мусора (GC сканирует только UPROPERTY-поля), сериализацию assets и save games без ручного boilerplate, мост к Blueprint VM, репликацию по сети, редактор Details Panel — всё это работает через одну и ту же рефлексионную таблицу, которую UHT собирает из ваших макросов. Классы вне UObject-иерархии не получают ничего из перечисленного — даже если унаследовать их косвенно через TSharedPtr, они остаются «слепыми» для движка.
Три макроса — UCLASS, UPROPERTY, UFUNCTION — это три двери в эту систему. Каждый открывает свой набор возможностей: UCLASS регистрирует тип и создаёт CDO (Class Default Object — шаблон, с которого копируются все инстансы); UPROPERTY добавляет поле в таблицу свойств (что включает GC tracking, сериализацию, BP exposure, replication); UFUNCTION делает то же для методов (BP-вызовы, RPC, exec-консоль, делегаты). Спецификаторы внутри макросов — это второй уровень управления: они говорят, что именно включить (только редактор? и BP тоже? с записью? с репликацией?). Полная карта — в слоях ниже.
Карта темы
- UObject как базовый класс — корень UE object model, чем отличается от обычных C++ классов, что даёт включение в иерархию.
- Макрос UCLASS — что регистрирует, какие генерируются метаданные, основные спецификаторы класса (
Blueprintable,Abstract,NotPlaceable). - GENERATED_BODY — placeholder, который UHT раскрывает в реальный boilerplate: типовая информация, конструкторы, accessors, RTTI-хелперы.
- UHT и компиляция — двухстадийный пайплайн: UHT парсит заголовки, генерирует
.generated.hи.gen.cpp, потом обычный C++ компилятор собирает всё вместе. - Рефлексионная система —
UClass,FProperty,UFunction, итерация полей и методов в runtime, метаданные. - Макрос UPROPERTY — три роли: GC tracking, Blueprint exposure, editor visibility. Без него поле невидимо для движка.
- Макрос UFUNCTION —
BlueprintCallable, RPC,Exec,Server/Client/NetMulticast, делегаты. - Property спецификаторы — две оси:
Edit*/Visible*(редактор) иBlueprintReadWrite/BlueprintReadOnly(BP), плюсReplicated,Transient,SaveGame. - Blueprint exposure — что нужно собрать, чтобы поле/метод стали видны в BP: макрос + спецификатор + правильный тип.
- Архитектура Blueprint — Blueprint — это
UClass, расширяющий нативный, со своим bytecode, исполняемым BP VM. - Class Default Object —
CDO, шаблон умолчаний на класс, отношение instance ↔ CDO, delta serialization. - Жизненный цикл объекта — Constructor →
PostInitProperties→PreInitializeComponents→BeginPlay→Tick→BeginDestroy. - Сборка мусора — почему
UPROPERTY= GC-tracked, как сканируется граф достижимости, что бывает без макроса. - Типы указателей и ссылок — сырой
UPROPERTY*,TObjectPtr,TWeakObjectPtr,TSoftObjectPtr,TSubclassOf— что они означают для GC и загрузки. - Мягкие ссылки —
TSoftObjectPtr/TSoftClassPtr, async loading,FStreamableManager, asset streaming.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Забыть UPROPERTY() на UObject* | GC не видит ссылку: объект собирается, указатель становится dangling, крэш через несколько кадров |
Использовать std::shared_ptr<UObject> | RAII-обёртки несовместимы с GC; ссылка не учитывается, объект собирается, краш |
| Логика геймплея в конструкторе | Конструктор выполняется и для CDO — там нет world, нет других актёров; крэш или undefined |
Объявить UCLASS(), забыть GENERATED_BODY() | Link error: UHT-сгенерированные функции отсутствуют |
Поле без UPROPERTY(), но с BlueprintReadWrite | UHT-ошибка компиляции |
| Считать BP-default той же ссылкой, что CDO | BP-instance хранит delta против CDO; touched-property игнорирует CDO |
Rename UPROPERTY без CoreRedirects | Saved assets теряют значение поля — silent data loss |
| Полагать, что Live Coding обновит UHT-таблицы | LC патчит машинный код, но не рефлексию; нужен restart редактора |
Считать BlueprintImplementableEvent синонимом BlueprintNativeEvent | Первый запрещает C++-тело; второй требует _Implementation |
Хранить UObject* без UPROPERTY в TSharedPtr | Аналог первого пункта: GC не видит, dangling |
Использовать dynamic_cast вместо Cast<> | C++ RTTI работает, но Cast<> — это UClass-aware; единственный канонический способ |
TSoftObjectPtr без LoadSynchronous или async loading | Не получите объект; Get() вернёт nullptr, пока не загружен |
Значение для собеседований
UObject и рефлексия — обязательная senior-тема для любого UE5 интервью. Это фундамент, на котором стоит весь движок. Проверяется:
- Что
UObjectи его рефлексия — отдельная система, не C++ RTTI. RTTI даёт только тип иdynamic_cast; рефлексия даёт enumeration полей и методов по имени. - Что UHT — отдельная программа, запускающаяся перед C++ компилятором, и она генерирует
.generated.h. Если UHT не запущен — сборка ломается. - Что
UPROPERTY— не просто хинт для редактора: это вход в GC, сериализацию, репликацию, Blueprint, networking. - Что CDO — это template instance, который держит дефолты класса. Instance — это delta против CDO, не копия.
- Что жизненный цикл UObject не равен C++-конструктору: есть
PostInitProperties,BeginPlay,BeginDestroy. Логика, зависящая от world, должна быть вBeginPlay. - Что
TObjectPtrvsTWeakObjectPtrvsTSoftObjectPtr— это разные GC-семантики, не разные синтаксисы. Один держит объект живым, второй не держит и null'ится при сборке, третий вообще асинхронный путь.
Типичный неверный ответ: «UObject — это просто базовый класс, как Object в Java или Python». Реальный ответ: UObject — это точка входа в рефлексию, без которой не работает ни одна штатная подсистема движка. И самое главное — рефлексия в Unreal сгенерирована UHT, не выведена компилятором; именно поэтому изменения в заголовке требуют restart редактора, а не Live Coding.