90. UE5 RPG 实现技能的装配

在上一篇里,我们实现了在技能面板,点击技能能够显示出技能的相关描述以及下一级的技能的对应描述。

在这一篇里,我们实现一下技能的装配。

在之前,我们实现了点击按钮时,在技能面板控制器里存储了当前选中的技能的相关信息,有了这个信息以后,我们在实现装配时,可以使用这个数据进行处理。

当选中技能后,我们接着点击下面的技能插槽时,如果符合装配条件,我们将实现装配到对应的插槽。

添加技能类型配置

在技能装配这里,我们分了主动技能和被动技能,不同类型的技能无法装配。所以,我们需要在技能数据里设置对应的类型,我们还是使用类型标签设置

我们在技能数据结构体内增加一个配置项,用于设置类型

然后在标签管理这里,增加三项,主动技能,被动技能和空

cpp 复制代码
	FGameplayTag Abilities_Type_Offensive; //技能类型 主动技能
	FGameplayTag Abilities_Type_Passive; //技能类型 被动技能
	FGameplayTag Abilities_Type_None; //技能类型 空 受击等技能设置

并且注册到标签管理器

cpp 复制代码
	/*
	 * 当前技能类型标签
	*/
	GameplayTags.Abilities_Type_Offensive = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("Abilities.Type.Offensive"),
			FString("主动技能")
			);
	GameplayTags.Abilities_Type_Passive = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("Abilities.Type.Passive"),
			FString("被动技能")
			);
	GameplayTags.Abilities_Type_None = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("Abilities.Type.None"),
			FString("啥也不是")
			);

接着打开UE,在技能配置里,设置技能的对应类型

添加技能装配事件触发

我们也注意到,技能分为两大类型,主动技能和被动技能,为了区分它们,我们需要设置对应的类型标签。

首先在装配按钮这里添加可设置的类型变量,注意把眼睛打开。

然后在外部设置它对应的类型

被动技能也设置

然后给按钮绑定一个点击事件

添加技能装配处理逻辑

我们需要在技能面板控制器里增加一个点击按钮,用来实现点击事件

cpp 复制代码
	UFUNCTION(BlueprintCallable)
	void EquipButtonPressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType); //装配技能按钮按下事件

在实现这里,我首先判断条件是否达成,然后调用ASC里的实际处理技能装配的逻辑

cpp 复制代码
void USpellMenuWidgetController::EquipButtonPressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType)
{
	const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();

	//获取装配技能的类型
	const FGameplayTag& SelectedAbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
	if(!SelectedAbilityType.MatchesTagExact(AbilityType)) return; //类型不同无法装配

	//获取装配技能的输入标签
	const FGameplayTag& SelectedAbilityInputTag = GetRPGASC()->GetInputTagFromAbilityTag(SelectedAbility.Ability);
	if(SelectedAbilityInputTag.MatchesTagExact(SlotTag)) return; //如果当前技能输入和插槽标签相同,证明已经装配,不需要再处理

	//调用装配技能函数,进行处理
	GetRPGASC()->ServerEquipAbility(SelectedAbility.Ability, SlotTag);
}

接着,我们在ASC里增加多个函数,用于实现这个逻辑,为什么在ASC里,因为GA是属于GAS系统的,GAS相关的内容就放在GAS相关的类里实现处理

cpp 复制代码
	UFUNCTION(Server, Reliable) //在服务器处理技能装配,传入技能标签和装配的技能标签
	void ServerEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Slot); 
	
	UFUNCTION(Client, Reliable) //在客户端处理技能装配
	void ClientEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot);

	void ClearSlot(FGameplayAbilitySpec* Spec); //清除技能装配插槽的技能

	void ClearAbilitiesOfSlot(const FGameplayTag& Slot); //根据输入标签,清除技能装配插槽的技能

	static bool AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot); //判断当前技能实例是否处于目标技能装配插槽

首先,我们看一下后面三个函数,他们是为了处理技能实现的函数。

首先是AbilityHasSlot,需要传入一个技能实例和一个输入标签(插槽标识),用来判断技能是否属于这个插槽

cpp 复制代码
bool URPGAbilitySystemComponent::AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot)
{
	for(FGameplayTag Tag : Spec->DynamicAbilityTags)
	{
		if(Tag.MatchesTagExact(Slot))
		{
			return true;
		}
	}
	return false;
}

然后就是清除掉技能的装配的插槽,其实就是清除掉GA的输入标签

