37. UE5 RPG创建自定义的Ability Task

在前面的文章中,我们实现了一个火球术的一些基本功能,火球术技能的释放,在技能释放后,播放释放动画,在动画播放到需要释放火球术的位置时,将触发动画通知,在动画通知中触发标签事件,然后再技能中监听事件完成火球术的创建。接下来,我们将继续优化火球术技能,并研究点新的东西。

在上一篇文章中,我们使用了PlayMontageAndWait节点实现蒙太奇的播放,这个节点实现是基于Ability Task(AT)实现的,它的主要特点是可以在技能蓝图中使用异步,我们要通过Ability Task类实现一个存储鼠标点击位置信息的Task,并且可以实现将数据传递到服务器,实现服务器同步播放动画。

创建TargetDataUnderMouse

我们首先实现一个AbilityTask(AT),用于保存触发技能时鼠标拾取的数据。

将类名称设置为TargetDataUnderMouse

首先在类里面增加一个静态函数,这个函数用来创建类的实例,也就是我们将节点添加到蓝图,它就会创建一个实例。参数配置我在上一片文章使用播放蒙太奇的节点时也介绍了一下,这里在解释一下。

DisplayName 为在蓝图搜索时,可以直接搜索这个名称,节点上也会显示这个名称。

HidePin 是隐藏一个参数的引脚,设置以后,在蓝图中无法设置它的属性。

DefaultToSelf 将类或者蓝图实例作为默认参数 这两项一起使用,我们就不需要设置OwningAbility的值了,默认设置了蓝图实例。

BlueprintInternalUseOnly 设置了此函数只能在蓝图中使用。

cpp 复制代码
	UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta=(DisplayName = "TargetDataUnderMouse", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true"))
	static UTargetDataUnderMouse* CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility);

这个静态函数的视线,我们就直接创建一个实例返回

cpp 复制代码
UTargetDataUnderMouse* UTargetDataUnderMouse::CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility)
{
	UTargetDataUnderMouse* MyObj = NewAbilityTask<UTargetDataUnderMouse>(OwningAbility);
	return MyObj;
}

这就是一个最简单的AbilityTask(AT)实现,编译后,我们在技能蓝图中搜索名称,效果如下,右上角的时钟图标代表它是一个异步节点,这是一个没有任何功能的AT,只实现创建,没有其它功能。

接着,在类里面添加一个委托,看看效果,首先增加一个委托宏,返回一个向量

cpp 复制代码
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMouseTargetDataSignature, const FVector&, Data);

接着定义一个变量 ,作为蓝图可调用的委托

cpp 复制代码
	UPROPERTY(BlueprintAssignable)
	FMouseTargetDataSignature ValidData;

运行打开,会发现右侧多了两个引脚,一个是回调广播后可以执行引脚,另一个则是数据引脚。

接下来,我们要实现这个委托的广播,并获取到鼠标拾取点位的位置传递给技能实例。为了实现这个功能,我们将覆盖默认的执行函数,这函数将在触发左键引脚时,执行内部的内容。

cpp 复制代码
private:

	virtual void Activate() override;

在实现这里,你不需要调用它的父调用,因为它在父类里面只做了打印,没有执行其它逻辑,但是你如果需要调式的时候可以调用。

在函数中,我们首先要获取到它的PlayerController,因为在PC上面可以去拾取点击位置的坐标,然后使用PC上面的坐标拾取函数去获取结果,然后直接广播出去坐标。

cpp 复制代码
void UTargetDataUnderMouse::Activate()
{
	APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
	FHitResult CursorHit;
	PC->GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
	ValidData.Broadcast(CursorHit.Location);
}

编译,打开UE,修改技能蓝图,在委托回调里接收到广播以后,绘制调试球体,然后结束技能

会发现每点击一次,敌人身上生成一个调试模型。

那么重点来了,如果我们开启两个客户端,一个运行在服务器端,另一个运行在单独客户端

你会发现,在客户端自己的位置是正确的,但是服务器端显示效果位置不正确,这是因为当前广播的数据没有上传到服务器,所以出现了数据在原点显示的bug,接下来,我们将实现从客户端将数据上传到服务器,解决这个bug。

使用TargetData传递数据到服务器

