在上一篇里,我们编辑完成了技能的UI面板。按照MVC的思路,我们还需要制作它的控制器,用于处理UI上面的交互。
到现在为止,我们控制器已经有了三种,首先是我们创建的控制器基类RPGWidgetController.h,为所有控制器需要继承的类,里面有一些公用的函数和属性。通过基类我们实现三种控制器:

  1. 用于显示在战斗场景界面的UI的控制器OverlayWidgetController.h,主要显示角色等级血量技能等的状态
  2. 用于展现角色属性面板的UI的控制器AttributeMenuWidgetController.h,主要用于显示角色属性值和属性加点功能
  3. 还有我们在这篇文章里要创建的技能面板的UI的控制器SpellMenuWidgetController.h,用于设置玩家的技能等级,技能键位等操作。

我们接下来,会先创建技能面板控制器,然后整理代码,讲通用的部分整理到基类里,并优化一些功能。

创建技能面板控制器

我们基于控制器基类创建一个新的控制器子类

85. UE5 RPG 创建技能面板控制器_ue5


然后在类里继承覆写基类的两个函数,它们分别为初始化广播数据,用于在创建时,显示角色当前的信息,另一个是在属性变动时的委托广播。

并且我们要设置标识,让其可以作为蓝图中转换的类和蓝图继承的类

UCLASS(BlueprintType, Blueprintable)
class RPG_API USpellMenuWidgetController : public URPGWidgetController
{
	GENERATED_BODY()

public:

	virtual void BindCallbacksToDependencies() override;
	virtual void BroadcastInitialValues() override;
	
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

Hud类里添加设置技能面板控制器

我们是在自定义的HUD里面设置UI的蓝图类,并有配置项去配置使用的控制器,我们接着讲技能面板的控制器添加进去。
按照之前,我们首先创建两个属性,一个用于设置使用的技能面板控制器类,一个用于存储类的实例,防止多次创建

UPROPERTY()
	TObjectPtr<USpellMenuWidgetController> SpellMenuWidgetController;

	UPROPERTY(EditAnywhere)
	TSubclassOf<USpellMenuWidgetController> SpellMenuWidgetControllerClass;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

然后我们增加一个用于获取技能面板控制器的函数

USpellMenuWidgetController* GetSpellMenuWidgetController(const FWidgetControllerParams& WCParams);
  • 1.

在实现时,首先判断当前是否被实例过,如果实例过直接返回,没有则实例一次

USpellMenuWidgetController* ARPGHUD::GetSpellMenuWidgetController(const FWidgetControllerParams& WCParams)
{
	if(SpellMenuWidgetController == nullptr)
	{
		SpellMenuWidgetController = NewObject<USpellMenuWidgetController>(this, SpellMenuWidgetControllerClass);
		SpellMenuWidgetController->SetWidgetControllerParams(WCParams);
		SpellMenuWidgetController->BindCallbacksToDependencies(); //绑定监听数值变化
	}
	return SpellMenuWidgetController;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

创建获取RPG类型的变量

随着子类增多,我们每次切换类型去获取自定义的ASC PS PC AS的次数增多,还影响代码阅读,所以,我们在控制器基类增加对自定义的类型的获取。
首先,我们增加四个变量,用于存储对应的类型

UPROPERTY()
	TObjectPtr<ARPGPlayerController> RPGPlayerController;

	UPROPERTY()
	TObjectPtr<ARPGPlayerState> RPGPlayerState;

	UPROPERTY()
	TObjectPtr<URPGAbilitySystemComponent> RPGAbilitySystemComponent;

	UPROPERTY()
	TObjectPtr<URPGAttributeSet> RPGAttributeSet;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

然后,我们增加四个函数,用于获取自定义的参数

ARPGPlayerController* GetRPGPC();
	ARPGPlayerState* GetRPGPS();
	URPGAbilitySystemComponent* GetRPGASC();
	URPGAttributeSet* GetRPGAS();
  • 1.
  • 2.
  • 3.
  • 4.

然后就是函数实现,在实现函数里,我们首先判断当前的变量是否设置的值,如果没有,那么将类型转换,并返回

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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

转换完成后,编译,发现子类使用的一些变量名称出现的冲突

85. UE5 RPG 创建技能面板控制器_初始化_02


我们在子类里面可以将转换类型的内容删除,并直接调用函数获取

85. UE5 RPG 创建技能面板控制器_UI_03

将技能应用的广播修改到基类

由于技能的更新在UOverlayWidgetController里面和USpellMenuWidgetController里面都需要应用,所以我们将属性对应内容的委托函数移动到基类里

85. UE5 RPG 创建技能面板控制器_初始化_04


首先,我们将委托定义移动到基类

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityInfoSignature, const FRPGAbilityInfo, Info); //技能更新UI回调
  • 1.

将技能表格数据定义也设置到基类

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

然后将委托变量移动到基类,并创建一个绑定委托的函数

UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
	FAbilityInfoSignature AbilityInfoDelegate; //技能数据更新委托回调

	void BroadcastAbilityInfo(); //广播技能信息
  • 1.
  • 2.
  • 3.
  • 4.

然后将实现内容移动到函数中

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);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

在OverlayWidgetController.cpp里面,我们修改绑定函数

//绑定ASC相关委托的回调
	if(GetRPGASC())
	{
		if(GetRPGASC()->bStartupAbilitiesGiven)
		{
			//如果执行到此处时,技能的初始化工作已经完成,则直接调用初始化回调
			BroadcastInitialValues();
		}
		else
		{
			//如果执行到此处,技能初始化还未完成,将通过绑定委托,监听广播的形式触发初始化完成回调
			GetRPGASC()->AbilityGivenDelegate.AddUObject(this, &ThisClass::BroadcastInitialValues);
		}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

由于技能绑定我们函数不需要传入ASC,所以,我们在自定义ASC里,将返回一个参数取消,并修改对应参数

DECLARE_MULTICAST_DELEGATE(FAbilityGiven) //技能初始化应用后的回调委托
  • 1.

在蓝图函数库增加获取控制器函数

和获取其它函数一致,我们增加一个函数用于获取技能面板控制器

//获取属性面板的控制器
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|WidgetController")
	static USpellMenuWidgetController* GetSpellMenuWidgetController(const UObject* WorldContextObject);
  • 1.
  • 2.
  • 3.

实现也和其它函数一致

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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

在这里,你会发现这几个获取控制器的代码的重复度很高,基本都是为了获取到对应的数据以后,通过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);
  • 1.
  • 2.

在实现这里,我们还是同样的获取参数,去修改传入的参数

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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

接着,我们将其它获取控制器的函数都修改掉

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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

我们修改参数

85. UE5 RPG 创建技能面板控制器_ue5_05

创建蓝图

我们编译打开UE,在UE里创建一个基于技能面板的控制器蓝图,用于设置参数

85. UE5 RPG 创建技能面板控制器_ue5_06


将资产设置上去

85. UE5 RPG 创建技能面板控制器_UI_07


打开HUD蓝图,设置使用的技能面板控制器类

85. UE5 RPG 创建技能面板控制器_数据_08


在技能面板UI里面,我们获取一下控制器,发现不需要再设置世界空间了

85. UE5 RPG 创建技能面板控制器_数据_09


我们技能面板UI的事件里面设置控制器,并打印名称,查看是否能够正确获取到

85. UE5 RPG 创建技能面板控制器_初始化_10


如果能正确打印名称,证明没有问题

85. UE5 RPG 创建技能面板控制器_UI_11