Published on

创建异步节点

前言

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

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

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


  • 2020.11.24更新:

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

github

image-20201124134801666

  • 2021.2.25更新:

基于UBlueprintAsyncActionBase的异步节点 跳转

UK2Node_AIMoveTo

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

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;//分类

};

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

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

代理类

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,我们制作一个输入一个路径点数组,完成逐步沿着各个点移动的节点

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

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

	UPROPERTY(BlueprintAssignable,BlueprintCallable)
		FMoveResult OnFail;
	UPROPERTY(BlueprintAssignable, BlueprintCallable)
		FMoveResult OnFinished;
	UPROPERTY(BlueprintAssignable, BlueprintCallable)
		FMoveResult OnOneStep;

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

	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个函数,第一个函数让函数库调用,后两个用来内部控制移动的逻辑

	UPROPERTY()
	`TArray<FVector>` CurPath;
	UPROPERTY()
	APawn* CurPawn;
	UPROPERTY()
	float fAcceptanceRadius;
	UPROPERTY()
	bool bStopOnOverlap;

	bool bIsInit = false;

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

直接贴代码

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);
}
void UAIMoveToByPathProxy::MoveEnd(EPathFollowingResult::Type result)
{
	//UKismetSystemLibrary::PrintString(CurPawn, TEXT("End!"));
	OnFail.Broadcast(result);
}
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,移动完成和失败分别广播对应事件

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


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的改变

//.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);
};
//.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);
}

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


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

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

void AInteractableActorBase::AddStat_Implementation(FGameplayTag StatTag)
{
	StatTags.AddTag(StatTag);
	OnStatChanged.Broadcast(StatTag, true);
}