78. UE5 RPG 创建技能数据并初始化技能ui

在上一篇文章里,我们创建了技能的UI,接下来,我们要考虑如何实现对技能UI的填充,肯定不能直接写死,需要有一些方法去实现技能的更新。我们期望能够创建一个技能数据,然后根据数据通过回调的方式实现数据的更新。
为了实现这个功能,我们会先创建一个结构体,用于存储技能的相关数据(Tag,使用的图片等),然后创建一个DataAsset,然后创建回调函数,在注册技能的时候,将技能相关的数据广播出去,在UI里接受,更新UI显示。

创建DataAsset

首先,我们基于DataAsset创建一个新的类,用于设置技能需要的相关配置
在这里插入图片描述
创建命名 AbilityInfo,技能数据
在这里插入图片描述
在类里面,我们首先创建一个结构体,用于设置技能所需哪些配置,如果需要,我们后续还可以继续添加,这里添加了四项数据

USTRUCT(BlueprintType)
struct FRPGAbilityInfo
{
	GENERATED_BODY()

	//技能标签
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	FGameplayTag AbilityTag = FGameplayTag();

	//技能输入映射标签
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	FGameplayTag InputTag = FGameplayTag();

	//技能图标
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<const UTexture2D> Icon = nullptr;

	//背景材质
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<UMaterialInterface> BackgroundMaterial = nullptr;
};

接着,我们在数据类里面增加一个参数,用于在蓝图中使用此类后,可以设置一个技能数据数组,并增加一个通过技能标签获取对应数据的方法

/**
 * 
 */
UCLASS()
class RPG_API UAbilityInfo : public UDataAsset
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="AbilityInformation")
	TArray<FRPGAbilityInfo> AbilityInformation;

	//通过技能标签获取到技能相关数据
	FRPGAbilityInfo FindAbilityInfoForTag(const FGameplayTag& AbilityTag, bool bLogNotFound = false) const;
};

在获取技能数据的函数实现这里,我们直接遍历数组,查找到相同的技能标签返回,并增加一个参数,如果无法查询到,打印一个错误信息,方便后续调试

FRPGAbilityInfo UAbilityInfo::FindAbilityInfoForTag(const FGameplayTag& AbilityTag, const bool bLogNotFound) const
{
	for(const FRPGAbilityInfo& Info : AbilityInformation)
	{
		if(Info.AbilityTag == AbilityTag)
		{
			return Info;
		}
	}

	if(bLogNotFound)
	{
		//如果获取不到数据,打印消息
	}

	return FRPGAbilityInfo();
}

实现日志分类

在打印这里,我们想实现对于技能设置不同的打印通道,和其它默认的区分开来,这样调试起来会更加的方便。为了实现这个功能,我们需要额外的创建一个.h 和 .cpp文件
直接项目文件夹上面,右键选择添加,文件
在这里插入图片描述
在弹出窗口这里写入需要创建的文件名称
在这里插入图片描述
我们将两个文件都创建出来
在这里插入图片描述
在.h文件中,我们设置#pragma once 可以实现一次编译,多次复用。然后引入基础头文件和打印相关的头文件,并通过宏定义了一个我们自定义的打印通道。

#pragma once

#include "CoreMinimal.h"
#include "Logging/LogMacros.h"

DECLARE_LOG_CATEGORY_EXTERN(LogRPG, Log, All);

然后在cpp文件中,使用DEFINE_LOG_CATEGORY对一个打印通道进行实例化,这个宏与DECLARE_LOG_CATEGORY_EXTERN宏一起使用来实现一个新的打印通道。

#include "RPGLogChannels.h"

DEFINE_LOG_CATEGORY(LogRPG);

接着,我们可以在获取技能相关数据的函数中,引入此文件

#include "RPG/RPGLogChannels.h"

并在查询不到对应数据时,在我们自定义的日志分类中打印

	if(bLogNotFound)
	{
		//如果获取不到数据,打印消息
		UE_LOG(LogRPG, Error, TEXT("无法通过技能标签[%s]在技能数据[%s]查找到对应的技能数据"), *AbilityTag.ToString(), *GetNameSafe(this));
	}

应用技能数据

