Async Object Blending in Unreal Engine 5

In this blog post, I will introduce leveraing UFUNCTION specifiers Latent, LatentInfo, ExpandEnumAsExecs and CustomThunk to implement an async object blending blueprint function.

Goal

Have you ever used the Delay node in Unreal Engine? It's a really powerful node to allow you to perform an action with a delayed time duration. It's called a Latent function or Async Latent action.

Basically there are three ways to customize an async action:

  1. Defining a UFUNCTION with meta specifiers Latent and LatenInfo, e.g., Delay and Delay Until Next Tick.
  2. Inheriting the UBlueprintAsyncActionBase class, e.g., Move Component To.
  3. Inheriting the UK2Node_BaseAsyncTask class, e.g., AI Move To.

This post aims to implement a custom async action which sets any property of a given object with blending. Blending means the source property value will gradullay shift to the target property value. For example, if the property of interest is an int and its original value is 0. With a target value of 1 and a blending duration of 2 seconds, the property will smoothly transits its value from 0 to 1 within 2 seconds, depending on how the blending curve is specified.

As shown in the following figure, we are going to implement a blueprint node named Set Property By Name, which takes six arguments:

  • Object: The obejct you would like to modify the property of.
  • Property Name: The property you would like to set. It can be nested, i.e., {Property A}.{Property B}...
  • Value: The target property value. It's a wildcard parameter so you should ensure its type matches the property's type.
  • Duration: Blending duration.
  • Func: Blending curve function controlling the blending process.
  • Exp: Blending curve function exponential. It's used to control the steepness of the curve.

As we'd like our blueprint node to be as generic as possible, we won't restrict the type of the input object and the property. This requires us to receive a wildcard argument Value and attempt to match its type with the one specified by Object and Property. We must find the corresponding instance to the object's property through the chain of properties via reflection, cache the source values and perform blending at each tick.

Besides, this node should also provide the ability of callbacks, allowing us to customize what we can do when it completes blending, when it ticks at each frame, and when it's interrupted.

Generally, we must meet the following requirements:

  • It should receive a wildcard argument as the target property value.
  • It should be conceptually generic to all UObject types and all primitive and composite types for property.
  • It should be able to cache the source property values.
  • It should provide customizable callbacks when it ticks at each frame, when it's completed and when it's interrupted.

Implementation

Before we start, we must decide which async method fits in our demands. Because we have a wildcard input pin, the best choice in our case would be the UFUNCTION approach in that we can easily implement a CustomThunk function to process the wildcard input.

Let's dive into the details of our implementation.

Blueprint Function Declaration

Our first step is to declare a function that can be called in blueprint. The BlueprintCallable specifier serves this purpose. As we need to receive a wildcard input argument, we must add the CustomThunk specifier. In addition, the function should be latent. This requires us to add a Latent and LatentInfo specifier. Last, adding a ExpandEnumAsExecs specifier allows us to have multiple output execute pins.

1
2
3
4
5
6
7
8
// Callable: makes this function callable in blueprint.
// CustomThunk: requires implementing a custom thunk function that is executed by blueprint.
// Latent: indicates a latent function.
// LatentInfo: specifies which parameter serves as the latent info structure.
// CustomStructureParam: specifies which parameter serves as the wildcard input.
// ExpandEnumAsExecs: specifies which parameter defines the output execute pins.
UFUNCTION(BlueprintCallable, CustomThunk, meta = (Latent, LatentInfo = "LatentInfo", WorldContext = "WorldContextObject", CustomStructureParam = "Value", ExpandEnumAsExecs = "OutPin", AdvancedDisplay = 4))
static void SetPropertyByName(const UObject* WorldContextObject, UObject* Object, FName PropertyName, const int32& Value, double Duration, TEnumAsByte<EEasingFunc::Type> Func, double Exp, FLatentActionInfo LatentInfo, ELatentOutputPins& OutPin);

As the declaration tells, this function receives six visible arguments, other parameters WorldContextObject, LatentInfo are automatically assigned. Object is the object of which we set the desired property, PropertyName is the property of interest, which can be nested using ., Value is a wildcard input serving as the target value. It can be any type (except UObject and container types in the current form) but should match the type of the property of Object. Duration, Func and Exp are parameters relating to property value blending.

