99. UE5 GAS RPG 被动技能实现

在这一篇,我们在之前打下的基础下,实现一下被动技能。

被动技能需要我们在技能栏上面选择升级解锁技能后,将其设置到技能栏,我们先增加被动技能使用的标签。

cpp 复制代码
	FGameplayTag Abilities_Passive_HaloOfProtection; //被动技能-守护光环
	FGameplayTag Abilities_Passive_LifeSiphon; //被动技能-生命回复
	FGameplayTag Abilities_Passive_ManaSiphon; //被动技能-蓝量回复

注册一下

cpp 复制代码
	/*
	 * 被动技能
	*/
	GameplayTags.Abilities_Passive_HaloOfProtection = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("Abilities.Passive.HaloOfProtection"),
			FString("守护光环")
			);
	GameplayTags.Abilities_Passive_LifeSiphon = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("Abilities.Passive.LifeSiphon"),
			FString("生命自动回复")
			);
	GameplayTags.Abilities_Passive_ManaSiphon = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("Abilities.Passive.ManaSiphon"),
			FString("蓝量自动回复")
			);

添加被动技能基类

我们基于技能基类创建一个派生类,用于作为被动技能的基类

命名为RPGPassiveAbility

在类里增加两个函数,一个是覆写激活技能函数,在技能被调用激活时,绑定结束回调监听,如果ASC调用了结束技能,并且此被动技能刚好有对应的标签,我们可以通过第二个技能结束此技能实力的激活。

cpp 复制代码
public:
	/**
	 * 覆写激活技能函数
	 * @param Handle 技能实力的句柄
	 * @param ActorInfo 技能拥有者
	 * @param ActivationInfo 激活信息
	 * @param TriggerEventData 游戏事件信息
	 */
	virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;

	/**
	 * 接收到技能结束回调函数
	 * @param AbilityTag 结束的技能标识标签
	 */
	void ReceiveDeactivate(const FGameplayTag& AbilityTag);

我们在ASC里增加一个新的委托定义,用来定义技能结束

cpp 复制代码
DECLARE_MULTICAST_DELEGATE_OneParam(FDeactivatePassiveAbility, const FGameplayTag& /*技能标签*/); //中止一个技能激活的回调

并在ASC类里新增一个变量

cpp 复制代码
FDeactivatePassiveAbility DeactivatePassiveAbility; //取消技能激活的委托

在被动技能基类里,激活技能时,绑定委托的监听

cpp 复制代码
void URPGPassiveAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

	//获取到ASC
	if(URPGAbilitySystemComponent* RPGASC = Cast<URPGAbilitySystemComponent>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo())))
	{
		//绑定技能取消回调
		RPGASC->DeactivatePassiveAbility.AddUObject(this, &URPGPassiveAbility::ReceiveDeactivate);
	}
}

在回调里,判断委托返回的标签是否为当前被动技能的标识,如果是,将调用结束技能

cpp 复制代码
void URPGPassiveAbility::ReceiveDeactivate(const FGameplayTag& AbilityTag)
{
	//判断技能标签容器里是否包含此标签
	if(AbilityTags.HasTagExact(AbilityTag))
	{
		EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
	}
}

创建对应被动技能蓝图

有了被动技能基类,我们在UE里创建对应的蓝图

基于之前创建的基类创建三个被动技能

在技能标识标签这里设置对应的标签

然后在DA_AbilityInfo里添加对应的技能的相关数据

接下来,我们在UI里实现设置按钮显示哪个技能标签

接着运行,查看按钮是否能够正确显示以及操作

记得在主窗口的UI上设置对应的按钮Tag

运行设置后查看,是否主界面也能够跟随变动

实现被动技能的装配时激活

技能可以实现了装配,并且,我们在技能里监听了取消事件,在触发对应回调时,技能会自动取消激活。

所以,我们在被动技能里设置调试节点,方便测试,在激活时和结束技能时都可以打印信息。

由于被动技能应用时就需要激活,所以,我们不需要预测它,直接在服务器初始化即可。

这是我们现在的ASC里装配技能时的处理逻辑,没有考虑到主动技能和被动技能的区别

接下来,我们将修改技能装配的逻辑,让其兼容对被动技能的处理。

前面的这一段还是一样,通过技能标签获取到技能实例,并获取到技能未修改前装配的槽位和状态,对技能的状态进行判断

