创建异步节点

前言

如图所示,我们经常在UE4内看到如此的异步节点,简单说此类节点的输出并非像函数一样瞬间完成,而是拥有自己的生命周期,此类节点一般在右上角有一个时钟标志

本文讲解如何制作类似AI_MoveTo的异步节点

另一篇AI_MoveTo简单分析和扩展介绍AI_MoveTo的简单运行机制和其他扩展方式


  • 2020.11.24更新:

更新各类移动扩展函数库以及K2Node

github

image-20201124134801666

  • 2021.2.25更新:

基于UBlueprintAsyncActionBase的异步节点 跳转

UK2Node_AIMoveTo

我们先看一下这个节点的所有申明

1
2
3
4
5
6
7
8
9
10
UCLASS()
class AIGRAPH_API UK2Node_AIMoveTo : public UK2Node_BaseAsyncTask//继承类
{
GENERATED_UCLASS_BODY() //加入UCLASS生成默认构造函数

virtual FText GetTooltipText() const override; //鼠标移动到节点的说明
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;//节点的名字
virtual FText GetMenuCategory() const override;//分类

};

短短没几行代码,主要内容在构造函数的定义

1
2
3
4
5
6
7
UK2Node_AIMoveTo::UK2Node_AIMoveTo(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
ProxyFactoryFunctionName = GET_FUNCTION_NAME_CHECKED(UAIBlueprintHelperLibrary, CreateMoveToProxyObject);
ProxyFactoryClass = UAIBlueprintHelperLibrary::StaticClass();
ProxyClass = UAIAsyncTaskBlueprintProxy::StaticClass();
}
  1. ProxyFactoryFunctionName = GET_FUNCTION_NAME_CHECKED(UAIBlueprintHelperLibrary, CreateMoveToProxyObject);
    1. 参数1:调用函数的类
    2. 参数2:调用的函数
  2. ProxyFactoryClass:工厂类,一般就是上述参数1的类
  3. ProxyClass:代理类,用来执行MoveTo操作并且监听派发的UObject

代理类

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
class AAIController;

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOAISimpleDelegate, EPathFollowingResult::Type, MovementResult);

UCLASS(MinimalAPI)
class UAIAsyncTaskBlueprintProxy : public UObject
{
GENERATED_UCLASS_BODY()

UPROPERTY(BlueprintAssignable)
FOAISimpleDelegate OnSuccess;

UPROPERTY(BlueprintAssignable)
FOAISimpleDelegate OnFail;

public:
UFUNCTION()
void OnMoveCompleted(FAIRequestID RequestID, EPathFollowingResult::Type MovementResult);

void OnNoPath();
void OnAtGoal();

//~ Begin UObject Interface
virtual void BeginDestroy() override;
//~ End UObject Interface

TWeakObjectPtr<AAIController> AIController;
FAIRequestID MoveRequestId;
TWeakObjectPtr<UWorld> MyWorld;

FTimerHandle TimerHandle_OnInstantFinish;
};

代理类的代码也不多,实际上最关键的就是几个多播代理,异步节点的扩展节点就是由这几个多播代理决定的,代理类里什么时候广播就决定异步节点的运行了;

大概看明白了以后我们自己来扩展一个高级版的AIMoveTo

UK2Node

代理类

参考AI_MoveTo,我们制作一个输入一个路径点数组,完成逐步沿着各个点移动的节点

1
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMoveResult, EPathFollowingResult::Type, MovementResult);

先定义一个多播代理,用来申明我们自己的代理变量

1
2
3
4
5
6
UPROPERTY(BlueprintAssignable,BlueprintCallable)
FMoveResult OnFail;
UPROPERTY(BlueprintAssignable, BlueprintCallable)
FMoveResult OnFinished;
UPROPERTY(BlueprintAssignable, BlueprintCallable)
FMoveResult OnOneStep;

直接来3个代理变量,意味着我们这个异步节点会有3个扩展节点

1
2
3
4
5
6
UFUNCTION(BlueprintCallable)
void MoveTo(APawn* Pawn, const TArray<FVector>& Path, float AcceptanceRadius = 5.0, bool StopOnOverlap = false);
UFUNCTION()
void MoveEnd(EPathFollowingResult::Type result);
UFUNCTION()
void MoveSuccess(EPathFollowingResult::Type result);

再来3个函数,第一个函数让函数库调用,后两个用来内部控制移动的逻辑

1
2
3
4
5
6
7
8
9
10
UPROPERTY()
TArray<FVector> CurPath;
UPROPERTY()
APawn* CurPawn;
UPROPERTY()
float fAcceptanceRadius;
UPROPERTY()
bool bStopOnOverlap;

bool bIsInit = false;

主要用CurPath这个数组,记录实时路径点

