UE5.8 Enhanced Input 架构

📑 目录

引子:一个周末的输入重构

老陈的独立项目《碎星》已经开发了八个月。移动、射击、换弹、驾驶载具全写在同一个 Pawn 的 SetupPlayerInputComponent 里。策划每改一次键位,老陈就要重新编译一遍 C++,再让美术和测试重新跑一遍打包版本。

周一早上,策划发来一份需求:本周内要同时支持键鼠和手柄,并且玩家进入载具后,F 键要从「互动」变成「离开载具」,按住 Ctrl 要切换成蹲伏。旧系统里所有绑定都堆在 Project Settings 的 Input 面板,换个 Context 几乎等于重写半份输入代码。

老陈决定直接迁移到 UE5.8 的 Enhanced Input。下面把整个迁移过程中遇到的要点整理出来。

旧输入系统与 Enhanced Input 的对比

UE4 遗留的输入系统把 Axis 和 Action Mapping 集中放在项目设置里。这种设计在小项目里很直观,但项目一旦变大,几个痛点就会暴露。

第一个痛点是键位与行为紧耦合。MoveForward 直接绑定到 W 键和手柄左摇杆 Y 轴,策划想改成 E 键,必须改项目设置里的 Axis Mapping。代码里访问的是 InputAxisValue,根本不知道这个值来自键盘还是手柄。

第二个痛点是上下文切换困难。射击状态和载具状态共用同一个 Action Mapping,判断当前该执行什么逻辑全堆在回调函数里。玩家坐在载具里按 F,回调函数要先判断 bIsInVehicle,再决定是互动还是下车。类似的判断多了之后,Interact 函数会变成一坨状态机。

第三个痛点是 Trigger 表达力弱。旧 Action 只能区分 Pressed / Released,长按、连点、组合键要自己写计时器和状态机。策划想要「按住 F 进入载具,短按 F 互动」,程序员得额外维护一个 HoldTime 变量。

第四个痛点是多平台适配麻烦。同一套映射要同时兼容键鼠、手柄和移动触控,代码里到处都是平台分支。摇杆死区、鼠标灵敏度、触屏手势全都耦合在 Pawn 逻辑里。

Enhanced Input 把输入拆成三层:硬件事件、映射上下文、抽象动作。键位和逻辑解耦,运行时可以通过添加或移除 Input Mapping Context 来切换输入方案。Trigger 和 Modifier 用资产配置,不再需要手写条件判断。

用公式概括一个 Action Value 的变换过程:

vfinal=SMswizzle(Mnegate(vraw))v_{final} = S \cdot M_{swizzle}\bigl(M_{negate}(v_{raw})\bigr)

vrawv_{raw} 是硬件原始值,MnegateM_{negate} 是取反,MswizzleM_{swizzle} 是轴向交换,SS 是标量缩放。这些 Modifier 在 Input Mapping Context 里按顺序排列,美术和策划可以直接在编辑器里调整,不需要改代码。

核心资产:Input Action 与 Input Mapping Context

Input Action

Input Action 是 Enhanced Input 的最小单位。它只描述「玩家想干什么」,不描述「按了哪个键」。每个 Action 有一个 Value Type:

  • Boolean:按下或松开,例如跳跃、开火。
  • Axis1D:单轴浮点,例如油门、缩放。
  • Axis2D:二维向量,例如移动、瞄准。
  • Axis3D:三维向量,较少使用,例如自由飞行或六轴手柄。

创建 Input Action 的方法是在内容浏览器右键 -> Input -> Input Action。打开资产后,在 Details 面板里选择 Value Type。Boolean 类型的 Action 不需要读取数值,回调里可以直接忽略 Value;Axis1D 和 Axis2D 的回调则要从 FInputActionValue 里解出对应类型。

Action 本身不绑定任何按键。它只声明这个输入会产生什么样的值。多个 IMC 可以把同一个 Action 映射到不同按键,也可以在不同的上下文里给同一个 Action 附加不同的 Trigger。例如 IA_Jump 在默认 IMC 里绑定空格,在载具 IMC 里可以绑定手柄 A 键,两者互不冲突。

Input Mapping Context

Input Mapping Context 是一张映射表。每一行 Mapping 包含:

  • 一个 Input Action
  • 一个或多个硬件输入键,例如 Keyboard_EGamepad_LeftStick_Up
  • 零到多个 Modifier
  • 零到多个 Trigger