首先我们分析一下,为什么出现这个问题,在服务器端的位置为0,在技能被激活后,触发我们制作的Task实例,然后在内部进行鼠标拾取位置,并广播出来。需要同步的数据是在AT激活和返回的拾取坐标,由于是需要客户端提交到服务器端,有网络的延迟,你无法确定哪个数据先被提交成功,比如当前问题就是AT激活时,坐标位置还没有提交到服务器,所以在服务器向后运行时是没有坐标值的。

好在,GAS系统里面想到了这一点,它内置一套TargetData的系统(FGameplayAbilityTargetData)帮助我们实现从客户端提交数据并实现了异步等待数据提交完成再向后执行的逻辑。通过ServerSetReplicatedTargetData()函数将数据上传到服务器,并在服务器生成FAbilityTargetDataSetDelegate委托,并将数据广播出去,在技能里面,通过在AbilityTargetDataMap(Key是技能实例,value是TargetData)获取到实际数据,来运行后续的逻辑。

在服务器端,我们首先绑定TargetSet的委托,这样,如果激活逻辑先上传到服务器,我们可以通过委托的广播来获取数据。如果是数据先到的服务器,在激活时无法获取到委托的广播,我们可以使用CallReplicatedTargetDataDelegateIfSet()函数获取。

双击Shift键,在文件GameplayAbilityTargetTypes.h中,我们可以看到内置TargetData给我们定义了多个格式,每个格式传递的内容也不相同。

UE的GAS为我们派生三种类型:

  1. FGameplayAbilityTargetData_LocationInfo 这个类用于表示基于位置的目标数据。例如,一个技能可能需要在某个特定的地点释放效果,而不是针对某个特定的角色。FGameplayAbilityTargetData_LocationInfo 包含了这样的位置信息,如世界坐标或其他与位置相关的数据。
  2. FGameplayAbilityTargetData_ActorArray 这个类用于表示基于一组角色的目标数据。如果一个技能需要影响多个角色,那么可以使用这个类。它包含了一个角色数组(通常是 AActor 或其派生类的实例),这样技能就可以对数组中的每个角色应用效果。
  3. FGameplayAbilityTargetData_SingleTargetHit 这个类用于表示单个角色作为目标的数据。当一个技能只影响一个角色时,可以使用这个类。它通常包含了关于这个单一目标的信息,比如该角色的位置、健康状态或其他相关数据。

下面,我就实现客户端的数据提交并实现服务器端的数据接收并处理。

增加一个私有函数,用于内部用于实现数据从客户端提交到服务器端

cpp 复制代码
	//客户端向服务器端提交数据
	void SendMouseCursorData();

在函数内顶部,我们添加一个预测窗口

cpp 复制代码
	//创建一个预测窗口,该窗口允许客户端在不确定服务器响应的情况下,对游戏状态进行预测性更新。
	FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent.Get(), true);

接下来,我们需要创建一个提交的TargetData,这里选择讲一个单一的目标上传,所以创建一个FGameplayAbilityTargetData_SingleTargetHit变量,并将我们从鼠标拾取的结果设置给Data

cpp 复制代码
	//获取鼠标拾取结果
	APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
	FHitResult CursorHit;
	PC->GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);

	//创建需要上传服务器端的TargetData
	FGameplayAbilityTargetData_SingleTargetHit* Data = new FGameplayAbilityTargetData_SingleTargetHit();
	Data->HitResult = CursorHit;

将TargetData上传至服务器端需要它的句柄,我们将创建一个FGameplayAbilityTargetDataHandle 用于存储Data

cpp 复制代码
	//创建TargetData句柄,上传到服务器端需要上传句柄
	FGameplayAbilityTargetDataHandle DataHandle;
	DataHandle.Add(Data);

接着调用上传函数ASC的ServerSetReplicatedTargetData

cpp 复制代码
	//将TargetData上传至服务器端
	AbilitySystemComponent->ServerSetReplicatedTargetData(
		GetAbilitySpecHandle(),
		GetActivationPredictionKey(),
		DataHandle,
		FGameplayTag(),
		AbilitySystemComponent->ScopedPredictionKey);

由于上传的是DataHandle,我们将委托宏的值也修改为DataHandle

cpp 复制代码
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMouseTargetDataSignature, const FGameplayAbilityTargetDataHandle&, DataHandle);

判断当前是否可以触发委托函数,这个触发需要在服务器端通过验证后触发,由于网络限制,这时候都没有执行完成

cpp 复制代码
	//判断服务器端是否通过验证
	if(ShouldBroadcastAbilityTaskDelegates())
	{
		ValidData.Broadcast(DataHandle);
	}

