前文中在蓝图中使用输入事件配合TryActivateAbility来实现技能的触发是一种比较初级的技能调用方案,目前作为触发的方案来讲并不会有太大的问题。但其实质上并没有在内部将输入与技能绑定起来,具体来说,GA中有一些监听输入的任务,如WaitForInputRelease,并不会响应我们目前简单绑定的输入响应。


如图,我们对GA_FirstAbility做出简单修改,添加了一个输入相关的异步任务。从打印结果,可见WaitInputRelease的异步任务并没有正常响应。我们希望在能够直接将输入系统中设定的输入和对应的技能进行绑定。同时在调试调试信息中显示,创建的技能和异步任务也一直存在于内存中没有正确释放:

简单来说,本文的最终结果就是令GA在包含上面的逻辑功能时能够正常执行并且结束。
在UE的最新版本中引入了Enhanced Input(基于原输入系统拓展的一套进阶输入控制方案),本文将分别实现两种输入系统的输入 - GA的绑定。
先将原先创建的通过T来TryActivateAbility的蓝图内容删除(以及GiveAbility,后续会给出更合适的写法和位置),防止干扰我们的后续内容。
针对旧输入系统的配置:
EGASAbilityInputID;GA_OldInputBindAbility,直接继承前面修改过的GA_FirstAbility即可,无须其他逻辑;
UENUM(BlueprintType)
enum class EGASAbilityInputID : uint8
{
// 0 None
None UMETA(DisplayName = "None"),
// 1 Jump
Jump UMETA(DisplayName = "Jump"),
// 2 T
OldTestInput UMETA(DisplayName = "OldTestInput"),
};

针对EnhancedInput输入系统的配置:
EnhancedInput,并且重启编辑器;EnhancedInput的内容(EnhancedInput的类是继承自默认输入(旧输入)系统的,所以在启用EnhancedInput时,不会干扰旧的输入,及旧输入系统定义的按键绑定也可以生效);EnhancedInput输入系统测试的InputAction:IA_EnhancedInputTest,并将其添加到创建的默认的InputMappingContext里,设定按键映射到E,即IMC_Default - IA_EnhancedInputTest - E;EnhancedInput输入系统测试的GA:GA_EnhancedInputBindAbility,直接继承前面修改过的GA_FirstAbility即可,无须其他逻辑;项目名.build.cs文件中添加EnhancedInput的模块依赖;

PrivateDependencyModuleNames.AddRange(new string[]
{
"GameplayAbilities",
"GameplayTasks",
"GameplayTags",
"EnhancedInput"
});

在角色类GASCharacterBase的头文件中,添加技能和输入的容器:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GAS|Binding")
TMap<EGASAbilityInputID, TSubclassOf<UGameplayAbility>> OldInputDefaultAbilities;
这里就是负责保存输入和技能映射的位置。
在该类PossessedBy方法中,在初始化技能系统组件之后,加入技能的赋予相关代码:
if (IsValid(AbilitySystemComponent))
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
if (GetLocalRole() == ROLE_Authority)
{
for (TTuple<EGASAbilityInputID, TSubclassOf<UGameplayAbility>>& Ability : OldInputDefaultAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(Ability.Value, 1, static_cast<int32>(Ability.Key), this));
}
}
}
这样,在游戏开始后(具体是角色被控制器控制时),技能就被添加给角色(角色身上的技能系统组件)。
最后进行技能和输入之间的绑定,推荐的写的位置是SetupPlayerInputComponent方法中(最好是在GASCharacterBase的派生类中),这个方法会在前面提到的PossessedBy之后调用,确保技能系统组件已经正确初始化并且技能已经被赋予。
if (IsValid(AbilitySystemComponent))
{
AbilitySystemComponent->BindAbilityActivationToInputComponent(InputComponent,
FGameplayAbilityInputBinds(
FString(), FString(),
FString("EGASAbilityInputID"), -1, -1));
}
注意: 这里的EGASAbilityInputID就是之前定义好的输入的枚举,这个函数内部是依赖于字符串匹配的,所以需要注意拼写。
编译运行,进入到角色蓝图中,将已经预制好的输入和GA填入:

结果:
按下T键

松开T键