It's worth introducing LatentInfo and OutPin. LatentInfo is a structure of type FLatentActionInfo. It stores some basic information of this latent action needed to notify the next function when this latent action finishes its task. OutputPin is a powerful keyword allowing you to explicitly designate which output pin this function should execute. It's commonly used in conjunction with some conditions you'd like to check before choosing a correct output pin. Combining LatentInfo and OutputPin enables us to dynamically switch between output pins conditioned on the current state of our latent action. We will see this soon later.

Latent Action Class Definition (Part 1)

To use Latent and LatentInfo, you must create your own FPendingLatentAction class. FDelayAction is a good example to start with. FInterpolateComponentToAction is a more complex example but it has the same basic logic. The core part of this class is to implement your own UpdateOperation function. It's called every frame and you should determine if this latent action should finish or continue executing.

For our purpose, we define our custom class named FESetPropertyLatentAction. This class should several critical members:

  • Object: The object given as input in the blueprint function.
  • SrcProperty: The property of interest to modify.
  • SrcPtr: The pointer that points to the object that has the SrcProperty.
  • OutPin: The current output pin for this latent action. Note that it must be a reference to control the execution flow of the blueprint function. It's defined as:
1
2
3
4
5
6
7
8
9
10
UENUM()
enum class ELatentOutputPins : uint8
{
/** Execute each tick. */
OnTick,
/** Execute when the function completes its work. */
OnComplete,
/** Execute when the function is interrupted. */
OnInterrupt
};
  • Blending variables.
  • Latent info variables.
  • A boolean indicating if this latent action should be interrupted.

All these members are initialized in the constructor. Besides, the constructor also receives two additional arguments: InValueProperty and InValuePtr, the counterparts of SrcProperty and SrcPtr for the target value.

The function RecursivelyFindPropertyValue will recursively find nested properties and store them in some containers. The details will be introduced later.

Finally, the UpdateOperation function is overridden to set the ouput pin through OutPin at each frame, and trigger the corresponding link (execute the corresponding callback).

The function UpdateAllPropertyValues updates all property values at each frame. We will visit this function later.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class FESetPropertyLatentAction : public FPendingLatentAction
{
private:
// Container object
UObject* Object;
FProperty* SrcProperty;
void* SrcPtr;

// Blend variables
float BlendDuration;
TEnumAsByte<EEasingFunc::Type> BlendFunc;
float BlendExp;
float ElapsedTime;

// Latent info
ELatentOutputPins& OutPin;
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;

// Internal variables
bool bInterrupted { false };

public:
DECLARE_MULTICAST_DELEGATE(FOnActionCompletedOrInterrupted);
FOnActionCompletedOrInterrupted OnActionCompletedOrInterrupted;
FESetPropertyLatentAction(
UObject* InObject, // Input object
FProperty* InSrcProperty, // Input source property, can only be FNumericProperty or FStructProperty
void* InSrcPtr, // Input source value address
FProperty* InValueProperty, // Input target property
void* InValuePtr, // Input target value address
double InBlendDuration, // Blend time
TEnumAsByte<EEasingFunc::Type> InBlendFunc, // Blend function
double InBlendExp, // Blend exponential
ELatentOutputPins& OutPin, // Output execution pin
const FLatentActionInfo& LatentInfo // Latent info
)
: Object (InObject)
, SrcProperty (InSrcProperty)
, SrcPtr (InSrcPtr)
, BlendDuration (InBlendDuration)
, BlendFunc (InBlendFunc)
, BlendExp (InBlendExp)
, ElapsedTime (0)
, OutPin (OutPin)
, ExecutionFunction (LatentInfo.ExecutionFunction)
, OutputLink (LatentInfo.Linkage)
, CallbackTarget (LatentInfo.CallbackTarget)
{
if (BlendDuration <= 0)
{
BlendDuration = UE_KINDA_SMALL_NUMBER;
}

// Recursively find nested properties and store them in some container
RecursivelyFindPropertyValue(InSrcProperty, InValueProperty, InSrcPtr, InValuePtr);
}

void RecursivelyFindPropertyValue(FProperty* InSrcProperty, FProperty* InValueProperty, void* InSrcPtr, void* InValuePtr)
{
// How to define this function?
}

virtual void UpdateOperation(FLatentResponse& Response) override
{
ElapsedTime = FMath::Clamp(ElapsedTime + Response.ElapsedTime(), 0, BlendDuration);
bool bCompleted = ElapsedTime >= BlendDuration;

if (bInterrupted || !IsValid(Object))
{
OutPin = ELatentOutputPins::OnInterrupt;
}
else
{
// On each tick, updates property values
UpdateAllPropertyValues(ElapsedTime / BlendDuration, BlendExp, BlendFunc);

if (bCompleted)
{
OutPin = ELatentOutputPins::OnComplete;
}
else
{
OutPin = ELatentOutputPins::OnTick;
}
}

if (bCompleted || bInterrupted)
{
OnActionCompletedOrInterrupted.Broadcast();
Response.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget);

}
else
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
}
}
};

