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/

增强输入系统具体的概念与使用细节不在本文的讨论范围内,若读者不清楚相关内容可以查看官方文档。下面为笔者使用过程中对其的一些理解和总结:

  1. 资产化行为与映射,方便直接在编辑器中配置
  2. 输入的返回值最高可确定至三维,处理时更加灵活和方便
  3. 不同输入之间的优先级限定
  4. Modifier可方便处理输入的原始值,基本无需在业务逻辑内再对返回值进行修订
  5. Trigger可方便决定行为的触发方式
  6. 官方提供大量Modifiers和Triggers的同时,还可以自行扩展上述类以实现项目的特定需求

基本上,使用增强输入系统需进行如下几步:

  1. 确定InputAction(IA),决定IA的返回值类型(bool? float? Vector2D? ...)
  2. 确定InputMappingContext(IMC),完成物理输入与IA的绑定。同时添加相关的Modifier和Trigger
  3. 增强输入系统中添加IMC
  4. 绑定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的主要流程
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 存在的问题与思考

  1. GAS中的Tasks和增强输入系统间存在一定的重叠与冲突。如与输入相关的Tasks无法很好地与增强输入系统联系,因为未在GAS中对GA进行输入绑定。GAS的增强输入绑定有待官方的支持。
  2. Trigger灵活的配置项无法灵活地扩展GA的输入。由于两套系统间官方本身并未实现对接,因此若希望更灵活地实现相关功能需要搭建中间层。如可自定义扩展Trigger,在自定义的Trigger内部处理对GAS相关逻辑的监测。