技能及技能任务都正确触发。
这里整套的旧输入与技能的绑定方案参考并简化了GASDocumentation中的实现。
旧的输入绑定方案是建立在GAS本就对其进行了一定程度的支持的基础(主要是BindAbilityActivationToInputComponent)之上,所以在接入EnhancedInput后,我们需要稍稍对AbilitySystemComponent进行一些扩展,在实现功能的前提下,对函数接口进行统一(即新方案可以兼容旧方案,两者可以同时使用且不会有冲突)。
创建EnhancedAbilitySystemComponent类,继承AbilitySystemComponent,后面也将使用自定义扩展的技能系统组件:
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "InputAction.h"
#include "EnhancedAbilitySystemComponent.generated.h"
USTRUCT()
struct FAbilityInputBinding
{
GENERATED_BODY()
int32 InputID = 0;
uint32 OnPressedHandle = 0;
uint32 OnReleasedHandle = 0;
TArray<FGameplayAbilitySpecHandle> BoundAbilitiesStack;
};
UCLASS()
class INSIDEGAS_API UEnhancedAbilitySystemComponent : public UAbilitySystemComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UEnhancedAbilitySystemComponent(const FObjectInitializer& ObjectInitializer);
virtual void BindAbilityActivationToInputComponent(UInputComponent* InputComponent,
FGameplayAbilityInputBinds BindInfo) override;
void BindAbilityActivationToInputComponent(UEnhancedInputComponent* EnhancedInputComponent,
TMap<UInputAction*, TSubclassOf<UGameplayAbility>> InputAbilityMap);
UFUNCTION()
void OnAbilityBindedInputPressed(UInputAction* InputAction);
UFUNCTION()
void OnAbilityBindedInputReleased(UInputAction* InputAction);
UPROPERTY(Transient)
TMap<UInputAction*, FAbilityInputBinding> MappedAbilities;
};
#include "EnhancedAbilitySystemComponent.h"
#include "EnhancedInputComponent.h"
namespace AbilityInputBindingComponent_Impl
{
constexpr int32 InvalidInputID = 10;
int32 IncrementingInputID = InvalidInputID;
static int32 GetNextInputID()
{
return ++IncrementingInputID;
}
}
UEnhancedAbilitySystemComponent::UEnhancedAbilitySystemComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
void UEnhancedAbilitySystemComponent::BindAbilityActivationToInputComponent(UInputComponent* InputComponent,
FGameplayAbilityInputBinds BindInfo)
{
Super::BindAbilityActivationToInputComponent(InputComponent, BindInfo);
}
void UEnhancedAbilitySystemComponent::BindAbilityActivationToInputComponent(
UEnhancedInputComponent* EnhancedInputComponent, TMap<UInputAction*, TSubclassOf<UGameplayAbility>> InputAbilityMap)
{
if (ensureMsgf(EnhancedInputComponent,
TEXT("Project must use EnhancedInputComponent to support PlayerControlsComponent")))
{
for (auto& InputBinding : MappedAbilities)
{
const int32 NewInputID = AbilityInputBindingComponent_Impl::GetNextInputID();
InputBinding.Value.InputID = NewInputID;
for (FGameplayAbilitySpecHandle AbilityHandle : InputBinding.Value.BoundAbilitiesStack)
{
FGameplayAbilitySpec* FoundAbility = FindAbilitySpecFromHandle(AbilityHandle);
if (FoundAbility != nullptr)
{
FoundAbility->InputID = NewInputID;
}
}
}
for (auto InputAbility : InputAbilityMap)
{
FGameplayAbilitySpec AbilitySpec = BuildAbilitySpecFromClass(InputAbility.Value);
if (IsValid(AbilitySpec.Ability))
{
FGameplayAbilitySpecHandle AbilitySpecHandle = GiveAbility(AbilitySpec);
using namespace AbilityInputBindingComponent_Impl;
FGameplayAbilitySpec* BindingAbility = FindAbilitySpecFromHandle(AbilitySpecHandle);
UInputAction* InputAction = InputAbility.Key;
FAbilityInputBinding* AbilityInputBinding = MappedAbilities.Find(InputAction);
if (AbilityInputBinding)
{
FGameplayAbilitySpec* OldBoundAbility = FindAbilitySpecFromHandle(AbilityInputBinding->BoundAbilitiesStack.Top());
if (OldBoundAbility && OldBoundAbility->InputID == AbilityInputBinding->InputID)
{
OldBoundAbility->InputID = InvalidInputID;
}
}
else
{
AbilityInputBinding = &MappedAbilities.Add(InputAction);
AbilityInputBinding->InputID = GetNextInputID();
}
if (BindingAbility)
{
BindingAbility->InputID = AbilityInputBinding->InputID;
}
AbilityInputBinding->BoundAbilitiesStack.Push(AbilitySpecHandle);
// Pressed event
if (AbilityInputBinding->OnPressedHandle == 0)
{
AbilityInputBinding->OnPressedHandle = EnhancedInputComponent->BindAction(InputAction, ETriggerEvent::Started, this, &UEnhancedAbilitySystemComponent::OnAbilityBindedInputPressed, InputAction).GetHandle();
}
// Released event
if (AbilityInputBinding->OnReleasedHandle == 0)
{
AbilityInputBinding->OnReleasedHandle = EnhancedInputComponent->BindAction(InputAction, ETriggerEvent::Completed, this, &UEnhancedAbilitySystemComponent::OnAbilityBindedInputReleased, InputAction).GetHandle();
}
}
}
}
}
void UEnhancedAbilitySystemComponent::OnAbilityBindedInputPressed(UInputAction* InputAction)
{
using namespace AbilityInputBindingComponent_Impl;
FAbilityInputBinding* FoundBinding = MappedAbilities.Find(InputAction);
if (FoundBinding && ensure(FoundBinding->InputID != InvalidInputID))
{
AbilityLocalInputPressed(FoundBinding->InputID);
}
}
void UEnhancedAbilitySystemComponent::OnAbilityBindedInputReleased(UInputAction* InputAction)
{
using namespace AbilityInputBindingComponent_Impl;
FAbilityInputBinding* FoundBinding = MappedAbilities.Find(InputAction);
if (FoundBinding && ensure(FoundBinding->InputID != InvalidInputID))
{
AbilityLocalInputReleased(FoundBinding->InputID);
}
}
其中BindAbilityActivationToInputComponent是核心函数接口,也是主要进行重载的函数,使其可以对扩展输入EnhancedInputComponent进行绑定。
注意: 另外InvalidInputID是我们所使用的InputID的起始位置,如果设定其为0的话,会跟旧输入的绑定相冲突,这里建议设定一定的偏移(我这里设定为10,可以根据使用的旧输入的数量进行适当的修改)。
到角色部分进行绑定的调用,不过首先要进行默认组件的修改:
AInsideGASCharacter::AInsideGASCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UEnhancedAbilitySystemComponent>(
AGASCharacterBase::AbilitySystemComponentName))
然后到SetupInputComponent中添加输入绑定代码:
EnhancedAbilitySystemComponent = Cast<UEnhancedAbilitySystemComponent>(AbilitySystemComponent);
UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent);
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetController<APlayerController>()->GetLocalPlayer()->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
check(Subsystem);
if (DefaultInputMappingContext)
{
Subsystem->AddMappingContext(DefaultInputMappingContext, InputPriority);
}
if (IsValid(EnhancedAbilitySystemComponent) && IsValid(EnhancedInputComponent))
{
EnhancedAbilitySystemComponent->BindAbilityActivationToInputComponent(
EnhancedInputComponent, EnhancedInputDefaultAbilities);
}
进入到角色蓝图中,挂载引用:

结果:
按下E键

松开E键

功能需求满足。
这里的方案参考了古代山谷项目中的实现方法,古代山谷和Lyra项目中使用GameFeature(具体是FeatureAction的AddAbilities)进行调用,并将功能整合在一个额外的PawnComponent上。我在本文中对这个方案进行了一定程度的简化,摒弃了GameFeature的调用,而直接在角色身上进行绑定。好处是简单直观,但是相比之下,缺少了一定的灵活性。
本文从应用层面对GAS的输入触发进行了一定程度的归纳,并分别针对旧/新输入系统提出了与GAS结合使用的技术方案。需要注意的是,这里的方案不建议直接应用到实际项目,其中还缺少了一些生命周期相关的逻辑(本文关注激活与绑定,并没有处理卸载和解绑)。
后续的文章将采用最新的EnhancedInput输入系统,输入的绑定方案也将采用上述的第二种。