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:
Defining a UFUNCTION with meta specifiers
Latent and LatenInfo, e.g., Delay
and Delay Until Next Tick.
Inheriting the UBlueprintAsyncActionBase class, e.g.,
Move Component To.
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)) staticvoidSetPropertyByName(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.
// Recursively find nested properties and store them in some container RecursivelyFindPropertyValue(InSrcProperty, InValueProperty, InSrcPtr, InValuePtr); }
voidRecursivelyFindPropertyValue(FProperty* InSrcProperty, FProperty* InValueProperty, void* InSrcPtr, void* InValuePtr) { // How to define this function? }
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); } returnGetNestedPropertyFromObjectStruct(Object, Object->GetClass(), PropertyName.ToString()); }
The GetNestedPropertyFromObjectStruct is defined as
follows.
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.")); } elseif (PropAsStruct != nullptr) { returnGetNestedPropertyFromObjectStruct(Value, PropAsStruct->Struct, NextProperty); } elseif (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); returnGetNestedPropertyFromObjectStruct(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)); }
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.
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.
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.
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.
Action = newFESetPropertyLatentAction(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:
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.
We use the GetNestedPropertyFromObject function to get
the source property pointer and the corresponding value pointer.
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.
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.
Otherwise, we create a new FEsetPropertyLatentAction
and add it to the list. We also add a callback for when it gets complete
or interrupted.
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
classFESetPropertyLatentAction : public FPendingLatentAction { // Member variables as before // ... // Constructor as before // ... // UpdateOperation(FLatentResponse& Response) as before // ...
// 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.
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.
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.