上面,我们实现在客户端数据的发送,接下来,我们将修改Task的Activate()函数,在函数内,我们首先判断当前task是否由本地玩家控制

cpp 复制代码
	//是否由客户端控制
	const bool bIsLocallyControlled = Ability->GetCurrentActorInfo()->IsLocallyControlled();

如果是由客户端控制,那么我们将执行数据提交服务器端的逻辑。

cpp 复制代码
	if(bIsLocallyControlled)
	{
		//如果是客户端控制器控制,实现将数据发射到服务器端
		SendMouseCursorData();
	}

如果不是本地控制,那肯定当前是在服务器端运行,我们在服务器端监听数据提交成功。

首先创建两个值,用于实现委托SpecHandle 为当前技能的标示,ActivationPredictionKey 为预测键,用于同步客户端和服务器之间的预测性操作

cpp 复制代码
const FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle();  
const FPredictionKey ActivationPredictionKey = GetActivationPredictionKey();

接着设置委托,当服务器端接收到目标数据时,这个回调函数会被触发。

cpp 复制代码
AbilitySystemComponent.Get()->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).AddUObject(this, &UTargetDataUnderMouse::OnTargetDataReplicatedCallback);

通过调用CallReplicatedTargetDataDelegatesIfSet函数来检查是否已经为特定的SpecHandle和ActivationPredictionKey调用了委托。如果已经调用过,bCalledDelegate将为true。

cpp 复制代码
		//判断在服务器端,上面的委托是否已经广播过
		const bool bCalledDelegate = AbilitySystemComponent.Get()->CallReplicatedTargetDataDelegatesIfSet(SpecHandle, ActivationPredictionKey);

如果当前委托还未广播,我们将调用SetWaitingOnRemotePlayerData()让服务器端正在等待客户端上传目标数据。

cpp 复制代码
		if(!bCalledDelegate)
		{
			//设置服务器端等待PlayerData数据的上传
			SetWaitingOnRemotePlayerData();
		}

接下来就是委托函数的视线,我们先定义一个函数,委托会返回两个值,数据的句柄和激活的标签

cpp 复制代码
	//当数据提交到服务器端后的委托回调
	void OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& DataHandle, FGameplayTag ActivationTag);

在回调函数中,我们首先将数据应用到本地客户端,并将缓存的数据清除掉,如果数据通过了验证,则广播数据。

cpp 复制代码
void UTargetDataUnderMouse::OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& DataHandle, FGameplayTag ActivationTag)
{
	//通知客户端 服务器端已经接收并处理了从客户端复制的目标数据(将服务器的TargetData应用到客户端,并清除掉缓存)
	AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey());
	//判断服务器端是否通过验证
	if(ShouldBroadcastAbilityTaskDelegates())
	{
		ValidData.Broadcast(DataHandle);
	}
}

还有重要的一项,如果我们要使用TargetData,UE默认是不开启此功能的,我们需要代码开启,在资源管理器StartInitialLoading()函数中开启。

cpp 复制代码
void UMyAssetManager::StartInitialLoading()
{
	Super::StartInitialLoading();

	FMyGameplayTags::InitializeNativeGameplayTags();

	//如果使用TargetData,必须开启此项
	UAbilitySystemGlobals::Get().InitGlobalData();
}

接着,我们打开技能蓝图,修改蓝图,由于Task返回的内容在代码里面修改掉了,现在返回的是TargetDataHandle了,我们通过Get Hit Result from Target Data 节点来获取设置的数据,从里面找到目标的位置数据绘制调试球体。

修改一下网络模式,并设置一下接口,以监听服务器运行,主窗口将为服务器端,其它窗口为客户端

然后在客户端上面点击哥布林,发现服务器可以获取到相关的数据了。

接下来,我把制作的AT的代码放在下面

TargetDataUnderMouse.h

cpp 复制代码
// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "TargetDataUnderMouse.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMouseTargetDataSignature, const FGameplayAbilityTargetDataHandle&, DataHandle);

/**
 * 
 */
UCLASS()
class AURA_API UTargetDataUnderMouse : public UAbilityTask
{
	GENERATED_BODY()

public:

	UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta=(DisplayName = "TargetDataUnderMouse", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true"))
	static UTargetDataUnderMouse* CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility);

	UPROPERTY(BlueprintAssignable)
	FMouseTargetDataSignature ValidData;

private:

	virtual void Activate() override;

	//客户端向服务器端提交数据
	void SendMouseCursorData();

