UObject и рефлексия
Базовый класс UObject в Unreal, система рефлексии, макросы UCLASS/UPROPERTY/UFUNCTION и жизненный цикл объектов.
20 вопросов
JuniorТеорияОчень частоЧто такое UObject и почему это центральный класс в Unreal Engine?
Что такое UObject и почему это центральный класс в Unreal Engine?
UObject — базовый класс почти для каждого управляемого типа Unreal. Он даёт рефлексию, сериализацию, сборку мусора, репликацию и доступ из Blueprints. Классы вне иерархии UObject не получают ни одной из этих служб движка.
Типичные ошибки
- ✗Думать, что обычные C++-классы получают рефлексию или сборку мусора без наследования от
UObject - ✗Путать службы
UObjectсо стандартным C++ RTTI - ✗Считать, что каждый
UObject— это Actor, размещённый в мире
Уточняющие вопросы
- →Как движок обнаруживает свойства подкласса
UObjectво время выполнения? - →Что теряет класс, если наследуется от обычного C++
class?
JuniorТеорияОчень частоДля чего используются макросы UPROPERTY() и UFUNCTION()?
Для чего используются макросы UPROPERTY() и UFUNCTION()?
UPROPERTY() открывает поле класса системе рефлексии — это даёт редактирование в редакторе, сериализацию, репликацию и учёт сборщиком мусора. UFUNCTION() делает то же для метода — позволяет вызовы из Blueprints, RPC и привязку делегатов.
Типичные ошибки
- ✗Думать, что сериализация или учёт GC происходят без
UPROPERTY() - ✗Считать, что
UPROPERTY()генерирует getter'ы и setter'ы - ✗Предполагать, что макросы влияют только на панель Details в редакторе
Уточняющие вопросы
- →Какой спецификатор
UPROPERTYуправляет редактируемостью в редакторе? - →Что происходит с
UObject*безUPROPERTYпри сборке мусора?
JuniorТеорияЧастоВ чём разница между BlueprintCallable и BlueprintPure?
В чём разница между BlueprintCallable и BlueprintPure?
Функции BlueprintCallable имеют execution-пин и могут менять состояние. Функции BlueprintPure не имеют exec-пина, считаются без побочных эффектов и пересчитываются каждый раз при чтении их вывода. Pure используют только для дешёвых детерминированных геттеров.
Типичные ошибки
- ✗Помечать дорогую функцию как
BlueprintPure, из-за чего она многократно пересчитывается - ✗Наделять функцию
BlueprintPureпобочными эффектами - ✗Думать, что результаты
BlueprintPureкешируются на кадр
Уточняющие вопросы
- →Почему нода
BlueprintPureможет выполниться несколько раз в одном графе? - →Когда геттер всё же должен быть
BlueprintCallable?
JuniorТеорияЧастоКак соотносятся UObject, AActor, UActorComponent и USceneComponent?
Как соотносятся UObject, AActor, UActorComponent и USceneComponent?
AActor и UActorComponent оба наследуются от UObject. AActor можно разместить на уровне, и он владеет компонентами. UActorComponent — это присоединяемое поведение; USceneComponent — это UActorComponent с трансформом, поэтому он имеет позицию в мире.
Типичные ошибки
- ✗Думать, что
UActorComponentможет существовать на уровне без владеющего Actor'а - ✗Считать
USceneComponentбазовым, аUActorComponent— специализированным типом - ✗Предполагать, что каждый компонент имеет трансформ в мире
Уточняющие вопросы
- →Почему
UActorComponentбез трансформа всё равно полезен? - →Что устанавливает присоединение одного
USceneComponentк другому?
JuniorТеорияЧастоЧто означают EditAnywhere, VisibleAnywhere, BlueprintReadOnly и BlueprintReadWrite?
Что означают EditAnywhere, VisibleAnywhere, BlueprintReadOnly и BlueprintReadWrite?
EditAnywhere позволяет менять свойство в панели Details редактора; VisibleAnywhere показывает его там только для чтения. BlueprintReadWrite позволяет графам Blueprint читать и записывать его; BlueprintReadOnly — только читать. Edit/Visible — про редактор, Blueprint* — про графы.
Типичные ошибки
- ✗Думать, что
VisibleAnywhereскрывает свойство, а не показывает его только для чтения - ✗Путать спецификаторы редактора со спецификаторами Blueprint
- ✗Считать, что эти спецификаторы управляют репликацией
Уточняющие вопросы
- →В чём разница между
EditAnywhereиEditDefaultsOnly? - →Какой спецификатор управляет репликацией вместо них?
MiddleТеорияЧастоЧем различаются конструктор, PostInitializeComponents, BeginPlay и Tick?
Чем различаются конструктор, PostInitializeComponents, BeginPlay и Tick?
Конструктор задаёт умолчания и создаёт компоненты — он выполняется без мира. PostInitializeComponents выполняется после регистрации компонентов, до начала игры. BeginPlay выполняется при старте геймплея. Tick выполняется каждый кадр. Каждый этап — более поздний и полный.
Типичные ошибки
- ✗Считать, что у конструктора есть доступ к валидному миру
- ✗Помещать покадровую логику в
BeginPlayвместоTick - ✗Не знать, что
PostInitializeComponentsвыполняется доBeginPlay
Уточняющие вопросы
- →Почему ссылку на компонент можно безопасно использовать в
PostInitializeComponents, но не в конструкторе? - →Как отключить
Tickради экономии производительности?
MiddleТеорияЧастоЧто такое Class Default Object (CDO) в Unreal Engine?
Что такое Class Default Object (CDO) в Unreal Engine?
CDO — это единственный шаблонный экземпляр, который движок создаёт для каждого UClass. Он хранит значения по умолчанию для каждого свойства. Новые экземпляры инициализируются копированием CDO, а редактор правит сам CDO, когда вы меняете умолчания класса.
Типичные ошибки
- ✗Думать, что CDO существует на экземпляр, а не на класс
- ✗Выполнять геймплейную логику в конструкторе, который работает и на CDO
- ✗Считать, что CDO отбрасывается во время выполнения
Уточняющие вопросы
- →Почему конструктор выполняется на CDO при загрузке редактора?
- →Как правка умолчания класса распространяется на существующие экземпляры?
MiddleТеорияЧастоЧто произойдёт, если забыть UPROPERTY() у ссылки UObject*?
Что произойдёт, если забыть UPROPERTY() у ссылки UObject*?
Без UPROPERTY() сборщик мусора не видит ссылку, поэтому не удерживает объект живым и не обнуляет указатель, когда объект собран. В итоге получается висячий указатель, который падает позже, — отложенный и трудноотлавливаемый баг.
Типичные ошибки
- ✗Ожидать ошибку компиляции вместо тихого runtime-бага
- ✗Думать, что GC сканирует сырые указатели без
UPROPERTY() - ✗Считать, что симптом — утечка, а не висячий указатель
Уточняющие вопросы
- →Почему падение часто проявляется намного позже самой ошибки?
- →Когда сырой
UObject*безUPROPERTY()действительно допустим?
MiddleТеорияЧастоЧто делают макросы UCLASS() и GENERATED_BODY()?
Что делают макросы UCLASS() и GENERATED_BODY()?
UCLASS() помечает класс, чтобы Unreal Header Tool сгенерировал для него метаданные рефлексии. GENERATED_BODY() — это плейсхолдер внутри тела класса, который UHT разворачивает в нужный boilerplate: информацию о типе, хелперы приведения и аксессоры. Оба макроса обязательны.
Типичные ошибки
- ✗Считать, что метаданные рефлексии строятся во время выполнения, а не UHT при компиляции
- ✗Думать, что
GENERATED_BODY()необязателен - ✗Размещать
GENERATED_BODY()вне тела класса
Уточняющие вопросы
- →Что пойдёт не так, если разместить
GENERATED_BODY()в неверной секции доступа? - →В чём разница между
GENERATED_BODY()и старымGENERATED_UCLASS_BODY()?
MiddleТеорияЧастоЧем различаются UObject*, TObjectPtr, TWeakObjectPtr, TSoftObjectPtr и TSubclassOf?
Чем различаются UObject*, TObjectPtr, TWeakObjectPtr, TSoftObjectPtr и TSubclassOf?
UObject* и TObjectPtr — сильные ссылки, удерживающие объект живым. TWeakObjectPtr не владеет и обнуляется при сборке. TSoftObjectPtr — ленивая ссылка по пути на возможно незагруженный ассет. TSubclassOf — типобезопасная ссылка на класс, а не на экземпляр.
Типичные ошибки
- ✗Думать, что
TWeakObjectPtrудерживает свою цель живой - ✗Путать
TSubclassOf(класс) с указателем на экземпляр - ✗Считать, что
TSoftObjectPtrвсегда загружен и готов к разыменованию
Уточняющие вопросы
- →Как безопасно разыменовать
TSoftObjectPtr? - →Когда стоит заменить сырой
UObject*наTObjectPtr?
JuniorТеорияИногдаКак открыть значение дизайнерам, но ограничить его диапазоном от 0 до 10?
Как открыть значение дизайнерам, но ограничить его диапазоном от 0 до 10?
Пометьте свойство UPROPERTY(EditAnywhere, meta = (ClampMin = "0", ClampMax = "10")). ClampMin/ClampMax жёстко ограничивают вводимые значения; UIMin/UIMax ограничивают слайдер. Ограничение применяет редактор, поэтому код всё ещё может задать значение вне диапазона.
Типичные ошибки
- ✗Ожидать, что
ClampMin/ClampMaxтакже ограничивают присваивания в рантайме - ✗Путать
ClampMin/ClampMaxсUIMin/UIMax - ✗Думать, что написанный вручную сеттер — единственный вариант
Уточняющие вопросы
- →Что меняют
UIMin/UIMaxпо сравнению сClampMin/ClampMax? - →Как обеспечить соблюдение диапазона ещё и в рантайме?
MiddleТеорияИногдаВ чём разница между BlueprintImplementableEvent и BlueprintNativeEvent?
В чём разница между BlueprintImplementableEvent и BlueprintNativeEvent?
BlueprintImplementableEvent не имеет тела на C++ — он реализуется целиком в Blueprint. BlueprintNativeEvent имеет реализацию по умолчанию на C++ (функцию _Implementation), которую Blueprint может переопределить. Native используют, когда нужен запасной вариант в коде.
Типичные ошибки
- ✗Писать тело на C++ для
BlueprintImplementableEvent(его быть не должно) - ✗Забывать суффикс
_ImplementationдляBlueprintNativeEvent - ✗Путать, какой из них предоставляет реализацию по умолчанию на C++
Уточняющие вопросы
- →Как Blueprint вызывает C++-реализацию по умолчанию
BlueprintNativeEvent? - →Какая ошибка компоновки возникает, если задать неверное имя функции?
MiddleТеорияИногдаПочему нужно быть осторожным с геймплейной логикой в конструкторе UObject?
Почему нужно быть осторожным с геймплейной логикой в конструкторе UObject?
Конструктор выполняется также на CDO при загрузке редактора, когда нет мира, других актёров и геймплейного контекста. Код, который спавнит актёров, читает мир или рассчитывает на runtime-состояние, поведёт себя неверно или упадёт. Такую логику размещают в BeginPlay.
Типичные ошибки
- ✗Забывать, что конструктор выполняется на CDO без мира
- ✗Спавнить актёров или вызывать
GetWorld()из конструктора - ✗Путать роль конструктора с ролью
BeginPlay
Уточняющие вопросы
- →Что безопасно делать в конструкторе — создавать компоненты или запрашивать мир?
- →Куда вместо этого помещать логику, которой нужен валидный мир?
MiddleТеорияИногдаПочему Unreal нужна своя система рефлексии вместо C++ RTTI?
Почему Unreal нужна своя система рефлексии вместо C++ RTTI?
C++ RTTI даёт лишь идентичность типа и dynamic_cast; он не умеет перечислять поля или читать метаданные. Рефлексия Unreal открывает свойства и функции по имени, обеспечивая работу редактора, Blueprints, сети и сериализации — для этого в стандартном C++ нет аналога.
Типичные ошибки
- ✗Считать, что C++ RTTI умеет перечислять члены класса
- ✗Думать, что система рефлексии существует лишь для замены
dynamic_cast - ✗Предполагать, что рефлексия — это просто оптимизация производительности RTTI
Уточняющие вопросы
- →Какую информацию хранит
UClass, которой нет вstd::type_info? - →Как рефлексия обеспечивает вызовы из Blueprint в C++?
MiddleТеорияИногдаКогда стоит использовать TSoftObjectPtr вместо жёсткой ссылки?
Когда стоит использовать TSoftObjectPtr вместо жёсткой ссылки?
Используйте TSoftObjectPtr, когда нужно сослаться на ассет, не затягивая его в память. Жёсткая ссылка загружает всю цепочку зависимостей вместе с владельцем. Мягкая ссылка откладывает загрузку, снижая потребление памяти и время загрузки для опциональных или крупных ассетов.
Типичные ошибки
- ✗Считать, что мягкая ссылка автоматически разрешается при доступе
- ✗Использовать жёсткие ссылки для крупных опциональных ассетов и раздувать память
- ✗Думать, что выбор мягкой и жёсткой ссылки — про скорость доступа, а не про момент загрузки
Уточняющие вопросы
- →Как
FStreamableManagerзагружает мягкую ссылку асинхронно? - →В чём разница между
TSoftObjectPtrиTSoftClassPtr?
MiddleТеорияИногдаКак работает Unreal Header Tool на высоком уровне?
Как работает Unreal Header Tool на высоком уровне?
Перед запуском компилятора C++ UHT разбирает заголовки с макросами UCLASS/UPROPERTY/UFUNCTION и генерирует файлы .generated.h и .gen.cpp с таблицами рефлексии и boilerplate. Затем обычный компилятор собирает ваш код вместе со сгенерированным.
Типичные ошибки
- ✗Думать, что UHT запускается во время выполнения или при старте игры
- ✗Считать, что UHT заменяет компилятор C++
- ✗Не знать, что включение
.generated.hобязательно и должно быть последним
Уточняющие вопросы
- →Почему
#include "X.generated.h"должен быть последним include в заголовке? - →Какая ошибка сборки возникает, если тип
UPROPERTYне рефлексируемый?
SeniorТеорияИногдаПочему правка умолчания Blueprint иногда не меняет уже размещённый экземпляр?
Почему правка умолчания Blueprint иногда не меняет уже размещённый экземпляр?
Каждый размещённый экземпляр хранит только дельту относительно CDO — свойства, которые он явно переопределил. Правка умолчания Blueprint меняет CDO, и новое значение распространяется лишь на свойства, которые экземпляр не переопределял. Как только дизайнер тронул свойство на экземпляре, у того появляется своя дельта и CDO игнорируется. Reset-to-default стирает дельту.
Типичные ошибки
- ✗Думать, что экземпляр хранит полную копию значения, а не дельту относительно CDO
- ✗Считать, что правка умолчания доходит до каждого экземпляра независимо от локальных переопределений
- ✗Не знать, что reset-to-default убирает дельту экземпляра и снова привязывает его к CDO
Уточняющие вопросы
- →Как редактор решает, какие свойства показать жирным (переопределёнными)?
- →Что происходит с дельтами экземпляров при смене родителя Blueprint?
SeniorПроизводительностьРедкоЧем TObjectPtr, TWeakObjectPtr и TSoftObjectPtr различаются по стоимости GC при масштабе?
Чем TObjectPtr, TWeakObjectPtr и TSoftObjectPtr различаются по стоимости GC при масштабе?
UPROPERTY TObjectPtr — это сильная ссылка: GC обходит её при каждом проходе достижимости, и она удерживает цель живой, поэтому миллионы таких ссылок удлиняют фазу пометки. TWeakObjectPtr не обходится и ничего не удерживает — дёшев для сканирования, разрешается проверкой серийного номера. TSoftObjectPtr — это просто путь, никогда не трассируется и может быть вообще не загружен.
Типичные ошибки
- ✗Верить, что
TWeakObjectPtrтрассируется GC и потому удлиняет фазу пометки - ✗Думать, что
TWeakObjectPtrудерживает цель живой как сильная ссылка - ✗Считать число ссылок неважным, ведь GC якобы обходит только таблицу свойств
Уточняющие вопросы
- →Как
TWeakObjectPtrобнаруживает, что его цель уничтожена? - →Когда замена сильных ссылок на слабые действительно сокращает время прохода GC?
SeniorДебаггингРедкоКакие ловушки рефлексии делают Live Coding / hot-reload ненадёжным для изменений UObject?
Какие ловушки рефлексии делают Live Coding / hot-reload ненадёжным для изменений UObject?
Live Coding патчит машинный код на месте, но не может пересобрать рефлексию: новое или переименованное UPROPERTY, изменённая раскладка UCLASS или другой vtable требуют свежего .generated.h от UHT, который применяет лишь полный перезапуск редактора. Если всё же запатчить, останутся устаревшие vtable, свойства, невидимые для GC и сериализатора, и часто assert в GC. Изменения заголовков и CDO всегда требуют перезапуска.
Типичные ошибки
- ✗Добавить
UPROPERTYво время сессии Live Coding и ждать, что оно станет видимым для GC - ✗Считать, что Live Coding перезапускает UHT, и рефлексия остаётся синхронной с патчем
- ✗Воспринимать assert в GC при hot-reload как случайный, а не симптом устаревшей раскладки
Уточняющие вопросы
- →Какие виды правок действительно безопасно применять через Live Coding?
- →Почему устаревший vtable может пережить патч и вызвать неверный виртуальный вызов?
SeniorДебаггингРедкоКак переименовать или сменить тип UPROPERTY, не сломав уже сохранённые ассеты?
Как переименовать или сменить тип UPROPERTY, не сломав уже сохранённые ассеты?
Сохранённые ассеты хранят значения свойств по отражённому имени, поэтому простое переименование молча теряет старое значение. Добавьте запись CoreRedirects в DefaultEngine.ini, сопоставляющую старое имя новому, чтобы загрузчик перепривязал его. Смену типа перенаправить нельзя: реализуйте Serialize или PostLoad с кастомной версией (FCustomVersion), чтобы прочитать старую раскладку и сконвертировать её.
Типичные ошибки
- ✗Переименовать
UPROPERTYв коде и считать, что сохранённые значения последуют за новым именем - ✗Верить, что
CoreRedirectsсправится и со сменой типа, а не только с переименованием - ✗Забыть поднять кастомную версию, из-за чего старые и новые данные не различить при загрузке
Уточняющие вопросы
- →Где регистрируется
FCustomVersionи какPostLoadего читает? - →Чем перенаправления свойств отличаются от перенаправлений классов и enum?