我们将技能数据的DataAsset创建完成,接下来,要实现对其的应用,我们在蓝图中创建了技能数据,需要有一个地方去设置,并可以应用。这些数据是在在UI上使用的,我们将其设置在OverlayWidgetController里面,增加一个对齐配置的配置项。OverlayWidgetController是配置在HUD类上面的,在项目运行时,就会初始化OverlayWidgetController,并应用对OverlayWidget,覆盖屏幕的OverlayWidget就会从OverlayWidgetController中获取数据。

	//技能的表格数据
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Widget Data")
	TObjectPtr<UAbilityInfo> AbilityInfo;

我们现实现对数据的设置,编译代码,打开UE,创建一个数据资产
在这里插入图片描述
类就选择我们创建的数据资产的类
在这里插入图片描述
命名为DA_AbilityInfo
在这里插入图片描述
接着打开基于OverlayWidgetController创建的蓝图,将创建的数据资产设置上去,这样,我们就可以在后续UI更新中,使用此数据,并能够实现对数据的获取。
在这里插入图片描述

给数据资产添加第一条数据

有了数据资产,我们首先将玩家角色的第一个技能添加进去,就是普通攻击火球术,我们还没有其相关的技能标签
我们首先创建一个火球术的技能标签

FGameplayTag Abilities_Fire_FireBolt; //火球术技能标签

然后在cpp里面对其注册

	GameplayTags.Abilities_Fire_FireBolt = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("Abilities.Fire.FireBolt"),
			FString("火球术技能标签")
			);

然后编译,进行设置
在这里插入图片描述
这里考虑到有可能后续玩家会修改触发技能的按键,对于技能的输入tag设置,我们后续将修改为在程序中动态设置它,并且,后续将其在蓝图中设置的功能关闭。

	//技能输入映射标签
	UPROPERTY(BlueprintReadOnly)
	FGameplayTag InputTag = FGameplayTag();

重新编译
在这里插入图片描述

广播技能数据

有了技能数据,我们需要实现在ASC应用角色技能时,UI上也能够获取到应用通知,跟随更新数据。
为了实现这点,我们需要在ASC中增加委托,并在应用技能后,进行广播触发回调。
我们在自定义ASC中增加一个委托宏,这个宏用于在技能初始化应用完成后广播回调

DECLARE_MULTICAST_DELEGATE_OneParam(FAbilityGiven, URPGAbilitySystemComponent*) //技能初始化应用后的回调委托

使用宏创建一个委托

FAbilityGiven AbilityGivenDelegate; //技能初始化应用后的回调委托

由于我们无法确定运行起来后,是技能的初始化完成,还是UI的初始化完成,所以,我们通过一个变量来记录,在技能初始化应用完成后,将其设置为true

bool bStartupAbilitiesGiven = false; //初始化应用技能后,此值将被设置为true,用于记录当前是否被初始化完成

接着在初始化应用技能的函数里,将变量设置为true,并将委托广播出去