直接贴代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void UAIMoveToByPathProxy::MoveTo(APawn* Pawn, const TArray<FVector>& Path, float AcceptanceRadius /*= 5.0*/, bool StopOnOverlap /*= false*/)
{
if (Path.Num()<=0)
{
return;
}
if (!bIsInit)
{
bIsInit = true;
CurPath = Path;
CurPawn = Pawn;
fAcceptanceRadius = AcceptanceRadius;
bStopOnOverlap = StopOnOverlap;

}
UAIAsyncTaskBlueprintProxy* p=UAIBlueprintHelperLibrary::CreateMoveToProxyObject(Pawn, Pawn, Path[0], nullptr, AcceptanceRadius, StopOnOverlap);
p->OnFail.AddDynamic(this, &UAIMoveToByPathProxy::MoveEnd);
p->OnSuccess.AddDynamic(this, &UAIMoveToByPathProxy::MoveSuccess);
}
1
2
3
4
5
void UAIMoveToByPathProxy::MoveEnd(EPathFollowingResult::Type result)
{
//UKismetSystemLibrary::PrintString(CurPawn, TEXT("End!"));
OnFail.Broadcast(result);
}
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
void UAIMoveToByPathProxy::MoveSuccess(EPathFollowingResult::Type result)
{
if (CurPath.Num()<=0)
{
OnOneStep.Broadcast(result);
OnFinished.Broadcast(result);
//UKismetSystemLibrary::PrintString(CurPawn, TEXT("Finish!"));
}
else
{
OnOneStep.Broadcast(result);
if (CurPath.Num() > 1)
{
CurPath.RemoveAt(0);
MoveTo(CurPawn, CurPath, fAcceptanceRadius, bStopOnOverlap);

}
else
{
MoveTo(CurPawn, CurPath, fAcceptanceRadius, bStopOnOverlap);
CurPath.RemoveAt(0);
}

//UKismetSystemLibrary::PrintString(CurPawn, TEXT("OneSTEP!"));
}
}

此类移动一次成功后就会派发OneStep,移动完成和失败分别广播对应事件

1
2
3
4
5
6
UAIMoveToByPathProxy* UBPLib_MoveTo::V_AI_MoveToByPath(APawn* Pawn, const TArray<FVector>& Path, float AcceptanceRadius /*= 5.0*/, bool StopOnOverlap /*= false*/)
{
UAIMoveToByPathProxy* p = NewObject<UAIMoveToByPathProxy>(Pawn);
p->MoveTo(Pawn, Path, AcceptanceRadius, StopOnOverlap);
return p;
}

K2Node

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

UUK2Node_MoveByPath::UUK2Node_MoveByPath(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
ProxyFactoryFunctionName = GET_FUNCTION_NAME_CHECKED(UBPLib_MoveTo, V_AI_MoveToByPath);
ProxyFactoryClass = UBPLib_MoveTo::StaticClass();
ProxyClass = UAIMoveToByPathProxy::StaticClass();
}

FText UUK2Node_MoveByPath::GetTooltipText() const
{

return FText::FromString(TEXT("move by a path"));
}

FText UUK2Node_MoveByPath::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
return FText::FromString(TEXT("MoveByPath"));
}

FText UUK2Node_MoveByPath::GetMenuCategory() const
{
return FText::FromString(TEXT("MoveByPath"));
}

最终效果

UBlueprintAsyncActionBase

用法比K2Node的简单一些, 但是一般是建立在能获取多播代理的情况下,如下类是监听tag的改变

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
//.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnStatChanged, FGameplayTag, NewStatTag, bool, bIsAdd);

UCLASS()
class INTERACTIONEXTRA_API UListenForStatChanged : public UBlueprintAsyncActionBase
{
GENERATED_BODY()

public:
UPROPERTY(BlueprintAssignable)
FOnStatChanged OnStatChanged;

// 监听状态标签改变
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UListenForStatChanged* ListenForStatChanged(AInteractableActorBase* Actor);


UFUNCTION(BlueprintCallable)
void EndTask();

AInteractableActorBase* ListenActor;

protected:

void TagChanged(const FGameplayTag& Tag, bool bIsAdd);
};
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
//.cpp
void UListenForStatChanged::EndTask()
{
if (ListenActor)
{
ListenActor->GetStatChangedDelegate().RemoveAll(this);

}
SetReadyToDestroy();
MarkPendingKill();
}



UListenForStatChanged* UListenForStatChanged::ListenForStatChanged(AInteractableActorBase* Actor)
{
if (!Actor)
{
return nullptr;
}
UListenForStatChanged* task = NewObject<UListenForStatChanged>();
task->ListenActor = Actor;
Actor->GetStatChangedDelegate().AddUObject(task, &UListenForStatChanged::TagChanged);


return task;
}

void UListenForStatChanged::TagChanged(const FGameplayTag& Tag, bool bIsAdd)
{
OnStatChanged.Broadcast(Tag, bIsAdd);
}

然后我们被监听的类内需要实现如下内容

1
2
3
4
5
6
7

DECLARE_MULTICAST_DELEGATE_TwoParams(FOnTagChanged, const FGameplayTag&, bool);
virtual FOnTagChanged& GetStatChangedDelegate()
{
return OnStatChanged;
};
FOnTagChanged OnStatChanged;

主要是这个代理需要此类在合适的时机去广播, 比如下面方法内

1
2
3
4
5
void AInteractableActorBase::AddStat_Implementation(FGameplayTag StatTag)
{
StatTags.AddTag(StatTag);
OnStatChanged.Broadcast(StatTag, true);
}