cpp 复制代码
void URPGAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Slot)
{
	const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();

	//获取到技能实例
	if(FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
	{
		const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec); //技能之前装配的插槽
		const FGameplayTag& Status = GetStatusTagFromSpec(*AbilitySpec); //当前技能的状态标签

		//判断技能的状态,技能状态只有在已装配或者已解锁的状态才可以装配
		if(Status == GameplayTags.Abilities_Status_Equipped || Status == GameplayTags.Abilities_Status_Unlocked)
		{

接着,我们先处理目标槽位,判断目标槽位现在是否装配技能,如果装配,我们则获取到装配的技能实例,如果槽位装配的技能和我们需要装配的技能相同,则不做处理。

如果槽位的技能是被动技能,我们将通过委托结束技能(被动技能基类在激活技能时,会监听技能结束委托)

并且,我们将会清除槽位所有装配的技能(清除装配技能上设置的输入标签)

cpp 复制代码
//判断插槽是否有技能,有则需要将其清除
if(!SlotIsEmpty(Slot))
{
	//获取目标插槽现在装配的技能
	if(const FGameplayAbilitySpec* SpecWithSlot = GetSpecWithSlot(Slot))
	{
		//技能槽位装配相同的技能,直接返回,不做额外的处理
		if(AbilityTag.MatchesTagExact(GetAbilityTagFromSpec(*SpecWithSlot)))
		{
			ClientEquipAbility(AbilityTag, Status, Slot, PrevSlot);
			return;
		}

		//如果是被动技能,我们需要先将技能取消执行
		if(IsPassiveAbility(*SpecWithSlot))
		{
			DeactivatePassiveAbility.Broadcast(GetAbilityTagFromSpec(*SpecWithSlot));
		}

		ClearAbilitiesOfSlot(Slot); //清除目标插槽装配的技能
	}
}

接下来,我们对需要装配的技能判断,如果它之前没有被装配到技能槽位,并且还是被动技能,证明技能还未被激活,我们需要将技能激活。

cpp 复制代码
//技能没有设置到插槽(没有激活)
if(!AbilityHasAnySlot(*AbilitySpec))
{
	//如果是被动技能,装配即激活
	if(IsPassiveAbility(*AbilitySpec))
	{
		TryActivateAbility(AbilitySpec->Handle);
	}
}

然后修改技能的的输入标签为装配的槽位

cpp 复制代码
//修改技能装配的插槽
AssignSlotToAbility(*AbilitySpec, Slot);

最后网络同步和触发装配委托回调

cpp 复制代码
//回调更新UI
ClientEquipAbility(AbilityTag, Status, Slot, PrevSlot);
MarkAbilitySpecDirty(*AbilitySpec); //立即将其复制到每个客户端

逻辑梳理完成,下面为使用到的一些函数

cpp 复制代码
bool URPGAbilitySystemComponent::SlotIsEmpty(const FGameplayTag& Slot)
{
	FScopedAbilityListLock ActiveScopeLoc(*this);
	for(FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
	{
		if(AbilityHasSlot(AbilitySpec, Slot))
		{
			return false;
		}
	}
	return true;
}

bool URPGAbilitySystemComponent::AbilityHasSlot(const FGameplayAbilitySpec& Spec, const FGameplayTag& Slot)
{
	return Spec.DynamicAbilityTags.HasTagExact(Slot);
}

bool URPGAbilitySystemComponent::AbilityHasAnySlot(const FGameplayAbilitySpec& Spec)
{
	//通过判断动态标签是否含有Input的标签来判断技能是否装配到槽位
	return Spec.DynamicAbilityTags.HasTag(FGameplayTag::RequestGameplayTag(FName("InputTag")));
}

FGameplayAbilitySpec* URPGAbilitySystemComponent::GetSpecWithSlot(const FGameplayTag& Slot)
{
	FScopedAbilityListLock ActiveScopeLoc(*this);
	for(FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
	{
		if(AbilityHasSlot(AbilitySpec, Slot))
		{
			return &AbilitySpec;
		}
	}
	return nullptr;
}

bool URPGAbilitySystemComponent::IsPassiveAbility(const FGameplayAbilitySpec& Spec) const
{
	//从技能配置数据里获取到技能对于的配置信息
	UAbilityInfo* AbilityInfo = URPGAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
	const FGameplayTag AbilityTag = GetAbilityTagFromSpec(Spec);
	const FRPGAbilityInfo& Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
	//判断信息里配置的技能类型是否为被动技能
	const FGameplayTag AbilityType = Info.AbilityType;
	return AbilityType.MatchesTagExact(FRPGGameplayTags::Get().Abilities_Type_Passive);
}

void URPGAbilitySystemComponent::AssignSlotToAbility(FGameplayAbilitySpec& Spec, const FGameplayTag& Slot)
{
	const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
	ClearSlot(&Spec);
	Spec.DynamicAbilityTags.AddTag(Slot);
	Spec.DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Unlocked);
	Spec.DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Equipped);
}
void URPGAbilitySystemComponent::ClearSlot(FGameplayAbilitySpec* Spec)
{
	const FGameplayTag Slot = GetInputTagFromSpec(*Spec);
	Spec->DynamicAbilityTags.RemoveTag(Slot);
	// MarkAbilitySpecDirty(*Spec);
}

void URPGAbilitySystemComponent::ClearAbilitiesOfSlot(const FGameplayTag& Slot)
{
	FScopedAbilityListLock ActiveScopeLock(*this);
	for(FGameplayAbilitySpec& Spec : GetActivatableAbilities())
	{
		if(AbilityHasSlot(&Spec, Slot))
		{
			ClearSlot(&Spec);
		}
	}
}

最后展示一下实现装配的所有代码

cpp 复制代码
void URPGAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Slot)
{
	const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();

	//获取到技能实例
	if(FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
	{
		const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec); //技能之前装配的插槽
		const FGameplayTag& Status = GetStatusTagFromSpec(*AbilitySpec); //当前技能的状态标签

		//判断技能的状态,技能状态只有在已装配或者已解锁的状态才可以装配
		if(Status == GameplayTags.Abilities_Status_Equipped || Status == GameplayTags.Abilities_Status_Unlocked)
		{
			//判断插槽是否有技能,有则需要将其清除
			if(!SlotIsEmpty(Slot))
			{
				//获取目标插槽现在装配的技能
				if(const FGameplayAbilitySpec* SpecWithSlot = GetSpecWithSlot(Slot))
				{
					//技能槽位装配相同的技能,直接返回,不做额外的处理
					if(AbilityTag.MatchesTagExact(GetAbilityTagFromSpec(*SpecWithSlot)))
					{
						ClientEquipAbility(AbilityTag, Status, Slot, PrevSlot);
						return;
					}

					//如果是被动技能,我们需要先将技能取消执行
					if(IsPassiveAbility(*SpecWithSlot))
					{
						DeactivatePassiveAbility.Broadcast(GetAbilityTagFromSpec(*SpecWithSlot));
					}

					ClearAbilitiesOfSlot(Slot); //清除目标插槽装配的技能
				}
			}

			//技能没有设置到插槽(没有激活)
			if(!AbilityHasAnySlot(*AbilitySpec))
			{
				//如果是被动技能,装配即激活
				if(IsPassiveAbility(*AbilitySpec))
				{
					TryActivateAbility(AbilitySpec->Handle);
				}
			}

			//修改技能装配的插槽
			AssignSlotToAbility(*AbilitySpec, Slot);

			//回调更新UI
			ClientEquipAbility(AbilityTag, Status, Slot, PrevSlot);
			MarkAbilitySpecDirty(*AbilitySpec); //立即将其复制到每个客户端
		}
	}
}

运行查看装配后,对应的打印是否能够正常打印。

添加被动技能表现特效

现在技能可以激活了,我们需要让玩家能够知道被动技能生效的效果,参照之前的debuff应用,我们将参照之前的方式实现,即使出现了问题,那是一种比较好的解耦方式。

首先,我们创建一个被动技能表现基类

命名为PassiveNiagaraComponent

在基类里,我们需要一个设置标签,用于启动时对应的被动技能标签,然后添加一个监听回调的函数。

cpp 复制代码
UCLASS()
class RPG_API UPassiveNiagaraComponent : public UNiagaraComponent
{
	GENERATED_BODY()

public:
	UPassiveNiagaraComponent();

	//激活此被动技能特效的技能标签
	UPROPERTY(EditDefaultsOnly)
	FGameplayTag PassiveSpellTag;

protected:
	virtual void BeginPlay() override;

	/**
	 * 监听技能变动后的委托回调,用于设置此实例是否需要激活
	 * @param AbilityTag 对应的技能的标签
	 * @param bActivate 激活还是关闭
	 */
	void OnPassiveActivate(const FGameplayTag& AbilityTag, bool bActivate);
};

在构造函数里,将特效自动激活关闭

在事件开始时,绑定ASC里被动技能应用委托,通过监听被动技能应用委托来触发回调

在回调里,判断标签是否对应,根据需要开启和关闭设置特效组件的开启关闭。

cpp 复制代码
UPassiveNiagaraComponent::UPassiveNiagaraComponent()
{
	bAutoActivate = false;
}

void UPassiveNiagaraComponent::BeginPlay()
{
	Super::BeginPlay();

	if(URPGAbilitySystemComponent* RPGASC = Cast<URPGAbilitySystemComponent>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetOwner())))
	{
		RPGASC->ActivatePassiveEffect.AddUObject(this, &UPassiveNiagaraComponent::OnPassiveActivate);
	}
	else if(ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetOwner()))
	{
		//AddWeakLambda 这种绑定方式的主要好处是,当绑定的对象被销毁时,委托不会保持对象的引用,从而避免悬空指针问题和内存泄漏。
		CombatInterface->GetOnASCRegisteredDelegate().AddWeakLambda(this,[this](UAbilitySystemComponent* InASC)
		{
			if(URPGAbilitySystemComponent* RPGASC = Cast<URPGAbilitySystemComponent>(InASC))
			{
				RPGASC->ActivatePassiveEffect.AddUObject(this, &UPassiveNiagaraComponent::OnPassiveActivate);
			}
		});
	}
}