void URPGAbilitySystemComponent::AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities)
{
	for(const TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
	{
		FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
		if(const URPGGameplayAbility* AbilityBase = Cast<URPGGameplayAbility>(AbilitySpec.Ability))
		{
			AbilitySpec.DynamicAbilityTags.AddTag(AbilityBase->StartupInputTag); //设置技能激活输入标签
			GiveAbility(AbilitySpec); //只应用不激活
			// GiveAbilityAndActivateOnce(AbilitySpec); //应用技能并激活一次
		}
	}

	bStartupAbilitiesGiven = true;
	AbilityGivenDelegate.Broadcast(this);
}

这样,我们就实现了技能初始化应用的委托,然后,我们在Overlay的Controller的类里面,绑定此委托的回调,完成和ASC的交互,我们在OverlayWidgetController里面创建一个回调函数

void OnInitializeStartupAbilities(URPGAbilitySystemComponent* RPGAbilitySystemComponent) const; //技能初始化应用后的回调

刚好里面有我们之前书写的绑定委托的函数,我们在里面对此委托进行绑定。
这里逻辑是,我们获取到使用的ASC,将其转换为自定义ASC,通过判断变量,如果变量值为true,代表当前技能初始化应用已经完成,我们可以直接调用回调。如果变量为false,初始化还未完成状态,我们就需要去绑定委托,在技能初始化应用完成后,也可以触发委托的回调。
通过这两步,不管谁先谁后,都可以成功触发我们写在OverlayWidgetController里面的回调。并且我们也成功的获取到了ASC,并进行下一步处理。

void UOverlayWidgetController::BindCallbacksToDependencies()
{
	...

	if(URPGAbilitySystemComponent* RPGASC = Cast<URPGAbilitySystemComponent>(AbilitySystemComponent))
	{
		if(RPGASC->bStartupAbilitiesGiven)
		{
			//如果执行到此处时,技能的初始化工作已经完成,则直接调用初始化回调
			OnInitializeStartupAbilities(RPGASC);
		}
		else
		{
			//如果执行到此处,技能初始化还未完成,将通过绑定委托,监听广播的形式触发初始化完成回调
			RPGASC->AbilityGivenDelegate.AddUObject(this, &ThisClass::OnInitializeStartupAbilities);
		}
		
		//AddLambda 绑定匿名函数
		RPGASC->EffectAssetTags.AddLambda(
			[this](const FGameplayTagContainer& AssetTags) //中括号添加this是为了保证内部能够获取类的对象
			{
				for(const FGameplayTag& Tag : AssetTags)
				{

					//对标签进行检测,如果不是信息标签,将无法进行广播
					FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
					// "A.1".MatchesTag("A") will return True, "A".MatchesTag("A.1") will return False
					if(Tag.MatchesTag(MessageTag))
					{
						const FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
						MessageWidgetRowDelegate.Broadcast(*Row); //前面加*取消指针引用
					}
				
					//将tag广播给Widget Controller 测试代码
					// const FString Msg = FString::Printf(TEXT("GE Tag in Widget Controller: %s"), *Tag.ToString()); //获取Asset Tag
					// GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Cyan, Msg); //打印到屏幕上 -1 不会被覆盖

				}
			}
		);
	}
}

接下来,我们将实现技能的初始化回调的内容逻辑,它将实现对所有的应用的技能进行类型判断,并选出需要手动触发的技能,然后获取对应的技能UI数据,并通过Controller广播给用户控件的UI。虽然逻辑稍微复杂点,但是这种方式能够将逻辑拆分开来,不会造成代码之间的耦合度过高,造成报错问题。

实现UI技能委托

现在,当角色的技能初始化应用后,会触发UI的Controller里面的初始化回调。在回调里面,我接下来将实现的是,从里面获取到主动技能,然后获取其是否是需要按键激活的技能,然后通过技能Tag去获取数据,将数据广播出去。
接下来,我们实现技能初始化应用后的回调,在控制器里,初始化后,我们进行一次判断,当前技能是否初始化成功

if(!RPGAbilitySystemComponent->bStartupAbilitiesGiven) return; //判断当前技能初始化是否完成,触发回调时都已经完成

接下来,我们要遍历调用技能的实例,对技能进行处理,这里我们创建一个新的单播委托,它只能绑定一个回调函数

DECLARE_DELEGATE_OneParam(FForEachAbility, const FGameplayAbilitySpec&); //单播委托,只能绑定一个回调

在ASC中增加一个新的函数,用于遍历技能,并通过委托回调的形式广播出去,通过这种方式,降低了和OverlayWidgetController之间的耦合,即使你换一个其它的类,也可以调用。参数我们传入单播委托

void ForEachAbility(const FForEachAbility& Delegate); //遍历技能,并将技能广播出去

在函数实现这里,我们首先使用一次域锁,在执行下面的逻辑时,传入的内容内部的数据是无法被更改变动的。然后我们遍历所有可激活的技能,通过委托广播的形式调用。

void URPGAbilitySystemComponent::ForEachAbility(const FForEachAbility& Delegate)
{
	FScopedAbilityListLock ActiveScopeLock(*this); //使用域锁将此作用域this的内容锁定(无法修改),在遍历结束时解锁,保证线程安全
	for(const FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
	{
		if(!Delegate.ExecuteIfBound(AbilitySpec)) //运行绑定在技能实例上的委托,如果失败返回false
		{
			UE_LOG(LogRPG, Error, TEXT("在函数[%hs]运行委托失败"), __FUNCTION__);
		}
	}
}

有了这个函数,我们在OverlayWidgetController里面就可以实现对技能的遍历,而且还不需要类型转换等操作。上面的函数可以针对每个技能实例触发一次委托回调,所以,我们创建一个委托,并绑定回调函数,就可以实现对所有技能的处理。
具体实现如下,创建委托,绑定回调,然后通过函数调用,即可将技能实例进行遍历。

	//创建单播委托
	FForEachAbility BroadcastDelegate;
	//委托绑定回调匿名函数,委托广播时将会触发函数内部逻辑
	BroadcastDelegate.BindLambda([this](const FGameplayAbilitySpec& AbilitySpec)
	{
		...
	});
	//遍历技能并触发委托回调
	RPGAbilitySystemComponent->ForEachAbility(BroadcastDelegate);

我们在回调函数中,将针对于每个技能实例进行处理,获取技能的Tag标签,判断它是否属于技能,并获取技能的输入标签设置给技能数据,通过委托,广播出去。我们在技能的用户控件实例里面,就可以通过监听相关委托来实现修改技能图标。
我们在OverlayWidgetController里面新创建一个委托,用于在蓝图中对其进行监听,委托返回一个参数,对应的技能的数据

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityInfoSignature, const FRPGAbilityInfo, Info);

根据委托的类型创建一个参数,用于在蓝图中绑定监听

	UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
	FAbilityInfoSignature AbilityInfoDelegate;

我们还需要实现两个函数,它们可以通过传入的技能实例,从技能实例里面获取到对应的技能标签和输入标签。这里我们直接创建两个静态函数

	static FGameplayTag GetAbilityTagFromSpec(const FGameplayAbilitySpec& AbilitySpec);
	static FGameplayTag GetInputTagFromSpec(const FGameplayAbilitySpec& AbilitySpec);

它们的实现是,我们技能标签直接从AbilityTags里面获取,由于它不只可以设置一个标签,我们需要遍历。

FGameplayTag URPGAbilitySystemComponent::GetAbilityTagFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{
	if(AbilitySpec.Ability)
	{
		for(FGameplayTag Tag : AbilitySpec.Ability.Get()->AbilityTags) //获取设置的所有的技能标签并遍历
		{
			if(Tag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Abilities")))) //判断当前标签是否包含"Abilities"名称
			{
				return Tag;
			}
		}
	}
	return FGameplayTag();
}

FGameplayTag URPGAbilitySystemComponent::GetInputTagFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{
	for(FGameplayTag Tag : AbilitySpec.DynamicAbilityTags) //从技能实例的动态标签容器中遍历所有标签
	{
		if(Tag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("InputTag")))) //查找标签中是否设置以输入标签开头的标签
		{
			return Tag;
		}
	}

	return FGameplayTag();
}

