在上一篇文章里,我们创建了技能面板的控制器,接下来,我们将实现通过控制器绑定委托,来更新显示内容。
更新技能面板应用的技能
我们首先更新技能面板上面已经应用的技能,让其和WBP_Overlay上面一样,可以更新显示。
由于我们当前面板使用的是升级技能的UI按钮,所以,我们需要创建一个新的按钮,还能够通过点击按钮取消应用的技能。
复制一个按钮用户控件
将应用技能UI里的对应节点替换掉
在WBP_SpellMenuWidget里面,设置应用控制器,它们共同使用技能面板控制器SpellMenuWidgetController
然后,设置初始化
接着,我们在应用技能用户控件里,对每个按钮进行设置控制器
在按钮里面,我们首先将其转换为技能面板控制器
然后增加一个标签,用于设置技能的输入标签
被动技能没有对应的输入标签,我们创建两个被动技能标签用于识别
FGameplayTag InputTag_Passive_1; //被动技能1FGameplayTag InputTag_Passive_2; //被动技能2
注册
GameplayTags.InputTag_Passive_1 = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("InputTag.Passive.1"),FString("被动技能1"));GameplayTags.InputTag_Passive_2 = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("InputTag.Passive.2"),FString("被动技能2"));
在应用技能的组件里,设置其对应的标签
然后,在按钮里绑定技能事件,通过判断数据标签是否一致,来设置显示的笔刷
在其未设置技能时,我们将图标和背景设置为透明
在初始化构建是,我们去调用此函数
由于设置了笔刷半透明,而之前回调更新图标和背景的节点都是更新笔刷的,所以,我们要修改设置,在应用时创建一个新的笔刷
运行查看技能应用是否正确。
设置技能状态
接下来,我们将实现技能相关功能,在实现功能之前,我们将创建每个技能的状态Ability Status,它将分为:
- Locked已锁定,技能图标显示锁的状态
- Eligible 可解锁,当前技能可以通过技能点解锁,但处于未解锁状态,将显示技能图标
- Unlocked 已解锁,当前技能已通过技能点解锁,背景将被替换掉
- Equipped 已装配,当前技能已经被装配到技能栏,样式和已解锁没有区别
相应的,我们将创建对应的标签,通过标签来识别当前技能所处的状态。
我们还需要对技能的类型Ability Type进行识别:
它可以是主动技能,被动技能,以及空状态(受击技能,死亡技能,没错,它们也可以做成技能)
我们首先创建对应的标签
FGameplayTag Abilities_HitReact; //受击技能标签FGameplayTag Abilities_Status_Locked; //技能状态 已锁定FGameplayTag Abilities_Status_Eligible; //技能状态 可解锁FGameplayTag Abilities_Status_Unlocked; //技能状态 已解锁FGameplayTag Abilities_Status_Equipped; //技能状态 已装配FGameplayTag Abilities_Type_Offensive; //技能类型 主动技能FGameplayTag Abilities_Type_Passive; //技能类型 被动技能FGameplayTag Abilities_Type_None; //技能类型 空 受击等技能设置
然后在函数中注册到标签管理器
void FRPGGameplayTags::InitializeGAGameplayTags()
{/** Abilities 技能标签*/GameplayTags.Abilities_Attack = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.Attack"),FString("攻击技能标签"));GameplayTags.Abilities_Summon = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.Summon"),FString("召唤技能标签"));GameplayTags.Abilities_Fire_FireBolt = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.Fire.FireBolt"),FString("火球术技能标签"));/** 空类型技能标签*/GameplayTags.Abilities_HitReact = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.HitReact"),FString("受击技能标签"));/** 当前技能状态标签*/GameplayTags.Abilities_Status_Locked = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.Status.Locked"),FString("已锁定"));GameplayTags.Abilities_Status_Eligible = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.Status.Eligible"),FString("可解锁"));GameplayTags.Abilities_Status_Unlocked = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.Status.Unlocked"),FString("已解锁"));GameplayTags.Abilities_Status_Equipped = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.Status.Equipped"),FString("已装配"));/** 当前技能类型标签*/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("啥也不是"));}
还有一种添加标签的方式,就是使用宏的方式,我们在RPG.h文件里,来定义需要使用的标签
#define Abilities_Status_Locked FGameplayTag::RequestGameplayTag(FName("Abilities.Status.Locked"))
#define Abilities_Status_Equipped FGameplayTag::RequestGameplayTag(FName("Abilities.Status.Equipped"))
然后在使用时,直接使用定义的名称
接着,我们在ASC里增加一个用于获取技能的状态的函数
static FGameplayTag GetStatusTagFromSpec(const FGameplayAbilitySpec& AbilitySpec); //获取技能的状态标签
遍历动态标签容器,获取标签
FGameplayTag URPGAbilitySystemComponent::GetStatusTagFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{for(FGameplayTag Tag : AbilitySpec.DynamicAbilityTags) //从技能实例的动态标签容器中遍历所有标签{if(Tag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Abilities.Status")))) //查找标签中是否设置以输入标签开头的标签{return Tag;}}return FGameplayTag();
}
给技能面板主动技能增加状态修改
我们上面增加了状态的对应的标签,接下来,我们将实现对技能的状态修改,并能够显示到技能框上面。
首先我们对技能按钮增加一个参数,可以设置当前按钮需要显示的技能
接着在WBP_OffensiveSpellTreel里对按钮设置应显示的技能标签
之前,我们已经对WBP_OffensiveSpellTree设置了控制器,在WBP_OffensiveSpellTree的事件里面,我们对每个按钮设置控制器
在WBP_SpellGlobe_Button里,我们在设置控制器回调后,将控制器转换为技能面板控制器,方便后续使用
由于技能数据里没有对应的数据状态标签,我们需要在结构体里增加对技能状态标签的设置
然后在广播时,我们通过代码获取它的状态标签,并设置到数据上
接下来,我们在WBP_SpellGlobe_Button里添加数据委托的监听,并调用函数
在数据处理函数里,我们首先判断技能标签和设置在按钮上的标签是否一致,然后根据状态去修改ui显示的图标和背景
设置状态笔刷的函数里面是将对应的传入的图片和材质应用
最后我们测试查看效果
在代码修改查看另外的效果
设置根据等级更新技能状态
我们实现了更新技能状态的UI,接下来,我们实现可以根据角色的等级去修改技能的状态
为了实现此功能,我们需要在AbilityInfo里增加两个配置项,一个是当前技能解锁角色等级,另一个是技能类
//解锁技能所需角色的等级UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)int32 LevelRequirement;//当前技能使用的技能类UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)TSubclassOf<UGameplayAbility> Ability;
为了能够在服务器上获取到技能数据,我们在GameMode类里增加技能相关配置数据的参数
接着在蓝图函数库里增加一个获取技能数据的函数
//获取玩家角色技能配置数据UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|CharacterClassDefaults", meta=(DefaultToSelf = "WorldContextObject"))static UAbilityInfo* GetAbilityInfo(const UObject* WorldContextObject);
实现是获取到当前客户端GameMode,并从中获取角色技能的配置项数据
UAbilityInfo* URPGAbilitySystemBlueprintLibrary::GetAbilityInfo(const UObject* WorldContextObject)
{//获取到当前关卡的GameMode实例const ARPGGameMode* GameMode = Cast<ARPGGameMode>(UGameplayStatics::GetGameMode(WorldContextObject));if(GameMode == nullptr) return nullptr;//返回关卡的角色的配置return GameMode->AbilityInfo;
}
接下来编译,我们可以在技能的数据资产中设置对应的数据
然后在GameMode蓝图中设置对应的数据
为了方便测试,我们增加一个额外的技能,这个技能需要两级解锁,我们增加一个新的技能
这个技能是雷系的技能,雷击
在技能里,我们先链接调试代码,后续对其进行实现
技能还没有对应的技能标签,我们在代码中新增一个
FGameplayTag Abilities_Lightning_Electrocute; //雷击技能标签
GameplayTags.Abilities_Lightning_Electrocute = UGameplayTagsManager::Get().AddNativeGameplayTag(FName("Abilities.Lightning.Electrocute"),FString("电击技能标签"));
设置技能对应的内容
在技能面板的一个按钮上设置对应的技能标签
接下来,我们要实现根据角色等级更新,我们要遍历技能数据,并判断角色是否拥有。
所以我们先实现一个通过标签获取角色身上的技能实例
FGameplayAbilitySpec* GetSpecFromAbilityTag(const FGameplayTag& AbilityTag); //通过技能标签获取技能实例
在实现这里,我们需要先获取所有激活的技能实例,根据标签判断是否包含相同的,防止多线程出现问题,我们使用了域锁
FGameplayAbilitySpec* URPGAbilitySystemComponent::GetSpecFromAbilityTag(const FGameplayTag& AbilityTag)
{FScopedAbilityListLock ActiveScopeLoc(*this); //域锁//遍历已经应用的技能for(FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities()){for(FGameplayTag Tag : AbilitySpec.Ability.Get()->AbilityTags){if(Tag.MatchesTag(AbilityTag)){return &AbilitySpec;}}}return nullptr;
}
接着增加根据角色等级更新技能状态
void UpdateAbilityStatuses(int32 Level); //根据角色等级更新技能数据状态
然后在实现这里,我们对技能数据遍历,判断角色是否拥有此技能,如果未拥有,我们将创建一个技能实例应用到角色的ASC上。
void URPGAbilitySystemComponent::UpdateAbilityStatuses(const int32 Level)
{//从GameMode获取到技能配置数据UAbilityInfo* AbilityInfo = URPGAbilitySystemBlueprintLibrary::GetAbilityInfo(GetAvatarActor());for(const FRPGAbilityInfo& Info : AbilityInfo->AbilityInformation){if(!Info.AbilityTag.IsValid()) continue; //如果没有技能标签,取消执行if(Level < Info.LevelRequirement) continue; //如果当期等级未达到所需等级,取消执行//判断ASC中是否已存在当前技能实例if(GetSpecFromAbilityTag(Info.AbilityTag) == nullptr){//如果没有技能实例,将应用一个新的技能实例FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(Info.Ability, 1);AbilitySpec.DynamicAbilityTags.AddTag(FRPGGameplayTags::Get().Abilities_Status_Eligible);GiveAbility(AbilitySpec);MarkAbilitySpecDirty(AbilitySpec); //设置当前技能立即复制到每个客户端}}
}
接下来,我们将在角色升级时,去更新技能的状态,在玩家角色类里的提升等级函数中增加对技能数据状态的调用
void ARPGHero::AddToPlayerLevel_Implementation(int32 InPlayerLevel)
{ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();check(PlayerStateBase); //检测是否有效,无限会暂停游戏PlayerStateBase->AddToLevel(InPlayerLevel);if(URPGAbilitySystemComponent* RPGASC = Cast<URPGAbilitySystemComponent>(GetAbilitySystemComponent())){RPGASC->UpdateAbilityStatuses(PlayerStateBase->GetPlayerLevel());}
}
我们还要在自定义ASC增加一个委托用于广播新状态标签
DECLARE_MULTICAST_DELEGATE_TwoParams(FAbilityStatusChanged, const FGameplayTag& /*技能标签*/, const FGameplayTag& /*技能状态标签*/);
在ASC中增加委托参数
FAbilityStatusChanged AbilityStatusChanged; //技能状态更新委托
ASC是在服务器运行,我们再增加一个客户端执行的函数,用于广播到每个客户端
UFUNCTION(Client, Reliable)void ClientUpdateAbilityStatus(const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag); //技能状态更新后回调
在函数内对委托进行广播
void URPGAbilitySystemComponent::ClientUpdateAbilityStatus_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag)
{AbilityStatusChanged.Broadcast(AbilityTag, StatusTag);
}
接着在之前更新技能数据状态的函数中,如果需要更新,那么,我们对数据进行广播
接着在技能面板控制器中,绑定对技能状态修改后的回调,并将数据广播给UI
void USpellMenuWidgetController::BindCallbacksToDependencies()
{//绑定技能状态修改后的委托回调GetRPGASC()->AbilityStatusChanged.AddLambda([this](const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag){if(AbilityInfo){FRPGAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag); //获取到技能数据Info.StatusTag = StatusTag;AbilityInfoDelegate.Broadcast(Info);}});
}
运行查看效果。
如果在技能面板打开的情况下升级,那么技能将通过委托去更新状态数据,如果在关闭状态,升级时,那么技能的状态标签将会更新到技能实例的动态标签里,然后在开启技能面板时,通过技能标签去更新图标的状态。
更新所拥有的技能点显示
接下来,我们实现对技能点的显示更新,在技能面板中正确显示玩家角色所拥有的技能点数。
我们在控制器基类里有对玩家状态更新声明的宏
我们在技能面板控制器里,增加一个参数声明
UPROPERTY(BlueprintAssignable)FOnPlayerStateChangedSignature SpellPointChanged;
在初始化时,广播技能点
接下来就是在变动时,监听更新变动,在PlayerState上,我们拥有了技能点数变动的委托
那么,在绑定更改函数里,我们绑定对变动后的监听即可,并在函数内广播给UI
//绑定技能点变动回调GetRPGPS()->OnSpellPointsChangedDelegate.AddLambda([this](const int32 SpellPoints){SpellPointChanged.Broadcast(SpellPoints); //广播拥有的技能点});
代码实现完毕,接着我们就是修改UI,将显示技能点的UI设置成变量
然后给控件增加一个函数,更新它的值
在调用初始化之前,我们监听变动,并调用函数更新值,这样在初始化时,就能准确获取到值进行更新。
接着运行测试,打开技能面板显示默认一点技能点
然后杀怪升到三级,奖励给了我们两点技能点