Recursive Property Lookup

Let's go back to our blueprint function SetPropertyName. The biggest problem for now is that how can we find the object with the given property name from the given source object. It's particularly noteworthy that the property name can be nested in the format of A.B.C. To this end, let's define a function GetNestedPropertyFromObject to find the property of interest and the pointer that points to the object owning this property.

1
2
3
4
5
6
7
8
9
10
static std::pair<FProperty*, void*> GetNestedPropertyFromObject(UObject* Object, FName PropertyName)
{
if (!IsValid(Object))
{
UE_LOG(LogTemp, Warning, TEXT("Input Object is invalid when calling function GetNestedPropertyFromObject."));
return std::make_pair(nullptr, nullptr);
}

return GetNestedPropertyFromObjectStruct(Object, Object->GetClass(), PropertyName.ToString());
}

The GetNestedPropertyFromObjectStruct is defined as follows.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
static std::pair<FProperty*, void*> GetNestedPropertyFromObjectStruct(void* Object, UStruct* Struct, const FString& PropertyName)
{
int FoundIndex;
FString CurrentProperty;
FString NextProperty;
bool bFoundSeparator = PropertyName.FindChar('.', FoundIndex);

if (bFoundSeparator)
{
CurrentProperty = PropertyName.Mid(0, FoundIndex);
NextProperty = PropertyName.Mid(FoundIndex + 1, PropertyName.Len() - FoundIndex - 1);
}
else
{
CurrentProperty = PropertyName;
}

FProperty* Property = FindFProperty<FProperty>(Struct, FName(CurrentProperty));
if (Property != nullptr)
{
void* Value = Property->ContainerPtrToValuePtr<void>(Object);

if (NextProperty.IsEmpty())
{
if (Property->IsA<FNumericProperty>() || Property->IsA<FStructProperty>())
{
return std::make_pair(Property, Value);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Terminate property can only be numeric/struct type. Current type is %s."), *Property->GetClass()->GetName());
return std::make_pair(nullptr, nullptr);
}
}
else
{
const FStructProperty* PropAsStruct = CastField<FStructProperty>(Property);
const FObjectProperty* PropAsObject = CastField<FObjectProperty>(Property);
const FArrayProperty* PropAsArray = CastField<FArrayProperty>(Property);
const FSetProperty* PropAsSet = CastField<FSetProperty>(Property);
const FMapProperty* PropAsMap = CastField<FMapProperty>(Property);

if (PropAsArray != nullptr || PropAsSet != nullptr || PropAsMap != nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("Function GetNestedPropertyFromObjectStruct currently does not support container type."));
}
else if (PropAsStruct != nullptr)
{
return GetNestedPropertyFromObjectStruct(Value, PropAsStruct->Struct, NextProperty);
}
else if (PropAsObject != nullptr)
{
// Now Value points to the pointer that points to the real object. Must let it point to the object instead of the pointer. Ref: DiffUtils.cpp
UObject* PropObject = *((UObject* const*)Value);
return GetNestedPropertyFromObjectStruct(PropObject, PropObject->GetClass(), NextProperty);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Invalid property: %s. Non-terminal property can only be an object or struct."), *FString(CurrentProperty));
}

return std::make_pair(nullptr, nullptr);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Cannot find property %s from UStruct %s."), *FString(CurrentProperty), *Struct->GetName());
return std::make_pair(nullptr, nullptr);
}
}