void UPassiveNiagaraComponent::OnPassiveActivate(const FGameplayTag& AbilityTag, bool bActivate)
{
	//判断技能标签是否一致
	if(AbilityTag.MatchesTagExact(PassiveSpellTag))
	{
		//判断是否需要激活
		if(bActivate)
		{
			//不需要重复激活
			if(!IsActive()) Activate();
		}
		else
		{
			Deactivate();
		}
	}
}

在ASC类里,我们增加一个新的委托类型

cpp 复制代码
DECLARE_MULTICAST_DELEGATE_TwoParams(FActivePassiveEffect, const FGameplayTag& /*被动技能标签*/, bool /*激活或取消*/); //被动技能特效监听委托,对应特效是否开启

在ASC类里新增一个对应类型的属性

cpp 复制代码
FActivePassiveEffect ActivatePassiveEffect; //被动技能对应特效委托

增加一个多播函数,它会在每个客户端和服务器运行,保证都能够查看到对应效果,设置Unreliable,用来设置它不是重要的,不需要优先同步

cpp 复制代码
	/**
	 * 多网络被动特效委托广播,让每个客户端都可以看到特效
	 * @param AbilityTag 被动技能标签
	 * @param bActivate 激活或者关闭
	 */
	UFUNCTION(NetMulticast, Unreliable)
	void MulticastActivatePassiveEffect(const FGameplayTag& AbilityTag, bool bActivate);

