1. 这不是“换套API”那么简单:UE5 Enhanced Input 究竟在解决什么问题?
你打开UE5项目,新建一个Character蓝图,拖进一个InputAxis事件,再连个AddMovementInput——这流程熟得像呼吸。但很快你就卡住了:角色在攀爬时不该响应跳跃键,潜行模式下摇杆灵敏度要降低,VR手柄的拇指摇杆和触控板需要不同映射逻辑,而UI界面又得完全屏蔽游戏输入……这时候你会发现,老式InputAxis/InputAction那套“全局绑定+硬编码判断”的方式,就像用胶带把电路板焊死——能跑,但一改就崩,一扩就乱。
Enhanced Input(增强输入系统)不是UE5里某个可选插件,它是Unreal Engine 5.0起默认启用、深度重构整个输入管线的底层架构。它把“谁按了什么键”这件事,从过去“引擎直接发信号给Actor”的粗暴模式,升级为“输入源→动作定义→映射上下文→输入处理器→目标对象”的可插拔流水线。核心关键词InputCore是这套系统的底层模块名,它不暴露给蓝图用户,但所有Enhanced Input行为都依赖它提供的注册、分发、优先级仲裁与生命周期管理能力。换句话说,你写的每一个Input Action、每一条Mapping Context、每一次Bind到PlayerController上的Input Processor,背后都是InputCore在调度内存、处理多线程输入队列、执行冲突消解算法。
这套系统真正解决的,是中大型项目必然遭遇的三大硬伤:输入逻辑耦合严重(修改跳跃逻辑可能牵扯UI、动画、网络同步)、平台适配成本爆炸(PC键鼠/主机手柄/VR控制器/触摸屏共存时需写N套分支)、运行时动态切换失效(比如进入载具后摇杆控制转向而非移动,退出后自动恢复,老方案只能靠手动Enable/Disable一堆事件,极易遗漏)。我去年帮一个ARPG项目做输入重构,原方案用了47个独立InputAxis节点+23个布尔开关控制状态,光是排查“为什么蹲下时按E无法交互”就花了三天;换成Enhanced Input后,整个输入层压缩成3个Input Action(Move、Jump、Interact)、5个Mapping Context(Default、Crouch、InVehicle、InMenu、InDialogue),逻辑清晰到新来的实习生两天就能上手调试。这不是炫技,而是工程可维护性的分水岭。
2. 架构拆解:为什么必须放弃“InputAxis思维”,拥抱“动作-上下文-处理器”三层模型?
2.1 Input Action:定义“意图”,而非“物理按键”
老式InputAxis本质是监听硬件信号:LeftShift键按下→触发Jump事件。Enhanced Input的第一层革命,是把“跳”这个人类意图,从具体按键中剥离出来。你创建一个名为IA_Jump的Input Action,它本身不绑定任何键位,只声明这是一个“请求角色执行跳跃动作”的抽象指令。它可以是:
- PC端:Spacebar(主跳跃)、Ctrl(二段跳)、鼠标滚轮向上(特殊技能)
- 主机端:A键(主跳跃)、X键(空中冲刺)
- 移动端:屏幕右上角虚拟按钮(主跳跃)、双指上滑(紧急闪避)
这些物理输入,在Enhanced Input里叫Input Triggers(输入触发器),它们只是向IA_Jump这个动作“投递请求”。关键在于,同一个IA_Jump可以被多个Trigger激活,而同一个Trigger也可以激活多个Action(比如Spacebar同时触发IA_Jump和IA_PauseGame)。这种解耦让设计者专注“玩家想做什么”,而不是“玩家怎么按”。
提示:IA_Jump这类动作应严格遵循“动词+名词”命名规范(如IA_Interact、IA_Sprint、IA_UseItem),避免使用IA_SpaceBar_Jump这类硬件绑定命名。我在实际项目中见过团队因命名混乱,导致后期接入Switch Joy-Con体感时,不得不重写30%的输入逻辑——根源就在初期没把动作定义当契约来对待。
2.2 Mapping Context:解决“何时生效”的时空治理
如果IA_Jump是“跳”的意图,那么Mapping Context(映射上下文)就是它的“生效许可证”。它回答两个核心问题:在什么状态下允许这个动作?在什么设备上允许这个动作?
举个典型场景:角色在攀爬时,Jump键应该失效,但Inventory键仍可用。老方案是在Jump事件里加IsClimbing判断,一旦逻辑复杂(比如攀爬+受伤+水中+载具内),判断嵌套会失控。Enhanced Input的做法是:
- 创建MC_Default上下文:包含IA_Jump、IA_Move、IA_Crouch等基础动作
- 创建MC_Climbing上下文:仅包含IA_Drop、IA_AdjustGrip、IA_ExitClimb,不包含IA_Jump
- 在角色进入攀爬状态时,调用
PlayerInput->AddMappingContext(MC_Climbing, 0),同时RemoveMappingContext(MC_Default) - 退出时反向操作
这里的数字0是优先级(Priority)。Enhanced Input允许多个Context同时激活,按优先级排序:高优先级Context中的Action会覆盖低优先级同名Action。比如MC_InMenu(优先级10)和MC_Default(优先级0)共存时,MC_InMenu里的IA_Back会屏蔽MC_Default里的IA_Back(后者可能是返回上一级菜单,前者是关闭当前弹窗)。这种机制天然支持“模态状态”管理,比手写布尔开关健壮得多。
注意:Context不是开关,而是叠加层。我曾见有团队误以为
AddMappingContext是“启用”,RemoveMappingContext是“禁用”,结果在UI弹出时只Add了MC_Menu,却忘了Remove MC_Default,导致UI里按W键依然触发角色移动——因为MC_Default的IA_Move仍在生效。正确做法是:模态状态用高优先级Context覆盖,非模态状态用Add/Remove精确控制。
2.3 Input Processor:实现“如何响应”的策略中心
当IA_Jump被触发,最终谁来执行跳跃?不是蓝图直接响应,而是通过Input Processor(输入处理器)——这是Enhanced Input最易被忽视、却最关键的环节。它是一个继承自UInputProcessor的C++类(蓝图中不可见),负责将原始输入数据(如摇杆偏移量、触摸压力值、陀螺仪角速度)转换为游戏逻辑能理解的语义化参数。
例如:
- IA_Move接收的是左摇杆的2D向量(X/Y),但角色移动需要的是“前/后/左/右”的方向向量。Input Processor在这里做坐标系转换(将手柄坐标系转为世界坐标系)和死区过滤(剔除摇杆微小抖动)。
- IA_TouchSwipe接收的是触摸起点和终点,Processor计算出滑动角度,输出Enum(SwipeUp/SwipeDown/SwipeLeft/SwipeRight),供蓝图直接Switch。
- VR项目中,IA_Grip接收的是手柄触发器压力值(0.0~1.0),Processor将其映射为骨骼握紧程度(0~100),驱动手部动画。
这种设计让输入处理逻辑集中、可测试、可复用。同一套Processor可被多个Action复用(如所有摇杆输入都用同一个DeadZoneProcessor),而不同平台只需替换Processor实例(PC用KeyboardProcessor,移动端用TouchProcessor),上层Action和Context完全不变。我在做跨平台移植时,仅替换3个Processor类,就完成了从PC到iOS的输入适配,没有动一行蓝图。
3. 实操落地:从零搭建一个支持攀爬/潜行/载具的三级输入系统
3.1 基础资产创建:动作、上下文、处理器的标准化流程
第一步永远是资产规划。在Content Browser中右键 → Input → Create Input Action,批量创建以下动作(命名即契约):
- IA_Move:类型为Axis2D,用于摇杆/WSAD
- IA_Jump:类型为Trigger,用于空格/手柄A键
- IA_Crouch:类型为Trigger,用于Ctrl/手柄B键
- IA_Interact:类型为Trigger,用于E/手柄X键
- IA_Sprint:类型为Trigger,用于LeftShift/手柄LT
- IA_Pause:类型为Trigger,用于Esc/手柄Start
接着创建Mapping Context:
- MC_Default(Priority 0):添加IA_Move、IA_Jump、IA_Crouch、IA_Interact、IA_Sprint
- MC_Climbing(Priority 1):添加IA_Drop(新动作)、IA_AdjustGrip(新动作)、IA_ExitClimb(新动作),不添加IA_Jump
- MC_Stealth(Priority 2):添加IA_Move(降低灵敏度)、IA_Interact(静音交互)、IA_Sprint(禁用),不添加IA_Jump
- MC_Vehicle(Priority 3):添加IA_VehicleSteer(新动作)、IA_VehicleAccelerate(新动作)、IA_VehicleBrake(新动作),不添加IA_Move/IA_Jump
实操心得:Context优先级建议用10的倍数(0/10/20/30),预留中间档位给临时状态(如MC_Paused=5)。我吃过亏:某次加了个MC_BossFight=1,结果和MC_Menu=10冲突,Boss战UI里按Esc没反应——因为MC_BossFight优先级低于MC_Menu,IA_Pause被屏蔽了。现在所有项目都强制用10进制。
3.2 输入处理器开发:用C++写一个带死区和灵敏度调节的摇杆处理器
Blueprint无法创建Input Processor,必须用C++。新建类继承UInputProcessor,在头文件中声明:
// InputProcessor_Movement.h #pragma once #include "CoreMinimal.h" #include "InputCoreTypes.h" #include "InputProcessor_Movement.generated.h" UCLASS() class UInputProcessor_Movement : public UInputProcessor { GENERATED_BODY() public: virtual void ProcessInput(const FInputActionValue& InValue, const FInputActionInstance& Instance) override; // 可在蓝图中暴露的参数 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") float DeadZone = 0.2f; // 摇杆中心无效区域半径 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") float Sensitivity = 1.0f; // 整体灵敏度缩放 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") bool bInvertY = false; // 是否反转Y轴(适配某些手柄) };在CPP文件中实现核心逻辑:
// InputProcessor_Movement.cpp #include "InputProcessor_Movement.h" #include "EnhancedInputSubsystemInterface.h" #include "EnhancedInputSubsystems.h" void UInputProcessor_Movement::ProcessInput(const FInputActionValue& InValue, const FInputActionInstance& Instance) { // 获取原始2D向量(-1.0 ~ 1.0) FVector2D RawVector = InValue.Get<FVector2D>(); // 死区过滤:计算向量长度,小于DeadZone则归零 float VectorLength = RawVector.Size(); if (VectorLength < DeadZone) { // 输出零向量,表示无输入 Instance.SetValue(FInputActionValue(EInputActionValueType::Axis2D, FVector2D::ZeroVector)); return; } // 归一化并应用灵敏度 FVector2D Normalized = RawVector / VectorLength; FVector2D Scaled = Normalized * Sensitivity; // Y轴反转(手柄通常Y向下为正,游戏逻辑常需Y向上为正) if (bInvertY) { Scaled.Y *= -1.0f; } // 输出处理后的向量 Instance.SetValue(FInputActionValue(EInputActionValueType::Axis2D, Scaled)); }编译后,在IA_Move的Details面板中,找到Processor Class下拉框,选择InputProcessor_Movement。此时所有绑定IA_Move的地方,都会自动应用死区和灵敏度——无需在每个蓝图里重复写if (Length > 0.2) ...。
关键细节:Processor的
ProcessInput函数在输入子系统线程中执行,严禁在此调用Gameplay相关函数(如GetWorld()、SpawnActor)。它的唯一职责是“数据清洗”,所有逻辑应在蓝图或C++的InputHandler中处理。我曾因在Processor里调用GetPlayerController()->GetPawn()导致偶发崩溃,排查了两周才定位到线程安全问题。
3.3 PlayerController绑定:用C++注入输入处理器,实现运行时Context切换
PlayerController是输入的中枢。在你的PlayerController C++类中,添加以下成员变量和初始化逻辑:
// MyPlayerController.h UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "Input") UEnhancedInputLocalPlayerSubsystem* InputSubsystem; UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "Input") UEnhancedInputComponent* InputComponent; // 在BeginPlay中初始化 void AMyPlayerController::BeginPlay() { Super::BeginPlay(); // 获取Enhanced Input子系统 InputSubsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()); if (!InputSubsystem) return; // 获取Enhanced Input Component(替代老式InputComponent) InputComponent = Cast<UEnhancedInputComponent>(InputComponent); if (!InputComponent) return; // 绑定默认Context InputSubsystem->AddMappingContext(DefaultMappingContext, 0); // 绑定输入处理器(可选,若Processor已设在Action上则无需此步) // InputComponent->BindAction(IA_Move, ETriggerEvent::Triggered, this, &AMyPlayerController::OnMove); }Context切换的关键函数是AddMappingContext和RemoveMappingContext。在角色状态机中,当进入攀爬状态时:
// 在角色C++中 void AMyCharacter::EnterClimbingState() { // 通知PlayerController切换Context if (APlayerController* PC = GetController<APlayerController>()) { if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer())) { // 移除默认Context,添加攀爬Context Subsystem->RemoveMappingContext(DefaultMappingContext); Subsystem->AddMappingContext(ClimbingMappingContext, 1); } } }蓝图中同样可行:在Character蓝图的Event Graph里,调用Get Player Controller→Get Enhanced Input Local Player Subsystem→Add Mapping Context(传入MC_Climbing和Priority 1)。
3.4 蓝图响应:用Enhanced Input Component替代老式InputComponent
老式蓝图中,你在Event Graph里拖InputAxis事件。Enhanced Input要求你使用Enhanced Input Component(EIC)。在Character蓝图的Components面板中,删除旧InputComponent,添加Enhanced Input Component。
然后在Event Graph中:
- 右键 →Input Actions→ 选择IA_Move → 拖出
Bind Action节点 - 设置Trigger Event为Started(持续移动)、Triggered(单次跳跃)或Completed(释放)
- 将输出引脚连到你的移动逻辑(如
Add Movement Input)
关键区别:老式InputAxis输出的是float(-1.0~1.0),而IA_Move输出的是FInputActionValue结构体,需用Get Axis2D节点提取FVector2D。IA_Jump输出的是bool(Triggered时为true),直接连Branch即可。
实操陷阱:很多新手在Bind Action后发现没反应,原因90%是忘了在PlayerController中启用Enhanced Input。检查PlayerController的Details面板,确保bEnableEnhancedInputSystem勾选(UE5.3+默认开启,但旧项目升级后可能未自动勾选)。另一个常见错误是Action类型不匹配:IA_Move设为Axis2D,却用
Get Axis(取float)而非Get Axis2D(取FVector2D)——这会导致编译通过但运行时输出(0,0)。
4. 高阶技巧与避坑指南:那些文档里不会写的实战经验
4.1 多平台输入适配:一套配置,三端运行的终极方案
跨平台项目最头疼的不是代码,是输入配置。Enhanced Input的解决方案是物理输入层抽象:
- PC端:在Project Settings → Input → Enhanced Input → Key Mappings中,为IA_Move绑定WASD和方向键;IA_Jump绑定Spacebar
- 主机端:在Same Action下,添加Gamepad Mappings,为IA_Move绑定Left Stick,IA_Jump绑定A Button
- 移动端:创建Virtual Joystick组件,其输出绑定到IA_Move;创建Button Widget,其OnClick绑定到IA_Jump
关键点在于:**所有平台共享同一套IA_和MC_资产。你不需要为iOS写一套Action,为PS5写另一套。只需在不同平台的Input Settings中,为同一Action指定不同的物理输入源。我在一个上线项目中,PC/Mac/iOS/Android四端共用87%的输入资产,仅需维护3套Platform-Specific Mappings,而非4套独立输入系统。
独家技巧:移动端虚拟摇杆的“跟随模式”(摇杆随手指移动)和“固定模式”(摇杆位置固定)可通过同一个IA_Move实现。在VirtualJoystick组件中,根据触摸ID判断是否为首次触摸,首次触摸时启动“跟随模式”,后续触摸保持“固定模式”,但输出始终是标准化的FVector2D——上层IA_Move完全无感。这比为两种模式创建IA_Move_Follow和IA_Move_Fixed聪明得多。
4.2 输入调试:实时查看输入流,告别“按键失灵”玄学
Enhanced Input内置强大的调试工具。在编辑器中按~打开控制台,输入:
enhancedinput.debug 1即可开启实时输入流显示。屏幕上会出现浮动窗口,列出:
- 当前激活的所有Mapping Context(按Priority排序)
- 每个Context中绑定的Action及其状态(Triggered/Started/Completed)
- 物理输入源(如“Gamepad Left Stick X: 0.82”)
- 最终分发到Action的值(如“IA_Move: (0.82, 0.15)”)
当玩家报告“跳跃键没反应”,你不再需要猜是蓝图断线、状态机卡死还是按键冲突。直接开debug,看:
- IA_Jump是否出现在激活Context列表中?
- 按下空格时,物理输入源是否有信号?
- 信号是否成功抵达IA_Jump(右侧显示“Triggered”)?
- 如果抵达了,说明问题在蓝图响应端;如果没抵达,说明Context没加载或优先级被覆盖。
实战案例:某次QA反馈“VR模式下抓取失灵”,debug显示IA_Grip始终为Completed(释放状态)。追踪发现VR手柄的Trigger Button在固件更新后,从“按下即触发”变为“按下到阈值才触发”,而我们的Processor没做阈值校准。加一行
if (RawValue > 0.3f) Triggered立刻解决——没有debug工具,这问题可能要花一天去查固件日志。
4.3 性能优化:避免每帧遍历,理解Enhanced Input的内存模型
Enhanced Input不是魔法,它消耗内存和CPU。关键优化点:
- Context数量控制:每个AddMappingContext会创建一个FEnhancedPlayerMappableConfigHandle,存储Action到Trigger的映射表。100个Context意味着100张哈希表。实践中,Context应按“模态状态”而非“功能模块”划分。比如不要为“射击”、“装弹”、“换弹”各建一个Context,而应建MC_InCombat(包含全部射击相关Action)。
- Action复用:IA_Move用于角色移动,也用于UI滚动、载具转向。避免创建IA_UI_Scroll、IA_Vehicle_Turn等冗余Action。
- Processor轻量化:Processor在输入子系统线程中高频调用(120Hz+)。避免在ProcessInput中做字符串操作、浮点除法(用乘法替代)、或调用虚函数。我的性能准则:Processor函数内代码行数≤15行,且无分支预测失败风险(如避免if-else链,改用查表)。
UE5.4新增的bRunProcessorInGameThread选项(默认false)值得警惕:设为true会让Processor在Game Thread执行,虽简化调试,但会阻塞主线程。生产环境务必保持false,并确保Processor绝对线程安全。
4.4 常见问题速查表:从“没反应”到“乱响应”的根因分析
| 现象 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 按键完全无反应 | PlayerController未启用Enhanced Input | 检查PC的Details面板 → bEnableEnhancedInputSystem | 勾选该选项,或在C++中SetEnableEnhancedInputSystem(true) |
| 按键响应延迟1帧 | InputProcessor中调用了Game Thread函数 | 在Processor中搜索GetWorld()、GetPlayerController() | 将逻辑移至蓝图的Input Handler,Processor只做数据转换 |
| Context切换后旧Action仍生效 | 未正确Remove旧Context,或Priority设置错误 | 打开enhancedinput.debug,观察Context列表 | 确保Remove与Add成对出现;检查Priority数值,高者覆盖低者 |
| 移动端触摸无响应 | Virtual Joystick未绑定到IA_Move,或Widget未设置bIsFocusable | 检查Joystick的Output Axis是否连到IA_Move;检查Button的bIsFocusable | 在Widget Blueprint中勾选bIsFocusable;确认Joystick的Axis Output正确 |
| VR手柄触发器响应不灵敏 | Trigger Button的Threshold未适配硬件 | debug显示物理输入值在0.1~0.2间波动,但Processor死区设为0.3 | 降低Processor DeadZone,或在Processor中添加动态阈值校准 |
血泪教训:我们曾因VR手柄固件更新,导致Trigger输出范围从0.0~1.0变为0.0~0.7,而Processor死区仍为0.3,结果70%的触发被过滤。解决方案不是改死区,而是让Processor读取手柄型号,动态加载预设阈值——这需要在Processor构造函数中调用
FString DeviceName = IInputInterface::Get().GetDeviceName();,再查表。这个技巧现在已成为我们所有VR项目的标配。
5. 生态延展:Enhanced Input如何与UE5其他系统协同作战?
5.1 与动画蓝图联动:用Input Action驱动状态机,告别硬编码过渡
传统动画蓝图中,跳跃状态切换靠IsJumping布尔变量,而该变量来自Character的C++函数。Enhanced Input让动画逻辑更纯粹:在Anim Blueprint的State Machine中,直接监听IA_Jump的Triggered事件。
- 在Anim Instance中,添加
InputAction变量,类型为UInputAction,引用IA_Jump - 在Event Graph中,右键 →Input Actions→ Bind Action → 选择IA_Jump → Triggered
- 输出连到State Machine的Transition Rule,条件为
Is Valid(确保Action存在)
这样,动画状态机完全脱离Character逻辑,只关心“玩家是否发出了跳跃意图”。当Character C++层重构跳跃逻辑(如加入空气控制、二段跳冷却),动画层无需改动——因为输入意图IA_Jump从未变过。我在一个格斗游戏中,用此法实现了“跳跃中可取消为上段拳”的动画逻辑,所有状态切换由IA_Jump和IA_Punch共同驱动,而非一堆bIsAirborne && bCanCancel布尔组合。
5.2 与Gameplay Ability System(GAS)集成:输入即能力,构建响应式技能系统
GAS的核心是Ability——能力。Enhanced Input与GAS的天然契合点在于:*IA_就是Ability的触发器。
- 创建Gameplay Ability类(如UGA_Jump),在C++中重写
ActivateAbility,实现跳跃逻辑 - 在Player State或Character中,为IA_Jump绑定Ability:
InputComponent->BindAction(IA_Jump, ETriggerEvent::Triggered, this, &AMyCharacter::TryActivateAbility, JumpAbility) TryActivateAbility中调用AbilitySystemComponent->TryActivateAbilityByClass(JumpAbilityClass)
优势在于:Ability的激活受GAS规则约束(如资源足够、冷却结束、不在禁用状态),而这些规则与输入完全解耦。玩家按空格,系统自动检查“是否有足够Jump Stamina”,失败时播放失败动画,成功时执行跳跃——所有逻辑在Ability中,输入层只负责“发起请求”。这比在蓝图里写if (Stamina > 10) then Jump优雅得多,且易于扩展(如添加“跳跃消耗Mana”的变体,只需新Ability,不改输入)。
5.3 与Niagara VFX协同:输入脉冲驱动粒子特效,实现“所见即所得”反馈
Enhanced Input的Trigger事件可直接驱动Niagara。在Niagara System中:
- 添加Input Action Parameter模块
- 选择IA_Jump作为Parameter
- 在Update Script中,用
InputAction.Triggered作为Spawn Rate的输入
当玩家按下跳跃键,粒子系统瞬间爆发——不是靠蓝图每帧Check IsJumping,而是输入事件直达VFX系统。我在一个忍者游戏里,用IA_Slash触发刀光Niagara,IA_Dash触发残影粒子,IA_Block触发护盾涟漪。所有特效的启动时机精准到帧,且与输入硬件延迟无关(因为Enhanced Input在输入子系统线程中立即捕获硬件中断)。
最后分享一个小技巧:在多人游戏中,客户端预测的输入特效(如跳跃粒子)需与服务器权威状态同步。我的做法是:客户端播放IA_Jump触发的粒子,同时发送RPC到服务器;服务器验证后,广播
ServerJumpConfirmed事件;客户端收到后,用Niagara的Reset模块重置粒子系统,确保视觉与权威状态一致。这套“预测+校正”模式,让输入反馈既即时又可靠。
我在实际项目中发现,真正决定Enhanced Input成败的,从来不是技术难度,而是团队是否接受“输入即契约”的思维转变。当美术开始参与Mapping Context设计(比如UI设计师定义MC_InMenu的优先级),当策划用Excel管理IA_*命名规范,当TA为不同平台预设Processor参数——这时,Enhanced Input才从技术方案,变成团队协作的语言。它不承诺让你少写代码,但它保证你写的每一行输入逻辑,都有清晰的归属、明确的边界、可验证的行为。这或许就是UE5时代,输入系统该有的样子。