一个项目通常会有多个 IMC:默认移动、战斗、载具、UI、菜单。运行时把这些 IMC 加到 Enhanced Input Local Player Subsystem 里,通过优先级决定谁生效。

下面是 Enhanced Input 的整体架构。

architecture-beta
    group hardware(cloud)[硬件输入]
    group engine(server)[引擎层]
    group game(database)[游戏逻辑]
    service keyboard(server)[键盘鼠标手柄]
    service legacyInput(database)[旧输入系统]
    service enhanced(server)[Enhanced Input Subsystem]
    service imc(database)[Input Mapping Context]
    service action(database)[Input Action]
    service pc(server)[PlayerController]
    service pawn(database)[Pawn/Character]
    keyboard:R -- L:enhanced
    legacyInput:T -- B:enhanced
    enhanced:R -- L:imc
    imc:R -- L:action
    pc:R -- L:action
    action:R -- L:pawn

Trigger 类型

Trigger 决定什么条件下 Action 会触发回调。UE5.8 内置的 Trigger 已经覆盖了大部分需求。

flowchart TD
    A[按键事件] --> B{Trigger 类型}
    B -->|Pressed| C[按下时触发一次]
    B -->|Released| D[松开时触发一次]
    B -->|Held| E[按住超过阈值时间]
    B -->|Down| F[当前帧按下]
    B -->|Up| G[当前帧松开]
    B -->|Tap| H[快速点击]
    B -->|Chorded| I[组合键同时按下]
  • Pressed:按键按下瞬间触发一次。适合跳跃、换弹、切换武器。
  • Released:按键松开瞬间触发一次。适合蓄力射箭、长按技能结束。
  • Held:按键持续按住超过 tholdt_{hold} 秒后触发,可配置是否只触发一次或持续触发。适合长按冲刺、长按装填、长按交互进入载具。
  • Down:当前帧处于按下状态,每帧都会触发。适合需要持续检测的技能,例如持续喷火。
  • Up:当前帧处于松开状态,每帧都会触发。用途较少,通常和 Down 成对出现。
  • Tap:快速点击,按下和松开间隔小于阈值。适合轻攻击、双击开门、快速换弹取消。
  • Chorded:组合键,必须另一个 Action 处于触发状态才会生效。例如 Shift + W 触发冲刺,Ctrl + 鼠标左键触发特殊攻击。

多个 Trigger 可以叠加在一个 Mapping 上。例如一个狙击瞄准 Action 可以同时使用 Pressed 进入瞄准、Held 超过 1.5 秒切换高倍镜、Released 退出瞄准。逻辑不再分散在回调里,而是集中在 IMC 资产中。策划改手感时,只需要调 Trigger 的参数,不需要进代码。

Modifier 类型

Modifier 负责在原始硬件值到达 Action 之前进行预处理。最常用的有三种。

graph LR
    A[原始轴值 v_raw] -->|Negate| B[取反]
    B -->|SwizzleAxis| C[轴交换]
    C -->|Scalar| D[缩放 S 倍]
    D --> E[最终 Action Value v_final]
  • Negate:把输入值取反。例如 W 向前、S 向后,可以共用同一个 MoveForward Action,给 S 加一个 Negate Modifier。二维移动里,A 和 D 也可以分别用 Negate 处理 X 轴。
  • SwizzleAxis:轴向交换或取反。例如手柄右摇杆 X 轴要映射到鼠标 Y 轴,或者把二维向量的 X 和 Y 互换。对于 Move 这种 Axis2D Action,常用 Swizzle 把左摇杆的输入对齐到 Unreal 的坐标系。
  • Scalar:标量缩放。例如摇杆灵敏度、鼠标 DPI 补偿、载具转向速度,都可以通过 Scalar Modifier 调整。策划想把手柄瞄准速度降到 0.6,只需要改 Scalar 值。

Modifier 在 IMC 的 Mapping 里按顺序执行。顺序很重要:先 Negate 再 Swizzle,和先 Swizzle 再 Negate,结果可能完全不同。策划调整手感时,可以直接在编辑器里拖动 Modifier 顺序,不需要等待程序员改代码。

除了这三个,UE5.8 还内置了 DeadZone、Smooth、FOVScaling 等 Modifier。项目也可以自定义 Modifier,实现例如非线性曲线、平台适配、输入缓冲等高级需求。自定义 Modifier 需要继承 UInputModifier,重写 ModifyRaw 函数。

