前文中在蓝图中使用输入事件配合TryActivateAbility
来实现技能的触发是一种比较初级的技能调用方案,目前作为触发的方案来讲并不会有太大的问题。但其实质上并没有在内部将输入与技能绑定起来,具体来说,GA中有一些监听输入的任务,如WaitForInputRelease,并不会响应我们目前简单绑定的输入响应。
如图,我们对GA_FirstAbility
做出简单修改,添加了一个输入相关的异步任务。从打印结果,可见WaitInputRelease
的异步任务并没有正常响应。我们希望在能够直接将输入系统中设定的输入和对应的技能进行绑定。同时在调试调试信息中显示,创建的技能和异步任务也一直存在于内存中没有正确释放:
简单来说,本文的最终结果就是令GA在包含上面的逻辑功能时能够正常执行并且结束。
在UE的最新版本中引入了Enhanced Input
(基于原输入系统拓展的一套进阶输入控制方案),本文将分别实现两种输入系统的输入 - GA的绑定。
1. 输入测试环境搭建
先将原先创建的通过T来TryActivateAbility的蓝图内容删除(以及GiveAbility,后续会给出更合适的写法和位置),防止干扰我们的后续内容。
针对旧输入系统的配置:
- 在输入配置中添加按键绑定:OldTestInput - T;
- 在代码中添加枚举
EGASAbilityInputID
; - 创建专为旧输入系统测试的GA:
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"
});
2. GA和旧输入系统的绑定方案
在角色类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中的实现。
3. GA和新输入系统(Enhanced Input)的绑定方案
旧的输入绑定方案是建立在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的调用,而直接在角色身上进行绑定。好处是简单直观,但是相比之下,缺少了一定的灵活性。
4. 总结
本文从应用层面对GAS的输入触发进行了一定程度的归纳,并分别针对旧/新输入系统提出了与GAS结合使用的技术方案。需要注意的是,这里的方案不建议直接应用到实际项目,其中还缺少了一些生命周期相关的逻辑(本文关注激活与绑定,并没有处理卸载和解绑)。
后续的文章将采用最新的EnhancedInput
输入系统,输入的绑定方案也将采用上述的第二种。