99. UE5 GAS RPG 被动技能实现

news/2024/10/21 15:37:58/

在这一篇,我们在之前打下的基础下,实现一下被动技能。
被动技能需要我们在技能栏上面选择升级解锁技能后,将其设置到技能栏,我们先增加被动技能使用的标签。

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

注册一下

	/** 被动技能*/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调用了结束技能,并且此被动技能刚好有对应的标签,我们可以通过第二个技能结束此技能实力的激活。

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里增加一个新的委托定义,用来定义技能结束

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

并在ASC类里新增一个变量

FDeactivatePassiveAbility DeactivatePassiveAbility; //取消技能激活的委托

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

void URPGPassiveAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);//获取到ASCif(URPGAbilitySystemComponent* RPGASC = Cast<URPGAbilitySystemComponent>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo()))){//绑定技能取消回调RPGASC->DeactivatePassiveAbility.AddUObject(this, &URPGPassiveAbility::ReceiveDeactivate);}
}

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

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

创建对应被动技能蓝图

有了被动技能基类,我们在UE里创建对应的蓝图
在这里插入图片描述
基于之前创建的基类创建三个被动技能
在这里插入图片描述
在技能标识标签这里设置对应的标签
在这里插入图片描述
然后在DA_AbilityInfo里添加对应的技能的相关数据
在这里插入图片描述
接下来,我们在UI里实现设置按钮显示哪个技能标签
在这里插入图片描述
接着运行,查看按钮是否能够正确显示以及操作
在这里插入图片描述
记得在主窗口的UI上设置对应的按钮Tag
在这里插入图片描述
运行设置后查看,是否主界面也能够跟随变动
在这里插入图片描述

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

技能可以实现了装配,并且,我们在技能里监听了取消事件,在触发对应回调时,技能会自动取消激活。
所以,我们在被动技能里设置调试节点,方便测试,在激活时和结束技能时都可以打印信息。
在这里插入图片描述
由于被动技能应用时就需要激活,所以,我们不需要预测它,直接在服务器初始化即可。
在这里插入图片描述
这是我们现在的ASC里装配技能时的处理逻辑,没有考虑到主动技能和被动技能的区别
在这里插入图片描述
接下来,我们将修改技能装配的逻辑,让其兼容对被动技能的处理。
前面的这一段还是一样,通过技能标签获取到技能实例,并获取到技能未修改前装配的槽位和状态,对技能的状态进行判断

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); //立即将其复制到每个客户端

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

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

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

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);//回调更新UIClientEquipAbility(AbilityTag, Status, Slot, PrevSlot);MarkAbilitySpecDirty(*AbilitySpec); //立即将其复制到每个客户端}}
}

运行查看装配后,对应的打印是否能够正常打印。
在这里插入图片描述

添加被动技能表现特效

现在技能可以激活了,我们需要让玩家能够知道被动技能生效的效果,参照之前的debuff应用,我们将参照之前的方式实现,即使出现了问题,那是一种比较好的解耦方式。
首先,我们创建一个被动技能表现基类
在这里插入图片描述
命名为PassiveNiagaraComponent
在这里插入图片描述
在基类里,我们需要一个设置标签,用于启动时对应的被动技能标签,然后添加一个监听回调的函数。

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里被动技能应用委托,通过监听被动技能应用委托来触发回调
在回调里,判断标签是否对应,根据需要开启和关闭设置特效组件的开启关闭。

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类里,我们增加一个新的委托类型

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

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

FActivePassiveEffect ActivatePassiveEffect; //被动技能对应特效委托

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

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

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

void URPGAbilitySystemComponent::MulticastActivatePassiveEffect_Implementation(const FGameplayTag& AbilityTag, bool bActivate)
{ActivatePassiveEffect.Broadcast(AbilityTag, bActivate);
}

然后在我们装配技能时,取消被动执行时,调用它传入false
在这里插入图片描述
在激活一个被动技能时,我们设置对应的特效激活
在这里插入图片描述
最后,就是在角色类里,我们需要对应的特效组件,对每一种特效都创建一个对应的特效

	//光环被动技能特效组件UPROPERTY(VisibleAnywhere)TObjectPtr<UPassiveNiagaraComponent> HaloOfProtectionNiagaraComponent;//回血被动技能特效组件UPROPERTY(VisibleAnywhere)TObjectPtr<UPassiveNiagaraComponent> LifeSiphonNiagaraComponent;//回蓝被动技能特效组件UPROPERTY(VisibleAnywhere)TObjectPtr<UPassiveNiagaraComponent> ManaSiphonNiagaraComponent;//被动技能挂载的组件UPROPERTY(VisibleAnywhere)TObjectPtr<USceneComponent> EffectAttachComponent;

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

	//实例化被动技能组件,并挂载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);

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

virtual void Tick(float DeltaSeconds) override;

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

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

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

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