IMC 的添加、移除与优先级

运行时管理 IMC 是 Enhanced Input 最有价值的部分。每个本地玩家都有一个 UEnhancedInputLocalPlayerSubsystem,通过它添加或移除 IMC。

sequenceDiagram
    participant H as 硬件
    participant EIS as Enhanced Input Subsystem
    participant IMC as Input Mapping Context
    participant IA as Input Action
    participant PC as PlayerController
    participant P as Pawn
    H->>EIS: 按键/轴事件
    EIS->>IMC: 按优先级查询映射
    IMC->>IA: 应用 Modifier 和 Trigger
    IA->>PC: 触发回调
    PC->>P: 执行移动或技能

添加 IMC:

UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
Subsystem->AddMappingContext(DefaultIMC, 0);
Subsystem->AddMappingContext(VehicleIMC, 1);

移除 IMC:

Subsystem->RemoveMappingContext(DefaultIMC);

清空所有 IMC:

Subsystem->ClearAllMappingContext();

优先级数字越高,越优先处理。当两个 IMC 把同一个按键映射到不同 Action 时,高优先级的 Mapping 会拦截事件,低优先级的 Mapping 不会触发。利用这个机制,玩家进入载具时,只需要把 VehicleIMC 的优先级设得比 OnFootIMC 高,F 键就会自动从「互动」变成「离开载具」。

同一 IMC 内部也可以有多个 Mapping 绑定同一个按键,但这种情况通常意味着设计可以进一步拆分。另外,移除一个并未添加的 IMC 不会报错,可以放心在状态切换时调用。

与 PlayerController 的绑定

Enhanced Input 的绑定不在 Pawn 的 SetupPlayerInputComponent 里做硬编码,而是在 Pawn 或 PlayerController 里把 Input Action 资产与回调函数关联。

void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent);
    if (!EnhancedInput) return;

    EnhancedInput->BindAction(JumpAction, ETriggerEvent::Started, this, &AMyCharacter::Jump);
    EnhancedInput->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::Move);
    EnhancedInput->BindAction(FireAction, ETriggerEvent::Started, this, &AMyCharacter::StartFire);
    EnhancedInput->BindAction(FireAction, ETriggerEvent::Completed, this, &AMyCharacter::StopFire);
}

这段代码只有不到三十行,但已经把跳跃、移动、开火都绑定好了。Action 资产在头文件里用 UPROPERTY(EditDefaultsOnly) 暴露,策划可以直接在 Blueprint 子类里替换。

回调签名取决于 Action 的 Value Type:

  • Boolean / 无值:void OnJump(const FInputActionValue& Value)
  • Axis1D:void OnZoom(const FInputActionValue& Value),用 Value.Get<float>() 读取。
  • Axis2D:void OnMove(const FInputActionValue& Value),用 Value.Get<FVector2D>() 读取。

ETriggerEvent 有五个状态:

  • Started:Trigger 条件刚满足,例如 Pressed 触发瞬间。
  • Triggered:Action 持续触发,例如 Axis 每帧都在触发。
  • Ongoing:Trigger 条件正在评估但还没满足,例如 Held 还没达到阈值。
  • Completed:Trigger 条件结束,例如 Released。
  • Canceled:Action 被更高优先级的 Mapping 覆盖或 IMC 被移除。

PlayerController 本身也可以绑定 Action。例如打开菜单、切换队伍这种全局操作,放在 PlayerController 里比放在 Pawn 里更合适,因为 Pawn 可能被销毁或替换,PlayerController 通常一直存在。

C++ 与 Blueprint 用法

Enhanced Input 的资产是 UObject,C++ 和 Blueprint 都能创建和编辑。团队可以根据分工选择混合方案。

  • 程序员在 C++ 里定义 Input Action 和回调接口。
  • 策划在编辑器里创建 IMC,把按键、Modifier、Trigger 配置好。
  • 蓝图脚本师在 Blueprint 子类里替换 Action 资产,实现不同的角色手感。

一个典型的工作流是:

  1. 在 C++ 头文件声明 Action 指针。
  2. 在内容浏览器右键创建 UInputAction 和 UInputMappingContext 资产。
  3. 在 Blueprint 子类的 Details 面板把资产填进 Action 指针。
  4. 在 IMC 里完成键位映射。

