1. 项目概述为什么我们需要一个跨平台的输入系统如果你做过Unity项目尤其是涉及到PC、手机甚至主机多端发布的游戏肯定对输入管理这块深有体会。早些年我们可能得写一堆if (Input.GetKeyDown(KeyCode.Space))或者if (Input.touchCount 0)这样的条件判断代码里混杂着各种平台特定的逻辑维护起来简直就是一场噩梦。更别提要适配手柄了Xbox、PlayStation、Switch Pro手柄的键位映射各不相同光是想一想就头大。这就是Unity新的Input System要解决的核心痛点。它不是一个简单的API替换而是一套全新的、基于事件的输入架构。它的目标很明确让你用一套统一的逻辑处理来自键盘、鼠标、触摸屏、手柄无论什么品牌甚至未来可能出现的各种新奇输入设备的信号。对于“从零构建PC、移动、主机三端通用的角色控制器”这个目标来说Input System几乎是目前最优雅、最高效的解决方案。它把我们从繁琐的平台检测和原始输入值处理中解放出来让我们能更专注于游戏逻辑本身——也就是“角色想做什么”而不是“用户按了哪个物理按键”。简单来说这个项目的价值在于你写一次控制器逻辑就能在多个平台上基本无缝运行。无论是PC玩家用WASD移动手机玩家滑动虚拟摇杆还是主机玩家推动左摇杆你的角色控制器接收到的都是经过Input System标准化处理的“移动”输入向量后续的逻辑完全一致。这极大地提升了开发效率降低了多平台适配的复杂度。接下来我们就一步步拆解如何利用Input System实现这个目标。2. Input System核心概念与项目设计思路在动手写代码之前我们必须先理解Input System的几个核心概念这决定了我们整个控制器的架构设计。2.1 核心概念Actions动作、Action Maps动作映射与Bindings绑定这是Input System的基石理解它们的关系至关重要。Actions动作这是最高层的抽象代表游戏角色可以执行的一个“逻辑操作”比如“移动”、“跳跃”、“攻击”、“视角旋转”。它不关心这个操作具体由哪个物理按键触发。你为每个Action定义一个类型比如Move是Vector2类型对应摇杆或WASD的二维方向Jump是Button类型对应按下/松开状态。Action Maps动作映射这是一组相关Actions的集合。通常我们会按游戏模式来划分Action Maps。最经典的例子就是“Player”玩家控制角色时和“UI”打开菜单、操作界面时。你可以同时启用多个Action Maps但通常同一时间只启用一个来控制游戏流程比如在游戏中禁用“UI”Map打开菜单时禁用“Player”Map。Bindings绑定这是连接“物理输入”和“逻辑动作”的桥梁。一个Action下面可以有多个Bindings。例如“Move”这个Action可以绑定到Gamepad/leftStick手柄左摇杆Keyboard/wKeyboard/sKeyboard/aKeyboard/d键盘WASD组合Touchscreen/primaryTouch/delta触摸屏滑动 当玩家使用手柄时左摇杆的输入就会触发“Move” Action切换到键盘后WASD的输入会触发同一个“Move” Action。你的代码只需要监听“Move” Action的事件完全不用管当前是哪种输入设备。2.2 项目整体设计思路基于以上概念我们设计通用角色控制器的思路就非常清晰了创建Input Actions Asset在Unity编辑器中创建一个.inputactions资源文件。在这里面我们定义好所有需要的Action Maps和Actions。设计“Player” Action Map这是我们控制器的核心。里面会包含诸如Move(Vector2),Look(Vector2),Jump(Button),Sprint(Button),Interact(Button) 等Actions。为每个Action配置多平台BindingsMove: 绑定键盘WASD、手柄左摇杆。对于移动端我们通常不直接绑定触摸屏而是通过UI虚拟摇杆来发送模拟的向量值后面会详细讲。Look: 绑定鼠标Delta、手柄右摇杆。Jump: 绑定键盘空格键、手柄South按钮A/X。以此类推为每个功能键都设置好PC和手柄的备选方案。在代码中引用与监听通过C#脚本生成Input Actions的C#类一个很方便的功能然后在角色控制器脚本中引用这个类并订阅Subscribe相关Action的started,performed,canceled事件或者直接在每个Update中读取Action的当前值。实现平台特定的输入UI对于移动端我们需要在Canvas上创建虚拟摇杆和按钮这些UI控件在玩家操作时会向Input System发送模拟的输入事件从而触发我们绑定的Actions。这是实现“一套逻辑”的关键移动端的输入被“转换”成了Input System能理解的标准事件。处理输入切换与提示高级一点的功能可以实时检测当前活跃的设备类型键盘鼠标、手柄并动态更新游戏内的操作提示图标比如在按键提示处根据当前设备显示“E”或“X”图标。这个设计最大的优势是解耦。你的角色移动逻辑只依赖于MoveAction读出的Vector2值至于这个值是来自摇杆、键盘还是虚拟摇杆它根本不关心。当需要新增一个平台比如新的主机或新的输入设备时你大部分工作只是在Input Actions Asset里添加新的Bindings核心控制器代码可能一行都不用改。3. 实战第一步创建与配置Input Actions资源理论说再多不如动手做一遍。我们打开Unity开始配置。安装与启用Input System通过Package Manager安装“Input System”包。安装后Unity可能会提示你重启编辑器并切换激活的输入后端选择“是”即可。这意味着新输入系统将完全接管旧输入系统Input类。创建Input Actions Asset在Project窗口右键 - Create - Input Actions。我通常命名为PlayerControls.inputactions。双击这个文件会打开一个专门的配置窗口。设计“Player” Action Map点击窗口中的“”号新建一个Action Map命名为“Player”。在“Player” Map下点击“Add Action”开始添加动作。Move类型选择“Value”控制类型选择“Vector2”。这个动作将用于角色平面移动。Look类型同样选择“Value”控制类型“Vector2”。用于控制摄像机或角色视角旋转。Jump类型选择“Button”。用于跳跃。Sprint类型选择“Button”并将“Interaction”添加一个“Hold”长按。这样我们可以区分轻按和长按或者实现按住奔跑。Interact类型选择“Button”。用于交互。你可以根据游戏需要添加更多如Crouch,Attack,Reload等。为Actions配置多平台Bindings关键步骤点击MoveAction在右侧看到“Bindings”列表。点击“” - “Add Binding”路径选择Gamepad/leftStick。这样就把手柄左摇杆绑定了。再次点击“” - “Add Binding”这次路径比较复杂因为WASD是组合键。我们需要分别绑定上下左右。先绑定“上”路径Keyboard/w。然后在这个Binding上点击“” - “Add Up/Down/Left/Right Composite”。这会在Move下创建一个“Up/Down/Left/Right”的复合绑定。将w拖入“Up”栏s拖入“Down”栏a拖入“Left”栏d拖入“Right”栏。这样Input System就会将WASD四个键的组合输出为一个Vector2值。对于LookAction绑定Mouse/delta鼠标移动。绑定Gamepad/rightStick手柄右摇杆。注意手柄摇杆的输入值范围-1到1和鼠标Delta像素位移是不同的我们通常需要在代码中乘以不同的灵敏度系数来处理。对于JumpAction绑定Keyboard/space。绑定Gamepad/buttonSouth即Xbox的A键PS的X键。为Sprint绑定Keyboard/leftShift和Gamepad/leftTrigger左扳机键。为Interact绑定Keyboard/e和Gamepad/buttonWest即Xbox的X键PS的方块键。提示在绑定手柄时你可以利用“Path”下拉菜单快速搜索比如输入“leftstick”就能找到。对于复合绑定一定要确保方向键的对应关系正确否则移动方向会是反的。生成C#脚本在Input Actions Asset的Inspector面板中找到“Generate C# Class”选项勾选它。然后点击“Apply”或“Save Asset”。Unity会在同级目录下生成一个同名的C#脚本如PlayerControls.cs。这个脚本里定义了一个与我们配置完全对应的类包含了所有的Actions和Action Maps我们将在代码中直接使用它。至此我们的输入“配置表”就做好了。它就像一个中央交换机定义了所有可能的输入信号如何映射到游戏逻辑指令。接下来就是编写角色控制器来接收和处理这些指令。4. 编写核心角色控制器脚本控制器脚本需要做几件事获取输入、处理移动逻辑、处理视角逻辑、响应按钮事件。我们创建一个名为UniversalCharacterController的脚本。4.1 初始化与输入获取using UnityEngine; using UnityEngine.InputSystem; // 关键命名空间 public class UniversalCharacterController : MonoBehaviour { // 引用生成的C#类 private PlayerControls playerControls; // 移动相关 private CharacterController characterController; // 使用Unity自带的CharacterController组件 private Vector2 moveInput; private Vector3 finalMoveDirection; [SerializeField] private float moveSpeed 5f; [SerializeField] private float sprintMultiplier 1.5f; private bool isSprinting false; // 视角/镜头相关 [SerializeField] private Transform cameraPivot; // 一个空物体作为摄像机的父节点用于上下旋转 [SerializeField] private float lookSensitivity 1f; [SerializeField] private float gamepadLookSensitivity 50f; // 手柄灵敏度需要调高 private Vector2 lookInput; private float xRotation 0f; [SerializeField] private float minLookAngle -80f; [SerializeField] private float maxLookAngle 80f; // 跳跃相关 [SerializeField] private float jumpHeight 2f; [SerializeField] private float gravity -9.81f; private Vector3 velocity; // 用于计算重力速度 private bool isGrounded; private void Awake() { playerControls new PlayerControls(); characterController GetComponentCharacterController(); // 如果没找到CharacterController可以动态添加 if (characterController null) { characterController gameObject.AddComponentCharacterController(); } // 锁定鼠标并隐藏 Cursor.lockState CursorLockMode.Locked; Cursor.visible false; } private void OnEnable() { // 启用“Player” Action Map并订阅事件 playerControls.Player.Enable(); // 订阅按钮事件Jump, Sprint等 playerControls.Player.Jump.started OnJumpStarted; playerControls.Player.Sprint.started OnSprintStarted; playerControls.Player.Sprint.canceled OnSprintCanceled; // Interact等事件类似... } private void OnDisable() { // 禁用Action Map并取消订阅防止内存泄漏 playerControls.Player.Disable(); playerControls.Player.Jump.started - OnJumpStarted; playerControls.Player.Sprint.started - OnSprintStarted; playerControls.Player.Sprint.canceled - OnSprintCanceled; } private void Update() { // 1. 每帧获取“值”类型的输入Move, Look HandleMovementInput(); HandleLookInput(); // 2. 应用移动和旋转 ApplyMovement(); ApplyLook(); // 3. 处理重力与跳跃 ApplyGravity(); } private void HandleMovementInput() { // 直接从Action读取当前值 moveInput playerControls.Player.Move.ReadValueVector2(); // 判断是否在地面用于后续移动计算 isGrounded characterController.isGrounded; if (isGrounded velocity.y 0) { velocity.y -2f; // 一个小的向下力让角色更稳定地贴地 } } private void HandleLookInput() { lookInput playerControls.Player.Look.ReadValueVector2(); } }4.2 实现移动与视角逻辑private void ApplyMovement() { // 将2D输入转换为3D世界方向 // 注意移动方向应该相对于摄像机朝向而不是角色自身朝向除非是坦克式移动 Vector3 move transform.right * moveInput.x transform.forward * moveInput.y; // 归一化防止斜向移动更快但保留输入强度以实现摇杆的模拟量控制 if (move.magnitude 1f) move.Normalize(); // 应用冲刺系数 float currentSpeed moveSpeed; if (isSprinting) { currentSpeed * sprintMultiplier; } // 使用CharacterController移动 characterController.Move(move * currentSpeed * Time.deltaTime); } private void ApplyLook() { if (lookInput.magnitude 0.01f) return; // 忽略微小输入防止抖动 // 区分处理鼠标和手柄输入 float lookX lookInput.x; float lookY lookInput.y; // 假设我们通过设备类型判断这里简化处理通常手柄输入值较小需要放大 // 更专业的做法是检测当前设备或为鼠标和手柄设置不同的Action这里用一个简单判断 // 注意这不是最佳实践最佳实践见后面“设备切换”章节 float sensitivity lookSensitivity; if (Gamepad.current ! null Gamepad.current.leftStick.ReadValue().magnitude 0 || Gamepad.current.rightStick.ReadValue().magnitude 0) { // 粗略判断当前可能在使用手柄 sensitivity gamepadLookSensitivity; } // 水平旋转角色左右转 transform.Rotate(Vector3.up * lookX * sensitivity * Time.deltaTime); // 垂直旋转摄像机上下看 xRotation - lookY * sensitivity * Time.deltaTime; xRotation Mathf.Clamp(xRotation, minLookAngle, maxLookAngle); if (cameraPivot ! null) { cameraPivot.localRotation Quaternion.Euler(xRotation, 0f, 0f); } else { // 如果没指定cameraPivot假设主摄像机是子物体 Camera.main.transform.localRotation Quaternion.Euler(xRotation, 0f, 0f); } } private void ApplyGravity() { // 应用重力 velocity.y gravity * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } // 按钮事件处理 private void OnJumpStarted(InputAction.CallbackContext context) { if (isGrounded) { // 计算跳跃初速度v sqrt(2 * g * h) velocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); } } private void OnSprintStarted(InputAction.CallbackContext context) { isSprinting true; } private void OnSprintCanceled(InputAction.CallbackContext context) { isSprinting false; }将脚本挂载到角色物体上并配置好Camera Pivot通常是一个作为摄像机父物体的空物体位置在角色眼睛高度。运行游戏你现在应该可以用WASD移动、鼠标环顾四周、空格键跳跃、Shift奔跑了。如果连接了手柄推动左摇杆和右摇杆同样可以移动和旋转视角按A键跳跃。注意事项上面视角控制中判断设备的方式非常粗糙仅用于演示。在实际项目中更推荐使用Input System的InputUser或监听InputSystem.onDeviceChange事件来精确管理当前活跃设备并动态调整灵敏度、UI提示等。我们会在后续章节详细讨论。5. 移动端适配虚拟摇杆与触摸输入PC和主机端基本搞定了但移动端是另一回事。手机上没有物理摇杆和按键我们需要在屏幕上绘制虚拟控件并将触摸操作“翻译”成Input System能理解的输入事件。这里我们通常需要借助UI系统和一个中间层。5.1 使用On-Screen Controls官方方案Unity Input System Package自带了一个On-Screen Controls的样例和预制体这是一个快速上手的方案。在Package Manager中导入Input System后在菜单栏选择Window - Analysis - Input System打开输入调试窗口。在Samples选项卡找到On-Screen Controls并导入。导入后在项目里会有一个Plugins/Input System/Samples/OnScreenControls文件夹里面有一个OnScreenControls场景和一个VirtualMouse脚本。更简单的方法是直接搜索OnScreenControl组件。你可以创建一个Canvas然后添加On-Screen Stick虚拟摇杆和On-Screen Button虚拟按钮。关键一步将这些On-Screen控件的Control Path绑定到我们之前在PlayerControls.inputactions中定义的Action上。例如将虚拟摇杆的Control Path设置为Player/Move将虚拟按钮的Control Path设置为Player/Jump。这样当玩家触摸屏幕上的虚拟摇杆时On-Screen Stick组件就会模拟一个游戏手柄左摇杆的输入并发送到MoveAction。因为我们的MoveAction已经绑定了Gamepad/leftStick所以它能正确接收到输入。你的角色控制器代码完全不需要任何修改它依然在读取playerControls.Player.Move.ReadValueVector2()只是这个值的来源从物理设备变成了屏幕虚拟控件。5.2 自定义虚拟摇杆方案更灵活的控制官方方案简单但自定义样式和逻辑可能受限。更常见的做法是自己实现虚拟摇杆UI然后通过Input System的API手动发送输入事件。创建UI在Canvas下创建一个Image作为摇杆背景Joystick Background其下再创建一个Image作为摇杆手柄Joystick Handle。编写虚拟摇杆脚本using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; using UnityEngine.UI; public class VirtualJoystick : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler { [SerializeField] private RectTransform background; [SerializeField] private RectTransform handle; [SerializeField] private float handleRange 1f; // 手柄移动范围相对于背景半径的比例 private Vector2 inputVector Vector2.zero; private InputAction moveAction; // 我们要模拟的Action private void Start() { // 找到我们生成的PlayerControls实例中的Move Action // 假设有一个全局访问点这里简化处理实际项目中你可能通过单例或依赖注入来获取 var playerControls FindObjectOfTypeUniversalCharacterController()?.GetPlayerControls(); if (playerControls ! null) { moveAction playerControls.Player.Move; } // 或者更直接地通过字符串路径获取不推荐容易出错 // moveAction new InputAction(binding: Gamepad/leftStick); } public void OnPointerDown(PointerEventData eventData) { OnDrag(eventData); } public void OnDrag(PointerEventData eventData) { Vector2 pos; // 将屏幕触摸点转换到摇杆背景的本地坐标 if (RectTransformUtility.ScreenPointToLocalPointInRectangle(background, eventData.position, eventData.pressEventCamera, out pos)) { pos.x (pos.x / background.sizeDelta.x); pos.y (pos.y / background.sizeDelta.y); inputVector new Vector2(pos.x * 2, pos.y * 2); // 映射到(-1, 1)范围 inputVector (inputVector.magnitude 1.0f) ? inputVector.normalized : inputVector; // 更新手柄UI位置 Vector2 handlePos inputVector * (background.sizeDelta.x * 0.5f * handleRange); handle.anchoredPosition handlePos; // **核心手动触发Input System的Move Action** if (moveAction ! null) { // 我们需要模拟一个Gamepad设备并更新其左摇杆的值 // 一种方法是使用InputSystem.QueueEvent但更简单的是直接修改一个虚拟Gamepad的状态 // 这里演示一个更实用的方法使用“Vector2Control”和“InputSystem.QueueEvent”的简化思路 // 实际上对于自定义输入更推荐使用“PlayerInput”组件的“SendMessage”或“Invoke CSharp Events”行为并自己定义一个输入中继器。 // 为了清晰我们换一种思路不直接驱动Action而是设置一个公共变量由控制器在Update中读取。 } } } public void OnPointerUp(PointerEventData eventData) { inputVector Vector2.zero; handle.anchoredPosition Vector2.zero; // 同样通知Input System输入已取消 } // 提供一个方法让角色控制器获取当前的输入向量 public Vector2 GetInputVector() { return inputVector; } }修改角色控制器以兼容虚拟输入上面的脚本展示了思路但直接驱动Input Action比较麻烦。一个更清晰的架构是角色控制器不再直接ReadValue而是定义一个接口IInputProvider。为PC/主机实现一个InputSystemProvider内部调用playerControls.Player.Move.ReadValueVector2()。为移动端实现一个MobileInputProvider内部调用VirtualJoystick.GetInputVector()和虚拟按钮的状态。角色控制器在Awake或Start时根据当前平台实例化对应的IInputProvider。这样角色控制器的输入逻辑完全与Input System解耦只是从一个抽象的“输入提供器”读取值。这是更高级、更干净的设计模式。实操心得对于中小型项目直接使用官方的On-Screen Controls样例并微调UI是最快最稳的。对于需要高度自定义操控感如《王者荣耀》式的轮盘施法的项目才需要自己实现完整的虚拟输入系统。自己实现时务必注意触摸ID的管理、多点触控的区分以及输入事件的开始、持续、结束状态的准确传递。6. 高级话题与常见问题排查6.1 动态设备检测与UI提示切换一个专业的通用控制器应该能感知玩家当前使用的是键盘鼠标还是手柄并动态更新游戏内的操作提示图标比如从“按E交互”变成“按X交互”。实现思路监听设备变化InputSystem.onDeviceChange (device, change) { if (change InputDeviceChange.Added || change InputDeviceChange.Removed) { UpdateCurrentControlScheme(); } };检测当前控制方案使用PlayerInput组件可以更方便地管理。给角色挂上PlayerInput组件将Behavior设置为Send Messages或Invoke CSharp Events。然后它会在设备切换时自动调用配置好的回调函数。public class DeviceDisplayManager : MonoBehaviour { public PlayerInput playerInput; public GameObject keyboardMousePrompts; public GameObject gamepadPrompts; private void Awake() { if (playerInput null) playerInput GetComponentPlayerInput(); playerInput.onControlsChanged OnControlsChanged; OnControlsChanged(playerInput); // 初始化一次 } private void OnControlsChanged(PlayerInput obj) { string scheme obj.currentControlScheme; Debug.Log($控制方案切换为: {scheme}); if (scheme.Contains(Keyboard) || scheme.Contains(Mouse)) { keyboardMousePrompts.SetActive(true); gamepadPrompts.SetActive(false); } else if (scheme.Contains(Gamepad)) { keyboardMousePrompts.SetActive(false); gamepadPrompts.SetActive(true); } // 可以扩展触摸方案 } }在PlayerInput组件上你需要关联之前创建的PlayerControls.inputactions资源并设置好Control Schemes控制方案。在Input Actions Asset的编辑窗口中你可以定义不同的控制方案比如“KeyboardMouse”和“Gamepad”并为每个Binding指定它属于哪个方案。这样PlayerInput就能自动识别了。6.2 输入缓冲、连按检测与手感调优直接读取输入有时太“硬核”我们需要一些技巧来提升操作手感。输入缓冲Input Buffering比如跳跃玩家可能在落地前几帧就按了跳跃键。我们可以设置一个短暂的缓冲窗口如0.2秒在这个窗口内按下的跳跃指令会被记住并在角色落地后立即执行。private float jumpBufferTime 0.2f; private float jumpBufferCounter; private void Update() { // 如果按下跳跃键开始缓冲计时 if (playerControls.Player.Jump.triggered) { jumpBufferCounter jumpBufferTime; } else { jumpBufferCounter - Time.deltaTime; } // 在应用移动和重力的逻辑中检查缓冲 if (jumpBufferCounter 0 isGrounded) { PerformJump(); jumpBufferCounter 0; // 消耗掉缓冲 } }连按检测通过记录每次按键的时间间隔来判断是否是连按。Input System的Button交互器Interactions里自带MultiTap和SlowTap等可以直接在Input Actions Asset中配置无需写代码。摇杆死区Deadzone与响应曲线手柄摇杆有物理回中误差轻微触碰可能产生微小输入。我们需要设置死区来过滤这些噪音。可以在读取摇杆值后处理Vector2 stickInput playerControls.Player.Move.ReadValueVector2(); float deadzone 0.2f; if (stickInput.magnitude deadzone) stickInput Vector2.zero;更高级的做法是使用Stick Deadzone处理器直接在Input Actions Asset中为Binding添加处理器Processors比如Stick Deadzone(min0.125, max0.925)。你还可以添加Axis Deadzone或自定义的Scale Vector2处理器来调整响应曲线让摇杆在推到底时输出更强烈。6.3 常见问题排查实录问题Input System安装后旧的Input.GetKey代码失效了。原因启用新Input System后旧输入API默认被禁用。解决在Player Settings - Other Settings - Active Input Handling可以选择Both新旧并存但官方不推荐可能引起冲突。建议将旧代码迁移到新系统。问题手柄连接了但游戏没反应。排查首先在编辑器的Window - Analysis - Input Debugger中查看设备是否被识别以及输入事件是否正常发出。检查Input Actions Asset中相关Action的Bindings路径是否正确例如Xbox手柄是XInputController/buttonSouthPS手柄是DualShockGamepad/buttonSouth通用手柄是Gamepad/buttonSouth。使用通用路径Gamepad/buttonSouth兼容性最好。检查脚本中是否正确启用了对应的Action MapplayerControls.Player.Enable()。问题移动端虚拟摇杆没反应。排查确保Canvas的渲染模式是Screen Space - Overlay或Screen Space - Camera并且事件相机Event Camera设置正确。检查虚拟摇杆UI的层级确保没有被其他UI元素遮挡。如果使用自定义脚本检查IPointerDownHandler等接口是否实现以及Graphic Raycaster组件是否存在于Canvas上。在Input Debugger中查看当触摸摇杆时是否有对应的输入事件被触发。问题鼠标视角旋转时角色会跟着“抖动”或“平移”。原因很可能是在处理Look输入时错误地使用了transform.Rotate并同时依赖于鼠标的绝对位置或delta值的累积方式有问题。也可能是帧率不稳定导致Time.deltaTime波动大。解决确保使用Mouse.delta而不是Mouse.position。在ApplyLook中将灵敏度乘以Time.deltaTime以实现帧率无关的平滑旋转。考虑使用UnityEngine.InputSystem.EnhancedTouch.Touch的delta时注意其单位与鼠标不同。问题打包到安卓/iOS后输入无效。排查确保在Player Settings中Input System的后端已正确配置。检查移动端触摸输入是否被正确绑定或模拟。对于虚拟摇杆方案确保相关的脚本和UI在移动端构建时被包含。在真机上用ADB或Xcode日志查看是否有输入相关的错误信息。跨平台输入管理是游戏开发中的基础设施前期花时间搭建一个健壮、可扩展的输入系统能为后续所有 gameplay 开发节省大量时间和避免无数头疼的兼容性问题。Unity Input System 虽然学习曲线稍陡但它提供的抽象层和强大的编辑器集成让“一次编写多端运行”的理想变得非常可行。