Unreal Engine 增强输入框架 EnhancedInput

2,484 阅读7分钟

下面的内容为 [中文直播]第39期 | 虎跳龙拿--新一代增强输入框架EnhancedInput | Epic 大钊 的学习笔记,大量的内容来自视频中的。

增强输入系统(Enhanced Input System)是对默认输入系统做了一个扩展,通过模块化的方式,解耦了从输入的按键配置到事件处理的逻辑处理过程,提供了更灵活、便利的输入配置和处理功能,同时又能向后兼容虚幻引擎4(UE4)中的默认输入系统。

问题由来

当前的系统输入的流程

旧系统问题

  • 旧系统实现基础的功能比较简单,但在想构建更复杂机制上就得需要在User层做更多的工作。例如角色在不同情景下的输入变化。(近战/远程/载具)
  • 过于简陋,原来的输入系统只是告诉你事件,需自己实现众多行为。例如按住/双击。

新系统目标

  • 重新梳理简化。由原来的 Axis/Action 简化为 Action
  • 运行时重映射输入场景。UInputMappingContext
  • 对初级用户易配置。大量默认行为实现,Tap/Hold...
  • 对高级用户易扩展,可继承子类扩展。
  • 修改器:修改输入值
  • 触发器:决定触发条件
  • 优先级:配置输入场景优先级
  • 模块化,不再只依赖ini配置,以资源asset方式配置,堆栈式分隔逻辑。
  • 提高性能,不需要检查所有的输入,只需关心当前的场景和绑定。
  • UE5 正式替换掉旧有输入系统

基础用法

使用前的配置

在 Plugins 中,开启 Enhanced Input 插件,并重启编辑器。

在 Project Settings -> Input 分类中,替换默认类型(Default Classes)。原来的值可能为 UInputComponent,需要修改为 UEnhancedInputComponent。UEnhancedInputComponent 是 UInputComponent 的子类。

项目中添加 "EnhancedInput" 模块。可以在 PublicDependencyModuleNames 中添加,也可以在 PrivateDependencyModuleNames 中添加:

public class LeveForLight : ModuleRules
{
	public LeveForLight(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
                PrivateDependencyModuleNames.AddRange(new string[] { "EnhancedInput" });
	}
}

使用的大致流程

下面为使用的大致流程,具体的设置会在下面详细介绍。

创建 InputAction

创建 InputMapingContext

InputMapingContext 中记录了按键和 Action 的绑定关系。一个按键和 action 是多对多的关系,即:一个按键可以和多个 Action 进行绑定,不同的按键可以绑定同一个 Action。

绑定 Action 委托

绑定 Action 的委托。

蓝图方式如下:

代码方式如下:首先在类中定义了可以处理的 Action,然后在 SetupPlayerInputComponent 中进行绑定 Action 的回调方法。

	UPROPERTY(EditDefaultsOnly, Category = "Input|Action")
	TObjectPtr<UInputAction> IA_LookUp;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	class UInputAction* JumpAction;
// Called to bind functionality to input
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
	{
		if (IA_LookUp)
		{
			EnhancedInputComponent->BindAction(IA_LookUp, ETriggerEvent::Triggered, this, &AMyCharacter::LookUp);
		}
	}
}

void AMyCharacter::LookUp(const FInputActionValue& InputValue)
{
}

应用 InputMappingContext

下面为在 Character 中,先获取 PlayerController,再获取 UEnhancedInputLocalPlayerSubsystem,最后应用 InputMappingContent 的过程。其中:

  • Priority:优先级,数字约大,优先级越高。

蓝图方式如下:

代码方式如下:现在类中定义需要处理 InputMappingContext,然后在 SetupPlayerInputComponent 进行绑定

UPROPERTY(EditDefaultsOnly, Category = Input)
TObjectPtr<UInputMappingContext> InputMappingContext;
#include "MyCharacter.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"

// Called to bind functionality to input
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (APlayerController* PC = CastChecked<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(InputMappingContext, 100);
		}
	}
	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
	{
		if (IA_LookUp)
		{
			EnhancedInputComponent->BindAction(IA_LookUp, ETriggerEvent::Triggered, this, &AMyCharacter::LookUp);
		}
	}
}

void AMyCharacter::LookUp(const FInputActionValue& InputValue)
{
}

UEnhancedInputComponent 是 UInputComponent 的子类。

另外:AddMappingContext 和 BindAction 的执行先后顺序没有限制,它是两个独立的过程

调试命令:ShowDebug enhancedinput,下面有更多的 Debug 命令。

核心概念

EnhancedPlayerlnput

存储按键映射:Key->InputAction

InputModifier

修改器和触发器可以在 Maping 和 InputAciton 中同时设置:

  • Mapping.Modifiers / Triggers 针对当前 IMC 场景,和按键强相关的
  • InputAction.Modifiers / Triggers 针对全局,不需要关心按键,主要关心值怎么动,处理逻辑相关的,

两个地方都可以配置 Triggers 和 Modifiers,上面两个是链式处理的。先经过 Maping 再经过 Action。

