UE5增强输入系统最简C++实现:10分钟搞定Pawn移动与旋转
1. 项目概述:为什么我们需要一个“最简洁”的增强输入案例?
在UE5的C++开发中,处理玩家输入是每个项目都绕不开的基础环节。从早期的UPlayerInput到后来的InputComponent绑定,再到如今官方主推的“增强输入”(Enhanced Input)系统,每一次迭代都带来了更强大的功能和更复杂的配置。对于刚接触UE5 C++的开发者,或者从蓝图转向C++的同行来说,官方文档虽然详尽,但内容庞杂,概念繁多。当你只是想快速实现一个能用键盘WASD移动、鼠标控制视角的Pawn时,面对“输入动作资产”、“映射上下文”、“修饰器”、“触发器”这些新概念,很容易陷入配置文件的海洋,却连最基本的移动都跑不起来。
这就是我动手写这个案例的初衷。我不需要它展示增强输入所有炫酷的高级特性,比如复杂的上下文切换、径向死区处理或者输入重映射。我只需要一个最小化、可运行、易于理解的模板,它能清晰地展示从创建资产到C++代码绑定的完整链路,实现Pawn的基础移动与旋转。这个案例的目标是成为你UE5 C++输入处理的“Hello World”,让你在10分钟内搭建起可交互的原型,并透彻理解其背后的运作机制。理解了这套最简流程,你再去研究更复杂的输入情景,比如角色状态机切换、载具驾驶、技能系统(GAS)的输入绑定,就会觉得脉络清晰,有章可循。
2. 核心思路拆解:增强输入系统的四块基石
在动手写代码之前,我们必须先理清增强输入系统的几个核心概念。你可以把它想象成一个加工流水线:原始按键信号是原材料,最终驱动角色移动的指令是成品。这个流水线由四个关键工位组成。
2.1 输入动作(Input Action):定义“做什么”
这是整个系统的起点和终点。UInputAction是一个数据资产(Data Asset),它定义了一个抽象的“动作”,比如“移动”、“跳跃”、“瞄准”。它不关心这个动作具体由哪个键触发,只关心这个动作的类型(布尔型、1D轴、2D轴、3D轴)和最终要传递的值。
在我们的“移动旋转Pawn”案例中,至少需要两个输入动作:
- IA_Move:类型为
Axis2D(FVector2D)。它将接收来自键盘WASD或游戏手柄左摇杆的二维方向输入,值域通常在[-1, 1]之间,用于决定移动的方向和强度。 - IA_Look:类型同样为
Axis2D。它将接收来自鼠标移动或游戏手柄右摇杆的二维输入,用于控制视角的左右偏航(Yaw)和上下俯仰(Pitch)。
注意:为什么用
Axis2D而不是两个Axis1D?因为移动和视角控制本质上是二维的,FVector2D可以一次性封装X和Y值,在回调函数中直接获取,逻辑更清晰,也便于后续进行向量运算(如标准化)。
2.2 输入映射上下文(Input Mapping Context):定义“用什么键做”
UInputMappingContext也是一个数据资产,它的作用是将具体的硬件输入(如键盘按键W、鼠标X轴位移)映射到我们上面定义的抽象Input Action上。你可以把它理解为一个“按键配置表”或“输入方案”。
一个上下文里可以包含多个映射关系。例如,在我们的IMC_Default上下文中,我们会建立如下映射:
- 键盘
W键 ->IA_Move,并为其添加修饰器,使其输出值为(0, 1)(正Y方向)。 - 键盘
S键 ->IA_Move,添加修饰器,输出值为(0, -1)。 - 鼠标X轴位移 ->
IA_Look。 - 鼠标Y轴位移 ->
IA_Look。
这个上下文可以在运行时动态地添加给玩家或从玩家身上移除,从而实现不同状态下的输入控制(例如,平时走路一套键位,开车时换成另一套)。
2.3 输入修饰器(Input Modifier):加工原始输入值
这是增强输入系统强大且灵活的关键。原始输入值(比如按下W键产生的值1.0)在触发动作之前,会经过一系列“修饰器”的处理。修饰器就像流水线上的加工机器,可以对输入值进行修改。
常用内置修饰器包括:
Negate(取反):将输入值乘以-1。用于将S键的1.0变为-1.0,表示向后移动。SwizzleAxis(轴交换):改变输入值向量的分量顺序。对于W/S键(默认是1D的X轴输入),我们需要用YXZ交换,将X轴的值1.0或-1.0转移到Y轴上,因为我们的移动FVector2D的Y分量对应前后。DeadZone(死区):为摇杆输入设置死区,避免轻微的摇杆漂移导致角色误动。Scalar(缩放):对输入值乘以一个系数,用于调整灵敏度。
在我们的案例中,为WASD键配置正确的修饰器组合,是将一维按键输入“翻译”成二维移动向量的关键步骤。
2.4 输入触发器(Input Trigger):决定“何时触发”
触发器决定了在什么条件下,经过修饰器处理后的输入值可以“触发”对应的输入动作。默认是“已开始”(Started)就触发,也就是按下即生效。其他常用的有:
Pressed/Released:按下瞬间或释放瞬间触发一次。Hold:长按一段时间后触发。Pulse:按一定频率重复触发。
对于移动(IA_Move)和视角(IA_Look)这种需要持续反馈的操作,我们通常使用Triggered或Ongoing事件,它们会在输入持续期间每帧都触发回调函数,从而实现平滑的移动和旋转。
理解了这四块基石,我们就能勾勒出最简案例的工作流:创建动作资产 -> 创建映射上下文并绑定按键与修饰器 -> 在C++ Pawn中获取上下文并添加到子系统 -> 绑定动作的回调函数 -> 在回调函数中编写移动旋转逻辑。
3. 实操步骤:从零构建可移动旋转的Pawn
下面,我将一步步带你完成这个“最简洁的万能案例”。请确保你已创建一个启用C++的UE5项目(版本5.0以上,增强输入为默认插件)。
3.1 第一步:创建输入动作资产(Input Actions)
- 在内容浏览器中,右键 ->
输入(Input)->输入动作(Input Action)。 - 创建两个输入动作,分别命名为:
IA_Move:用于移动。双击打开,将Value Type设置为Axis2D (FVector2D)。IA_Look:用于视角旋转。同样设置为Axis2D (FVector2D)。
实操心得:资产命名最好有清晰的前缀(如
IA_)和含义,项目规模变大后,几十个输入动作混在一起会非常难管理。Axis2D类型足够应对绝大多数移动和视角需求,除非你需要六自由度飞行器,否则不需要Axis3D。
3.2 第二步:创建输入映射上下文(Input Mapping Context)
- 在内容浏览器中,右键 ->
输入(Input)->输入映射上下文(Input Mapping Context)。命名为IMC_Default。 - 双击打开
IMC_Default。点击添加映射(Add Mapping),然后选择我们刚才创建的IA_Move动作。 - 为
IA_Move添加具体的按键映射:- 点击
IA_Move下的添加(+),选择W键。 - 选中这个
W键映射,在右侧细节面板的修饰器(Modifiers)中,点击添加(+),选择Swizzle Input Axis Values,并将Order设置为YXZ。这样,W键产生的(1.0, 0.0)值就会变成(0.0, 1.0),即向前移动。 - 同理,添加
S键映射,并添加两个修饰器:Negate和Swizzle Input Axis Values (YXZ)。这样S键的(1.0, 0.0)会先取反为(-1.0, 0.0),再交换为(0.0, -1.0),即向后移动。 - 添加
A键映射,添加Negate修饰器。A键的(1.0, 0.0)取反为(-1.0, 0.0),即向左移动(X轴负方向)。 - 添加
D键映射,无需修饰器。D键的(1.0, 0.0)即向右移动(X轴正方向)。
- 点击
- 再次点击
添加映射,选择IA_Look动作。 - 为
IA_Look添加映射:- 添加
Mouse X轴映射。这对应鼠标左右移动。 - 添加
Mouse Y轴映射。这对应鼠标上下移动。 - (可选)为了符合大多数第一人称/第三人称游戏的操控习惯,我们通常希望鼠标上下移动是反向的(鼠标向前推,视角向上看)。可以为
Mouse Y轴添加一个Negate修饰器。
- 添加
至此,我们的输入资产配置完毕。这个上下文定义了:WASD四个键共同驱动一个2D向量IA_Move,鼠标的X和Y轴位移共同驱动另一个2D向量IA_Look。
3.3 第三步:创建C++ Pawn类并设置输入
- 在编辑器中选择
工具(Tools)->新建C++类(New C++ Class)。选择Pawn作为父类,命名为BP_SimplePawn(或任何你喜欢的名字)。 - 在IDE中打开生成的头文件(
.h)和源文件(.cpp)。
头文件(.h)关键代码:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Pawn.h" #include "InputActionValue.h" // 必须包含,用于FInputActionValue #include "SimplePawn.generated.h" class UInputMappingContext; // 前向声明 class UInputAction; // 前向声明 UCLASS() class YOURPROJECT_API ASimplePawn : public APawn { GENERATED_BODY() public: ASimplePawn(); protected: virtual void BeginPlay() override; virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; // 关键重写函数 // 输入动作和上下文的属性,方便在编辑器里赋值 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") TObjectPtr<UInputMappingContext> DefaultMappingContext; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") TObjectPtr<UInputAction> MoveAction; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") TObjectPtr<UInputAction> LookAction; // 移动速度参数,可在编辑器调整 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Movement") float MoveSpeed = 500.0f; // 视角旋转灵敏度参数 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Movement") float LookSensitivity = 1.0f; private: // 输入回调函数 void Move(const FInputActionValue& Value); void Look(const FInputActionValue& Value); };源文件(.cpp)关键代码:
#include "SimplePawn.h" #include "EnhancedInputComponent.h" // 增强输入组件 #include "EnhancedInputSubsystems.h" // 增强输入子系统 #include "GameFramework/Controller.h" ASimplePawn::ASimplePawn() { // 设置此Pawn每帧调用Tick() PrimaryActorTick.bCanEverTick = true; } void ASimplePawn::BeginPlay() { Super::BeginPlay(); } void ASimplePawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); // 确保我们有一个有效的PlayerController APlayerController* PC = Cast<APlayerController>(GetController()); if (!PC) { UE_LOG(LogTemp, Warning, TEXT("SimplePawn: No valid PlayerController found for input setup.")); return; } // 获取增强输入本地玩家子系统并添加映射上下文 UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()); if (Subsystem && DefaultMappingContext) { Subsystem->AddMappingContext(DefaultMappingContext, 0); // 优先级设为0 } else { UE_LOG(LogTemp, Error, TEXT("SimplePawn: Failed to get EnhancedInput subsystem or DefaultMappingContext is not set.")); } // 将输入动作绑定到回调函数 UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent); if (EnhancedInputComponent) { if (MoveAction) { // 绑定移动,使用Triggered事件使其每帧触发 EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ASimplePawn::Move); } if (LookAction) { // 绑定视角,同样使用Triggered事件 EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ASimplePawn::Look); } } else { UE_LOG(LogTemp, Error, TEXT("SimplePawn: Failed to cast InputComponent to EnhancedInputComponent.")); } } void ASimplePawn::Move(const FInputActionValue& Value) { // 输入的Value是一个FVector2D,因为我们IA_Move是Axis2D类型 FVector2D MovementVector = Value.Get<FVector2D>(); // 如果控制器有效,根据输入向量和速度进行移动 if (Controller && !MovementVector.IsZero()) { // 获取控制器的旋转,但只取Yaw(水平旋转),用于计算前后左右方向 FRotator ControlRotation = Controller->GetControlRotation(); ControlRotation.Pitch = 0.0f; // 忽略俯仰角,防止向斜上方移动 ControlRotation.Roll = 0.0f; // 忽略滚转角 // 计算前向和右向向量 FVector ForwardDirection = FRotationMatrix(ControlRotation).GetUnitAxis(EAxis::X); FVector RightDirection = FRotationMatrix(ControlRotation).GetUnitAxis(EAxis::Y); // 根据输入向量合成最终移动方向 FVector DesiredMovement = (ForwardDirection * MovementVector.Y) + (RightDirection * MovementVector.X); DesiredMovement.Normalize(); // 标准化,防止斜向移动更快 // 应用移动 AddMovementInput(DesiredMovement, MovementVector.Size() * MoveSpeed * GetWorld()->GetDeltaSeconds()); } } void ASimplePawn::Look(const FInputActionValue& Value) { // 输入的Value是一个FVector2D,对应鼠标X和Y的位移量 FVector2D LookAxisVector = Value.Get<FVector2D>(); if (Controller && !LookAxisVector.IsZero()) { // 添加控制器Yaw(左右看)和Pitch(上下看)输入 // 注意:这里直接加到Controller的输入上,由Controller的InputYawScale等参数控制最终速度 // 更精细的控制可以在这里乘以一个灵敏度系数 AddControllerYawInput(LookAxisVector.X * LookSensitivity); AddControllerPitchInput(LookAxisVector.Y * LookSensitivity); } }3.4 第四步:在编辑器中配置并测试
- 编译你的C++代码。
- 在内容浏览器中,基于
ASimplePawn创建一个蓝图类,例如BP_SimplePawn。 - 双击打开
BP_SimplePawn蓝图,在类默认值(Class Defaults)或细节(Details)面板中,找到输入(Input)分类。 - 将之前创建的
IMC_Default资产赋值给Default Mapping Context。 - 将
IA_Move和IA_Look资产分别赋值给对应的属性。 - 调整
Move Speed和Look Sensitivity到你喜欢的值。 - 将游戏世界的
GameMode中Default Pawn Class设置为你的BP_SimplePawn。 - 运行游戏。你现在应该可以使用
WASD移动,鼠标控制视角了。
4. 核心环节深度解析与避坑指南
上面的代码跑起来后,你可能会有几个疑问。我们来深入拆解几个关键点,这些都是我趟过的坑。
4.1 为什么使用AddMovementInput和AddControllerYawInput?
这是UE移动组件(UPawnMovementComponent或其子类,如UFloatingPawnMovement)提供的接口。AddMovementInput告诉移动组件:“我想朝这个方向移动,强度是这么多。”移动组件会综合物理、碰撞等因素,在Tick中实际更新角色的位置。同理,AddControllerYawInput/PitchInput是告诉玩家控制器更新其控制旋转。
重要提示:确保你的Pawn拥有一个移动组件。在
ASimplePawn的构造函数中,你可以添加UFloatingPawnMovement组件,这是最简单的方式:ASimplePawn::ASimplePawn() { // ... 其他初始化 // 创建并附加一个浮动Pawn移动组件 FloatingPawnMovement = CreateDefaultSubobject<UFloatingPawnMovement>(TEXT("MovementComponent")); FloatingPawnMovement->UpdatedComponent = RootComponent; // 通常将根组件设为更新目标 }并在头文件中声明
TObjectPtr<UFloatingPawnMovement> FloatingPawnMovement;。这样AddMovementInput才会生效。
4.2 输入值FInputActionValue的处理
Value.Get<FVector2D>()是安全且必须的,它确保了类型匹配。如果你为Axis2D类型的动作绑定了回调,却尝试Get<float>(),运行时会导致断言失败。增强输入系统会根据动作类型自动填充正确的值。
对于移动向量MovementVector,X和Y分量通常在[-1, 1]区间。MovementVector.Size()用于获取向量的长度(模),在标准化方向向量后,用它乘以速度和时间增量,可以确保斜向移动(同时按W和A)的速度不会比单向移动快(因为标准化后的方向向量长度为1)。
4.3 映射上下文优先级与动态切换
Subsystem->AddMappingContext(DefaultMappingContext, 0);中的第二个参数是优先级。当多个上下文包含对同一个输入动作的映射时,优先级高的上下文生效。这为实现输入覆盖提供了可能。
例如,你可以创建另一个上下文IMC_UI,优先级为10,当打开菜单时将其添加,它里面将Escape键映射到“关闭菜单”动作。由于它的优先级高于默认上下文中可能将Escape映射到“打开控制台”的动作,因此在菜单打开时,按Escape会执行关闭菜单的操作。关闭菜单后,移除IMC_UI上下文,Escape键又恢复原来的功能。
动态切换上下文的模式非常强大,是构建复杂交互系统的基石。
4.4 常见问题排查实录
问题1:编译通过,但运行后按键毫无反应。
- 检查点1:确保
DefaultMappingContext、MoveAction、LookAction在Pawn的蓝图实例中已被正确赋值。这是最常见的问题。 - 检查点2:在
SetupPlayerInputComponent函数开始处打一个断点或打印日志,确认函数被调用了。如果没调用,检查Pawn是否被玩家控制器所控制。 - 检查点3:确认
ULocalPlayer和UEnhancedInputLocalPlayerSubsystem获取成功。在本地单玩家游戏中,这通常没问题,但在网络多人游戏中,需要注意客户端和服务器的区别。 - 检查点4:使用控制台命令
showdebug enhancedinput。在游戏中按~打开控制台,输入此命令。屏幕上会显示当前激活的输入动作和它们的值。当你按键时,观察对应的动作值是否变化。这是调试输入问题最直接的工具。
问题2:移动方向不对,比如按W角色向右走。
- 检查点:回顾
IMC_Default中W键的修饰器配置。确保Swizzle Input Axis Values的Order设置为YXZ。同时检查Move函数中,ForwardDirection和RightDirection的计算是否正确对应了输入向量的Y和X分量。记住,在Unreal的世界坐标系中,X通常是前向(红色箭头),Y是右向(绿色箭头)。
问题3:鼠标视角旋转上下是反的。
- 解决:在
IMC_Default中,为Mouse Y轴的映射添加一个Negate修饰器。或者在Look函数中,将AddControllerPitchInput的参数改为-LookAxisVector.Y * LookSensitivity。
问题4:斜向移动(同时按两个键)速度比单方向快。
- 解决:正如代码中所示,在计算
DesiredMovement后,调用DesiredMovement.Normalize()。这会将方向向量长度固定为1,确保任何方向的移动速度一致。然后乘以MovementVector.Size()(此时它可能大于1,例如同时按W和D时向量为(1,1),长度为√2)和速度、时间增量。如果你想保持原始输入强度,可以去掉标准化,但通常标准化是更符合玩家直觉的做法。
问题5:移动有延迟或不平滑。
- 检查点1:确保
Move和Look函数绑定的是ETriggerEvent::Triggered或Ongoing事件,而不是Started或Completed。前者每帧触发,后者只在开始或结束时触发一次。 - 检查点2:检查Pawn的移动组件属性,如
Max Speed、Acceleration、Deceleration。UFloatingPawnMovement默认有加速度,会显得不是“立即”达到最大速度。如果你需要即时响应,可以将加速度设得非常大。 - 检查点3:确保你在
Tick或每帧的回调中进行的移动计算,乘以了GetWorld()->GetDeltaSeconds()。这是将“每帧移动距离”转换为“每秒移动距离”的关键,能保证帧率变化时移动速度恒定。
5. 从“能用”到“好用”:进阶优化与扩展思路
最简案例跑通后,我们可以在此基础上做一些优化和扩展,让它更健壮、更易用。
5.1 封装输入绑定逻辑
我们可以将获取子系统、添加上下文、绑定动作的通用逻辑封装成一个函数,甚至一个基类,方便其他Pawn或Character复用。
// 在某个工具类或Pawn基类中 static bool BindEnhancedInputMapping(UInputComponent* PlayerInputComponent, APlayerController* PlayerController, UInputMappingContext* MappingContext, int32 Priority = 0) { if (!PlayerInputComponent || !PlayerController || !MappingContext) return false; UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()); if (!Subsystem) return false; Subsystem->AddMappingContext(MappingContext, Priority); return true; } static bool BindEnhancedInputAction(UInputComponent* PlayerInputComponent, UInputAction* InputAction, ETriggerEvent TriggerEvent, UObject* Object, FName FunctionName) { UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent); if (!EnhancedInputComponent || !InputAction) return false; // 这里简化处理,实际绑定需要更复杂的方式传递函数指针,通常还是在各自类内绑定更方便。 // 更实用的做法是提供一个模板函数或宏。 return true; }5.2 处理输入缩放与设备适配
不同的输入设备(鼠标、手柄)可能需要不同的灵敏度。我们可以在Look函数中引入一个可配置的缩放系数结构体。
USTRUCT(BlueprintType) struct FInputSensitivitySettings { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input") float MouseSensitivity = 1.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input") float GamepadLookSensitivity = 1.0f; // 还可以区分X和Y轴的灵敏度 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input") float LookSensitivityYMultiplier = 1.0f; // 通常用于反转Y轴或调整垂直灵敏度 }; // 在Pawn类中 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") FInputSensitivitySettings SensitivitySettings; void ASimplePawn::Look(const FInputActionValue& Value) { FVector2D LookAxisVector = Value.Get<FVector2D>(); if (Controller && !LookAxisVector.IsZero()) { // 这里可以扩展:根据当前输入设备类型(可通过UEnhancedPlayerInput查询)选择不同的灵敏度 float FinalSensitivity = SensitivitySettings.MouseSensitivity; // 简单示例,实际需判断设备 AddControllerYawInput(LookAxisVector.X * FinalSensitivity); AddControllerPitchInput(LookAxisVector.Y * FinalSensitivity * SensitivitySettings.LookSensitivityYMultiplier); } }5.3 与运动组件(Movement Component)深度集成
对于更复杂的移动需求(如跳跃、蹲伏、冲刺),我们不应在输入回调里直接处理状态,而应该将输入转化为对移动组件的指令。
例如,我们可以创建布尔类型的输入动作IA_Jump和IA_Sprint,在回调中设置移动组件对应的标志位。
// 在Pawn头文件中 void OnJumpActionStarted(); void OnJumpActionReleased(); void OnSprintActionStarted(); void OnSprintActionReleased(); // 在SetupPlayerInputComponent中绑定 EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ASimplePawn::OnJumpActionStarted); EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ASimplePawn::OnJumpActionReleased); // Sprint同理 void ASimplePawn::OnJumpActionStarted() { if (UCharacterMovementComponent* CharMoveComp = Cast<UCharacterMovementComponent>(GetMovementComponent())) { CharMoveComp->DoJump(); // 假设有这个方法,实际是设置bPressedJump为true } }这种模式将输入处理与具体的移动逻辑解耦,符合UE的组件化设计思想。
5.4 为蓝图暴露更多功能
为了让关卡设计师或动画师也能调整参数,我们可以将更多功能暴露给蓝图。例如,将绑定输入的函数做成蓝图可调用(BlueprintCallable),或者将输入动作和上下文作为蓝图可编辑变量(就像我们已经做的那样)。我们还可以创建蓝图事件(BlueprintImplementableEvent),在C++中触发,让蓝图侧处理具体的游戏逻辑反馈,比如播放脚步声、触发动画蒙太奇等。
这个最简案例就像一颗种子,理解了它的每一个环节,你就能生长出适应各种复杂需求的输入处理系统。它剥离了所有花哨的外壳,直击增强输入在C++中应用的核心流程。下次当你需要为角色添加一个新的技能键,或者为载具设计一套独立的操控方案时,你会清楚地知道,该去创建新的InputAction,修改InputMappingContext,并在C++中绑定一个新的回调函数。