然后在函数里调用委托,让每个客户端对应的ASC都会调用此函数

cpp 复制代码
void URPGAbilitySystemComponent::MulticastActivatePassiveEffect_Implementation(const FGameplayTag& AbilityTag, bool bActivate)
{
	ActivatePassiveEffect.Broadcast(AbilityTag, bActivate);
}

然后在我们装配技能时,取消被动执行时,调用它传入false

在激活一个被动技能时,我们设置对应的特效激活

最后,就是在角色类里,我们需要对应的特效组件,对每一种特效都创建一个对应的特效

cpp 复制代码
	//光环被动技能特效组件
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<UPassiveNiagaraComponent> HaloOfProtectionNiagaraComponent;

	//回血被动技能特效组件
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<UPassiveNiagaraComponent> LifeSiphonNiagaraComponent;

	//回蓝被动技能特效组件
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<UPassiveNiagaraComponent> ManaSiphonNiagaraComponent;

	//被动技能挂载的组件
	UPROPERTY(VisibleAnywhere)
	TObjectPtr<USceneComponent> EffectAttachComponent;

然后在构造函数里创建实例,并挂载到特效根组件,我们创建特效根组件的原因是为了保证特效不会跟随角色旋转

cpp 复制代码
	//实例化被动技能组件,并挂载
	EffectAttachComponent = CreateDefaultSubobject<USceneComponent>("EffectAttachPoint");
	EffectAttachComponent->SetupAttachment(GetRootComponent());
	HaloOfProtectionNiagaraComponent = CreateDefaultSubobject<UPassiveNiagaraComponent>("HaloOfProtectionComponent");
	HaloOfProtectionNiagaraComponent->SetupAttachment(EffectAttachComponent);
	LifeSiphonNiagaraComponent = CreateDefaultSubobject<UPassiveNiagaraComponent>("LifeSiphonComponent");
	LifeSiphonNiagaraComponent->SetupAttachment(EffectAttachComponent);
	ManaSiphonNiagaraComponent = CreateDefaultSubobject<UPassiveNiagaraComponent>("ManaSiphonComponent");
	ManaSiphonNiagaraComponent->SetupAttachment(EffectAttachComponent);