There are several steps in this function:

  1. We parse the input PropertyName to find the names of the current property and the next property. If we find a valid property, we go to the next step. Otherwise, the input property name is invalid and a warning message is thrown.
  2. Once the current property of type FProperty* is found, its object pointer can be obtained by calling Property->ContainerPtrToValuePtr. We then check if we may have next property. If this is the last property, we return the [property pointer, value pointer] pair if the current property is of a numeric or struct type, otherwise we throw a warning. In this step, we are restricting that the target property should be either of a numeric (float, double, int, etc) type or a struct type. We can in fact extend to an objecy type. You can implement this for your interest.
  3. If this is not the destination property to find, we must check its actual property type and recursively call the function. More concretely, if the property is a struct type, we can use Property->Struct to get its meta type information. If the property is an object type, we should use Property->GetClass() to obtain its meta information. If the property is neither of above types, it will be invalid. The container types (i.e., array, set, map) are not implemented here but it's feasible.
  4. Repeat this process and recursively find the target property and its value pointer. The resulting value will be a pair of type [FProperty*, void*]. The first element is a pointer to the source property of interest, and the second element is a pointer that points to the instance object owning this property.

Custom Thunk Implementation

A wildcard input requires us to implement our own custom thunk function, that is, a function executed by blueprint. I won't dive into the details of the "magic" macros and the mechanism of the blueprint virtual machine. It's always good practice to take a look at the official code where these macros and custom thunk functions are used.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
DECLARE_FUNCTION(execSetPropertyByName)
{
P_GET_OBJECT(UObject, WorldContextObject);
P_GET_OBJECT(UObject, Object);
P_GET_PROPERTY(FNameProperty, PropertyName);
Stack.StepCompiledIn<FProperty>(NULL);
void* ValuePtr = Stack.MostRecentPropertyAddress;
FProperty* ValueProperty = Stack.MostRecentProperty;
P_GET_PROPERTY(FDoubleProperty, Duration);
P_GET_PROPERTY(FByteProperty, Func);
P_GET_PROPERTY(FDoubleProperty, Exp);
P_GET_STRUCT(FLatentActionInfo, LatentInfo);
P_GET_ENUM_REF(ELatentOutputPins, OutPin);

P_FINISH;
P_NATIVE_BEGIN;
auto [SrcProperty, SrcPtr] = GetNestedPropertyFromObject(Object, PropertyName);
if (SrcProperty == nullptr || ValueProperty == nullptr)
{
return;
}

bool bSameType = SrcProperty->SameType(ValueProperty);
bool bFloatType = SrcProperty->IsA<FFloatProperty>() && ValueProperty->IsA<FDoubleProperty>();
if (bSameType || bFloatType)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
TArray<FESetPropertyLatentAction*>& ActionList = GetActionList<FESetPropertyLatentAction>();
FESetPropertyLatentAction** ActionPtr = ActionList.FindByPredicate([SrcProperty = SrcProperty, SrcPtr = SrcPtr](FESetPropertyLatentAction* ThisAction) { return ThisAction->IsSameProperty(SrcProperty, SrcPtr); });
FESetPropertyLatentAction* Action = ActionPtr == nullptr ? nullptr : *ActionPtr;
if (Action != nullptr)
{
Action->SetInterrupt(true);
}

Action = new FESetPropertyLatentAction(Object, SrcProperty, SrcPtr, ValueProperty, ValuePtr, Duration, EEasingFunc::Type(Func), Exp, OutPin, LatentInfo);
Action->OnActionCompletedOrInterrupted.AddLambda([&ActionList, &Action]() { ActionList.Remove(Action); });
ActionList.Add(Action);
World->GetLatentActionManager().AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, Action);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("The found property %s does not has the same type as given property %s, respectively are %s and %s"),
*SrcProperty->NamePrivate.ToString(), *ValueProperty->NamePrivate.ToString(), *SrcProperty->GetCPPType(), *ValueProperty->GetCPPType());
}

P_NATIVE_END;
}