	//当数据提交到服务器端后的委托回调
	void OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& DataHandle, FGameplayTag ActivationTag);
};

TargetDataUnderMouse.cpp

cpp 复制代码
// 版权归暮志未晚所有。


#include "AbilitySystem/AbilityTasks/TargetDataUnderMouse.h"

#include "AbilitySystemComponent.h"

UTargetDataUnderMouse* UTargetDataUnderMouse::CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility)
{
	UTargetDataUnderMouse* MyObj = NewAbilityTask<UTargetDataUnderMouse>(OwningAbility);
	return MyObj;
}

void UTargetDataUnderMouse::Activate()
{
	//是否由客户端控制
	const bool bIsLocallyControlled = Ability->GetCurrentActorInfo()->IsLocallyControlled();
	if(bIsLocallyControlled)
	{
		//如果是客户端控制器控制,实现将数据发射到服务器端
		SendMouseCursorData();
	}
	else
	{
		const FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle();
		const FPredictionKey ActivationPredictionKey = GetActivationPredictionKey();
		AbilitySystemComponent.Get()->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).AddUObject(this, &UTargetDataUnderMouse::OnTargetDataReplicatedCallback);

		//判断在服务器端,上面的委托是否已经广播过
		const bool bCalledDelegate = AbilitySystemComponent.Get()->CallReplicatedTargetDataDelegatesIfSet(SpecHandle, ActivationPredictionKey);
		if(!bCalledDelegate)
		{
			//设置服务器端等待PlayerData数据的上传
			SetWaitingOnRemotePlayerData();
		}
	}
}

void UTargetDataUnderMouse::SendMouseCursorData()
{
	//创建一个预测窗口,该窗口允许客户端在不确定服务器响应的情况下,对游戏状态进行预测性更新。
	FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent.Get(), true);
	
	//获取鼠标拾取结果
	APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
	FHitResult CursorHit;
	PC->GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);

	//创建需要上传服务器端的TargetData
	FGameplayAbilityTargetData_SingleTargetHit* Data = new FGameplayAbilityTargetData_SingleTargetHit();
	Data->HitResult = CursorHit;

	//创建TargetData句柄,上传到服务器端需要上传句柄
	FGameplayAbilityTargetDataHandle DataHandle;
	DataHandle.Add(Data);

	//将TargetData上传至服务器端
	AbilitySystemComponent->ServerSetReplicatedTargetData(
		GetAbilitySpecHandle(),
		GetActivationPredictionKey(),
		DataHandle,
		FGameplayTag(),
		AbilitySystemComponent->ScopedPredictionKey);

	//判断服务器端是否通过验证
	if(ShouldBroadcastAbilityTaskDelegates())
	{
		ValidData.Broadcast(DataHandle);
	}
}

void UTargetDataUnderMouse::OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& DataHandle, FGameplayTag ActivationTag)
{
	//通知客户端 服务器端已经接收并处理了从客户端复制的目标数据(将服务器的TargetData应用到客户端,并清除掉缓存)
	AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey());
	//判断服务器端是否通过验证
	if(ShouldBroadcastAbilityTaskDelegates())
	{
		ValidData.Broadcast(DataHandle);
	}
}
相关推荐
ue星空3 小时前
UE5材质系统之PBR材质
ue5·材质
NoneCoder10 小时前
CSS系列(26)-- 动画性能优化详解
前端·css·性能优化
苏三说技术14 小时前
Redis 性能优化的18招
数据库·redis·性能优化
程序猿会指北16 小时前
【鸿蒙(HarmonyOS)性能优化指南】内存分析器Allocation Profiler
性能优化·移动开发·harmonyos·openharmony·arkui·组件化·鸿蒙开发
程序猿会指北19 小时前
【鸿蒙(HarmonyOS)性能优化指南】启动分析工具Launch Profiler
c++·性能优化·harmonyos·openharmony·arkui·启动优化·鸿蒙开发
咖肥猫1 天前
【ue5学习笔记2】在场景放入一个物体的蓝图输入事件无效?
笔记·学习·ue5
彭亚川Allen1 天前
优化了2年的性能,没想到最后被数据库连接池坑了一把
数据库·后端·性能优化
MClink1 天前
Go怎么做性能优化工具篇之pprof
开发语言·性能优化·golang
京东零售技术1 天前
Taro小程序开发性能优化实践
性能优化·taro
理想不理想v2 天前
wepack如何进行性能优化
性能优化