蓝图里也可以使用 Enhanced Input。只需在 Blueprint 的 SetupPlayerInputComponent 事件里调用 Bind Action 节点,指定 Input Action 资产和回调事件即可。适合快速原型,也适合没有 C++ 背景的设计师。

蓝图和 C++ 的 Trigger Event 枚举保持一致:Started、Triggered、Ongoing、Completed、Canceled。这样回调逻辑在两种语言里可以无缝迁移。

版本控制方面,IMC 和 Input Action 都是二进制 uasset,策划修改键位不会产生代码冲突。程序员只维护回调接口和 Action 引用,输入映射交给策划负责。

UE5.8 的新改进

UE5.8 对输入系统做了进一步整合。根据官方发布说明,主要变化集中在三个方面。

第一,Enhanced Input 与 Common UI / Common Input 统一。过去 Common UI 有自己的输入抽象层,和 Enhanced Input 是两套资产。UE5.8 把这两套系统对齐,减少了重复数据,也降低了跨平台输入的维护成本。项目里做菜单导航和 gameplay 输入时,可以用同一套 Action 资产。

第二,新增了 Enhanced Input Debugger。调试输入曾经很麻烦:你不知道某个 Action 为什么没触发,也不知道 Trigger 在哪个阶段被过滤掉。UE5.8 的 Input Debugger 可以实时显示每个 Action 的当前值、Trigger 状态、Modifier 处理结果,还能列出当前激活的 IMC 和优先级。排查组合键失效、摇杆死区等问题时,能省下大量日志打印。

第三,事件处理更稳定,Trigger 边界情况得到修复。例如快速切图、Possess 切换 Pawn、本地玩家加入和离开等场景下,Enhanced Input 的事件顺序和 Completed / Canceled 触发更加一致。Widget 绑定行为也有改进,UI 和游戏输入之间的冲突减少。

另外,UE5.8 的 Unreal Engine Remote 应用支持在编辑器里预览移动触控和手势,做手游输入适配时不需要每次都打包到设备上。这对把 Enhanced Input 的 Touch Action 和虚拟摇杆一起调优很有帮助。

迁移建议

老陈在《碎星》里的迁移分了三步。

第一步,把旧项目设置里的 Axis 和 Action Mapping 全部列出来,抽象成 Input Action 资产。不要直接照搬旧名,而是按行为命名:IA_JumpIA_InteractIA_EnterVehicle

第二步,按游戏状态拆 IMC。走路一个、持枪一个、驾驶一个、菜单一个。每个 IMC 只负责自己状态内的键位,交叉逻辑用 Chorded Trigger 或高优先级 IMC 处理。

第三步,在 Pawn 和 PlayerController 里把旧回调迁移到 BindAction。回调内部尽量只处理游戏逻辑,不要再判断按键和平台。输入条件交给 Trigger 和 Modifier。

迁移过程中有几个坑需要注意。旧系统的 BindAxis 是每帧都回调,而 Enhanced Input 的 Axis2D Action 只有在输入非零时才会触发。如果代码里依赖每帧清零逻辑,需要改成在 Tick 里根据一个标志位处理。

另一个坑是 Possess 切换。玩家上下载具会切换 Pawn,新的 Pawn 要在 SetupPlayerInputComponent 里重新绑定。PlayerController 全局的 Action 则不需要重新绑定。

迁移完成后,策划改键位只需要改 IMC 资产,不再需要编译。手柄适配也只需要在 IMC 里加一行 Gamepad 映射。项目上线前做按键自定义界面时,直接修改 IMC 的 Mappings 并保存即可。

测试阶段建议先在 PIE 里用 Input Debugger 验证所有 Action,再打包到目标设备确认手感。键位自定义功能要在 PC、手柄、触控三种环境下都跑一遍。同一 Action 在不同平台可能有不同的 Modifier 顺序,保存和加载时要按平台分开处理。

Action Value 的读取细节

FInputActionValue 是 Enhanced Input 的数值封装。回调里用 Value.Get<T>() 读取具体数值,模板类型必须和 Input Action 的 Value Type 一致。

  • Boolean 类型可以忽略 Value,也可以调用 Value.Get<bool>()
  • Axis1D 对应 float,例如 Value.Get<float>()
  • Axis2D 对应 FVector2D,例如 Value.Get<FVector2D>()
  • Axis3D 对应 FVector,例如 Value.Get<FVector>()

