UE:Hook UObject

Hook is a mechanism that allows you to meet your requirements by intercepting and hooking into certain events. Unlike traditional low-level hooks, this article mainly introduces how to use similar hooking mechanisms in UE to achieve business needs.

Some requirements necessitate the global modification of all objects of a specific class, such as playing a uniform sound effect for a certain type of Button in the UI. If every control needs to listen to its OnClicked and then play the sound, it leads to a lot of redundant operations. So, I want to find a method that can globally listen to the click events of all UButtons and handle them collectively. Alternatively, if I want to control an attribute that is invisible in blueprints, modifying the engine’s code for simple needs seems counterproductive.

These requirements can be achieved through UE’s reflection mechanism. This article provides a simple implementation analysis.

Listening to Object Creation and Executing Operations

With the requirement established, to modify the properties of all objects of a specified type, the general approach is as follows:

  1. First, you need to know when an object of the specified type is created.
  2. After the object is completed being created, modify its properties.

If we can achieve these two points, our requirement can be fulfilled. The key is to first find a way to know when an object is created.

By reviewing UE’s code, it was discovered that when objects are created and destroyed, Listeners can be registered to receive notifications:

1
2
3
4
5
6
7
8
9
void FUObjectArray::AllocateUObjectIndex(UObjectBase* Object, bool bMergingThreads /*= false*/)
{
// ...
for (int32 ListenerIndex = 0; ListenerIndex < UObjectCreateListeners.Num(); ListenerIndex++)
{
UObjectCreateListeners[ListenerIndex]->NotifyUObjectCreated(Object, Index);
}
// ...
}

Call stack:

Since we know that when creating a UObject, all Listeners will be called, we can register our own object.

The type of UObjectCreateListeners is:

1
TArray<FUObjectCreateListener*> UObjectCreateListeners;

FUObjectCreateListener is an abstract class that defines two virtual functions to be used as an interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FUObjectCreateListener
{
public:
virtual ~FUObjectCreateListener() {}
/**
* Provides notification that a UObjectBase has been added to the uobject array
*
* @param Object object that has been destroyed
* @param Index index of object that is being deleted
*/
virtual void NotifyUObjectCreated(const class UObjectBase *Object, int32 Index) = 0;

/**
* Called when UObject Array is being shut down, this is where all listeners should be removed from it
*/
virtual void OnUObjectArrayShutdown() = 0;
};

Additionally, there’s an interface to listen for object deletions, FUObjectDeleteListener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Base class for UObjectBase delete class listeners
*/
class FUObjectDeleteListener
{
public:
virtual ~FUObjectDeleteListener() {}

/**
* Provides notification that a UObjectBase has been removed from the uobject array
*
* @param Object object that has been destroyed
* @param Index index of object that is being deleted
*/
virtual void NotifyUObjectDeleted(const class UObjectBase *Object, int32 Index) = 0;

/**
* Called when UObject Array is being shut down, this is where all listeners should be removed from it
*/
virtual void OnUObjectArrayShutdown() = 0;
};

I wanted to listen for both object creation and deletion, so I wrote a class that inherits from both:

1
2
3
4
5
6
7
8
9
10
11
12
struct FUButtonListener : public FUObjectArray::FUObjectCreateListener, public FUObjectArray::FUObjectDeleteListener
{
static FButtonListenerMisc* Get()
{
static FButtonListenerMisc StaticIns;
return &StaticIns;
}
// Listener
virtual void NotifyUObjectCreated(const class UObjectBase *Object, int32 Index);
virtual void NotifyUObjectDeleted(const class UObjectBase *Object, int32 Index);
FORCEINLINE virtual void OnUObjectArrayShutdown() override { }
};

Having created it, a crucial step is to register our class into GUObjectArray, which provides two sets of Add and Remove functions to add and remove Listeners.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Adds a new listener for object creation
* @param Listener listener to notify when an object is deleted
*/
void AddUObjectCreateListener(FUObjectCreateListener* Listener);

/**
* Removes a listener for object creation
* @param Listener listener to remove
*/
void RemoveUObjectCreateListener(FUObjectCreateListener* Listener);

/**
* Adds a new listener for object deletion
* @param Listener listener to notify when an object is deleted
*/
void AddUObjectDeleteListener(FUObjectDeleteListener* Listener);

/**
* Removes a listener for object deletion
* @param Listener listener to remove
*/
void RemoveUObjectDeleteListener(FUObjectDeleteListener* Listener);

OK, now we know how to add them, but when should we add the Listener?

Of course, it needs to be added before the UObject is created at runtime; otherwise, if the object has already been created, we won’t be able to listen to it after binding.

Since PIE and packaged builds are differentiated, the handling for PIE Play and packaged runtime needs to be handled separately:

In the editor, we can start the listening process for UObject creation by listening to the following two events:

1
2
3
4
#if WITH_EDITOR
FEditorDelegates::PreBeginPIE.AddStatic(&PreBeginPIE);
FGameDelegates::Get().GetEndPlayMapDelegate().AddRaw(FHookerMisc::Get(), &FHookerMisc::Shutdown);
#endif

In the packaged process, which does not involve these two events, the following two delegates can be used as replacements:

1
2
3
4
#if !WITH_EDITOR
FCoreDelegates::OnPostEngineInit.AddRaw(FHookerMisc::Get(), &FHookerMisc::Init);
FCoreDelegates::OnPreExit.AddRaw(FHookerMisc::Get(), &FHookerMisc::Shutdown);
#endif

In the functions called by these two delegates, we can handle adding and removing the Listeners:

1
2
3
4
5
6
7
8
9
10
void FHookerMisc::Init()
{
GUObjectArray.AddUObjectCreateListener(FHookerMisc::Get());
GUObjectArray.AddUObjectDeleteListener(FHookerMisc::Get());
}
void FHookerMisc::Shutdown()
{
GUObjectArray.RemoveUObjectCreateListener(FHookerMisc::Get());
GUObjectArray.RemoveUObjectDeleteListener(FHookerMisc::Get());
}

Then, we can override the following two functions to obtain Object creation and deletion events:

1
2
void FHookerMisc::NotifyUObjectCreated(const UObjectBase* Object, int32 Index) {}
void FHookerMisc::NotifyUObjectDeleted(const UObjectBase* Object, int32 Index) {}

Note: When the NotifyUObjectCreated function is called, the UObject being passed here is not the fully created object, as it has not been initialized yet, and its constructor hasn’t been called. Therefore, modifying the Object directly at this point will not have any effect because, in the engine’s subsequent processes, the constructor is called on the memory of this Object.

Call stack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
UObject* StaticConstructObject_Internal
(
const UClass* InClass,
UObject* InOuter /*=GetTransientPackage()*/,
FName InName /*=NAME_None*/,
EObjectFlags InFlags /*=0*/,
EInternalObjectFlags InternalSetFlags /*=0*/,
UObject* InTemplate /*=NULL*/,
bool bCopyTransientsFromClassDefaults /*=false*/,
FObjectInstancingGraph* InInstanceGraph /*=NULL*/,
bool bAssumeTemplateIsArchetype /*=false*/
)
{
// ...

bool bRecycledSubobject = false;
Result = StaticAllocateObject(InClass, InOuter, InName, InFlags, InternalSetFlags, bCanRecycleSubobjects, &bRecycledSubobject);
check(Result != NULL);
// Don't call the constructor on recycled subobjects, they haven't been destroyed.
if (!bRecycledSubobject)
{
STAT(FScopeCycleCounterUObject ConstructorScope(InClass, GET_STATID(STAT_ConstructObject)));
(*InClass->ClassConstructor)(FObjectInitializer(Result, InTemplate, bCopyTransientsFromClassDefaults, true, InInstanceGraph));
}

// ...
return Result;
}

As can be seen, it first creates a UObject (essentially allocating memory for the UObject) via StaticAllocateObject, then in the subsequent process, retrieves the constructor for the current class from UClass and executes it.

What is a constructor? A constructor can be understood as a mold; it takes a chunk of memory, and through this mold, a specific object is generated. The constructor is a way to initialize that memory—how to interpret that memory and provide it with initial values.

The constructor will call the base class constructor, execute class internal initializations, call the constructors of class members, and modify that memory to the default state of the object.

Thus, we cannot directly modify the UObject in NotifyUObjectCreated; we must wait until it is fully constructed.

At this point, the Object carries the RF_NeedInitialization Flag, marking it for initialization. We can use the presence or absence of this FLAG to determine whether to modify it.

When the NotifyUObjectCreated event is called, we can store the passed Object in a list. During the next creation event or the next frame, we can check all objects in the list to see if they still possess the RF_NeedInitialization Flag. If they don’t, it indicates that the object has been successfully initialized, and we can modify it without worrying about the data being overwritten.

This FLAG is cleared in the PostConstructInit function of FObjectInitializer (called by ~FObjectInitializer):

1
2
3
4
5
6
void FObjectInitializer::PostConstructInit()
{
// ...
Obj->ClearFlags(RF_NeedInitialization);
// ...
}

Thus, when an object no longer bears the RF_NeedInitialization FLAG, we can operate on it.

Modifying UClass to Control Reflective Properties

Sometimes, we want to control an object’s properties in the editor, but even though the property is UProperty, it’s not marked as EditAnywhere, making it invisible in the editor.

For instance:

1
2
3
4
5
6
7
8
9
class ANetActor: public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
int32 ival;
UPROPERTY()
int32 ival2;
};

