Unreal的增强输入系统和GAS均是灵活性和扩展性很高的Gameplay相关框架,输入控制与技能的关联十分重要。然而GAS中默认的输入绑定功能较少,扩展性较弱,如果能将增强输入系统的输入配置与GAS进行联动,可以增强技能系统中输入配置的灵活性,以满足更多项目中的需求。实际上,笔者调查二者的关联时发现Epic官方并未提供一种明确的关联方案,因此笔者根据相关的研究提供一种联动思路。
1. 概念介绍
1.1 增强输入系统(Enhanced Input System)
Unreal的增强输入系统是一套新引入的输入系统,以插件形式存在,在UE 5.1后默认添加至工程中,并且Epic官方开始逐渐废弃旧的输入系统,因此研究增强输入系统比较有必要。
https://docs.unrealengine.com/5.2/zh-CN/enhanced-input-in-unreal-engine/
增强输入系统具体的概念与使用细节不在本文的讨论范围内,若读者不清楚相关内容可以查看官方文档。下面为笔者使用过程中对其的一些理解和总结:
- 资产化行为与映射,方便直接在编辑器中配置
- 输入的返回值最高可确定至三维,处理时更加灵活和方便
- 不同输入之间的优先级限定
- Modifier可方便处理输入的原始值,基本无需在业务逻辑内再对返回值进行修订
- Trigger可方便决定行为的触发方式
- 官方提供大量Modifiers和Triggers的同时,还可以自行扩展上述类以实现项目的特定需求
基本上,使用增强输入系统需进行如下几步:
- 确定InputAction(IA),决定IA的返回值类型(bool? float? Vector2D? ...)
- 确定InputMappingContext(IMC),完成物理输入与IA的绑定。同时添加相关的Modifier和Trigger
- 增强输入系统中添加IMC
- 绑定IA对应的回调函数,实现相应的业务逻辑
本文中所讨论的增强输入系统的操作主要基于上述几个步骤。
1.2 技能系统(Gameplay Ability System)
GAS系统为高度灵活的技能框架,经常用于RPG、MOBA等项目中,其具体的教程可参考GASDocumentation。
https://docs.unrealengine.com/5.2/zh-CN/enhanced-input-in-unreal-engine/
GAS中,与输入相关的模块主要分布于GameplayAbility(GA)中,GA用于处理游戏的技能配置。针对于GA的调用流程,我总结了一份简单的思维导图。
简单来说,GA需要先被授予(Give),即在AbilitySystemComponent(ASC)上注册GASpec。然后根据不同的激发(Activate)方式激发技能。
2. 实现思路
2.1 思路分析
实现增强输入系统和GAS的联动,本质上是实现对键盘输入、输入行为、技能响应、技能实现的一系列控制,因此据此对各个部分进行拆解分析:
简单来说,将GAS中技能的输入绑定部分交给增强输入系统中完成,GA被赋予后不用通过GAS的输入绑定激发GA,而是通过手动调用TryActivateAbilityByClass()函数手动激发GA,而调用的位置自然就是在IA的回调中。
当玩家按下一个按键后,根据已注册至增强输入系统中的IMC的输入与IA的映射配置,以及相关的Modifiers和Triggers的调整,IA的回调被调用,在IA回调内则激发对应的GA,而激发GA后GA内部的逻辑被执行。以此完成增强输入系统和GAS的联动。
2.2 具体实现参考
以下展示我的Demo项目中的实现,仅供参考。
2.2.1 输入相关的配置
输入的配置基本为IA和IMC,一个技能对应一个IA。
IMC中配置好输入的映射关系,以及Modifiers和Triggers
2.2.2 绑定IMC
可以选择通过蓝图或C++的方式绑定IMC,笔者仅展示C++的方式。笔者选择将输入相关的配置对接至PlayerController中。
// MPPlayerController.h
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Input)
TObjectPtr<class UInputMappingContext> DefaultMappingContext;
// MPPlayerController.cpp
void AMPPlayerController::BeginPlay()
{
Super::BeginPlay();
if (DefaultMappingContext)
{
if (const ULocalPlayer* LocalPlayer = GetLocalPlayer())
{
if (UEnhancedInputLocalPlayerSubsystem* InputSystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
InputSystem->AddMappingContext(DefaultMappingContext, 0);
}
}
}
}
2.2.3 赋予GA
ASC中需要提前赋予GA。笔者的ASC存放在PlayerState中,若存放在Pawn上,可进行相应的调整。
// MPPlayerState.h
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
TArray<TSubclassOf<class UGameplayAbility>> DefaultAbilities;
// MPPlayerState.cpp
void AMPPlayerState::BeginPlay()
{
Super::BeginPlay();
if (!AbilitySystemComp)
{
return;
}
// register ability
for (const TSubclassOf<UGameplayAbility> DefaultAbility : DefaultAbilities)
{
AbilitySystemComp->GiveAbility(FGameplayAbilitySpec(DefaultAbility));
}
}
2.2.4 对接GA
在IA的回调中激发或取消GA。
GA的激发利用ASC::TryActivateAbilityByClass函数。笔者在PlayerController中封装了ActivateAbility函数以方便在蓝图中快速配置需要对接的GA,其底层调用的函数即为TryActivateAbilityByClass()。
// MPPlayerController.h
public:
UFUNCTION(BlueprintCallable, Category = Ability)
void ActivateAbility(TSubclassOf<class UGameplayAbility> Ability) const;
// MPPlayerController.cpp
void AMPPlayerController::ActivateAbility(TSubclassOf<UGameplayAbility> Ability) const
{
if (UMPAbilitySystemComponent* ASC = GetPlayerAbilitySystemComponent())
{
ASC->TryActivateAbilityByClass(Ability);
}
}
由于Epic官方并未将GA取消的函数暴露到蓝图中,并且官方也未提供根据GA类模板取消技能的函数,因此笔者决定仿照TryActivateAbilityByClass来实现TryCancelAbilityByClass,并扩展至项目扩展的ASC子类。
// MPAbilitySystemComponent.cpp
bool UMPAbilitySystemComponent::TryCancelAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToCancel)
{
bool bSuccess = false;
const UGameplayAbility* const InAbilityCDO = InAbilityToCancel.GetDefaultObject();
for (const FGameplayAbilitySpec& Spec : ActivatableAbilities.Items)
{
if (Spec.IsActive() && Spec.Ability == InAbilityCDO)
{
CancelAbilityHandle(Spec.Handle);
bSuccess = true;
break;
}
}
return bSuccess;
}
再在PlayerController中暴露DeActivateAbility蓝图方法。
// MPPlayerController.h
public:
UFUNCTION(BlueprintCallable, Category = Ability)
void DeactivateAbility(TSubclassOf<class UGameplayAbility> Ability) const;
// MPPlayerController.cpp
void AMPPlayerController::DeactivateAbility(TSubclassOf<UGameplayAbility> Ability) const
{
if (UMPAbilitySystemComponent* ASC = GetPlayerAbilitySystemComponent())
{
ASC->TryCancelAbilityByClass(Ability);
}
}
2.2.5 GA实现
ActivateAbility后GA的ActivateAbility将会被执行,GA的相关逻辑可实现在此。GA的结束可以在ActivateAbility内部执行,配合Tasks和Events等进行控制。也可以在IA的回调中调用DeactivateAbility结束技能。技能结束后会调用OnEndAbility函数,一些重置的逻辑可在此处添加。以下是加速技能的GA实现:
2.3 存在的问题与思考
- GAS中的Tasks和增强输入系统间存在一定的重叠与冲突。如与输入相关的Tasks无法很好地与增强输入系统联系,因为未在GAS中对GA进行输入绑定。GAS的增强输入绑定有待官方的支持。
- Trigger灵活的配置项无法灵活地扩展GA的输入。由于两套系统间官方本身并未实现对接,因此若希望更灵活地实现相关功能需要搭建中间层。如可自定义扩展Trigger,在自定义的Trigger内部处理对GAS相关逻辑的监测。