Briefly speaking, this function does the following things:

  1. Retrieve arguments from the runtime virtual machine stack in the order they're declared in our SetPropertyByName function. Note that when we access the wildcard Value argument, we simultaneously retrieve its target value pointer and target property pointer.
  2. We use the GetNestedPropertyFromObject function to get the source property pointer and the corresponding value pointer.
  3. After that, we check whether the source property has the same type as the target property. There is a special case: as blueprint only supports double for floating-point types, we must check if the source property type is float. The test will also be passed if it is.
  4. If the source property matches the target property type, we then get existing latent actions that are performing property blending. Function GetActionList get the full list of latent actions, and FindByPredicate finds any action that is operating on the same object and property. If such one is found, we view the old action as interrupted.
  5. Otherwise, we create a new FEsetPropertyLatentAction and add it to the list. We also add a callback for when it gets complete or interrupted.

The GetActionList function is defined as follows:

1
2
3
4
5
6
template<typename ActionType>
static TArray<ActionType*>& GetActionList()
{
static TArray<ActionType*> ActionList {};
return ActionList;
}

Latent Action Class Definition (Part 2)

We're very close to finish! The last ingredient is to complete our definition of class FESetPropertyLatentAction. Since we've already found the source property (pointer), the source value pointer, the target property (pointer) and the target value pointer, and the source property must match the target property, we can now analyze the type information encapsulated by the property, retrieving all the primitive types (float, double, int, bool, etc) nested inside the property, and cache them in some container data structures. Why do we need to retrieve primitive types? Two reasons. First, we can only blend on primitive types. We can not directly blend a struct before we know what it is made up of. Second, all composite types are composed of primitive types. We can decompose our target of blending a struct into blending all primitive type subobjects within it.

We now finish the definition of FESetPropertyLatentAction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FESetPropertyLatentAction : public FPendingLatentAction
{
// Member variables as before
// ...
// Constructor as before
// ...
// UpdateOperation(FLatentResponse& Response) as before
// ...

// What else here?
// ...
// ...
// ...

void RecursivelyFindPropertyValue(FProperty* InSrcProperty, FProperty* InValueProperty, void* InSrcPtr, void* InValuePtr)
{
// Definition
}
}

Step 1: Create a container class used for storing values for each primitive type.

We leverage template to achieve this goal. The template argument is a property type, and its type trait defines its corresponding primitive type.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
template <typename PropertyType>
struct FPropertyValuePack
{
using TCppType = typename PropertyType::TCppType;

UObject* ContainerObject;
FProperty* SourceProperty;

TCppType SourceValue;
TCppType TargetValue;
TCppType* SourceValuePtr;

void UpdateValue(const float& Progress, const float& Exp, const TEnumAsByte<EEasingFunc::Type>& Func)
{
if (IsValid(ContainerObject))
{
*SourceValuePtr = (TCppType)UKismetMathLibrary::Ease(SourceValue, TargetValue, Progress, Func, Exp);
}
}
};

typedef TArray<FPropertyValuePack<FFloatProperty>> FFloatPropertyList;
typedef TArray<FPropertyValuePack<FDoubleProperty>> FDoublePropertyList;
typedef TArray<FPropertyValuePack<FUInt64Property>> FUInt64PropertyList;
typedef TArray<FPropertyValuePack<FUInt32Property>> FUInt32PropertyList;
typedef TArray<FPropertyValuePack<FUInt16Property>> FUInt16PropertyList;
typedef TArray<FPropertyValuePack<FInt64Property>> FInt64PropertyList;
typedef TArray<FPropertyValuePack<FIntProperty>> FIntPropertyList;
typedef TArray<FPropertyValuePack<FInt16Property>> FInt16PropertyList;
typedef TArray<FPropertyValuePack<FInt8Property>> FInt8PropertyList;
typedef TArray<FPropertyValuePack<FByteProperty>> FBytePropertyList;

// Member variables as before plus container members
FFloatPropertyList FloatPropertyList{};
FDoublePropertyList DoublePropertyList{};
FUInt64PropertyList UInt64PropertyList{};
FUInt32PropertyList UInt32PropertyList{};
FUInt16PropertyList UInt16PropertyList{};
FInt64PropertyList Int64PropertyList{};
FIntPropertyList IntPropertyList{};
FInt16PropertyList Int16PropertyList{};
FInt8PropertyList Int8PropertyList{};
FBytePropertyList BytePropertyList{};

