在上一篇里,我们编辑完成了技能的UI面板。按照MVC的思路,我们还需要制作它的控制器,用于处理UI上面的交互。
到现在为止,我们控制器已经有了三种,首先是我们创建的控制器基类RPGWidgetController.h,为所有控制器需要继承的类,里面有一些公用的函数和属性。通过基类我们实现三种控制器:
- 用于显示在战斗场景界面的UI的控制器OverlayWidgetController.h,主要显示角色等级血量技能等的状态
- 用于展现角色属性面板的UI的控制器AttributeMenuWidgetController.h,主要用于显示角色属性值和属性加点功能
- 还有我们在这篇文章里要创建的技能面板的UI的控制器SpellMenuWidgetController.h,用于设置玩家的技能等级,技能键位等操作。
我们接下来,会先创建技能面板控制器,然后整理代码,讲通用的部分整理到基类里,并优化一些功能。
创建技能面板控制器
我们基于控制器基类创建一个新的控制器子类
然后在类里继承覆写基类的两个函数,它们分别为初始化广播数据,用于在创建时,显示角色当前的信息,另一个是在属性变动时的委托广播。
并且我们要设置标识,让其可以作为蓝图中转换的类和蓝图继承的类
UCLASS(BlueprintType, Blueprintable)
class RPG_API USpellMenuWidgetController : public URPGWidgetController
{
GENERATED_BODY()
public:
virtual void BindCallbacksToDependencies() override;
virtual void BroadcastInitialValues() override;
};
Hud类里添加设置技能面板控制器
我们是在自定义的HUD里面设置UI的蓝图类,并有配置项去配置使用的控制器,我们接着讲技能面板的控制器添加进去。
按照之前,我们首先创建两个属性,一个用于设置使用的技能面板控制器类,一个用于存储类的实例,防止多次创建
UPROPERTY()
TObjectPtr<USpellMenuWidgetController> SpellMenuWidgetController;
UPROPERTY(EditAnywhere)
TSubclassOf<USpellMenuWidgetController> SpellMenuWidgetControllerClass;
然后我们增加一个用于获取技能面板控制器的函数
USpellMenuWidgetController* GetSpellMenuWidgetController(const FWidgetControllerParams& WCParams);
在实现时,首先判断当前是否被实例过,如果实例过直接返回,没有则实例一次
USpellMenuWidgetController* ARPGHUD::GetSpellMenuWidgetController(const FWidgetControllerParams& WCParams)
{
if(SpellMenuWidgetController == nullptr)
{
SpellMenuWidgetController = NewObject<USpellMenuWidgetController>(this, SpellMenuWidgetControllerClass);
SpellMenuWidgetController->SetWidgetControllerParams(WCParams);
SpellMenuWidgetController->BindCallbacksToDependencies(); //绑定监听数值变化
}
return SpellMenuWidgetController;
}
创建获取RPG类型的变量
随着子类增多,我们每次切换类型去获取自定义的ASC PS PC AS的次数增多,还影响代码阅读,所以,我们在控制器基类增加对自定义的类型的获取。
首先,我们增加四个变量,用于存储对应的类型
UPROPERTY()
TObjectPtr<ARPGPlayerController> RPGPlayerController;
UPROPERTY()
TObjectPtr<ARPGPlayerState> RPGPlayerState;
UPROPERTY()
TObjectPtr<URPGAbilitySystemComponent> RPGAbilitySystemComponent;
UPROPERTY()
TObjectPtr<URPGAttributeSet> RPGAttributeSet;
然后,我们增加四个函数,用于获取自定义的参数
ARPGPlayerController* GetRPGPC();
ARPGPlayerState* GetRPGPS();
URPGAbilitySystemComponent* GetRPGASC();
URPGAttributeSet* GetRPGAS();
然后就是函数实现,在实现函数里,我们首先判断当前的变量是否设置的值,如果没有,那么将类型转换,并返回
ARPGPlayerController* URPGWidgetController::GetRPGPC()
{
if(RPGPlayerController == nullptr)
{
RPGPlayerController = Cast<ARPGPlayerController>(PlayerController);
}
return RPGPlayerController;
}
ARPGPlayerState* URPGWidgetController::GetRPGPS()
{
if(RPGPlayerState == nullptr)
{
RPGPlayerState = Cast<ARPGPlayerState>(PlayerState);
}
return RPGPlayerState;
}
URPGAbilitySystemComponent* URPGWidgetController::GetRPGASC()
{
if(RPGAbilitySystemComponent == nullptr)
{
RPGAbilitySystemComponent = Cast<URPGAbilitySystemComponent>(AbilitySystemComponent);
}
return RPGAbilitySystemComponent;
}
URPGAttributeSet* URPGWidgetController::GetRPGAS()
{
if(RPGAttributeSet == nullptr)
{
RPGAttributeSet = Cast<URPGAttributeSet>(AttributeSet);
}
return RPGAttributeSet;
}
转换完成后,编译,发现子类使用的一些变量名称出现的冲突
我们在子类里面可以将转换类型的内容删除,并直接调用函数获取
将技能应用的广播修改到基类
由于技能的更新在UOverlayWidgetController里面和USpellMenuWidgetController里面都需要应用,所以我们将属性对应内容的委托函数移动到基类里
首先,我们将委托定义移动到基类
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityInfoSignature, const FRPGAbilityInfo, Info); //技能更新UI回调
将技能表格数据定义也设置到基类
//技能的表格数据
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Widget Data")
TObjectPtr<UAbilityInfo> AbilityInfo;
然后将委托变量移动到基类,并创建一个绑定委托的函数
UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
FAbilityInfoSignature AbilityInfoDelegate; //技能数据更新委托回调
void BroadcastAbilityInfo(); //广播技能信息
然后将实现内容移动到函数中
void URPGWidgetController::BroadcastAbilityInfo()
{
if(!GetRPGASC()->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);
});
//遍历技能并触发委托回调
GetRPGASC()->ForEachAbility(BroadcastDelegate);
}
在OverlayWidgetController.cpp里面,我们修改绑定函数
//绑定ASC相关委托的回调
if(GetRPGASC())
{
if(GetRPGASC()->bStartupAbilitiesGiven)
{
//如果执行到此处时,技能的初始化工作已经完成,则直接调用初始化回调
BroadcastInitialValues();
}
else
{
//如果执行到此处,技能初始化还未完成,将通过绑定委托,监听广播的形式触发初始化完成回调
GetRPGASC()->AbilityGivenDelegate.AddUObject(this, &ThisClass::BroadcastInitialValues);
}
由于技能绑定我们函数不需要传入ASC,所以,我们在自定义ASC里,将返回一个参数取消,并修改对应参数
DECLARE_MULTICAST_DELEGATE(FAbilityGiven) //技能初始化应用后的回调委托
在蓝图函数库增加获取控制器函数
和获取其它函数一致,我们增加一个函数用于获取技能面板控制器
//获取属性面板的控制器
UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|WidgetController")
static USpellMenuWidgetController* GetSpellMenuWidgetController(const UObject* WorldContextObject);
实现也和其它函数一致
USpellMenuWidgetController* URPGAbilitySystemBlueprintLibrary::GetSpellMenuWidgetController(const UObject* WorldContextObject)
{
//获取到playerController, 需要传入一个世界空间上下文的对象,用于得到对应世界中的PC列表,0为本地使用的PC
if(APlayerController* PC = UGameplayStatics::GetPlayerController(WorldContextObject, 0))
{
//从PC获取到HUD,我们就可以从HUD获得对应的Controller
if(ARPGHUD* HUD = Cast<ARPGHUD>(PC->GetHUD()))
{
ARPGPlayerState* PS = PC->GetPlayerState<ARPGPlayerState>();
UAbilitySystemComponent* ASC = PS->GetAbilitySystemComponent();
UAttributeSet* AS = PS->GetAttributeSet();
const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS);
return HUD->GetSpellMenuWidgetController(WidgetControllerParams);
}
}
return nullptr;
}
在这里,你会发现这几个获取控制器的代码的重复度很高,基本都是为了获取到对应的数据以后,通过HUD函数来获取对应的控制器,接下来,我们将简化代码,将相同的部分修改为一个通用的函数。
通用函数需要获取到两项内容,HUD对象和创建控制器所需的FWidgetControllerParams 参数,所以,我们创建一个函数用于返回FWidgetControllerParams参数,并在参数里返回HUD。
这个函数,返回一个布尔类型,用于判断参数是否获取成功,并传入了所需值的地址引用,可以修改它的地址,将外部的值修改掉。
这里注意我们设置的元数据,meta=(DefaultToSelf = "WorldContextObject")
,这样即使我们不设置它,也会有默认值就是自身
UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|WidgetController", meta=(DefaultToSelf = "WorldContextObject"))
static bool MakeWidgetControllerParams(const UObject* WorldContextObject, FWidgetControllerParams& OutWcParams, ARPGHUD*& OutRPGHUD);
在实现这里,我们还是同样的获取参数,去修改传入的参数
bool URPGAbilitySystemBlueprintLibrary::MakeWidgetControllerParams(const UObject* WorldContextObject, FWidgetControllerParams& OutWcParams, ARPGHUD*& OutRPGHUD)
{
//获取到playerController, 需要传入一个世界空间上下文的对象,用于得到对应世界中的PC列表,0为本地使用的PC
if(APlayerController* PC = UGameplayStatics::GetPlayerController(WorldContextObject, 0))
{
//从PC获取到HUD,我们就可以从HUD获得对应的Controller
if(ARPGHUD* HUD = Cast<ARPGHUD>(PC->GetHUD()))
{
OutRPGHUD = HUD; //修改指针的引用
ARPGPlayerState* PS = PC->GetPlayerState<ARPGPlayerState>();
//设置参数
OutWcParams.PlayerController = PC;
OutWcParams.PlayerState = PS;
OutWcParams.AbilitySystemComponent = PS->GetAbilitySystemComponent();
OutWcParams.AttributeSet = PS->GetAttributeSet();
return true;
}
}
return false;
}
接着,我们将其它获取控制器的函数都修改掉
UOverlayWidgetController* URPGAbilitySystemBlueprintLibrary::GetOverlayWidgetController(const UObject* WorldContextObject)
{
FWidgetControllerParams WCParams;
ARPGHUD* HUD = nullptr;
if(MakeWidgetControllerParams(WorldContextObject, WCParams, HUD))
{
return HUD->GetOverlayWidgetController(WCParams);
}
return nullptr;
}
UAttributeMenuWidgetController* URPGAbilitySystemBlueprintLibrary::GetAttributeMenuWidgetController(const UObject* WorldContextObject)
{
FWidgetControllerParams WCParams;
ARPGHUD* HUD = nullptr;
if(MakeWidgetControllerParams(WorldContextObject, WCParams, HUD))
{
return HUD->GetAttributeMenuWidgetController(WCParams);
}
return nullptr;
}
USpellMenuWidgetController* URPGAbilitySystemBlueprintLibrary::GetSpellMenuWidgetController(const UObject* WorldContextObject)
{
FWidgetControllerParams WCParams;
ARPGHUD* HUD = nullptr;
if(MakeWidgetControllerParams(WorldContextObject, WCParams, HUD))
{
return HUD->GetSpellMenuWidgetController(WCParams);
}
return nullptr;
}
我们修改参数
创建蓝图
我们编译打开UE,在UE里创建一个基于技能面板的控制器蓝图,用于设置参数
将资产设置上去
打开HUD蓝图,设置使用的技能面板控制器类
在技能面板UI里面,我们获取一下控制器,发现不需要再设置世界空间了
我们技能面板UI的事件里面设置控制器,并打印名称,查看是否能够正确获取到
如果能正确打印名称,证明没有问题