-
Notifications
You must be signed in to change notification settings - Fork 67
Latent actions
Latent actions can be used to define functions exposed to Blueprint which take an arbitrary amount of time to complete without having to define callback functions in Blueprint.
Latent actions were designed to be created in C++ so some classes are manually wrapped to support overriding important functions.
USharp supports 4 versions of latent actions provided by UE4:
- Latent functions (FLatentActionInfo)
- UGameplayTask
- UBlueprintAsyncActionBase
- UOnlineBlueprintCallProxyBase
This type of latent action can define multiple output execution pins, and keep invoking the action until the desired execution path is reached. One drawback of latent functions is that they require a function to be called every tick until the action is complete.
Note: There is currently a bug which requires latent functions to have an additional parameter (aside from FLatentActionInfo
). It will crash without an additional parameter. This crash happens with both C++ and C#.
One example of a C++ defined latent function is UKismetSystemLibrary::Delay (known simply as Delay to Blueprint). The following is a C# re-implementation of that latent function which has been slightly modified. The delay will have 3 output execution pins and execute 3 times (if the last exec pin is connected to the input to create a loop, this will run forever).
class FCustomDelay : FUSharpLatentAction
{
FLatentActionInfo latent;
FWeakObjectPtr callbackTarget;
public float TimeRemaining;
public ECustomDelayCycle Cycle;
public FCustomDelay(float duration, FLatentActionInfo latentAction)
{
TimeRemaining = duration;
Cycle = ECustomDelayCycle.First;
latent = latentAction;
callbackTarget = new FWeakObjectPtr(latent.CallbackTargetAddress);
}
// This is called every tick
public override void UpdateOperation(FLatentResponse response)
{
TimeRemaining -= response.ElapsedTime();
if (Cycle == ECustomDelayCycle.Final)
{
response.FinishAndTriggerIf(TimeRemaining <= 0.0f, latent.ExecutionFunction, latent.Linkage, callbackTarget);
}
else if (TimeRemaining <= 0.0f)
{
response.TriggerLink(latent.ExecutionFunction, latent.Linkage, callbackTarget);
}
}
// This will be displayed above the node in Blueprint whilst the action is running
public override string GetDescription()
{
return "Delay (" + TimeRemaining.ToString("0.000") + " seconds left)";
}
}
[UEnum]
enum ECustomDelayCycle : byte
{
First,
Second,
Final
}
[UClass]
class AMyActor : AActor
{
// Both [Latent] and [LatentInfo] are required.
// [ExpandEnumAsExecs] allows us to define an enum to specify the output execution pins (otherwise this will default to 1 output pin)
[UFunction, BlueprintCallable, Latent, LatentInfo("latentInfo"), ExpandEnumAsExecs("cycle")]
public void MyLatentFunction(float delay, out ECustomDelayCycle cycle, FLatentActionInfo latentInfo)
{
FLatentActionManager manager = World.GetLatentActionManager();
FCustomDelay action = manager.FindExistingAction<FCustomDelay>(latentInfo.CallbackTarget, latentInfo.UUID);
if (action != null)
{
if (action.Cycle != ECustomDelayCycle.Final)
{
action.TimeRemaining = delay;
action.Cycle = (ECustomDelayCycle)(((int)action.Cycle) + 1);
}
cycle = action.Cycle;
}
else
{
manager.AddNewAction(latentInfo.CallbackTarget, latentInfo.UUID, new FCustomDelay(delay, latentInfo));
cycle = ECustomDelayCycle.First;
}
}
}
Each gameplay task is owned by an object (which may also be a task), and can have 1 child task. When a task is ended the chain of child tasks are also ended. You get a reference to your gameplay task in the Blueprint node. Gameplay tasks can optionally tick. One drawback of gameplay tasks is that UGameplayTasksComponent
must be added to your actor in order to have a context in which to start the task (and as such are mostly limited to use on actors).
The following is a simple task which waits on another task UGameplayTask_WaitDelay
.
class FSimpleTaskDelegate : FMulticastDelegate<FSimpleTaskDelegate.Signature>
{
public delegate void Signature(int value);
}
[UClass]
class UTestTask : UUSharpGameplayTask
{
[UProperty, BlueprintAssignable]
public FSimpleTaskDelegate OnFinished { get; set; }
[UFunction, BlueprintCallable, BlueprintInternalUseOnly]
public static UTestTask RunTestTask(IGameplayTaskOwnerInterface taskOwner, float duration, byte priority = 100)
{
if (taskOwner != null)
{
UTestTask result = NewObject<UTestTask>();
result.InitTask(taskOwner, priority);
UGameplayTask_WaitDelay delayTask = UGameplayTask_WaitDelay.TaskWaitDelay(result, duration);
delayTask.OnFinish.Bind(result.OnFinish);
delayTask.ReadyForActivation();
return result;
}
return null;
}
[UFunction]
void OnFinish()
{
FMessage.Log("OnFinish!");
if (OnFinished.IsBound)
{
OnFinished.Invoke(100);
}
EndTask();
}
// Delegates are known to be bound and can be invoked from this point onward.
protected override void OnActivate()
{
base.OnActivate();
}
// The task has ended (EndTask has been called)
protected override void OnDestroy(bool ownerFinished)
{
base.OnDestroy(ownerFinished);
}
}
This is more of a 'raw' operation without additional helper code to manage the lifetime of the action.
The following example shows an example of an action which in the editor appears as RunTestAsyncAction
with two output exec pins OnSuccess
/ OnFailed
. It immediately invokes both of the exec pins with the input integer value as the param.
class FTestAsyncDelegate : FMulticastDelegate<FTestAsyncDelegate.Signature>
{
public delegate void Signature(int value);
}
[UClass]
class UTestAsyncAction : UUSharpAsyncActionBase
{
[UProperty, BlueprintAssignable]
public FTestAsyncDelegate OnSuccess { get; set; }
[UProperty, BlueprintAssignable]
public FTestAsyncDelegate OnFailed { get; set; }
private int val;
[UFunction, BlueprintCallable, BlueprintInternalUseOnly]
static UTestAsyncAction RunTestAsyncAction(int value)
{
UTestAsyncAction action = NewObject<UTestAsyncAction>();
action.val = value;
return action;
}
// This function is called after the delegates have been set up by Blueprint
protected override void OnActivate()
{
base.OnActivate();
if (OnSuccess.IsBound)
{
OnSuccess.Invoke(val);
}
if (OnFailed.IsBound)
{
OnFailed.Invoke(val + 100);
}
}
// This can be used to run any code cleanup (this is called when the garbage collector runs)
protected override void OnBeginDestroy()
{
base.OnBeginDestroy();
}
}
This example shows how to combine latent actions with coroutines.
class FTestAsyncDelegate : FMulticastDelegate<FTestAsyncDelegate.Signature>
{
public delegate void Signature(int value);
}
[UClass]
class UTestAsyncAction : UUSharpAsyncActionBase
{
[UProperty, BlueprintAssignable]
public FTestAsyncDelegate OnComplete { get; set; }
// owner is passed in as we need an object with a world in order to start the coroutine
private AActor owner;
private int val;
[UFunction, BlueprintCallable, BlueprintInternalUseOnly]
static UTestAsyncAction RunTestAsyncAction(AActor owner, int value)
{
UTestAsyncAction action = NewObject<UTestAsyncAction>();
action.owner = owner;
action.val = value;
return action;
}
// This function is called after the delegates have been set up by Blueprint
protected override void OnActivate()
{
base.OnActivate();
if (owner == null || owner.IsDestroyed)
{
return;
}
Coroutine coroutine = owner.StartCoroutine(SimpleCoroutine());
//coroutine.OnComplete += CoroutineOnComplete;
}
// This could be used as an alternative for invoking the OnComplete delegate
/*void CoroutineOnComplete(Coroutine coroutine)
{
if (OnComplete.IsBound)
{
OnComplete.Invoke(val + 100);
}
}*/
System.Collections.IEnumerator SimpleCoroutine()
{
for (int i = 0; i < 5; i++)
{
yield return Coroutine.WaitForSecondsRealtime(1);
FMessage.Log("Step " + (i + 1));
}
if (OnComplete.IsBound)
{
OnComplete.Invoke(val + 100);
}
}
}
This is essentially identical to UBlueprintAsyncActionBase, but is used solely for online functions. The C# class is called UUSharpOnlineBlueprintCallProxyBase
.