类型不匹配时,运行时会产生断言崩溃。移动回调里最常见的写法是:

void AMyCharacter::Move(const FInputActionValue& Value)
{
    FVector2D MovementVector = Value.Get<FVector2D>();
    AddMovementInput(FRotationMatrix(Controller->GetControlRotation()).GetUnitAxis(EAxis::X), MovementVector.Y);
    AddMovementInput(FRotationMatrix(Controller->GetControlRotation()).GetUnitAxis(EAxis::Y), MovementVector.X);
}

注意这里 X 和 Y 的分配。手柄左摇杆返回的是 X 左右、Y 前后,但 Unreal 的 AddMovementInput 默认以 Y 作为前后方向,所以通常把 Y 给前向、X 给右向。具体符号取决于你的 IMC 里是否加了 Negate。

调试输入的技巧

调试输入问题最麻烦的地方在于事件链路长。硬件事件先到 Enhanced Input Subsystem,再经过 IMC、Modifier、Trigger,最后才到回调。中间任何一环出问题,Action 都不会触发。

UE5.8 的 Input Debugger 把这个链路可视化。打开后可以看到:

  • 当前激活的 IMC 列表和优先级。
  • 每个 Action 的当前值和 Trigger 状态。
  • 每个 Mapping 上 Modifier 处理前后的数值。
  • 被拦截或取消的事件原因。

除了 Input Debugger,临时在回调里加日志也很有效。UE_LOG(LogTemp, Log, TEXT("Fire Started")); 配合 Value.ToString() 可以快速确认事件是否到达回调。如果日志没打印,问题在 Trigger 或 IMC 优先级;如果日志打印但角色没反应,问题在回调逻辑。

调试组合键时,建议先把 Chorded Trigger 拆开,单独验证两个 Action 是否都能触发,再合并成组合条件。

按键自定义界面的实现思路

游戏上线前通常要做按键自定义。Enhanced Input 让这件事变得简单,因为键位存在 IMC 资产里,运行时可以直接修改 Mapping 的 Keys。

实现方案大致如下:

  1. 在玩家设置里保存一份 IMC 的副本或引用。
  2. 捕获玩家按下的新键,替换对应 Mapping 的 Key。
  3. 调用 Subsystem->RemoveMappingContextAddMappingContext 刷新绑定。
  4. 把修改后的 Keys 序列化到 SaveGame 或配置文件。
  5. 启动时读取配置,恢复上次的键位。

需要注意:直接修改编辑器里的 IMC 资产会导致脏数据,建议运行时只修改内存中的副本,或者把默认 IMC 拷贝成玩家专用的实例。恢复默认设置时,重新加载原始 IMC 即可。如果玩家把两个 Action 绑到同一个键,保存前可以检查冲突并提示,处理策略包括覆盖、忽略或恢复默认。

多平台输入适配

Enhanced Input 对键鼠、手柄、触控做了统一抽象。同一个 IA_Move 可以在不同 IMC 里分别映射到 WASD、左摇杆和虚拟摇杆。平台相关的手感差异用 Modifier 处理,例如手柄加 DeadZone,触控加 Scalar 降低灵敏度。

实际项目中,经常为不同平台准备独立的 IMC。检测到当前是手机平台后,把 MobileTouchIMC 以较高优先级加入;检测到主机平台后,改用 GamepadIMC。同一 Action 例如 IA_Jump 在不同 IMC 里可以绑定空格、手柄 A 键或屏幕按钮。平台切换时只需要增减 IMC,回调逻辑完全不用改动。

UE5.8 的 Unreal Engine Remote 应用可以在编辑器里直接预览移动触控和手势,调试 Touch Action 时不需要每次打包到手机。这对调整触屏 UI 和虚拟摇杆布局很有帮助。

结语

Enhanced Input 把输入从「按键直接写进函数」的方式,改成了资产驱动的分层模型。Input Action 描述意图,Input Mapping Context 描述上下文,Trigger 描述触发条件,Modifier 描述数值处理。运行时通过优先级和添加/移除 IMC 来切换方案。

UE5.8 在这个基础上进一步统一了 Common UI,加入了 Input Debugger,修了不少边缘情况、时序和细节问题。老陈在《碎星》里迁移后,策划改键位不再需要编译,手柄和触控适配也只需要在编辑器里拖拖改改。如果你还在为旧输入系统的硬编码发愁,Enhanced Input 值得作为迁移目标。