In the above member variables, ival is accessible in Blueprints and editing because it has the EditAnywhere attribute, whereas ival2 does not, making it inaccessible.

For our own created classes, we can solve this by modifying the code, but modifying the relevant code directly for engine or other third-party module classes isn’t a good idea, as it brings additional constraints: needing to use the source version of the engine and manage the modified code version.

Is there a method to achieve our needs without altering the engine’s code?

Yes! Because whether an object is displayed in the editor is determined by the reflection information generated by UE for the object and its properties. If we can find a way to make the engine’s reflection information consider ival2 as needing to be displayed in the editor, that would suffice.

The idea is to modify the reflection information of ival2, allowing the editor to treat it as visible.

By analyzing the code, it was discovered that the display permission of a property in the Details is determined by checking the CPF_Edit Flag against UProperty.

1
2
3
4
5
6
enum EPropertyFlags : uint64
{
CPF_None = 0,
CPF_Edit = 0x0000000000000001, ///< Property is user-settable in the editor.
// ...
};

The description of CPF_Edit in EPropertyFlags aligns with this understanding.

Let’s examine the difference in generated reflection code for ival and ival2:

1
2
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_ANetActor_Statics::NewProp_iVal2 = { "iVal2", nullptr, (EPropertyFlags)0x0010000000000000, UE4CodeGen_Private::EPropertyGenFlags::Int, RF_Public|RF_Transient|RF_MarkAsNative, 1, STRUCT_OFFSET(ANetActor, iVal2), METADATA_PARAMS(Z_Construct_UClass_ANetActor_Statics::NewProp_iVal2_MetaData, UE_ARRAY_COUNT(Z_Construct_UClass_ANetActor_Statics::NewProp_iVal2_MetaData)) };
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_ANetActor_Statics::NewProp_ival = { "ival", nullptr, (EPropertyFlags)0x0010000000000001, UE4CodeGen_Private::EPropertyGenFlags::Int, RF_Public|RF_Transient|RF_MarkAsNative, 1, STRUCT_OFFSET(ANetActor, ival), METADATA_PARAMS(Z_Construct_UClass_ANetActor_Statics::NewProp_ival_MetaData, UE_ARRAY_COUNT(Z_Construct_UClass_ANetActor_Statics::NewProp_ival_MetaData)) };

We can see that the only difference between them is that ival has the flag (EPropertyFlags)0x0010000000000001, while ival2 has (EPropertyFlags)0x0010000000000000. The FLAG content for ival is simply the addition of CPF_Edit, which is the second attribute of EPropertyFlags, with a value of 0x01. The enum values in EPropertyFlags are represented bitwise.

UE’s reflection mechanism reads the generated reflection information during engine startup to create FProperty objects for reflective properties within classes, enabling retrieval of the reflection information for those properties at runtime.

The FProperty class is defined in Runtime/CoreUObject/Public/UObject/UnrealType.h, recording the current property’s offset within the class, element size, names, and the PropertyFlags required.

Based on the analysis above, the current key issue is: how to modify a class’s reflective property PropertyFlags at runtime (during the editor run).

The process consists of the following steps (before the properties window is created):

  1. Get the class’s reflection information (UClass).
  2. Retrieve the reflection information for the specified property (FProperty).
  3. Modify the property’s reflection information to add CPF_Edit.

The implementation code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
auto AddEditFlagLambda = [](UClass* Class, FName flagName) -> bool
{
bool bStatus = false;
if (Class)
{
for (TFieldIterator<FProperty> PropertyIter(Class); PropertyIter; ++PropertyIter)
{
FProperty* PropertyIns = *PropertyIter;
if (PropertyIns->GetName().Equals(flagName.ToString()))
{
if (!PropertyIns->HasAnyPropertyFlags(CPF_Edit))
{
PropertyIns->SetPropertyFlags(CPF_Edit);
bStatus = true;
}
}
}
}
return bStatus;
};

AddEditFlagLambda(ANetActor::StaticClass(), TEXT("ival2"));

Using the method described in the previous section’s NotifyUObjectCreated, we achieve modifications, but unlike that which targeted an instance, this is directly modifying a specified UClass, so there’s no need to assess the initialization completion of the object.

The results upon running:

Using this method, we can easily add editor support for reflective properties like EditAnywhere / Interp, thereby achieving our needs without modifying the engine’s core code.

Postscript

By utilizing the reflection mechanism, it’s convenient to modify reflective classes and properties. This method allows for fulfilling business requirements while avoiding the alteration of engine code. This article merely opens a thought process, presenting a way to consider reflective capabilities; reflection can accomplish more, and it would be worthwhile to analyze UE’s implementation of reflection and how these reflection details are utilized in the future.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE:Hook UObject
Author:LIPENGZHA
Publish Date:2020/11/08 22:35
World Count:7.3k Words
Link:https://en.imzlp.com/posts/15049/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!