接下来,我们编译打开蓝图,查看玩家角色蓝图是否生成了对应的组件
在这里插入图片描述
我们为每个特效组件设置对应的资产
在这里插入图片描述
并设置特效组件对应的被动技能,这样,在被动技能被应用时,特效也将会激活。
在这里插入图片描述
接下来就是运行查看效果。
在这里插入图片描述

实现被动技能效果

在前面,被动技能可以触发对应技能里的激活和结束回调节点,我们可以以此为出发点,来给玩家角色应用GE。
我们创建一个基础的被动技能蓝图,将一些公共的配置和函数在此函数完成
在这里插入图片描述
在蓝图里,我们添加一个设置GE类的变量
在这里插入图片描述
增加一个添加GE给自身的函数
在这里插入图片描述
再增加一个通过类删除GE的函数
在这里插入图片描述
默认激活技能时,调用添加函数,技能结束时删除GE
在这里插入图片描述
接着,我们增加一个新的GE类
在这里插入图片描述
这里我先做一个蓝量回复的类,类型设置为时间无限,每一秒执行一次
在这里插入图片描述
在Modifiers里,我们增加一个属性修改,属性基于之前设置的蓝量回复的值
在这里插入图片描述
在被动技能里,我们将被动技能基类修改为创建的蓝图基类
在这里插入图片描述
事件调用修改为调用父节点
在这里插入图片描述
父节点可以通过右键查找到添加
在这里插入图片描述
最后修改GE的默认值的类
在这里插入图片描述
在我们应用了被动技能后,你会发现蓝量在慢慢回复,并且是每一秒回复一次。
被动技能这里只是给大家一个实现思路,大家可以实现更多的被动技能。


http://www.ppmy.cn/news/1540826.html

相关文章

【微信小程序_17_生命周期】

摘要:本文介绍了小程序的生命周期,包括生命周期的定义、分类、生命周期函数等内容。生命周期分为应用生命周期和页面生命周期,生命周期函数由小程序框架提供,会按次序自动执行,开发人员可利用这些函数在特定时间点执行操作,如在页面加载时初始化数据。 微信小程序_17_生命…

【进阶OpenCV】 (21) --卷积神经网络实现人脸检测

文章目录 卷积神经网络实现人脸检测一、加载CNN人脸检测模型二、图像预处理三、绘制人脸矩形框 总结 卷积神经网络实现人脸检测 opencv可以直接通过readnet来读取神经网络。dlib也可以的。 任务&#xff1a;使用dlib库中的卷积神经网络&#xff08;CNN&#xff09;人脸检测模…

OpenGL、OpenCL 和 OpenAL 定义及用途

OpenGL 全称&#xff1a;Open Graphics Library&#xff0c;即开放图形库。是一种跨编程语言、跨平台的编程接口规格&#xff0c;用于二维和三维图形的绘制。它是一个功能强大、调用方便的底层图形库&#xff0c;提供了丰富的绘图函数&#xff0c;包括基本图形绘制、变换、光照…

【Lean 4 学习】用Lean 4证明自然数的平方差公式

引言 最近开始学习Lean 4来做数学证明&#xff0c;虽然挺有挑战&#xff0c;但是对于我这个30多岁的大叔来说有种刚学编程时候探索的乐趣hhh自然数平方差公式这个问题&#xff0c;是我刚学了平方和公式&#xff0c;想变变给自己练手用的&#xff0c;结果卡了我好久&#xff0c…

入侵及防护:7个迹象说明你的手机可能被入侵!

在现代社会中&#xff0c;手机已成为我们生活中不可或缺的一部分。然而&#xff0c;随着智能手机的普及&#xff0c;手机安全问题也日益严重。手机被入侵的风险不仅影响个人隐私&#xff0c;还可能导致财产损失。本文将为你介绍7个迹象&#xff0c;帮助你判断手机是否可能被入侵…

electron-vite_10electron-updater软件更新

网很多electron-updater更新文章&#xff0c;这里只简单写一下演示代码&#xff1b; 为什么选择 electron-updater插件可以自动更新应用程序,同时支持多个平台;比官方要强; 官方的autoUpdater仅支持macOS 和 Windows 自动更新; 注意是自动&#xff0c;直接更新那种; 脚手架中是…

LoadBalancer 类型的 Service工作期间,kube-proxy做了什么?

kube-proxy 是 Kubernetes 集群中一个重要的组件&#xff0c;负责实现服务的网络代理和负载均衡功能。当创建一个 LoadBalancer 类型的 Service 时&#xff0c;kube-proxy 在整个过程中扮演了关键角色。以下是 kube-proxy 在 LoadBalancer 类型 Service 创建和使用过程中所做的…

CentOS 8配置阿里云yum源

最近正好搞一套3.x体系的大数据技术集群&#xff0c;用CentOS8的系统&#xff0c;发现默认自带的所有yum源貌似无法使用&#xff0c;这里记录一下配置阿里的yum源。后面研究了一下&#xff0c;CentOS 8现在24年&#xff0c;最好是慎用&#xff0c;因为往后的系统升级就没有完全…