前面两篇,我们实现了制作属性面板的样式,并增加了打开和关闭属性面板的按钮。接下来,我们要实现在属性面板显示属性,并且在属性更新时,属性面板同步更新。
在开始之前,先回顾一下之前是如何实现ui上面属性绑定的(之前血量和蓝量都实现了)
我们抽离出了一个Controller作为数据和UI的中间层,用于它们的数据交互。在Controller中,创建对应属性的委托,我们创建的widget基类里面可以设置使用的Controller,Controller也是一个单例,在widget上设置Controller还有事件回调,我们在回调里绑定了
图下为给血和蓝的ui设置Controller
下图为,在设置了Controller回调里面,通过Controller的委托监听属性值的变化
那如何实现的委托呢,我们在OverlayWidgetController里面实现了委托宏
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAttributeChangedSignature, float, NewAttribute);
然后我们创建了对应的委托
virtual void BroadcastInitialValues() override;
virtual void BindCallbacksToDependencies() override;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnHealthChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnMaxHealthChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnManaChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnMaxManaChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
FMessageWidgetRowSignature MessageWidgetRowDelegate;
然后我们使用了AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute())函数,获取到对属性监听的委托,然后绑定一个匿名函数,在匿名函数里面触发我们自定义的委托。从而实现了对属性的监听。
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddLambda([this](const FOnAttributeChangeData& Data){OnHealthChanged.Broadcast(Data.NewValue);});
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetMaxHealthAttribute()).AddLambda([this](const FOnAttributeChangeData& Data){OnMaxHealthChanged.Broadcast(Data.NewValue);});
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetManaAttribute()).AddLambda([this](const FOnAttributeChangeData& Data){OnManaChanged.Broadcast(Data.NewValue);});
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetMaxManaAttribute()).AddLambda([this](const FOnAttributeChangeData& Data){OnMaxManaChanged.Broadcast(Data.NewValue);});
这里我们看到了,如果属性多了,我们代码增加太多,而且不利于维护,增加或修改一个属性需要修改的地方太多,所以,我们需要一种新的方式去实现对属性变化的监听。
我们通过ASC去实现对属性的监听,然后在Controller里面我们不再去单独的广播一个属性,而是在有属性修改时,委托就会触发,将变动的属性也一并广播出去。在Widget里面,我们可以通过监听对应的属性实现对属性的监听,委托的广播,不单单可以将数值传递,还可以传递结构体,所以,我们可以传递足够多的数据,接下来我们解析一下实现方式。
所以我们需要哪些内容:
- 我们首先需要一个AttributeMenuWIdgetController来接受来自ASC的广播
- 接着,我们需要一种逻辑来实现对来自广播的数据进行检索,知道去更新哪个属性。这里我们通过标签匹配去实现
- 为了实现上面的步骤,我们需要实现能够在c++和UE里面都能够获取的标签方式。
- 我们需要创建数据列表,获取到匹配的标签后,拿到对应的数据和数值,一并提交给Widget.
- 在Widget里面,可以自行根据标签来更新数据。
实现步骤
- 首先我们需要通过c++去实现创建GameplayTag,这样可以在c++和UE里同时获取到Tag
- 创建一个DataAsset类,用于设置tag对应的属性和显示内容
- 创建AttributeMenuWidgetController实现对应逻辑
接下来,我们将按照对应的步骤一步步的实现。
创建GameplayTags单例
因为需要在C++里面和UE里面都会用到GameplayTags,这里推荐直接使用C++创建GameplayTags。接下来,我将在c++里实现对GameplayTags的创建。
首先在Public根目录创建一个类,不继承任何父类
创建完成,关闭UE,可以看到编辑器内的文件
我们将代码删除掉,创建一个结构体类,将其制作一个单例,在.h文件内我们增加静态Get函数,用于从类上面获取单例,然后创建了一个InitializeNativeGameplayTags()用于初始化内部的Tag标签,最后创建了一个属性GameplayTags用于存储单例。
// 版权归暮志未晚所有。
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
/**
* GameplayTags 标签 单例模式
* 内部包含原生的项目中使用的标签
*/
struct FMyGameplayTags
{
public:
static const FMyGameplayTags& Get() { return GameplayTags; }
static void InitializeNativeGameplayTags();
protected:
private:
static FMyGameplayTags GameplayTags;
};
在cpp文件内,我们首先初始化了单例对象,然后实现初始化标签Tag,这里先增加了一个次级属性进行测试。
// 版权归暮志未晚所有。
#include "MyGameplayTags.h"
#include "GameplayTagsManager.h"
FMyGameplayTags FMyGameplayTags::GameplayTags;
void FMyGameplayTags::InitializeNativeGameplayTags()
{
UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Attributes.Secondary.Armor"), FString("减少受到的伤害,提高格挡率"));
}
我们创建完成了此类,它属于资源类型的类,我们需要在资源管理器内对其进行调用,这样在资源加载时,就可以对标签进行一次初始化。接下来,我们将自定义一个资源管理器类
创建自定义资源管理器
那么我们需要创建一个自己的AssetManager,打开UE,在Public目录创建一个继承AssetManager的子类
创建了此类,我们首先增加一个静态的Get函数,用于获取它的单例。
static UMyAssetManager& Get();
在实现这里,我们要先判断引擎,然后通过引擎获取到资源管理器的单例,获取到的是对其的引用,我们返回时需要加*来返回它的实例。
UMyAssetManager& UMyAssetManager::Get()
{
check(GEngine);
UMyAssetManager* MyAssetManager = Cast<UMyAssetManager>(GEngine->AssetManager);
return *MyAssetManager;
}
接着,我们覆盖其父类的StartInitialLoading()函数,在内部增加对自定义的标签FMyGameplayTags处理。
virtual void StartInitialLoading() override;
在实现这里,我们在其内部进行FMyGameplayTags初始化。
void UMyAssetManager::StartInitialLoading()
{
Super::StartInitialLoading();
FMyGameplayTags::InitializeNativeGameplayTags();
}
现在,只需要将项目的资源管理器从默认的替换为我们自定义的就能够实现对我们在c++中定义标签的实现。第一种方式,就是我们打开引擎,在项目的一般设置这里,修改
另一种方式是直接在文件内修改,它可以在Config文件夹内的DefaultEngine.ini文件中修改
在[/Script/Engine.Engine]配置项下,增加设置资源管理器的类名称,注意项目名称要写对。个人推荐第一种。
打开编辑器,如果发现我们创建的那一项tag存在,证明修改成功
记得将之前创建的MaxHealth和MaxMana删除掉,因为这两项属于次级属性,我们接下来将在c++中重新定义它
之前我们使用列表创建的主要属性也记得删除掉,因为我们也需要在c++中重新定义它,这样标签可以在c++中使用
就此,完成了对自定义资源管理器的创建
// 版权归暮志未晚所有。
#pragma once
#include "CoreMinimal.h"
#include "Engine/AssetManager.h"
#include "MyAssetManager.generated.h"
/**
*
*/
UCLASS()
class AURA_API UMyAssetManager : public UAssetManager
{
GENERATED_BODY()
public:
static UMyAssetManager& Get();
protected:
virtual void StartInitialLoading() override;
};
// 版权归暮志未晚所有。
#include "MyAssetManager.h"
#include "MyGameplayTags.h"
UMyAssetManager& UMyAssetManager::Get()
{
check(GEngine);
UMyAssetManager* MyAssetManager = Cast<UMyAssetManager>(GEngine->AssetManager);
return *MyAssetManager;
}
void UMyAssetManager::StartInitialLoading()
{
Super::StartInitialLoading();
FMyGameplayTags::InitializeNativeGameplayTags();
}
测试标签
我们前面实现了在C++中设置标签,现在对效果进行一下测试,首先,我们在FMyGameplayTags创建一个标签变量,可以将创建的标签存储下来
FGameplayTag Attributes_Secondary_Armor;
然后在初始化时,添加标签会返回它的实力,我们直接设置即可。注意,在静态函数内给静态变量设置需要引用。
FMyGameplayTags FMyGameplayTags::GameplayTags;
void FMyGameplayTags::InitializeNativeGameplayTags()
{
GameplayTags.Attributes_Secondary_Armor = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Attributes.Secondary.Armor"), FString("减少受到的伤害,提高格挡率"));
}
这样,我们就可以从它的单例身上获取到护甲的Tag标签了。接下来,我们找个地方测试,就找ASC被设置完成后的回调里面。
我们只需要获取到类,然后通过类获取到实例,然后通过 FMyGameplayTags::Get().Attributes_Secondary_Armor就可以获取到标签对象。
接着我们直接在角色应用了ASC后,会触发AbilityActorInfoSet(),那么就会打印Armor的字符串
void UAbilitySystemComponentBase::AbilityActorInfoSet()
{
OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAbilitySystemComponentBase::EffectApplied);
//测试
const FMyGameplayTags& GameplayTags = FMyGameplayTags::Get();
//GameplayTags.Attributes_Secondary_Armor.ToString() //标签的文本
GEngine->AddOnScreenDebugMessage(
-1,
10.f,
FColor::Blue,
FString::Printf(TEXT("Tag: %s"), *GameplayTags.Attributes_Secondary_Armor.ToString())
);
}
然后运行测试,只要能打印出来,就证明没问题,打印5个的原因是因为场景character有5个,他们每个都会调用一次那个方法。
使用C++创建所需标签
上面,我对使用C++添加标签进行了测试,并实现了对标签的添加。这种方式添加的标签,我们不但可以在UE里面使用,还可以在C++里面直接获取。在这里,我再展示一下如何实现一个属性Tag的添加。
首先声明一个变量,存储标签
FGameplayTag Attributes_Secondary_MaxHealth;
然后在InitializeNativeGameplayTags()函数内,将标签通过标签管理器添加到项目中
GameplayTags.Attributes_Secondary_MaxHealth = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.MaxHealth"),
FString("减少受到的伤害,提高格挡率")
);
UGameplayTagsManager::Get()是获取标签管理器的单例,AddNativeGameplayTag函数是添加标签,当然,标签你要写对,第二个参数是对应的描述。
接下来,我把编写好的代码复制一下
// 版权归暮志未晚所有。
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
/**
* GameplayTags 标签 单例模式
* 内部包含原生的项目中使用的标签
*/
struct FMyGameplayTags
{
public:
static const FMyGameplayTags& Get() { return GameplayTags; }
static void InitializeNativeGameplayTags();
/*
* Primary Attributes
*/
FGameplayTag Attributes_Primary_Strength;
FGameplayTag Attributes_Primary_Intelligence;
FGameplayTag Attributes_Primary_Resilience;
FGameplayTag Attributes_Primary_Vigor;
/*
* Secondary Attributes
*/
FGameplayTag Attributes_Secondary_MaxHealth;
FGameplayTag Attributes_Secondary_MaxMana;
FGameplayTag Attributes_Secondary_Armor;
FGameplayTag Attributes_Secondary_ArmorPenetration;
FGameplayTag Attributes_Secondary_BlockChance;
FGameplayTag Attributes_Secondary_CriticalHitChance;
FGameplayTag Attributes_Secondary_CriticalHitDamage;
FGameplayTag Attributes_Secondary_CriticalHitResistance;
FGameplayTag Attributes_Secondary_HealthRegeneration;
FGameplayTag Attributes_Secondary_ManaRegeneration;
protected:
private:
static FMyGameplayTags GameplayTags;
};
// 版权归暮志未晚所有。
#include "MyGameplayTags.h"
#include "GameplayTagsManager.h"
FMyGameplayTags FMyGameplayTags::GameplayTags;
void FMyGameplayTags::InitializeNativeGameplayTags()
{
/*
* Primary Attributes
*/
GameplayTags.Attributes_Primary_Strength = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Primary.Strength"),
FString("Increases physical damage")
);
GameplayTags.Attributes_Primary_Intelligence = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Primary.Intelligence"),
FString("Increases magical damage")
);
GameplayTags.Attributes_Primary_Resilience = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Primary.Resilience"),
FString("Increases Armor and Armor Penetration")
);
GameplayTags.Attributes_Primary_Vigor = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Primary.Vigor"),
FString("Increases Health")
);
/*
* Secondary Attributes
*/
GameplayTags.Attributes_Secondary_MaxHealth = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.MaxHealth"),
FString("Maximum amount of Health obtainable")
);
GameplayTags.Attributes_Secondary_MaxMana = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.MaxMana"),
FString("Maximum amount of Mana obtainable")
);
GameplayTags.Attributes_Secondary_Armor = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.Armor"),
FString("Reduces damage taken, improves Block Chance")
);
GameplayTags.Attributes_Secondary_ArmorPenetration = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.ArmorPenetration"),
FString("Ignored Percentage of enemy Armor, increases Critical Hit Chance")
);
GameplayTags.Attributes_Secondary_BlockChance = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.BlockChance"),
FString("Chance to cut incoming damage in half")
);
GameplayTags.Attributes_Secondary_CriticalHitChance = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitChance"),
FString("Chance to double damage plus critical hit bonus")
);
GameplayTags.Attributes_Secondary_CriticalHitDamage = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitDamage"),
FString("Bonus damage added when a critical hit is scored")
);
GameplayTags.Attributes_Secondary_CriticalHitResistance = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitResistance"),
FString("Reduces Critical Hit Chance of attacking Enemies")
);
GameplayTags.Attributes_Secondary_HealthRegeneration = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.HealthRegeneration"),
FString("Amount of Health regenerated every 1 second")
);
GameplayTags.Attributes_Secondary_ManaRegeneration = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Attributes.Secondary.ManaRegeneration"),
FString("Amount of Mana regenerated every 1 second")
);
}
接着编译打开UE,打开标签管理器,查看是否有问题
到这里,我们就实现了所有属性对应标签的创建。有了标签,我们接下来就可以创建DataAsset类,在类里填充数据,将Tag标签和对应的属性对应起来,后面我们就可以根据标签去获取对应的数据显示到UI面板上面。