MatchesTag的解释是A.1如果MatchesTag的A那么将返回true,相当于判断的它的标签下面的子级,我们的技能都是在"Abilities"下面
在这里插入图片描述
输入标签也是同理
在这里插入图片描述

输入标签的设置是我们在初始的时候设置在技能上的,我们可以通过蓝图设置它的输入
在这里插入图片描述
并在应用技能时,设置在技能实例的动态技能标签中,所以,我们要去技能标签中去获取判断,这种还可以实现如果玩家修改键位了,可以实现不同的键位,前提是你需要把之前默认的删除掉。
在这里插入图片描述
可以获取技能标签和输入标签,还有了蓝图可以绑定的回调,那么,我们就可以去实现回到的函数,首先获取技能标签,然后通过技能标签获取到技能对应的技能数据,并设置技能数据广播出去,完成整个逻辑。

//委托绑定回调匿名函数,委托广播时将会触发函数内部逻辑
	BroadcastDelegate.BindLambda([this](const FGameplayAbilitySpec& AbilitySpec)
	{
		//通过静态函数获取到技能实例的技能标签,并通过标签获取到技能数据
		FRPGAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(URPGAbilitySystemComponent::GetAbilityTagFromSpec(AbilitySpec));
		//获取到技能的输入标签
		Info.InputTag = URPGAbilitySystemComponent::GetInputTagFromSpec(AbilitySpec);
		//广播技能数据
		AbilityInfoDelegate.Broadcast(Info); 
	});

以下是初始化技能应用后的完整回调代码

