在上一篇文章里,我们创建了技能的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