Всем привет! Меня зовут Александр, я уже более 5 лет работаю с Unreal Engine, и почти все это время — с сетевыми проектами.
Поскольку сетевые проекты отличаются своими требованиями к разработке и производительности, нередко необходимо работать с более простыми объектами, такими как классы UObject, но их функциональность изначально урезана, что может создать сильные рамки. В этой статье я расскажу о том, как активировать различные функции в базовом классе UObject в Unreal Engine 4.
На самом деле, статью я написал скорее как справочник. Большую часть информации крайне сложно найти в документации или сообществе, а тут можно быстро открыть ссылку и скопировать нужный код. Решил заодно поделиться и с вами! Статья ориентирована на тех, кто уже немного знаком с UE4. Будет рассмотрен С++ код, хотя знать его не обязательно. Можете просто следовать инструкциям, если вам нужно то, о чем пойдет речь. Более того, не обязательно копировать все, вы можете вставить код из раздела с нужными свойствами и он должен работать.
Немного о UObject
UObject — базовый класс почти для всего, что есть в Unreal Engine 4. От него наследуется подавляющее большинство объектов, которые создаются у вас в мире или просто в памяти: объекты на сцене (AActor), компоненты (UActorComponent), разные типы для работы с данными и прочие.
Сам класс хоть и легче производных, но при этом достаточно функционален. Например, он содержит многие полезные события, такие как изменение значений переменных в редакторе и базовые функции для сети, которые не активны по умолчанию.
Объекты, созданные этим классом, не могут находиться на сцене и существуют исключительно в памяти. Их нельзя добавить как компоненты к Actor’ам, хотя он может являться своего рода компонентом, если самому реализовать необходимый функционал.
Для чего мне UObject, если AActor уже поддерживает все, что нужно? В общем-то, примеров использования масса. Самый простой — предметы для инвентаря. На сцене, где-то в небе, хранить их нецелесообразно, поэтому можно хранить в памяти, не нагружая рендер и не создавая лишних свойств. Для тех, кто любит технические сравнения, то AActor занимает килобайт (1016 байт), а пустой UObject всего 56 байт.
В чем проблема UObject?
Проблем в целом нет, ну или я просто не сталкивался с ними. Все, чем раздражает UObject, так это отсутствие различных возможностей, которые по умолчанию доступны в AActor или в компонентах. Вот проблемы, которые я выделил за свою практику:
- UObject’ы не реплицируются по сети;
- из-за первого пункта мы не можем вызывать RPC события;
- нельзя использовать обширный набор функций, требующих ссылку на мир в Блупринтах;
- в них нет стандартных событий вроде BeginPlay и Tick;
- нельзя добавлять компоненты из UObject’ов в AActor в Блупринтах.
Большую часть вещей можно легко решить. А вот с некоторыми придется повозиться.
Создание UObject
Прежде чем расширять наш класс возможностями, нам нужно его создать. Давайте воспользуемся редактором, чтобы генератор автоматически записал в хедер (.h) все, что нужно для работы.
Создать новый класс мы можем в Content Browser редактора, нажав кнопку New и выбрав там пункт New C++ Class.
Далее нам нужно выбрать сам класс. В общем списке его может не быть, поэтому раскрываем его и выбираем UObject.
Назовите ваш класс и выберите, в какой папке он будет храниться. Когда мы создали класс, можно зайти в студию, найти его там и начать встраивать все необходимые функции.
Новички, обратите внимание, что создается два файла: .h и .ccp. В .h вы будете объявлять переменные и функции, а в .cpp определять их логику. Найдите оба файла в вашем проекте. Если не меняли путь, то они должны быть в Project/Source/Project/.
Пока мы не продолжили, давайте в макросе UCLASS() над объявлением класса пропишем параметр Blueprintable. Должно получиться что-то вроде этого:
.h
UCLASS(Blueprintable)
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
}
Благодаря этому можно создавать Блупринты, которые будут наследовать все, что мы сделаем с этим объектом.
Репликация UObject
По умолчанию UObject’ы не реплицируются по сети. Как я описал выше, создается ряд ограничений, когда нужно синхронизировать данные или логику между сторонами, но при этом не хранить мусор в мире.
В Unreal Engine 4 репликация проходит как раз за счет мировых объектов. Значит просто создать объект в памяти и отреплицировать его никак не получится. Вам в любом случае понадобится владелец, который будет управлять передачей данных объекта между сервером и клиентами. Например, если ваш объект — это навык персонажа, то владельцем должен стать сам персонаж. Он же и будет проводником для передачи информации по сети.
Подготовим наш объект к репликации. Пока в хедере нам нужно задать лишь одну функцию:
.h
UCLASS(Blueprintable)
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
public:
virtual bool IsSupportedForNetworking () const override { return true; };
}
IsSupportedForNetworking() определит, что объект поддерживает сеть и может быть отреплицирован.
Однако не все так просто. Как я написал выше, нужен владелец, управляющий передачей объекта. Для чистоты эксперимента создадим AActor, который будет его реплицировать. Сделать это можно точно так же, как и UObject, только родительский класс, естественно, AActor.
Новички, если вам требуется реплицировать объект в персонаже, контроллере или еще где-то, создайте соответствующий базовый класс через редактор, добавьте в него необходимую логику и уже от этого класса наследуйтесь в Блупринтах.
Внутри нам нужны 3 функции: конструктор, функция репликации подобъектов, функция, определяющая, что внутри этого AActor реплицируется (переменные, ссылки на объекты и прочее) и место, где мы создадим наш объект.
Не забудем создать и переменную, по которой будет храниться наш объект:
.h
class MYPROPJECT_API AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor();
virtual bool ReplicateSubobjects (class UActorChannel *Channel, class FOutBunch *Bunch, FReplicationFlags *RepFlags) override;
void GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) const override;
virtual void BeginPlay ();
UPROPERTY(Replicated, BlueprintReadOnly, Category="Object")
class UMyObject* MyObject;
}
Внутри исходного файла мы должны все прописать:
.cpp
//Необходимые инклуды
#include "MyActor.h"
#include "Net/UnrealNetwork.h"
#include "Engine/World.h"
#include "Engine/ActorChannel.h"
#include "путь до вашего UObject/MyObject.h"
AMyActor::AMyActor()
{
//Реплицировать наш Actor по сети.
bReplicates = true
//Радиус репликации.
NetCullDistanceSquared = 99999;
//Частота репликации (раз в секунду).
NetUpdateFrequency = 1.f;
}
void AMyActor::GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps)
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Помечаем ссылку на наш объект на репликацию. Без репликации ссылки вы не сможете достать свой объект на клиенте.
DOREPLIFETIME(AMyActor, MyObject);
}
bool AMyActor::ReplicateSubobjects(UActorChannel * Channel, FOutBunch * Bunch, FReplicationFlags * RepFlags)
{
bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
// Реплицируем наш объект.
if (MyObject ) WroteSomething |= Channel->ReplicateSubobject(MyObject , *Bunch, *RepFlags);
return WroteSomething;
}
AMyActor::BeginPlay()
{
/*
Создадим наш объект при старте уровня (или спауне объекта) на сервере.
Обратите внимание на this. В качестве параметра передается ссылка на владельца объектом. Важно, чтобы владелец был со всей нужной логикой, иначе объект реплицироваться не будет.
*/
if(HasAuthority())
{
MyObject = NewObject<UMyObject>(this);
// Напишем в лог имя созданного объекта
if(MyObject) UE_LOG(LogTemp, Log, TEXT("%s created"), *MyObject->GetName());
}
}
Теперь ваш объект будет реплицироваться вместе с этим Actor’ом. Вы можете вывести его имя на тик, но уже на клиенте. Обратите внимание, что на Begin Play объект до клиента вряд ли успеет прийти, поэтому там смысла писать лог на нем нет.
Репликация переменных в UObject
В большинстве случаях реплицировать объект смысла нет, если в нем не содержится информация, которая так же будет синхронизироваться между сервером и клиентами. Поскольку наш объект уже реплицируется, то и передавать переменные не составит труда. Это делается так же, как и внутри нашего Actor’а:
.h
UCLASS(Blueprintable)
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
public:
virtual bool IsSupportedForNetworking () const override { return true; };
void GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) const override;
UPROPERTY(Replicated, BlueprintReadWrite, Category="Object")
int MyInteger;
// Остальной код
}
.cpp
//Необходимые инклуды
#include "MyObject.h"
#include "Net/UnrealNetwork.h"
UMyObject ::UMyObject ()
{
//Реплицировать наш Object по сети. Тоже это нужно указать тут.
bReplicates = true
//Радиус репликации и частота репликации наследуются от владельца, который отвечает за репликацию объекта.
}
void UMyObject ::GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps)
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Помечаем наш Integer на репликацию.
DOREPLIFETIME(UMyObject, MyInteger);
}
}
Добавив переменную и пометив на репликацию, мы сможем ее реплицировать. Тут все просто и так же, как в AActor.
Однако есть небольшой подводный камень, который виден не сразу, но может ввести вас в заблуждение. Это будет особенно заметно, если вы создаете ваш UObject не для работы в C++, а подготавливаете его для наследования и работы в Блупринтах.
Суть в том, что переменные, созданные в наследнике в Блупринтах, реплицироваться не будут. Движок автоматически их не помечает и изменение параметра на сервере в БП ничего не меняет в значении на клиенте. Но и от этого есть лекарство. Для корректной репликации переменных БП вам нужно заранее их пометить. Добавьте пару строчек в GetLifetimeReplicatedProps():
.cpp
void UMyObject ::GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps)
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Помечаем наш Integer на репликацию.
DOREPLIFETIME(UMyObject, MyInteger);
// Помечаем переменные из Блупринтов на репликацию
UBlueprintGeneratedClass* BPClass = Cast<UBlueprintGeneratedClass>(GetClass());
if (BPClass) BPClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps);
}
Теперь переменные в дочерних Блупринт классах будут реплицироваться как положено.
RPC события в UObject
RPC (Remote Procedure Call) события — это специальные функции, вызывающиеся на другой стороне сетевого взаимодействия проекта. С помощью них вы можете вызвать функцию с сервера на других клиентах и с клиента на сервере. Очень полезно и часто используется при написании сетевых проектов.
Если вы не знакомы с ними, рекомендую почитать один материал. В нем описывается использование в C++ и в Блупринтах.
В то время как в Actor или в компонентах с их вызовом проблем нет, в UObject события срабатывают на той же стороне, где и были вызваны, что приводит к невозможности выполнить удаленный вызов, когда это нужно.
Взглянув на код компонентов (UActorComponent), мы можем найти несколько функций, которые позволяют передавать вызовы по сети. Так как UActorComponent наследуется от UObject, мы можем просто скопировать необходимые участки кода и вставить в наш объект, чтобы он работал как нужно:
.h
//Добавляем нужный инклуд
#include "Engine/EngineTypes.h"
UCLASS(Blueprintable)
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
public:
virtual bool CallRemoteFunction (UFunction * Function, void * Parms, struct FOutParmRec * OutParms, FFrame * Stack) override;
virtual int32 GetFunctionCallspace (UFunction* Function, void* Parameters, FFrame* Stack) override;
// Остальной код
}
.cpp
//Добавляем необходимые инклуды
#include "Engine/NetDriver.h"
// Вызываем удаленную функцию и ставим дополнительные проверки.
bool UMyObject::CallRemoteFunction(UFunction * Function, void * Parms, FOutParmRec * OutParms, FFrame * Stack)
{
if (!GetOuter()) return false;
UNetDriver* NetDriver = GetOuter()->GetNetDriver();
if (!NetDriver) return false;
NetDriver->ProcessRemoteFunction(GetOuter(), Function, Parms, OutParms, Stack, this);
return true;
}
int32 UMyObject::GetFunctionCallspace(UFunction * Function, void * Parameters, FFrame * Stack)
{
return (GetOuter() ? GetOuter()->GetFunctionCallspace(Function, Parameters, Stack) : FunctionCallspace::Local);
}
С этими функциями мы сможем вызывать RPC события не только в коде, но и в Блупринтах.
Обратите внимание, что для вызова Client или Server событий необходим владелец, у которого Owner — наш игрок. Например объектом владеет персонаж пользователя или же предмет, у которого Owner — это Player Controller игрока.
Глобальные функции в Блупринтах
Если вы когда-либо создавали Object Блупринт, то могли заметить, что в них нельзя вызвать глобальные функции (статичные, но для понятности назовем так), которые доступны в остальных классах, например, GetGamemode(). Создается ощущение, что вы просто-напросто не можете делать в Object классах, из-за чего вам приходится либо передавать все ссылки при создании, либо же как-то извращаться, а иногда выбор и вовсе падает на класс Actor, который создается на сцене и поддерживает все на свете.
А вот в С++, конечно же, таких проблем нет. Однако гейм-дизайнеру, который играется с настройками и добавляет разные мелочи, не скажешь, что нужно открыть Visual Studio, найти соответствующий класс и в функции doSomething() получить игровой режим, изменив в нем очки. Поэтому крайне важно, чтобы дизайнер мог зайти в Блупринт и двумя щелчками мыши сделать то, в чем заключается его работа. Сэкономите и его время, и ваше. Впрочем, Блупринты для этого и придуманы.
Суть в том, что когда вы ищите или вызываете функции в контекстном меню в Блупринте, те самые глобальные функции, которым требуется ссылка на мир, пробуют вызвать функцию внутри вашего объекта, ссылающуюся на него. И если редактор видит, что функции нет, то понимает, что использовать ее он не сможет и не показывает в списке.
Впрочем, и от этого есть лекарство. Даже два.
Давайте вначале рассмотрим вариант для более удобного использования в редакторе. Нам нужно будет переопределить функцию, которая возвращает ссылку на мир и тогда редактор поймет, что в самой игре она сможет работать:
.h
UCLASS(Blueprintable)
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
// Переопределяем GetWorld() для возвращения корректной ссылки.
virtual UWorld* GetWorld() const override;
// Остальной код
}
.cpp
UWorld* UMyObject::GetWorld() const
{
// Возвращаем ссылку на мир из владельца объекта, если не работаем редакторе.
if (GIsEditor && !GIsPlayInEditorWorld) return nullptr;
else if (GetOuter()) return GetOuter()->GetWorld();
else return nullptr;
}
Теперь она определена и редактор будет понимать, что в целом объект способен получить нужный указатель (хоть он не валидный) и использовать глобальные функции в БП.
Обратите внимание, что владелец (GetOuter()) тоже должен иметь выход к миру. Это может быть другой UObject с определенным GetWorld(), компонент или Actor объект на сцене.
Есть и иной способ. Достаточно добавить в макрос UCLASS() при объявлении класса метку о том, что статичным функциям в БП будет добавляться параметр WorldContextObject, в который подается любой объект, служащий проводником в «мир» и глобальным функциям движка. Этот вариант подойдет тем, у кого в проекте может быть несколько миров одновременно (например, игровой мир и мир для спектатора):
.h
// Добавим вывод WorldContext парамерта в функции в БП
UCLASS(Blueprintable, meta=(ShowWorldContextPin))
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
// Остальной код
}
Если ввести в поиск в БП GetGamemode, он появится в списке, как и другие подобные функции, и в параметре будет WorldContextObject, в который нужно передавать ссылку на Actor.
К слову, можно просто подавать туда владельца нашего объекта. Я рекомендую создать функцию на Actor’а, это будет всегда полезно для объекта:
.h
UCLASS(Blueprintable, meta=(ShowWorldContextPin))
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
// Объявим БП чистую и публичную функцию, которая выводит владельца нашего объекта.
public:
UFUNCTION(BlueprintPure)
AActor* GetOwner() const {return Cast<AActor>(GetOuter());};
// Остальной код
}
Теперь можно просто использовать глобальные функции в сочетании с нашей Pure функцией для получения владельца.
Если вы во втором варианте так же объявите GetWorld() как и в первом варианте, то сможете подавать в параметр WorldContextObject ссылку на себя (Self или This).
BeginPlay и Tick события
Еще одна проблема, с которыми могут столкнуться разработчики на Блупринтах — в Object классе нет событий BeginPlay и Tick. Безусловно, вы можете их создать сами и вызывать из другого класса. Но согласитесь, что гораздо удобнее, когда это все работает из коробки.
Давайте для начала разберем, как сделать Begin Play. Мы можем создать доступную для перезаписи в БП функцию и вызывать ее в конструкторе класса, но тут есть ряд проблем, так как на момент конструктора ваш объект еще полностью не инициализирован.
Во всех классах существует функция PostInitProperties(), которая вызывается после инициализации большинства параметров и регистрации объекта в различных внутренних системах, например, для сборщика мусора. В ней как раз можно вызвать наше событие, которое будет использоваться в Блупринтах:
.h
UCLASS(Blueprintable)
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
// Перезаписываем функцию для вызова после инициализации.
virtual void PostInitProperties() override;
// Функция, которую определяем мы уже в Блупринтах.
UFUNCTION(BlueprintImplementableEvent)
void BeginPlay();
// Остальной код
}
.cpp
void UMyObject::PostInitProperties()
{
Super::PostInitProperties();
//Вызываем только в игре, когда есть мир. В редакторе BeginPlay вызван не будет
if(GetOuter() && GetOuter()->GetWorld())
BeginPlay();
}
Вместо if(GetOuter() && GetOuter()->GetWorld()) можно поставить просто if(GetWorld()) если вы его уже переопределили.
Будьте осторожны! По умолчанию PostInitProperties() вызывается и в редакторе.
Теперь мы можем зайти в наш БП объект и вызвать событие BeginPlay. Оно будет вызываться при создании объекта.
Перейдем к Event Tick. Тут простой функцией нам не обойтись. Tick объектов в движке вызывает специальный менеджер, к которому нужно как-то подцепиться. Однако, тут есть очень удобная хитрость — дополнительное наследование от FTickableGameObject. Это позволит автоматически сделать все, что нужно, и тогда достаточно будет просто подцепить необходимые функции:
.h
// Необходимый инклуд
#include "Tickable.h"
// Множественное наследование c FTickableGameObject
UCLASS(Blueprintable)
class MYPROPJECT_API UMyObject : public UObject, public FTickableGameObject
{
GENERATED_BODY()
public:
// Необходимые функции
virtual void Tick(float DeltaTime) override;
virtual bool IsTickable() const override;
virtual TStatId GetStatId() const override;
protected:
// Для определения в БП
UFUNCTION(BlueprintImplementableEvent)
void EventTick(float DeltaTime);
// Остальной код
}
.cpp
void UMyObject::Tick(float DeltaTime)
{
// Вызываем наше событие для определения в БП.
EventTick(DeltaTime);
// Остальной плюсовый код на тик.
}
// Необходимо тикать или нет
bool UMyObject::IsTickable() const
{
return true;
}
TStatId UMyObject::GetStatId() const
{
return TStatId();
}
Если вы отнаследуетесь от вашего объекта и создадите БП класс, то будет доступно событие EventTick, вызывающее логику каждый кадр.
Добавление компонентов из UObject’ов
В Блупринтах UObject нельзя спавнить компоненты для Actor’ов. Эта же проблема свойственна и Блупринтам ActorComponent. Не очень понятна логика Epic Games, так как в C++ это можно сделать. Более того, вы можете добавить компонент из Actor’а другому Actor объекту просто указав ссылку. Но сделать этого нельзя.
К сожалению, с данным пунктом я так и не смог разобраться. Если у кого найдется инструкция, как это сделать, буду рад выложить сюда.
Единственный вариант, который я могу предложить на данный момент — это сделать обертку в классе UObject, предоставляющую доступ к простому добавлению компонентов. Таким образом получится добавлять компоненты Actor’ам, но у вас не будет динамически создаваться входные параметры спауна. Зачастую, этим можно пренебречь.
Настройка экземпляра через редактор
В UE4 есть еще одна удобная «фича» для работы с объектами — это возможность создать экземпляр во время инициализации и менять его параметры через редактор, тем самым настроив его свойства, не создавая дочерний класс только ради настроек. Особенно полезно гейм-дизайнерам.
Допустим, у вас есть менеджер модификаторов для персонажа и сами модификаторы представлены классами, в которых описываются накладываемые эффекты. Гейм-дизайнер создал пару модификаторов и указывает в менеджере, какие используются.
В обычной ситуации выглядело бы вот так:
.h
class MYPROPJECT_API AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
TSubclassOf<class UMyObject> MyObjectClass;
}
Однако тут возникает проблема в том, что настроить модификаторы он не может и приходится создавать дополнительный класс для других значений. Согласитесь, не очень удобно иметь десятки классов в Content Browser, которые отличаются лишь значениями. Исправить это несложно. Можно добавить внутрь USTRUCT() пару полей, а также указать в объекте-контейнере, что наши объекты будут экземплярами, а не просто ссылками на несуществующий объект или классами:
.h
UCLASS(Blueprintable, DefaultToInstanced, EditInlineNew) // Добавляем мета-теги для поддержки редактирования экземпляра из окна параметров
class MYPROPJECT_API UMyObject : public UObject
{
GENERATED_BODY()
UPROPERTY(EditAnywhere) // Можно редактировать переменную в окне параметров
uint8 MyValue; // Переменная, которую мы отредактируем
// Остальной код
}
Одного этого мало, теперь необходимо указать, что та самая переменная с классом будет экземпляром. Это уже делается там, где вы храните объект, например, в менеджере модификаторов персонажа:
.h
class MYPROPJECT_API AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Instanced) // Добавляем тег Instanced для создания экземпляра
class UMyObject* MyObject; // Ссылка на объект
}
Обратите внимание, что мы используем именно ссылку на объект, а не на класс, так как экземпляр будет создан сразу при инициализации. Теперь мы можем зайти в окно редактора выбрать класс и настроить значения внутри экземпляра. Это гораздо удобнее и более гибко.
Info
Есть в Unreal Engine еще один интересный класс. Это AInfo. Класс, унаследованный от AActor, не имеющий визуального представления в мире. Info используют такие классы, как: игровой режим, GameState, PlayerState и прочие. То есть классы, которые поддерживают разные фишки от AActor, например, репликацию, но при этом не размещены на сцену.
Если вам нужно создать дополнительный, глобальный менеджер, который должен поддерживать сеть и все вытекающие Actor класса, то можно использовать его. Вам не придется манипулировать классом UObject, как описано выше, чтобы заставить его, например, реплицировать данные.
Однако учтите, что хоть и у объекта нет координат, визуальных компонентов и он не рендерится на экране, он все равно является наследником Actor класса, а значит, настолько же тяжелый, как и родитель. Резонно использование в малых количествах и ради удобства.
Заключение
UObject нужен очень часто, и я советую пользоваться им всегда, когда Actor в действительности не нужен. Жаль, что он немного ограничен, но это одновременно и плюс. Иногда приходится повозиться, когда вам нужно использовать нестандартный шаблон, но главное, что все основные ограничения можно снять.
Если вы будете часто работать с объектами из Блупринтов, но не хочется постоянно создавать классы и добавлять в них эти возможности, можно просто создать один класс UObject, с поддержкой всего, что вам может понадобиться в проекте, а дальше создавать дочерние от него Блупринты и работать в них.
Надеюсь, статья будет полезна тем, кто изучает или работает с Unreal Engine 4. Если вдруг какая-то часть не компилируется, то можете сообщить об этом в комментариях или в личку. Также буду очень благодарен, если кто-то знает еще различные полезности, связанные с UObject.