When it's constructed, we store the source value, target value and the source value pointer. The UpdateValue function does all the blending work.

The last few lines define ten primitive types used by Unreal. Then we add new container member variables.

Step 2: Recursively find all primitive types and their values.

RecursivelyFindPropertyValue does this work for us. Given source/target property pointer and source/target value pointer, we must recursively find all primitive types and their source/target values, store them in the containers we just defined, and iterate through them at each frame to blend the values.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
void RecursivelyFindPropertyValue(FProperty* InSrcProperty, FProperty* InValueProperty, void* InSrcPtr, void* InValuePtr)
{
if (InSrcProperty->IsA<FStructProperty>())
{
const FStructProperty* SrcPropAsStruct = CastField<FStructProperty>(InSrcProperty);
const FStructProperty* ValPropAsStruct = CastField<FStructProperty>(InValueProperty);
if (SrcPropAsStruct->Struct == ValPropAsStruct->Struct)
{
for (TFieldIterator<FProperty> PropertyIt(SrcPropAsStruct->Struct); PropertyIt; ++PropertyIt)
{
FProperty* StructProp = *PropertyIt;
if (StructProp->IsA<FNumericProperty>() || StructProp->IsA<FStructProperty>())
{
void* SubSrcPtr = StructProp->ContainerPtrToValuePtr<void>(InSrcPtr);
void* SubValPtr = StructProp->ContainerPtrToValuePtr<void>(InValuePtr);
RecursivelyFindPropertyValue(StructProp, StructProp, SubSrcPtr, SubValPtr);
}
}
}
}
else if (InSrcProperty->IsA<FFloatProperty>())
{
FFloatProperty::TCppType SourceValue = CastField<FFloatProperty>(InSrcProperty)->GetPropertyValue(InSrcPtr);
FFloatProperty::TCppType TargetValue
= InValueProperty->IsA<FDoubleProperty>()
? CastField<FDoubleProperty>(InValueProperty)->GetPropertyValue(InValuePtr)
: CastField<FFloatProperty>(InValueProperty)->GetPropertyValue(InValuePtr);

FPropertyValuePack<FFloatProperty> PropertyValuePack
{
.ContainerObject = Object,
.SourceProperty = InSrcProperty,
.SourceValue = SourceValue,
.TargetValue = TargetValue,
.SourceValuePtr = (FFloatProperty::TCppType*)InSrcPtr
};
FloatPropertyList.Add(PropertyValuePack);
}
else if (InSrcProperty->IsA<FDoubleProperty>())
{
ConvertAndAddPropertyValuePack(CastField<FDoubleProperty>(InSrcProperty), CastField<FDoubleProperty>(InValueProperty), InSrcPtr, InValuePtr);
}
else if (InSrcProperty->IsA<FUInt64Property>())
{
ConvertAndAddPropertyValuePack(CastField<FUInt64Property>(InSrcProperty), CastField<FUInt64Property>(InValueProperty), InSrcPtr, InValuePtr);
}
else if (InSrcProperty->IsA<FUInt32Property>())
{
ConvertAndAddPropertyValuePack(CastField<FUInt32Property>(InSrcProperty), CastField<FUInt32Property>(InValueProperty), InSrcPtr, InValuePtr);
}
else if (InSrcProperty->IsA<FUInt16Property>())
{
ConvertAndAddPropertyValuePack(CastField<FUInt16Property>(InSrcProperty), CastField<FUInt16Property>(InValueProperty), InSrcPtr, InValuePtr);
}
else if (InSrcProperty->IsA<FInt64Property>())
{
ConvertAndAddPropertyValuePack(CastField<FInt64Property>(InSrcProperty), CastField<FInt64Property>(InValueProperty), InSrcPtr, InValuePtr);
}
else if (InSrcProperty->IsA<FIntProperty>())
{
ConvertAndAddPropertyValuePack(CastField<FIntProperty>(InSrcProperty), CastField<FIntProperty>(InValueProperty), InSrcPtr, InValuePtr);
}
else if (InSrcProperty->IsA<FInt16Property>())
{
ConvertAndAddPropertyValuePack(CastField<FInt16Property>(InSrcProperty), CastField<FInt16Property>(InValueProperty), InSrcPtr, InValuePtr);
}
else if (InSrcProperty->IsA<FInt8Property>())
{
ConvertAndAddPropertyValuePack(CastField<FInt8Property>(InSrcProperty), CastField<FInt8Property>(InValueProperty), InSrcPtr, InValuePtr);
}
else if (InSrcProperty->IsA<FByteProperty>())
{
ConvertAndAddPropertyValuePack(CastField<FByteProperty>(InSrcProperty), CastField<FByteProperty>(InValueProperty), InSrcPtr, InValuePtr);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Cannot cast propert %s to numeric or struct property tye."), *InSrcProperty->GetName());
}
}