我们需要在帧更新里去修改特效根组件的旋转,让其保证相对于世界不会旋转,所以需要覆写帧更新函数

cpp 复制代码
virtual void Tick(float DeltaSeconds) override;

在帧更新函数里,我们每一帧都将其旋转值设置为相对于世界坐标默认为0

cpp 复制代码
void ARPGCharacterBase::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
	//防止特效跟随人物旋转,每一帧更新修改旋转为默认
	EffectAttachComponent->SetWorldRotation(FRotator::ZeroRotator);
}

之前我们没有使用帧更新,它是关掉的,现在我们需要将其开启

cpp 复制代码
 	// 将这个字符设置为true时,将每帧进行更新。不需要可以关闭提高性能。
	PrimaryActorTick.bCanEverTick = true;

接下来,我们编译打开蓝图,查看玩家角色蓝图是否生成了对应的组件

我们为每个特效组件设置对应的资产

并设置特效组件对应的被动技能,这样,在被动技能被应用时,特效也将会激活。

接下来就是运行查看效果。

实现被动技能效果

在前面,被动技能可以触发对应技能里的激活和结束回调节点,我们可以以此为出发点,来给玩家角色应用GE。

我们创建一个基础的被动技能蓝图,将一些公共的配置和函数在此函数完成

在蓝图里,我们添加一个设置GE类的变量

增加一个添加GE给自身的函数

再增加一个通过类删除GE的函数

默认激活技能时,调用添加函数,技能结束时删除GE

接着,我们增加一个新的GE类

这里我先做一个蓝量回复的类,类型设置为时间无限,每一秒执行一次

在Modifiers里,我们增加一个属性修改,属性基于之前设置的蓝量回复的值

在被动技能里,我们将被动技能基类修改为创建的蓝图基类

事件调用修改为调用父节点

父节点可以通过右键查找到添加

最后修改GE的默认值的类

在我们应用了被动技能后,你会发现蓝量在慢慢回复,并且是每一秒回复一次。

被动技能这里只是给大家一个实现思路,大家可以实现更多的被动技能。

相关推荐
BLOB_1010016 分钟前
【折腾一上午】Java POI 导出 Excel 自适应列宽行高
java·excel
布值倒区什么name17 分钟前
日常记录,使用springboot,vue2,easyexcel使实现字段的匹配导入
java·spring boot·后端
single59420 分钟前
【综合算法学习】(第十篇)
java·数据结构·c++·vscode·学习·算法·leetcode
wclass-zhengge22 分钟前
SpringBoot篇(自动装配原理)
java·spring boot·后端
哎呦没30 分钟前
中小企业设备管理效率提升:Spring Boot系统设计
java·spring boot·后端
ZWZhangYu33 分钟前
【MyBatis源码】SqlSource对象创建流程
java·tomcat·mybatis
Yaml435 分钟前
Spring Boot整合EasyExcel:实现大规模数据的并行导出与压缩下载
java·开发语言·spring boot
Kobebryant-Manba36 分钟前
sqlyog软件
java·sql
free_girl_fang40 分钟前
夯实根基之MySql从入门到精通(一)
java·数据结构·数据库·mysql
编程修仙40 分钟前
Java三大特性之继承
java·开发语言