void UOverlayWidgetController::OnInitializeStartupAbilities(URPGAbilitySystemComponent* RPGAbilitySystemComponent) const
{
	if(!RPGAbilitySystemComponent->bStartupAbilitiesGiven) return; //判断当前技能初始化是否完成,触发回调时都已经完成

	//创建单播委托
	FForEachAbility BroadcastDelegate;
	//委托绑定回调匿名函数,委托广播时将会触发函数内部逻辑
	BroadcastDelegate.BindLambda([this](const FGameplayAbilitySpec& AbilitySpec)
	{
		//通过静态函数获取到技能实例的技能标签,并通过标签获取到技能数据
		FRPGAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(URPGAbilitySystemComponent::GetAbilityTagFromSpec(AbilitySpec));
		//获取到技能的输入标签
		Info.InputTag = URPGAbilitySystemComponent::GetInputTagFromSpec(AbilitySpec);
		//广播技能数据
		AbilityInfoDelegate.Broadcast(Info); 
	});
	//遍历技能并触发委托回调
	RPGAbilitySystemComponent->ForEachAbility(BroadcastDelegate);
}

实现在技能UI上绑定委托回调

完成上面内容,我们可以编译代码打开UE,在UE里面对我们上一篇文章中制作的技能UI进行修改。
打开我们之前创建的WBP_SpellGlobe用户控件,我们需要添加一个标签,用于记录当前的UI需要显示哪个技能,因为我们输入的键位是固定的,需要在实例上面标识这个技能的出入标签。
在这里插入图片描述
接着我们添加逻辑,在控制器设置回调里面,去将控制器实例转换为目标类型,方便后续使用
在这里插入图片描述
然后绑定监听上面的委托回调,在目标委托广播后,将触发后续逻辑,返回一个技能相关数据,我们可以通过判断当前的输入标签和技能数据的输入标签是否一致,如果一致,使用技能数据的技能图标和背景材质更新当前的技能UI。
在这里插入图片描述
接着打开WBP_HealthManaSpells这个用户控件
在这里插入图片描述
我们首先需要在技能ui上面设置它的输入标签,按照对应的输入,设置对应的输入标签。
在这里插入图片描述
还需要一步,就是设置控件的控制器,这样就可以成功触发控制器设置回调
在这里插入图片描述
最后,我们在技能身上设置好对应的技能输入标签和技能标签
在这里插入图片描述
接着运行,查看是否能够在ui上面显示出来。
在这里插入图片描述
查看修改输入标签后是否也能够切换
在这里插入图片描述

处理多人玩法中的bug

如果我们开启两个玩家运行游戏,会发现第二个玩家的ui没有跟着更新。这个原因是因为技能初始化应用完成调用的广播是在服务器执行的,在客户端无法执行
在这里插入图片描述
我们查看源代码,可以看到在ASC里面,存储着当前激活的技能的容器,被修改后,会调用同步函数OnRep_ActivateAbilities
在这里插入图片描述
这个函数是一个虚函数,我们可以复写它,然后在里面调用,去初始化客户端的技能
在这里插入图片描述
我们在自定义的ASC中覆写它

virtual void OnRep_ActivateAbilities() override;

然后在函数内,判断当前是否已经初始化广播,如果没有,则调用广播

void URPGAbilitySystemComponent::OnRep_ActivateAbilities()
{
	Super::OnRep_ActivateAbilities();

	if(!bStartupAbilitiesGiven)
	{
		bStartupAbilitiesGiven = true;
		AbilityGivenDelegate.Broadcast(this);
	}
}

然后发现在客户端也能够顺利初始化技能ui
在这里插入图片描述

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-14 10:40:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-14 10:40:02       71 阅读
  3. 在Django里面运行非项目文件

    2024-07-14 10:40:02       58 阅读
  4. Python语言-面向对象

    2024-07-14 10:40:02       69 阅读

热门阅读

  1. UVA12342 Tax Calculator 题解

    2024-07-14 10:40:02       23 阅读
  2. Font Awesome 图表图标

    2024-07-14 10:40:02       27 阅读
  3. c++课后作业

    2024-07-14 10:40:02       24 阅读
  4. k8s开启deopos功能报错总结

    2024-07-14 10:40:02       25 阅读
  5. 【机器学习】ChatGLM2-6B 分词器 Tokenizer 的使用

    2024-07-14 10:40:02       35 阅读
  6. PHP文字ocr识别接口示例、人工智能的发展

    2024-07-14 10:40:02       23 阅读
  7. 等保测评是做什么的

    2024-07-14 10:40:02       20 阅读
  8. Ubuntu 22.04.4 LTS (linux) 使用shc 加密 shell script

    2024-07-14 10:40:02       22 阅读
  9. 流程循环控制语句

    2024-07-14 10:40:02       24 阅读