Overall it's very easy to understand. All we need is to take care of two special cases.

  • If the current visited property is a struct, we need to iterate through all its inner properties and recursively call this function.
  • If the source property is float, we must check the type of the target property (float or double) and cast the value accordingly. Then we create a FPropertyValuePack<FFloatProperty> value and add it to the list.

For all other types, we simply call the ConvertAndAddPropertyValuePack function to create a new FPropertyValuePack instance and add it to the correct container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename PropertyType>
void ConvertAndAddPropertyValuePack(PropertyType* InSrcProperty, PropertyType* InValueProperty, void* InSrcPtr, void* InValuePtr)
{
typename PropertyType::TCppType SourceValue = InSrcProperty->GetPropertyValue(InSrcPtr);
typename PropertyType::TCppType TargetValue = InValueProperty->GetPropertyValue(InValuePtr);
FPropertyValuePack<PropertyType> PropertyValuePack
{
.ContainerObject = Object,
.SourceProperty = InSrcProperty,
.SourceValue = SourceValue,
.TargetValue = TargetValue,
.SourceValuePtr = (typename PropertyType::TCppType*)InSrcPtr
};
GetSpecificPropertyList(InSrcProperty).Add(PropertyValuePack);
}

The GetSpecificPropertyList function returns the correct container of a given type (deduced from the input argument):

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
template <typename PropertyType>
auto&& GetSpecificPropertyList(PropertyType* Property)
{
if constexpr (std::is_same_v<PropertyType, FFloatProperty>)
{
return FloatPropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FDoubleProperty>)
{
return DoublePropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FUInt64Property>)
{
return UInt64PropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FUInt32Property>)
{
return UInt32PropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FUInt16Property>)
{
return UInt16PropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FInt64Property>)
{
return Int64PropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FIntProperty>)
{
return IntPropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FInt16Property>)
{
return Int16PropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FInt8Property>)
{
return Int8PropertyList;
}
else if constexpr (std::is_same_v<PropertyType, FByteProperty>)
{
return BytePropertyList;
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Cannot find matching numeric or struct property type for property %s."), *Property->GetName());
return nullptr;
}
}

Step 3: Blending at each frame.

The last step is to implement function UpdateAllPropertyValues, which is called at each frame to blend on each item in each container.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void UpdateAllPropertyValues(const float& Progress, const float& Exp, const TEnumAsByte<EEasingFunc::Type>& Func)
{
for (auto& PropertyValue : FloatPropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : DoublePropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : UInt64PropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : UInt32PropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : UInt16PropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : Int64PropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : IntPropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : Int16PropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : Int8PropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
for (auto& PropertyValue : BytePropertyList)
{
PropertyValue.UpdateValue(Progress, Exp, Func);
}
}

We're all done!

Example

One use case of this function is that you want the camera to zoom in or out when the player enters some areas. Note that we are not changing to a new camera. It's still the same camera in use with blending property values.

But hold on... Why don't you just define a function somewhere which implements custom blending and call that function whenever you want?

Conclusion

DO NOT use this heavily in your project. The runtime reflection can be very time-consuming and should NEVER be extensively used in production. The aim of this post is to teach the audience how to define a flexible async latent action for blending any type. If you refer to use it at runtime, you shoud do sufficient optimization and profiling to make sure everything is under budget.

Code is available at SetPropertyLatentAction.h and SetPropertyByName.h.