cpp 复制代码
void URPGAbilitySystemComponent::ClearSlot(FGameplayAbilitySpec* Spec)
{
	const FGameplayTag Slot = GetInputTagFromSpec(*Spec);
	Spec->DynamicAbilityTags.RemoveTag(Slot);
	MarkAbilitySpecDirty(*Spec);
}

然后就是根据输入标签(插槽)清除掉所有技能的对应的插槽,这个会用到上面的两个函数。

cpp 复制代码
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)
{
	if(FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
	{
		const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec); //技能之前装配的插槽
		const FGameplayTag& Status = GetStatusTagFromSpec(*AbilitySpec); //当前技能的状态标签

		//判断技能的状态,技能状态只有在已装配或者已解锁的状态才可以装配
		const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
		if(Status == GameplayTags.Abilities_Status_Equipped || Status == GameplayTags.Abilities_Status_Unlocked)
		{
			ClearAbilitiesOfSlot(Slot); //通过技能的输入标签清除掉插槽的技能
			ClearSlot(AbilitySpec); //清除掉当前技能的输入标签
			AbilitySpec->DynamicAbilityTags.AddTag(Slot); //将目标插槽的输入标签添加到技能实例的动态标签容器中

			//如果状态标签是已解锁,我们需要将其修改为已装配状态
			if(Status.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
			{
				AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Unlocked);
				AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Equipped);
			}
			ClientEquipAbility(AbilityTag, Status, Slot, PrevSlot);
			MarkAbilitySpecDirty(*AbilitySpec); //立即将其复制到每个客户端
		}
	}
}

在客户端,我们只进行一个委托的广播,然后让控制器监听去修改

所以,我们增加一个技能装配后的委托

cpp 复制代码
DECLARE_MULTICAST_DELEGATE_FourParams(FAbilityEquipped, const FGameplayTag& /*技能标签*/, const FGameplayTag& /*技能状态标签*/, const FGameplayTag& /*输入标签*/, const FGameplayTag& /*上一个输入标签*/);
cpp 复制代码
FAbilityEquipped AbilityEquipped; //技能装配更新回调

然后在客户端执行的函数里进行调用

cpp 复制代码
void URPGAbilitySystemComponent::ClientEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot)
{
	AbilityEquipped.Broadcast(AbilityTag, Status, Slot, PreviousSlot); //在客户端将更新后的标签广播
}

在控制器接收技能装配委托

接下来我们要在控制器实现对技能装配委托的监听,考虑到,技能装配后,在技能面板和Overlay里都需要使用它,我们将函数写到基类里,然后在对应的控制器里进行监听绑定。

我们在控制器基类增加一个委托回调函数

cpp 复制代码
	//监听技能装配后的处理
	void OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot) const;

然后,在实现里,我们通过传递过来的标签,实现技能数据的广播,然后在UI监听更新

cpp 复制代码
void URPGWidgetController::OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot) const
{
	const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();

	//清除旧插槽的数据
	FRPGAbilityInfo LastSlotInfo;
	LastSlotInfo.StatusTag = GameplayTags.Abilities_Status_Unlocked;
	LastSlotInfo.InputTag = PreviousSlot;
	LastSlotInfo.AbilityTag = GameplayTags.Abilities_None;
	AbilityInfoDelegate.Broadcast(LastSlotInfo);

	//更新新插槽的数据
	FRPGAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
	Info.StatusTag = Status;
	Info.InputTag = Slot;
	AbilityInfoDelegate.Broadcast(Info);
}

我们在需要监听的派生类里,添加对其的监听

cpp 复制代码
//监听技能装配的回调
	GetRPGASC()->AbilityEquipped.AddUObject(this, &USpellMenuWidgetController::OnAbilityEquipped);

编译打开代码,我们在数据接收这里,增加,在判断是否为对应插槽的更新数据,如果技能标签为空,则是清除旧插槽。

在技能按钮里,还需要多做一步操作就是处理技能冷却的监听处理

解决降级为0级技能还装配的问题

当技能等级降级为0级以后,技能的装配变为了无法装配,我们需要将此信息同步到技能栏和装配技能栏。

所以我们在降级函数中,将其增加一个委托,再调用客户端更新

接下来是效果展示

相关推荐
不是二师兄的八戒15 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
闲暇部落26 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
爱编程的小生27 分钟前
Easyexcel(2-文件读取)
java·excel
带多刺的玫瑰44 分钟前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导61 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study1 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言
Chris _data1 小时前
二叉树oj题解析
java·数据结构
牙牙7052 小时前
Centos7安装Jenkins脚本一键部署
java·servlet·jenkins
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
以后不吃煲仔饭2 小时前
Java基础夯实——2.7 线程上下文切换
java·开发语言