修改器有:

  • DeadZone: 限定值的范围
  • Scalar:缩放一个标量
  • Negate:取反
  • Smooth:多帧之间平滑
  • CurveExponential:指数曲线,XYZ
  • CurveUser:自定义指数曲线,CurveFloat
  • FOVScaling: FOV缩放
  • ToWorldSpace:输入设备坐标系向世界坐标系转换(调换XYZ顺序)
  • SwizzleAxis:互换轴值
  • Collection: 嵌套子修改器集合

比如:摇杆中的事件,为了防止误触,可以将较小的值约束为 0。

InputTrigger

  • ETriggerEvent:ETriggerState发生转变时触发的事件,BindXXX的时候关注某个事件
  • Down:值大于阈值(默认0.5)就触发
  • Pressed:不激活到激活
  • Released:激活到不激活
  • Hold:按住大于某个时间
  • HoldAndRelease:按住大于某个时间后松开
  • Tap: 按下后快速抬起(默认0.2)
  • Chorded: 根据别的Action联动触发

InputAction

回调参数可以为:

  • FlnputActionValue:Action的值,XYZ,0/1。
  • FlnputActionlnstance:Action的运行时状态。

在 C++ 中,回调方法可以为:

  • void()
  • vold(const FinputActionValue&)
  • void(const FinputActioninstance&)

在蓝图(BP)中:

  • void(FlnputActionValue ActionValue, float Elapsed Time, float TriggeredTime)

Consume Input是否把事件吞噬掉,如果不吞噬的话,一个按键还是传送到之后的事件处理流程。

InputMappingContext

InputMappingContext 相当于一套当前的 Key->InputAction 的映射集合。多个IMC同时作用,高优先级的会先处理,如果没有则触发到低优先级的。高优先级的 Key 绑定会屏蔽低优先级的绑定。

EnhancedInput 处理流程

  1. 兼容 Input 处理
  2. 遍历 EnhancedActionMappings
  3. 通过 KeyStateMap 查询获取激活的 Action
  4. 应用 Mapping.Modifiers 修改值
  5. 应用 Mapping.Triggers 决定触发状态
  6. 应用 Action.Modifiers 修改值
  7. 应用 Action.Triggers 决定触发状态
  8. 遍历 InputStack 获取 EnhancedInputComponent 中的 Binding 回调
  9. 触发所有Delegates

注意:是先经过 Maping 修改,再经过 Action 修改。

AddMappingContext 流程

  • InputMappingContext 会根据优先级排序
  • IMC:Mappings 也会根据动作联动而优先级排序
  • IMC:Mappings 会复制添加到 Playerlnput 的 EnhancedActionMappings里

EnhancedInputSubsystem

最佳实践

IMC BindAction

  • 初始情况在应该在哪里开始应用 IMC
  • 后续运行时在蓝图中应该如何切换 IMC
  • 何时 Remove IMC?
  • 在哪里绑定 Action 和 Axis 在蓝图中如何 BindAction

PlayerController IMC+BindAction

  • C++ 中 SetupInputComponent 依然是最合适的位置
  • BP 中可在 BeginPlay 后 Add(IMC)
  • BP中可直接Add Action Event

Pawn IMC+BindAction

  • C++ SetuplnputComponent依然合适
  • BP中应在Possessed事件内Add(IMC)
  • BP中可直接Add Action Event

事件绑定的位置是卸载 Controller 中?还是 Pawn 上?

  • 和 Pawn 关联的 action 写在对应的 Pawn 上。
  • 基础的操作可以写在 controller 上

controller 控制的 Pawn 切换之后,会调用 Pawn 对应的 OnPossess 方法,继而调用 SetupInputComponent 方法。

/** 
 * Called when this Pawn is possessed. Only called on the server (or in standalone).
 * @param NewController The controller possessing this pawn
 */
virtual void PossessedBy(AController* NewController);

Pawn Remove(IMC)

  • C++ UnPossessed 事件可移除自身 IMC
  • BP 可在 Unpossessed 事件来移除自身 IMC

另外:PlayerController 的 IMC 不需要移除,因为PC一直在

IMC BindAction最佳实践

  • 分层的 IMC 设计:基本输入(移动),武器/载具,行为/Buff
  • Add(IMC,Priority),规划好 Priority 的值,IMC 的 Priorit 体现的是 Key 和 InputAction 之间的映射关系,但是找到 InputAction 之后,还是依然按照 InputStack 的顺序来处理。
  • Pawn上可携带多个 IMC,但只 Apply 一个
  • IMC 不一定跟 Pawn 绑定关联,可根据运行时逻辑灵活Add
  • IMC 代表输入的逻辑处理环境,BindAction 代表输入事件该由谁来处理的职责

Debug

ConsoleCommands:

  • Input.+action ActionName Value:强制添加某个Action的输入
  • Input.-action ActionName:移除某Action的输入
  • Input.+key Key Value:添加某Key的输入
  • Input.-key keyName:移除某 Key 的输入
  • ShowDebug Enhancedlnput:显示调试界面

Playerlnput::InputKey/Axis{}

UEnhancedPlayerlnput:::InjectlnputForAction{}

扩展

  • 继承扩展 InputModifier 和 InputTrigger,实现自己的输入值链式修改逻辑,自己的触发逻辑
  • 规划好灵活的IMC应用策略,可继承添加更多的信息
  • 利用调试注入Key/Action可方便安排自己的测试输入序列
  • EnhancedInput还在开发中,更多Todo改进

参考