1. 项目概述与核心价值最近在做一个需要同时上架Steam和移动端的项目最头疼的问题之一就是输入控制。PC玩家习惯用WASD和空格键手机玩家则依赖虚拟摇杆和屏幕按钮。如果为每个平台写一套独立的输入逻辑不仅代码冗余后期维护和添加新功能比如主机手柄支持更是噩梦。这正是Unity新一代输入系统——Input System的用武之地。它不是一个简单的API升级而是一套旨在彻底解决跨平台输入复杂性的架构。这个项目的核心就是利用Input System实现一套“写一次到处跑”的移动与跳跃控制逻辑。无论玩家在电脑前敲击键盘还是在公交车上滑动屏幕我们作为开发者只需要关心“玩家想要移动”和“玩家想要跳跃”这两个抽象意图Intent而不是去处理具体的“A键按下”或“屏幕某区域被触摸”这些底层事件。这种基于“动作”Action的抽象是Input System设计的精髓。它把输入设备键盘、鼠标、手柄、触屏的差异性封装起来向上提供统一的输入事件流让游戏逻辑与硬件解耦。对于中小团队或独立开发者而言掌握这套系统意味着开发效率的质变。你不再需要为每个平台编写条件编译代码测试时也可以无缝地在编辑器内模拟各种设备的输入。更重要的是它为游戏未来的多平台发行铺平了道路无论是后续接入Xbox、PlayStation手柄还是适配新的VR设备核心的游戏控制代码几乎无需改动。接下来我会从设计思路到代码实现完整拆解如何构建这样一个健壮的跨平台输入模块。2. Input System基础架构与核心概念解析在动手写代码之前必须理解Input System的几个核心概念否则很容易陷入旧Input Manager的思维定式把它用成一个更复杂的键位查询工具。2.1 Input Actions资源输入意图的蓝图Input Actions是一个.inputactions资源文件它是整个系统的配置中心。你可以把它想象成一个“输入意图清单”。在这个清单里你不定义“按A键”而是定义“移动”、“跳跃”、“攻击”这样的游戏内动作。每个动作Action都有其类型Action TypeButton用于跳跃、攻击等瞬时动作。它关心的是“按下”、“按住”、“松开”这些状态。Value用于移动、视角旋转等连续值动作。它返回一个向量如Vector2非常适合摇杆和WASD输入。Pass-Through一种特殊类型所有输入事件直接穿过不做任何处理常用于调试或需要原始输入流的场景。我们的移动和跳跃功能就需要一个Value类型的“Move”动作和一个Button类型的“Jump”动作。2.2 绑定Bindings连接意图与设备的桥梁动作定义好后是空的需要“绑定”到具体的物理输入上这就是Bindings。一个“Move”动作可以同时绑定到多个输入源键盘的WASD键组合成一个Vector2。游戏手柄的左摇杆。触摸屏上的一个浮动摇杆Stick Control。Input System的强大之处在于你可以为一个动作添加多个绑定它会自动合并这些输入源。当玩家使用键盘时系统采用键盘的输入值当玩家连接手柄后系统会自动无缝切换到摇杆的输入值无需你写任何切换代码。这种“输入源仲裁”机制是跨平台兼容性的基石。2.3 交互Interactions与处理器Processors精细化控制输入行为这是Input System相比旧系统的高阶能力允许你对原始输入信号进行加工。交互Interactions定义输入的触发逻辑。比如默认的Press交互就是按下触发。但你还可以为“跳跃”动作添加Tap点击、Hold长按、MultiTap多次点击等交互。例如你可以轻松实现“长按跳跃键进行蓄力跳”的功能而无需在游戏逻辑层写一堆计时器代码。处理器Processors对输入值进行后处理。比如摇杆的输入值通常需要Stick Deadzone处理器来消除摇杆回中时的小幅抖动你也可以为移动添加Invert Vector2处理器来反转Y轴如果需要。处理器在绑定层级或动作层级均可配置非常灵活。2.4 控制方案Control Schemes与设备匹配虽然Input System能自动仲裁但有时我们想明确区分“键鼠方案”和“触屏方案”尤其是在UI提示上。Control Schemes允许你对绑定进行分组。你可以创建一个“KeyboardMouse”方案将WASD和鼠标绑定的动作归入其中再创建一个“Touch”方案将屏幕摇杆和按钮绑定的动作归入其中。玩家切换设备时当前激活的控制方案会随之改变你可以在UI上动态更新操作提示图标。3. 实战构建从零创建跨平台移动与跳跃理论讲完我们进入实战。假设我们要为一个3D角色实现基础的移动八方向和跳跃功能。3.1 创建与配置Input Actions资源在Project窗口右键 -Create - Input Actions命名为PlayerControls。双击打开该资源会打开Input Actions编辑器窗口。创建Action Map一个Action Map可以理解为一套完整的操作上下文比如“Player”游戏内操控、“UI”界面导航。我们点击号添加一个Map命名为“Player”。创建Actions在“Player” Map下点击号添加Action命名为MoveAction Type选择ValueControl Type选择Vector2。这表示它需要一个二维向量输入。再次点击号添加Action命名为JumpAction Type选择Button。这表示它是一个按钮动作。为Move动作添加绑定选中Move动作在右侧点击号添加绑定。选择路径Keyboard/w。这会创建一个键盘绑定。但我们需要的是组合WASD。所以不要直接选单个键点击绑定点右侧的“Path”文字手动输入复合路径Keyboard/w, Keyboard/a, Keyboard/s, Keyboard/d。或者更规范的做法是添加一个“Up/Down/Left/Right Composite”绑定。点击Move动作下绑定点右侧的“”图标选择Add Up/Down/Left/Right Composite。系统会自动生成一个复合绑定结构分别将Up、Down、Left、Right绑定到W、S、A、D键。这是最标准的方式因为它明确了上下左右的方向语义。接着为Move添加手柄支持。再次点击号添加绑定路径选择Gamepad/leftStick。最后添加触屏支持。点击号路径选择Touchscreen/primaryTouch/position等等这里有个坑。直接绑定位置是不对的因为位置是绝对坐标不是方向向量。我们需要的是虚拟摇杆。Input System内置了On-Screen Stick控件。你需要在场景中创建一个On-Screen StickGameObject - UI - On-Screen Stick。它会自动生成一个PlayerInput组件和对应的UI。在Input Actions编辑器中为Move添加绑定时路径可以选择Touchscreen/touch0/position但这需要自己处理逻辑。更简单的方式是利用PlayerInput组件它会自动将屏幕摇杆的输入映射到你在Move动作上绑定的VirtualMouse/delta或通过Stick Control来实现。实际上更常见的做法是在PlayerInput组件上设置UI Input Module并使用其自动生成的On-Screen Stick该摇杆会直接向Move动作发送Vector2输入前提是你在动作绑定了VirtualMouse/delta或使用Stick Control类型。更直接且推荐的方法是在动作绑定中直接选择Touchscreen/primaryTouch/delta需要处理器转换或者使用扩展包如Unity Input System Package自带的Touch Simulation在编辑器中模拟并依赖PlayerInput的自动UI控件生成。对于新手我建议先完成键鼠和手柄的绑定触屏通过On-Screen StickUI控件来间接绑定这样更直观。为Jump动作添加绑定选中Jump动作添加绑定路径选择Keyboard/space。添加绑定路径选择Gamepad/buttonSouth通常是A键。触屏绑定通常不直接绑定到某个触摸点而是通过PlayerInput自动生成的On-Screen ButtonUI控件来触发。我们可以在UI上放一个按钮并将其On Click()事件关联到Jump动作的触发。可选创建Control Schemes在编辑器左上角点击“Control Schemes”旁边的添加“KeyboardMouse”。再添加一个“Touch”。然后你可以将每个绑定拖到对应的Scheme下。例如将WASD和Space绑定拖到“KeyboardMouse”下将虚拟摇杆和屏幕按钮的绑定如果有拖到“Touch”下。这不是必须的但有助于管理。保存Asset。注意在编辑器内配置绑定时对于触屏往往不是直接在.inputactions文件里绑定一个屏幕区域而是通过UI控件来驱动。PlayerInput组件有一个“UI Input Module”选项当它检测到当前为移动平台时会自动实例化预设的屏幕控制UI如摇杆、按钮并将这些UI的输入自动路由到你定义的Move和Jump动作上。这是实现触屏输入的关键一步比手动绑定触摸位置更可靠。3.2 编写角色控制器代码C#现在我们来编写处理这些输入并驱动角色运动的脚本。这里采用消息监听Event-driven的方式这是更高效和模块化的方式。using UnityEngine; using UnityEngine.InputSystem; // 关键命名空间 [RequireComponent(typeof(CharacterController))] // 假设我们使用CharacterController public class PlayerMovement : MonoBehaviour { [Header(移动参数)] [SerializeField] private float moveSpeed 5f; [SerializeField] private float jumpHeight 2f; [SerializeField] private float gravity -9.81f; [Header(输入引用)] [SerializeField] private InputActionReference moveActionRef; // 在Inspector中拖入Move动作资源 [SerializeField] private InputActionReference jumpActionRef; // 在Inspector中拖入Jump动作资源 private CharacterController characterController; private Vector2 currentMoveInput; // 存储当前移动输入向量 private Vector3 playerVelocity; // 用于计算重力与跳跃速度 private bool isGrounded; // 用于旋转角色面向移动方向可选 [SerializeField] private float rotationSpeed 10f; [SerializeField] private Transform cameraTransform; // 第三人称常用基于摄像机方向移动 private void Awake() { characterController GetComponentCharacterController(); // 如果未指定摄像机默认使用主摄像机 if (cameraTransform null Camera.main ! null) cameraTransform Camera.main.transform; } private void OnEnable() { // 启用输入动作并注册回调函数 if (moveActionRef ! null) { moveActionRef.action.Enable(); moveActionRef.action.performed OnMovePerformed; moveActionRef.action.canceled OnMoveCanceled; } if (jumpActionRef ! null) { jumpActionRef.action.Enable(); jumpActionRef.action.performed OnJumpPerformed; // 对于Button通常只需要performed按下事件 } } private void OnDisable() { // 禁用时取消注册并禁用动作防止内存泄漏 if (moveActionRef ! null) { moveActionRef.action.performed - OnMovePerformed; moveActionRef.action.canceled - OnMoveCanceled; moveActionRef.action.Disable(); } if (jumpActionRef ! null) { jumpActionRef.action.performed - OnJumpPerformed; jumpActionRef.action.Disable(); } } // 移动输入回调 private void OnMovePerformed(InputAction.CallbackContext context) { // 读取Value类型的输入值 currentMoveInput context.ReadValueVector2(); } private void OnMoveCanceled(InputAction.CallbackContext context) { // 输入取消时如松开按键将输入归零 currentMoveInput Vector2.zero; } // 跳跃输入回调 private void OnJumpPerformed(InputAction.CallbackContext context) { // 只有在地面时才允许跳跃 if (isGrounded) { // 根据物理公式 v sqrt(2 * g * h) 计算初始跳跃速度 playerVelocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); } } private void Update() { // 1. 检测是否在地面CharacterController的Grounded在下一帧才更新所以用额外检测 isGrounded characterController.isGrounded; if (isGrounded playerVelocity.y 0) { playerVelocity.y -2f; // 一个小的向下力确保角色紧贴地面 } // 2. 处理移动基于输入和摄像机方向 Vector3 moveDirection Vector3.zero; if (cameraTransform ! null) { // 第三人称将输入向量转换为相对于摄像机方向的世界空间向量 Vector3 forward cameraTransform.forward; Vector3 right cameraTransform.right; forward.y 0; right.y 0; forward.Normalize(); right.Normalize(); moveDirection (forward * currentMoveInput.y right * currentMoveInput.x).normalized; } else { // 第一人称或无需摄像机相对移动直接使用输入向量 moveDirection new Vector3(currentMoveInput.x, 0, currentMoveInput.y); } // 应用移动速度 characterController.Move(moveDirection * moveSpeed * Time.deltaTime); // 3. 处理旋转使角色面向移动方向 if (moveDirection ! Vector3.zero) { Quaternion targetRotation Quaternion.LookRotation(moveDirection); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); } // 4. 处理重力与跳跃速度 playerVelocity.y gravity * Time.deltaTime; characterController.Move(playerVelocity * Time.deltaTime); } }3.3 配置场景与PlayerInput将上述脚本挂载到你的玩家角色GameObject上。确保该GameObject上有CharacterController组件。在Inspector中将PlayerControls.inputactions资源中对应的Move和Jump动作分别拖拽到脚本的Move Action Ref和Jump Action Ref字段。为角色GameObject添加一个PlayerInput组件。将PlayerControls.inputactions资源拖入Actions字段。Behavior选择Invoke Unity Events这样我们可以自己控制事件更灵活或Send Messages较旧方式。上面代码使用的是直接引用InputActionReference不依赖PlayerInput的事件因此PlayerInput组件主要用来管理输入设备切换和UI控件生成。关键一步在PlayerInput组件中找到UI Input Module折叠栏。勾选“Auto-Switch UI Input Module”。在“Canvas”字段中指定你的游戏UI Canvas如果还没有创建一个Canvas设置其Render Mode为Screen Space - Overlay。在“Control Scheme Prompts”部分你可以关联不同的控制方案对应的提示图标例如键盘图标、Xbox手柄图标。最重要的是确保你的Canvas下有一个EventSystem对象创建Canvas时通常会自带。PlayerInput会自动为其添加或配置Input System UI Input Module组件这是屏幕控件工作的基础。创建触屏控件在UI Canvas下右键 -UI - On-Screen Stick。Unity会自动创建一个包含摇杆背景和手柄的UI元素并为其配置好On-Screen Stick组件。同样创建一个按钮右键 -UI - Button将其文本改为“跳跃”或使用图标。选中PlayerInput组件在Inspector底部你会看到“Auto-Switch UI Input Module”下方有一个“On-Screen Controls”列表可能需要展开。确保你的On-Screen Stick和Button都在这个列表中。PlayerInput会在运行时根据当前激活的控制方案如“Touch”自动启用这些屏幕控件并将其输入关联到对应的Move和Jump动作。实操心得PlayerInput组件的自动UI管理功能在快速原型阶段非常方便但对于复杂的UI布局或自定义输入逻辑你可能需要手动管理屏幕控件的显示/隐藏和输入绑定。一个更可控的方法是自己编写脚本监听PlayerInput的onControlsChanged事件当设备切换时手动激活或禁用对应的UI控件组。4. 进阶技巧与深度优化基础功能跑通后我们来看看如何让它更健壮、更专业。4.1 输入缓冲Input Buffering与 coyote time土狼时间这是提升操作手感的关键技巧在平台跳跃游戏中尤其重要。输入缓冲允许玩家在落地前几帧按下跳跃键角色一落地就会立刻起跳。实现方法是在Update中记录跳跃输入并在可跳跃时消费这个输入。Coyote Time允许玩家在离开平台边缘后的一个极短时间窗口内如0.1秒按下跳跃键依然有效。这能减少因时机判断过于苛刻带来的挫败感。[Header(手感优化)] [SerializeField] private float jumpBufferTime 0.15f; // 跳跃缓冲时间 [SerializeField] private float coyoteTime 0.1f; // 土狼时间 private float jumpBufferCounter; // 跳跃缓冲计时器 private float coyoteTimeCounter; // 土狼时间计时器 private void Update() { // ... 原有的地面检测和移动逻辑 ... // Coyote Time 逻辑 if (isGrounded) { coyoteTimeCounter coyoteTime; // 落地时重置土狼时间 } else { coyoteTimeCounter - Time.deltaTime; // 空中时递减 } // 跳跃缓冲逻辑在输入回调中不再直接跳跃而是设置计时器 // OnJumpPerformed 修改为 // jumpBufferCounter jumpBufferTime; // 在Update中处理跳跃 jumpBufferCounter - Time.deltaTime; if (jumpBufferCounter 0 (isGrounded || coyoteTimeCounter 0)) { // 执行跳跃 playerVelocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); jumpBufferCounter 0; // 消费掉这次缓冲 coyoteTimeCounter 0; // 使用土狼时间后清零 } // ... 重力和速度应用 ... }4.2 不同平台下的输入参数调优不同设备的输入特性不同需要在处理器中微调。手柄摇杆死区Deadzone在Move动作的绑定中为Gamepad/leftStick添加一个Stick Deadzone处理器。将Min和Max值调整到合适范围如Min0.125, Max0.925可以过滤掉摇杆的微小抖动和确保最大输入范围。键盘输入平滑键盘输入是瞬时的0或1移动会显得生硬。可以为Move动作添加一个Axis Deadzone处理器对于复合WASD绑定或者更高级的做法在代码中对currentMoveInput向量应用平滑插值Lerp。触屏摇杆灵敏度On-Screen Stick组件上有Movement Range摇杆移动范围和Dynamic Origin动态原点等参数。根据游戏类型调整这些参数Dynamic Origin设为True时摇杆会在玩家第一次触摸屏幕的位置出现体验更好。4.3 动态控制方案切换与UI反馈当玩家连接或断开手柄时游戏UI的操作提示应该相应变化。在PlayerInput组件上订阅onControlsChanged事件。在事件回调中通过PlayerInput.currentControlScheme获取当前激活的控制方案名称如“KeyboardMouse”或“Gamepad”。根据这个名称切换UI上显示的键位图标。你可以预先准备几套Sprite图集键盘、Xbox手柄、PlayStation手柄、任天堂手柄图标动态替换。public class UIControlPromptUpdater : MonoBehaviour { public PlayerInput playerInput; public Image jumpPromptImage; // 显示跳跃键图标的UI Image public Sprite keyboardJumpSprite; public Sprite gamepadJumpSprite; public Sprite touchJumpSprite; private void Start() { if (playerInput null) playerInput FindObjectOfTypePlayerInput(); if (playerInput ! null) { playerInput.onControlsChanged OnControlsChanged; UpdatePrompt(playerInput.currentControlScheme); // 初始化 } } private void OnControlsChanged(PlayerInput obj) { UpdatePrompt(obj.currentControlScheme); } private void UpdatePrompt(string scheme) { switch (scheme) { case KeyboardMouse: jumpPromptImage.sprite keyboardJumpSprite; break; case Gamepad: jumpPromptImage.sprite gamepadJumpSprite; break; case Touch: jumpPromptImage.sprite touchJumpSprite; break; default: break; } } }5. 常见问题排查与调试技巧即使按照步骤操作也可能会遇到一些坑。这里记录几个我踩过的雷和解决方法。5.1 输入无响应或动作未触发检查动作是否启用在代码中确保在OnEnable里调用了action.Enable()在OnDisable里调用了action.Disable()。或者在PlayerInput组件中确保Actions资源已正确赋值且Behavior设置正确。检查绑定路径在Input Actions编辑器中仔细检查每个绑定的路径是否正确。特别是复合绑定如WASD确保上下左右键没有绑定错。检查设备屏蔽在PlayerInput组件或InputSystem设置中是否无意中禁用了某些设备类型。使用Input Debugger菜单栏Window - Analysis - Input Debugger。这是最强大的调试工具。你可以实时查看所有连接的设备、原始输入信号、以及激活的动作和它们的值。如果动作被触发了这里一定会显示。5.2 触屏控件不显示或无效Canvas渲染模式确保放置触屏控件的Canvas的Render Mode是Screen Space - Overlay或Screen Space - Camera。World Space模式下的UI可能无法接收触摸输入。EventSystem场景中必须有且只有一个EventSystem对象并且其上有Input System UI Input Module组件PlayerInput通常会帮你配置。PlayerInput UI配置确认PlayerInput组件上的Canvas字段已正确指定并且On-Screen Controls列表里包含了你的摇杆和按钮。平台设置在Project Settings - Player - Resolution and Presentation中确保Disable Depth and Stencil未被勾选某些UI效果需要。对于移动端检查Default Orientation是否合适。5.3 移动输入向量值异常如Y轴反转处理器顺序检查Move动作绑定的处理器。Invert Vector2处理器会反转Y轴。如果你同时使用了摇杆和键盘确保处理器只应用在需要的绑定上而不是整个动作。摄像机相对计算错误这是最常见的原因。在代码中计算摄像机相对移动方向时务必确保将摄像机向量的Y分量归零forward.y 0; right.y 0;并重新归一化Normalize()。否则摄像机的俯仰角会影响水平移动方向。输入设备校准某些手柄可能需要校准。在PC上可以通过系统的游戏控制器设置进行校准。5.4 在编辑器中模拟不同设备这是Input System的一大优势你不需要真机就能测试。打开Window - Analysis - Input Debugger。在Debugger窗口的右侧点击“Add Device”。你可以添加Touchscreen、Gamepad、Joystick等模拟设备。添加后在Game视图你就可以用鼠标模拟触摸点击并拖动用键盘模拟手柄默认映射如箭头键模拟摇杆Enter模拟按钮。这极大方便了跨平台输入的开发调试。5.5 构建后输入失效特别是WebGL这是一个经典问题往往与输入系统初始化有关。初始化顺序确保所有依赖于Input System的脚本其OnEnable和Start中的初始化代码不会在Input System自身初始化之前执行。在Awake中订阅事件有时会导致问题。一个稳妥的做法是在Start中启用动作或者使用InputSystem.onSettingsChange事件来延迟初始化。WebGL的特定问题WebGL平台对输入处理有特殊要求。确保在Project Settings - Player - WebGL - Publishing Settings中“WebGL 1.0/2.0”的选项与你的输入需求兼容。有时需要启用“Auto Graphics API”或调整其他设置。最关键的WebGL构建后首次加载可能较慢输入系统初始化需要时间游戏启动时应有一个明确的“点击开始”界面等待输入系统准备就绪。动作资源未包含在构建中检查PlayerControls.inputactions文件是否在Resources文件夹下或者是否通过Addressables或直接依赖被打包。最简单的方法是将其放在Resources文件夹外并确保引用它的场景或预制体被打包。这套从键鼠到触屏的跨平台输入解决方案其核心思想是“抽象”与“解耦”。通过Input System我们将多变的硬件输入抽象为稳定的游戏意图让游戏逻辑保持纯净。实现过程中.inputactions资源的配置是地基PlayerInput组件是自动化脚手架而C#脚本则是连接输入与游戏世界的桥梁。掌握它不仅能解决眼前的多平台需求更能为你应对未来更复杂的输入交互如双人同屏、自定义按键映射打下坚实的基础。在实际项目中不妨从一个小功能开始尝试逐步将旧系统迁移过来你会立刻感受到代码清晰度和维护效率的提升。
Unity Input System跨平台输入实战:一套代码搞定PC与移动端控制
发布时间:2026/7/12 7:17:20
1. 项目概述与核心价值最近在做一个需要同时上架Steam和移动端的项目最头疼的问题之一就是输入控制。PC玩家习惯用WASD和空格键手机玩家则依赖虚拟摇杆和屏幕按钮。如果为每个平台写一套独立的输入逻辑不仅代码冗余后期维护和添加新功能比如主机手柄支持更是噩梦。这正是Unity新一代输入系统——Input System的用武之地。它不是一个简单的API升级而是一套旨在彻底解决跨平台输入复杂性的架构。这个项目的核心就是利用Input System实现一套“写一次到处跑”的移动与跳跃控制逻辑。无论玩家在电脑前敲击键盘还是在公交车上滑动屏幕我们作为开发者只需要关心“玩家想要移动”和“玩家想要跳跃”这两个抽象意图Intent而不是去处理具体的“A键按下”或“屏幕某区域被触摸”这些底层事件。这种基于“动作”Action的抽象是Input System设计的精髓。它把输入设备键盘、鼠标、手柄、触屏的差异性封装起来向上提供统一的输入事件流让游戏逻辑与硬件解耦。对于中小团队或独立开发者而言掌握这套系统意味着开发效率的质变。你不再需要为每个平台编写条件编译代码测试时也可以无缝地在编辑器内模拟各种设备的输入。更重要的是它为游戏未来的多平台发行铺平了道路无论是后续接入Xbox、PlayStation手柄还是适配新的VR设备核心的游戏控制代码几乎无需改动。接下来我会从设计思路到代码实现完整拆解如何构建这样一个健壮的跨平台输入模块。2. Input System基础架构与核心概念解析在动手写代码之前必须理解Input System的几个核心概念否则很容易陷入旧Input Manager的思维定式把它用成一个更复杂的键位查询工具。2.1 Input Actions资源输入意图的蓝图Input Actions是一个.inputactions资源文件它是整个系统的配置中心。你可以把它想象成一个“输入意图清单”。在这个清单里你不定义“按A键”而是定义“移动”、“跳跃”、“攻击”这样的游戏内动作。每个动作Action都有其类型Action TypeButton用于跳跃、攻击等瞬时动作。它关心的是“按下”、“按住”、“松开”这些状态。Value用于移动、视角旋转等连续值动作。它返回一个向量如Vector2非常适合摇杆和WASD输入。Pass-Through一种特殊类型所有输入事件直接穿过不做任何处理常用于调试或需要原始输入流的场景。我们的移动和跳跃功能就需要一个Value类型的“Move”动作和一个Button类型的“Jump”动作。2.2 绑定Bindings连接意图与设备的桥梁动作定义好后是空的需要“绑定”到具体的物理输入上这就是Bindings。一个“Move”动作可以同时绑定到多个输入源键盘的WASD键组合成一个Vector2。游戏手柄的左摇杆。触摸屏上的一个浮动摇杆Stick Control。Input System的强大之处在于你可以为一个动作添加多个绑定它会自动合并这些输入源。当玩家使用键盘时系统采用键盘的输入值当玩家连接手柄后系统会自动无缝切换到摇杆的输入值无需你写任何切换代码。这种“输入源仲裁”机制是跨平台兼容性的基石。2.3 交互Interactions与处理器Processors精细化控制输入行为这是Input System相比旧系统的高阶能力允许你对原始输入信号进行加工。交互Interactions定义输入的触发逻辑。比如默认的Press交互就是按下触发。但你还可以为“跳跃”动作添加Tap点击、Hold长按、MultiTap多次点击等交互。例如你可以轻松实现“长按跳跃键进行蓄力跳”的功能而无需在游戏逻辑层写一堆计时器代码。处理器Processors对输入值进行后处理。比如摇杆的输入值通常需要Stick Deadzone处理器来消除摇杆回中时的小幅抖动你也可以为移动添加Invert Vector2处理器来反转Y轴如果需要。处理器在绑定层级或动作层级均可配置非常灵活。2.4 控制方案Control Schemes与设备匹配虽然Input System能自动仲裁但有时我们想明确区分“键鼠方案”和“触屏方案”尤其是在UI提示上。Control Schemes允许你对绑定进行分组。你可以创建一个“KeyboardMouse”方案将WASD和鼠标绑定的动作归入其中再创建一个“Touch”方案将屏幕摇杆和按钮绑定的动作归入其中。玩家切换设备时当前激活的控制方案会随之改变你可以在UI上动态更新操作提示图标。3. 实战构建从零创建跨平台移动与跳跃理论讲完我们进入实战。假设我们要为一个3D角色实现基础的移动八方向和跳跃功能。3.1 创建与配置Input Actions资源在Project窗口右键 -Create - Input Actions命名为PlayerControls。双击打开该资源会打开Input Actions编辑器窗口。创建Action Map一个Action Map可以理解为一套完整的操作上下文比如“Player”游戏内操控、“UI”界面导航。我们点击号添加一个Map命名为“Player”。创建Actions在“Player” Map下点击号添加Action命名为MoveAction Type选择ValueControl Type选择Vector2。这表示它需要一个二维向量输入。再次点击号添加Action命名为JumpAction Type选择Button。这表示它是一个按钮动作。为Move动作添加绑定选中Move动作在右侧点击号添加绑定。选择路径Keyboard/w。这会创建一个键盘绑定。但我们需要的是组合WASD。所以不要直接选单个键点击绑定点右侧的“Path”文字手动输入复合路径Keyboard/w, Keyboard/a, Keyboard/s, Keyboard/d。或者更规范的做法是添加一个“Up/Down/Left/Right Composite”绑定。点击Move动作下绑定点右侧的“”图标选择Add Up/Down/Left/Right Composite。系统会自动生成一个复合绑定结构分别将Up、Down、Left、Right绑定到W、S、A、D键。这是最标准的方式因为它明确了上下左右的方向语义。接着为Move添加手柄支持。再次点击号添加绑定路径选择Gamepad/leftStick。最后添加触屏支持。点击号路径选择Touchscreen/primaryTouch/position等等这里有个坑。直接绑定位置是不对的因为位置是绝对坐标不是方向向量。我们需要的是虚拟摇杆。Input System内置了On-Screen Stick控件。你需要在场景中创建一个On-Screen StickGameObject - UI - On-Screen Stick。它会自动生成一个PlayerInput组件和对应的UI。在Input Actions编辑器中为Move添加绑定时路径可以选择Touchscreen/touch0/position但这需要自己处理逻辑。更简单的方式是利用PlayerInput组件它会自动将屏幕摇杆的输入映射到你在Move动作上绑定的VirtualMouse/delta或通过Stick Control来实现。实际上更常见的做法是在PlayerInput组件上设置UI Input Module并使用其自动生成的On-Screen Stick该摇杆会直接向Move动作发送Vector2输入前提是你在动作绑定了VirtualMouse/delta或使用Stick Control类型。更直接且推荐的方法是在动作绑定中直接选择Touchscreen/primaryTouch/delta需要处理器转换或者使用扩展包如Unity Input System Package自带的Touch Simulation在编辑器中模拟并依赖PlayerInput的自动UI控件生成。对于新手我建议先完成键鼠和手柄的绑定触屏通过On-Screen StickUI控件来间接绑定这样更直观。为Jump动作添加绑定选中Jump动作添加绑定路径选择Keyboard/space。添加绑定路径选择Gamepad/buttonSouth通常是A键。触屏绑定通常不直接绑定到某个触摸点而是通过PlayerInput自动生成的On-Screen ButtonUI控件来触发。我们可以在UI上放一个按钮并将其On Click()事件关联到Jump动作的触发。可选创建Control Schemes在编辑器左上角点击“Control Schemes”旁边的添加“KeyboardMouse”。再添加一个“Touch”。然后你可以将每个绑定拖到对应的Scheme下。例如将WASD和Space绑定拖到“KeyboardMouse”下将虚拟摇杆和屏幕按钮的绑定如果有拖到“Touch”下。这不是必须的但有助于管理。保存Asset。注意在编辑器内配置绑定时对于触屏往往不是直接在.inputactions文件里绑定一个屏幕区域而是通过UI控件来驱动。PlayerInput组件有一个“UI Input Module”选项当它检测到当前为移动平台时会自动实例化预设的屏幕控制UI如摇杆、按钮并将这些UI的输入自动路由到你定义的Move和Jump动作上。这是实现触屏输入的关键一步比手动绑定触摸位置更可靠。3.2 编写角色控制器代码C#现在我们来编写处理这些输入并驱动角色运动的脚本。这里采用消息监听Event-driven的方式这是更高效和模块化的方式。using UnityEngine; using UnityEngine.InputSystem; // 关键命名空间 [RequireComponent(typeof(CharacterController))] // 假设我们使用CharacterController public class PlayerMovement : MonoBehaviour { [Header(移动参数)] [SerializeField] private float moveSpeed 5f; [SerializeField] private float jumpHeight 2f; [SerializeField] private float gravity -9.81f; [Header(输入引用)] [SerializeField] private InputActionReference moveActionRef; // 在Inspector中拖入Move动作资源 [SerializeField] private InputActionReference jumpActionRef; // 在Inspector中拖入Jump动作资源 private CharacterController characterController; private Vector2 currentMoveInput; // 存储当前移动输入向量 private Vector3 playerVelocity; // 用于计算重力与跳跃速度 private bool isGrounded; // 用于旋转角色面向移动方向可选 [SerializeField] private float rotationSpeed 10f; [SerializeField] private Transform cameraTransform; // 第三人称常用基于摄像机方向移动 private void Awake() { characterController GetComponentCharacterController(); // 如果未指定摄像机默认使用主摄像机 if (cameraTransform null Camera.main ! null) cameraTransform Camera.main.transform; } private void OnEnable() { // 启用输入动作并注册回调函数 if (moveActionRef ! null) { moveActionRef.action.Enable(); moveActionRef.action.performed OnMovePerformed; moveActionRef.action.canceled OnMoveCanceled; } if (jumpActionRef ! null) { jumpActionRef.action.Enable(); jumpActionRef.action.performed OnJumpPerformed; // 对于Button通常只需要performed按下事件 } } private void OnDisable() { // 禁用时取消注册并禁用动作防止内存泄漏 if (moveActionRef ! null) { moveActionRef.action.performed - OnMovePerformed; moveActionRef.action.canceled - OnMoveCanceled; moveActionRef.action.Disable(); } if (jumpActionRef ! null) { jumpActionRef.action.performed - OnJumpPerformed; jumpActionRef.action.Disable(); } } // 移动输入回调 private void OnMovePerformed(InputAction.CallbackContext context) { // 读取Value类型的输入值 currentMoveInput context.ReadValueVector2(); } private void OnMoveCanceled(InputAction.CallbackContext context) { // 输入取消时如松开按键将输入归零 currentMoveInput Vector2.zero; } // 跳跃输入回调 private void OnJumpPerformed(InputAction.CallbackContext context) { // 只有在地面时才允许跳跃 if (isGrounded) { // 根据物理公式 v sqrt(2 * g * h) 计算初始跳跃速度 playerVelocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); } } private void Update() { // 1. 检测是否在地面CharacterController的Grounded在下一帧才更新所以用额外检测 isGrounded characterController.isGrounded; if (isGrounded playerVelocity.y 0) { playerVelocity.y -2f; // 一个小的向下力确保角色紧贴地面 } // 2. 处理移动基于输入和摄像机方向 Vector3 moveDirection Vector3.zero; if (cameraTransform ! null) { // 第三人称将输入向量转换为相对于摄像机方向的世界空间向量 Vector3 forward cameraTransform.forward; Vector3 right cameraTransform.right; forward.y 0; right.y 0; forward.Normalize(); right.Normalize(); moveDirection (forward * currentMoveInput.y right * currentMoveInput.x).normalized; } else { // 第一人称或无需摄像机相对移动直接使用输入向量 moveDirection new Vector3(currentMoveInput.x, 0, currentMoveInput.y); } // 应用移动速度 characterController.Move(moveDirection * moveSpeed * Time.deltaTime); // 3. 处理旋转使角色面向移动方向 if (moveDirection ! Vector3.zero) { Quaternion targetRotation Quaternion.LookRotation(moveDirection); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); } // 4. 处理重力与跳跃速度 playerVelocity.y gravity * Time.deltaTime; characterController.Move(playerVelocity * Time.deltaTime); } }3.3 配置场景与PlayerInput将上述脚本挂载到你的玩家角色GameObject上。确保该GameObject上有CharacterController组件。在Inspector中将PlayerControls.inputactions资源中对应的Move和Jump动作分别拖拽到脚本的Move Action Ref和Jump Action Ref字段。为角色GameObject添加一个PlayerInput组件。将PlayerControls.inputactions资源拖入Actions字段。Behavior选择Invoke Unity Events这样我们可以自己控制事件更灵活或Send Messages较旧方式。上面代码使用的是直接引用InputActionReference不依赖PlayerInput的事件因此PlayerInput组件主要用来管理输入设备切换和UI控件生成。关键一步在PlayerInput组件中找到UI Input Module折叠栏。勾选“Auto-Switch UI Input Module”。在“Canvas”字段中指定你的游戏UI Canvas如果还没有创建一个Canvas设置其Render Mode为Screen Space - Overlay。在“Control Scheme Prompts”部分你可以关联不同的控制方案对应的提示图标例如键盘图标、Xbox手柄图标。最重要的是确保你的Canvas下有一个EventSystem对象创建Canvas时通常会自带。PlayerInput会自动为其添加或配置Input System UI Input Module组件这是屏幕控件工作的基础。创建触屏控件在UI Canvas下右键 -UI - On-Screen Stick。Unity会自动创建一个包含摇杆背景和手柄的UI元素并为其配置好On-Screen Stick组件。同样创建一个按钮右键 -UI - Button将其文本改为“跳跃”或使用图标。选中PlayerInput组件在Inspector底部你会看到“Auto-Switch UI Input Module”下方有一个“On-Screen Controls”列表可能需要展开。确保你的On-Screen Stick和Button都在这个列表中。PlayerInput会在运行时根据当前激活的控制方案如“Touch”自动启用这些屏幕控件并将其输入关联到对应的Move和Jump动作。实操心得PlayerInput组件的自动UI管理功能在快速原型阶段非常方便但对于复杂的UI布局或自定义输入逻辑你可能需要手动管理屏幕控件的显示/隐藏和输入绑定。一个更可控的方法是自己编写脚本监听PlayerInput的onControlsChanged事件当设备切换时手动激活或禁用对应的UI控件组。4. 进阶技巧与深度优化基础功能跑通后我们来看看如何让它更健壮、更专业。4.1 输入缓冲Input Buffering与 coyote time土狼时间这是提升操作手感的关键技巧在平台跳跃游戏中尤其重要。输入缓冲允许玩家在落地前几帧按下跳跃键角色一落地就会立刻起跳。实现方法是在Update中记录跳跃输入并在可跳跃时消费这个输入。Coyote Time允许玩家在离开平台边缘后的一个极短时间窗口内如0.1秒按下跳跃键依然有效。这能减少因时机判断过于苛刻带来的挫败感。[Header(手感优化)] [SerializeField] private float jumpBufferTime 0.15f; // 跳跃缓冲时间 [SerializeField] private float coyoteTime 0.1f; // 土狼时间 private float jumpBufferCounter; // 跳跃缓冲计时器 private float coyoteTimeCounter; // 土狼时间计时器 private void Update() { // ... 原有的地面检测和移动逻辑 ... // Coyote Time 逻辑 if (isGrounded) { coyoteTimeCounter coyoteTime; // 落地时重置土狼时间 } else { coyoteTimeCounter - Time.deltaTime; // 空中时递减 } // 跳跃缓冲逻辑在输入回调中不再直接跳跃而是设置计时器 // OnJumpPerformed 修改为 // jumpBufferCounter jumpBufferTime; // 在Update中处理跳跃 jumpBufferCounter - Time.deltaTime; if (jumpBufferCounter 0 (isGrounded || coyoteTimeCounter 0)) { // 执行跳跃 playerVelocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); jumpBufferCounter 0; // 消费掉这次缓冲 coyoteTimeCounter 0; // 使用土狼时间后清零 } // ... 重力和速度应用 ... }4.2 不同平台下的输入参数调优不同设备的输入特性不同需要在处理器中微调。手柄摇杆死区Deadzone在Move动作的绑定中为Gamepad/leftStick添加一个Stick Deadzone处理器。将Min和Max值调整到合适范围如Min0.125, Max0.925可以过滤掉摇杆的微小抖动和确保最大输入范围。键盘输入平滑键盘输入是瞬时的0或1移动会显得生硬。可以为Move动作添加一个Axis Deadzone处理器对于复合WASD绑定或者更高级的做法在代码中对currentMoveInput向量应用平滑插值Lerp。触屏摇杆灵敏度On-Screen Stick组件上有Movement Range摇杆移动范围和Dynamic Origin动态原点等参数。根据游戏类型调整这些参数Dynamic Origin设为True时摇杆会在玩家第一次触摸屏幕的位置出现体验更好。4.3 动态控制方案切换与UI反馈当玩家连接或断开手柄时游戏UI的操作提示应该相应变化。在PlayerInput组件上订阅onControlsChanged事件。在事件回调中通过PlayerInput.currentControlScheme获取当前激活的控制方案名称如“KeyboardMouse”或“Gamepad”。根据这个名称切换UI上显示的键位图标。你可以预先准备几套Sprite图集键盘、Xbox手柄、PlayStation手柄、任天堂手柄图标动态替换。public class UIControlPromptUpdater : MonoBehaviour { public PlayerInput playerInput; public Image jumpPromptImage; // 显示跳跃键图标的UI Image public Sprite keyboardJumpSprite; public Sprite gamepadJumpSprite; public Sprite touchJumpSprite; private void Start() { if (playerInput null) playerInput FindObjectOfTypePlayerInput(); if (playerInput ! null) { playerInput.onControlsChanged OnControlsChanged; UpdatePrompt(playerInput.currentControlScheme); // 初始化 } } private void OnControlsChanged(PlayerInput obj) { UpdatePrompt(obj.currentControlScheme); } private void UpdatePrompt(string scheme) { switch (scheme) { case KeyboardMouse: jumpPromptImage.sprite keyboardJumpSprite; break; case Gamepad: jumpPromptImage.sprite gamepadJumpSprite; break; case Touch: jumpPromptImage.sprite touchJumpSprite; break; default: break; } } }5. 常见问题排查与调试技巧即使按照步骤操作也可能会遇到一些坑。这里记录几个我踩过的雷和解决方法。5.1 输入无响应或动作未触发检查动作是否启用在代码中确保在OnEnable里调用了action.Enable()在OnDisable里调用了action.Disable()。或者在PlayerInput组件中确保Actions资源已正确赋值且Behavior设置正确。检查绑定路径在Input Actions编辑器中仔细检查每个绑定的路径是否正确。特别是复合绑定如WASD确保上下左右键没有绑定错。检查设备屏蔽在PlayerInput组件或InputSystem设置中是否无意中禁用了某些设备类型。使用Input Debugger菜单栏Window - Analysis - Input Debugger。这是最强大的调试工具。你可以实时查看所有连接的设备、原始输入信号、以及激活的动作和它们的值。如果动作被触发了这里一定会显示。5.2 触屏控件不显示或无效Canvas渲染模式确保放置触屏控件的Canvas的Render Mode是Screen Space - Overlay或Screen Space - Camera。World Space模式下的UI可能无法接收触摸输入。EventSystem场景中必须有且只有一个EventSystem对象并且其上有Input System UI Input Module组件PlayerInput通常会帮你配置。PlayerInput UI配置确认PlayerInput组件上的Canvas字段已正确指定并且On-Screen Controls列表里包含了你的摇杆和按钮。平台设置在Project Settings - Player - Resolution and Presentation中确保Disable Depth and Stencil未被勾选某些UI效果需要。对于移动端检查Default Orientation是否合适。5.3 移动输入向量值异常如Y轴反转处理器顺序检查Move动作绑定的处理器。Invert Vector2处理器会反转Y轴。如果你同时使用了摇杆和键盘确保处理器只应用在需要的绑定上而不是整个动作。摄像机相对计算错误这是最常见的原因。在代码中计算摄像机相对移动方向时务必确保将摄像机向量的Y分量归零forward.y 0; right.y 0;并重新归一化Normalize()。否则摄像机的俯仰角会影响水平移动方向。输入设备校准某些手柄可能需要校准。在PC上可以通过系统的游戏控制器设置进行校准。5.4 在编辑器中模拟不同设备这是Input System的一大优势你不需要真机就能测试。打开Window - Analysis - Input Debugger。在Debugger窗口的右侧点击“Add Device”。你可以添加Touchscreen、Gamepad、Joystick等模拟设备。添加后在Game视图你就可以用鼠标模拟触摸点击并拖动用键盘模拟手柄默认映射如箭头键模拟摇杆Enter模拟按钮。这极大方便了跨平台输入的开发调试。5.5 构建后输入失效特别是WebGL这是一个经典问题往往与输入系统初始化有关。初始化顺序确保所有依赖于Input System的脚本其OnEnable和Start中的初始化代码不会在Input System自身初始化之前执行。在Awake中订阅事件有时会导致问题。一个稳妥的做法是在Start中启用动作或者使用InputSystem.onSettingsChange事件来延迟初始化。WebGL的特定问题WebGL平台对输入处理有特殊要求。确保在Project Settings - Player - WebGL - Publishing Settings中“WebGL 1.0/2.0”的选项与你的输入需求兼容。有时需要启用“Auto Graphics API”或调整其他设置。最关键的WebGL构建后首次加载可能较慢输入系统初始化需要时间游戏启动时应有一个明确的“点击开始”界面等待输入系统准备就绪。动作资源未包含在构建中检查PlayerControls.inputactions文件是否在Resources文件夹下或者是否通过Addressables或直接依赖被打包。最简单的方法是将其放在Resources文件夹外并确保引用它的场景或预制体被打包。这套从键鼠到触屏的跨平台输入解决方案其核心思想是“抽象”与“解耦”。通过Input System我们将多变的硬件输入抽象为稳定的游戏意图让游戏逻辑保持纯净。实现过程中.inputactions资源的配置是地基PlayerInput组件是自动化脚手架而C#脚本则是连接输入与游戏世界的桥梁。掌握它不仅能解决眼前的多平台需求更能为你应对未来更复杂的输入交互如双人同屏、自定义按键映射打下坚实的基础。在实际项目中不妨从一个小功能开始尝试逐步将旧系统迁移过来你会立刻感受到代码清晰度和维护效率的提升。