1. 项目概述为什么我们需要新的输入系统如果你是从Unity 2019或更早版本一路用过来的开发者肯定对Input.GetKeyDown(KeyCode.Space)和Input.GetAxis(“Horizontal”)这类代码再熟悉不过了。这套经典的输入系统Legacy Input System简单直接对于快速原型和小型项目来说上手几乎零门槛。然而随着游戏平台和交互方式的爆炸式增长——从PC、主机到移动触屏、VR/AR手柄甚至体感设备——这套老系统的局限性就越来越明显了。我接手过一个从移动端移植到PC和Switch的多平台项目当时用的就是老输入系统。噩梦开始了移动端的触摸输入、PC的键鼠、Switch的Joy-Con手柄每种设备的输入逻辑、轴映射、按键响应都不同。代码里充满了#if UNITY_STANDALONE和#if UNITY_SWITCH这样的平台判断逻辑支离破碎维护起来苦不堪言。更头疼的是想支持玩家自定义按键几乎得从头自己造轮子。Unity新输入系统Input System Package就是为了解决这些痛点而生的。它不是对老系统的简单修补而是一次彻底的重构和升级。它的核心设计思想是“输入动作”而非“具体设备”。简单来说你不再关心玩家是按下了键盘的“空格键”还是手柄的“A键”你只关心玩家执行了一个“跳跃”动作。系统会自动帮你处理不同输入设备到抽象动作的映射。这带来的好处是巨大的跨平台支持变得异常简单、原生支持玩家重绑定按键、输入事件更加精细和可配置并且性能也更优。对于任何计划开发跨平台游戏或者希望提供更专业、更灵活操控体验的项目学习和迁移到新输入系统几乎是一个必选项。它代表了Unity在输入处理上的现代最佳实践。2. 新输入系统的核心概念与架构解析在动手配置之前我们必须先理解新输入系统的几个核心概念。这就像学开车前得先知道方向盘、油门、刹车在哪理解了它们后面的操作才会得心应手而不是对着文档照猫画虎。2.1 核心概念四要素1. Input Actions Asset (.inputactions 文件)这是新输入系统的配置中心一个可序列化的资产文件。你可以把它想象成一个“输入动作清单”或“操控方案蓝图”。所有关于玩家能做什么如移动、跳跃、攻击的定义以及这些动作如何映射到具体的物理设备键盘WASD、手柄摇杆都在这个文件里集中管理。它完全独立于场景和代码可以在不同项目间复用极大地提升了可维护性。2. Action动作动作是你游戏逻辑所关心的抽象输入事件。它是你与输入系统交互的主要接口。动作分为三种类型Value值类型用于获取连续变化的输入值最典型的就是摇杆的二维向量(x, y)或扳机键的压力值[0, 1]。处理移动、视角旋转、油门控制等场景。Button按钮类型用于判断按钮的按下、按住、松开等离散状态。它内部其实也包含一个[0, 1]的值如0表示松开1表示按下但提供了更易用的布尔值接口。处理跳跃、射击、交互等。Pass-Through直通类型一种特殊类型任何输入都会直接穿透并触发事件不进行任何过滤或处理。通常用于调试或需要捕获所有原始输入数据的场景日常使用较少。3. Binding绑定绑定是连接抽象动作和具体物理输入的桥梁。一个动作可以有一个或多个绑定。例如“移动”动作可以绑定到“键盘WASD”和“手柄左摇杆”。绑定的强大之处在于支持“复合输入”如WA同时按代表向左前移动和“处理器”如对摇杆输入值进行死区过滤或标准化。4. Action Map动作地图动作地图是一组相关动作的集合用于对输入进行逻辑分组。例如你可以创建一个“Player”动作地图包含“移动”、“跳跃”、“攻击”等动作再创建一个“UI”动作地图包含“导航”、“确认”、“取消”等动作。通过在同一时间只启用一个或几个动作地图你可以轻松实现“游戏模式”和“菜单模式”的输入切换避免菜单弹出时角色还在乱跑。5. PlayerInput 组件这是一个挂载在GameObject上的MonoBehaviour组件它是连接Input Actions Asset和你的游戏代码的桥梁。它负责读取.inputactions资产实例化内部的输入设备监听并将输入事件发送到你指定的处理脚本。它提供了几种消息传递模式如Send Messages、Unity Events、C# Events让你可以选择自己喜欢的方式响应输入。2.2 新老系统架构对比为了更直观地理解我们列个表对比一下特性维度传统输入系统 (Legacy Input)新输入系统 (Input System)设计哲学基于具体设备Device-Oriented基于抽象动作Action-Oriented配置方式硬编码或有限的Project Settings输入管理器可视化的.inputactions资产文件跨平台支持需大量手动代码适配和平台宏判断通过绑定多设备自动适配几乎无需平台特定代码按键重绑定需完全自行实现存储、加载和映射逻辑原生支持提供API可轻松实现运行时重绑定输入处理粒度较粗主要在Update中轮询极细提供Started/Performed/Canceled精确事件复合输入支持困难如组合键需自行判断原生支持如“ShiftW”作为独立绑定输入设备管理简单枚举功能有限强大的设备热插拔、按类型筛选、创建虚拟设备注意新系统并非在所有方面都碾压老系统。对于超简单的、仅针对单一平台如只做PC的快速原型老系统的简洁性仍有其价值。但一旦项目复杂度稍有提升新系统的优势就会指数级放大。3. 从零开始配置新输入系统完整实操指南理论讲完了我们进入实战环节。我会带你从安装包开始一步步创建一个完整的、可用于第三人称角色控制的输入配置。3.1 安装与项目设置打开Package Manager在Unity Editor中点击顶部菜单Window Package Manager。安装Input System在Package Manager窗口左上角确保来源是“Unity Registry”。在列表中找到“Input System”点击它然后点击右下角的“Install”按钮。这个过程会联网下载并安装包。启用新输入系统关键步骤安装完成后Unity会弹出一个对话框提示你需要禁用旧的输入后端。务必点击“Yes”或“Disable”。这步操作会修改项目的Project Settings Player Other Settings Configuration Active Input Handling选项为“Input System Package (New)”或“Both”。“Input System Package (New)”只使用新输入系统。Input类将指向新的API。推荐新项目选择此项。“Both”新旧系统并存。Input类仍指向旧API新系统需要通过UnityEngine.InputSystem命名空间访问。适用于渐进式迁移的老项目。实操心得如果你在安装后没看到提示或者不小心点了“No”可以手动去Project Settings里修改。如果选择“Both”在代码中要注意命名空间冲突虽然方便迁移但可能会混淆。对于全新项目我强烈建议直接切换到“New”模式逼自己完全适应新范式。3.2 创建并配置你的第一个 Input Actions Asset创建资产在Project窗口右键选择Create Input Actions。给它起个名字比如PlayerControls。打开编辑器双击这个.inputactions文件会打开一个专用的可视化编辑器窗口。这个窗口分为左中右三栏。创建第一个Action Map点击左上角的“”号选择“Add Action Map”。将其命名为Gameplay。这个地图将包含所有游戏过程中的操作。在Gameplay地图中添加动作在中间的“Actions”列表下点击“”号添加动作。动作1Move (Value类型 Vector2)名称Move动作类型Value控制类型Vector2添加绑定在右侧“Bindings”下点击“”选择Gamepad/leftStick。这样手柄左摇杆就绑定了。再添加一个绑定再次点击“”这次选择Keyboard/w然后不要松开鼠标继续选中Keyboard/a,Keyboard/s,Keyboard/d或者使用Keyboard/upArrow等方向键。你会看到它自动创建了一个“Up/Down/Left/Right”的复合2D向量绑定这正是我们需要的WASD控制。动作2Look (Value类型 Vector2)名称Look动作类型Value控制类型Vector2绑定Mouse/delta鼠标视角移动和Gamepad/rightStick手柄右摇杆视角控制。动作3Jump (Button类型)名称Jump动作类型Button绑定Keyboard/space和Gamepad/buttonSouth通常是手柄的A键。动作4Sprint (Button类型)名称Sprint动作类型Button绑定Keyboard/leftShift和Gamepad/leftTrigger左扳机键通常有模拟量但这里我们当按钮用。为摇杆绑定添加“死区”处理器手柄摇杆在自然状态下会有微小漂移我们需要过滤掉。选中Move动作下Gamepad/leftStick这个绑定。在绑定属性面板下方点击“Add Processor”。选择Stick Deadzone。保持默认的min0.125,max0.925即可。这个处理器会忽略摇杆中心微小区域的输入死区并将最大输入范围标准化。同样为Look动作下的Gamepad/rightStick也加上Stick Deadzone处理器。保存资产编辑完成后记得点击窗口上方的“Save Asset”按钮。你的输入配置就保存在这个.inputactions文件里了。3.3 在游戏对象上使用 PlayerInput 组件配置好了输入动作资产现在需要把它用起来。创建玩家角色在场景中创建一个胶囊体Capsule作为玩家角色命名为Player。挂载 PlayerInput 组件选中Player对象在Inspector面板点击Add Component搜索并添加PlayerInput组件。指定 Actions Asset在PlayerInput组件的Actions属性栏将我们刚才创建的PlayerControls资产拖拽赋值进去。选择行为模式Behavior属性决定了输入事件如何传递到你的脚本。有几种选择Send Messages向同一GameObject上所有脚本发送名为On[ActionName]的消息。这是旧式Unity消息系统不推荐用于新项目缺乏类型安全。Broadcast Messages类似Send Messages但会发送给该对象及其所有子对象的脚本。更不推荐。Invoke Unity Events最直观、编辑器友好的方式。为每个动作生成一个UnityEvent你可以像配置UI按钮一样在Inspector里拖拽指定响应函数。非常适合快速原型和简单逻辑。Invoke C# Events最灵活、性能最佳的方式。你需要编写一个脚本实现PlayerInput事件相关的接口如IOnAction或通过playerInput.onActionTriggered事件来接收所有输入。代码控制力最强。Player Input Manager用于多人本地游戏自动为每个新加入的玩家生成角色实例。 对于初学者我建议先从“Invoke Unity Events”开始直观易懂。配置Unity Events如果你选择了“Invoke Unity Events”展开PlayerInput组件下方会出现一个可折叠列表列出了Gameplay动作地图下的所有动作Move, Look, Jump, Sprint。点击每个动作旁边的“”号可以将场景中任何对象的公有方法无参或有一个InputAction.CallbackContext参数拖拽进来。例如你可以创建一个PlayerController脚本里面有OnMove,OnJump等方法然后在这里关联。3.4 编写代码处理输入C# Events 模式示例虽然Unity Events方便但更健壮和主流的方式是使用“Invoke C# Events”模式。下面是一个PlayerController脚本的示例演示如何接收和处理输入。using UnityEngine; using UnityEngine.InputSystem; // 关键引入新输入系统的命名空间 public class PlayerController : MonoBehaviour { // 用于接收输入的组件引用 private PlayerInput playerInput; // 缓存我们关心的输入动作便于快速访问 private InputAction moveAction; private InputAction lookAction; private InputAction jumpAction; private InputAction sprintAction; // 移动相关参数 public float moveSpeed 5f; public float sprintMultiplier 1.5f; public float jumpForce 5f; private Vector2 moveInput; private Vector2 lookInput; private bool isSprinting false; private Rigidbody rb; private bool isGrounded; void Awake() { rb GetComponentRigidbody(); playerInput GetComponentPlayerInput(); // 从PlayerInput组件中获取动作映射 // 注意这里假设动作地图名称就是Gameplay moveAction playerInput.actions[Gameplay/Move]; lookAction playerInput.actions[Gameplay/Look]; jumpAction playerInput.actions[Gameplay/Jump]; sprintAction playerInput.actions[Gameplay/Sprint]; } void OnEnable() { // 启用动作PlayerInput组件通常会自动处理但显式启用是良好实践 moveAction.Enable(); lookAction.Enable(); jumpAction.Enable(); sprintAction.Enable(); // 为按钮类型动作绑定事件Started, Performed, Canceled jumpAction.started OnJumpStarted; // 按下瞬间 sprintAction.started OnSprintStarted; sprintAction.canceled OnSprintCanceled; } void OnDisable() { // 禁用动作清理事件订阅防止内存泄漏 jumpAction.started - OnJumpStarted; sprintAction.started - OnSprintStarted; sprintAction.canceled - OnSprintCanceled; moveAction.Disable(); lookAction.Disable(); jumpAction.Disable(); sprintAction.Disable(); } void Update() { // 对于Value类型的动作如移动、视角在Update中读取当前值 // 这种方式是“轮询”适合连续变化的值 moveInput moveAction.ReadValueVector2(); lookInput lookAction.ReadValueVector2(); // 应用移动这里简化处理实际可能结合摄像机方向 Vector3 movement new Vector3(moveInput.x, 0, moveInput.y); float currentSpeed moveSpeed * (isSprinting ? sprintMultiplier : 1f); transform.Translate(movement * currentSpeed * Time.deltaTime, Space.Self); // 应用视角旋转示例绕Y轴旋转 if (lookInput.magnitude 0.1f) // 加入一个小的输入阈值 { float rotationAmount lookInput.x * 100f * Time.deltaTime; // 灵敏度系数 transform.Rotate(Vector3.up, rotationAmount); } } // --- 事件回调方法 --- private void OnJumpStarted(InputAction.CallbackContext context) { // 检查是否在地面上这里需要你自己的地面检测逻辑 if (isGrounded) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); Debug.Log(Jump!); } } private void OnSprintStarted(InputAction.CallbackContext context) { isSprinting true; Debug.Log(Sprint Started); } private void OnSprintCanceled(InputAction.CallbackContext context) { isSprinting false; Debug.Log(Sprint Canceled); } // 简单的地面检测示例实际项目需要更健壮的方法 void OnCollisionStay(Collision collision) { if (collision.contacts[0].normal.y 0.5f) isGrounded true; } void OnCollisionExit(Collision collision) { isGrounded false; } }代码关键点解析InputAction.ReadValueT()这是获取Value类型动作当前值的标准方法。在Update中调用获取每一帧的输入状态。InputAction.started/performed/canceled这些是事件。对于按钮操作started是按下瞬间触发一次performed在满足触发条件时触发对于按钮通常就是按下canceled是松开时触发。对于值类型performed会在值变化时持续触发。CallbackContext事件回调的参数包含丰富信息如触发动作的引用、输入设备、时间戳、读取的值对于值类型等。启用与禁用在OnEnable中启用动作和订阅事件在OnDisable中取消订阅并禁用动作这是防止对象失活后仍接收输入或内存泄漏的好习惯。4. 高级配置与实战技巧掌握了基础配置和代码交互你已经能应对80%的需求。下面这些高级技巧和实战经验能帮你解决另外20%的难题并让你的输入系统更健壮、更专业。4.1 实现玩家按键重绑定这是新输入系统的一大卖点。核心思路是监听玩家想要绑定的新按键然后用它替换掉原有绑定。public class RebindingManager : MonoBehaviour { public InputActionReference targetAction; // 在Inspector中指定要重绑定的动作如“Gameplay/Jump” public string bindingId; // 要替换的绑定ID可以在.inputactions编辑器中查看 private InputActionRebindingExtensions.RebindingOperation rebindingOperation; public void StartRebinding() { // 1. 先禁用目标动作避免重绑过程中触发它 targetAction.action.Disable(); // 2. 开始重绑定操作 rebindingOperation targetAction.action.PerformInteractiveRebinding() .WithTargetBinding(targetAction.action.bindings.IndexOf(b b.id.ToString() bindingId)) // 指定替换哪个绑定 .WithControlsExcluding(Mouse) // 可选排除某些设备类型比如这里排除鼠标防止误绑到鼠标移动 .OnMatchWaitForAnother(0.1f) // 等待组合键如CtrlC的时间 .OnComplete(operation RebindComplete()) .Start(); // 开始监听输入 } private void RebindComplete() { // 3. 获取新绑定的路径并保存例如保存到PlayerPrefs或配置文件 string newBindingPath rebindingOperation.selectedControl.path; PlayerPrefs.SetString(${targetAction.action.name}_{bindingId}, newBindingPath); // 4. 清理并重新启用动作 rebindingOperation.Dispose(); targetAction.action.Enable(); Debug.Log($Rebind successful: {newBindingPath}); } public void LoadSavedBindings() { // 游戏启动时加载保存的绑定 string savedPath PlayerPrefs.GetString(${targetAction.action.name}_{bindingId}, null); if (!string.IsNullOrEmpty(savedPath)) { // 找到对应的绑定索引并应用覆盖 int bindingIndex targetAction.action.bindings.IndexOf(b b.id.ToString() bindingId); targetAction.action.ApplyBindingOverride(bindingIndex, new InputBinding { path savedPath }); } } }注意事项重绑定的UI交互设计很重要。你需要一个UI界面让玩家点击按钮后触发StartRebinding()并显示“请按下新按键...”的提示。同时要处理好取消重绑定、重置为默认值的逻辑。4.2 处理多设备与设备丢失新输入系统能优雅地处理设备热插拔。void Start() { // 监听设备连接事件 InputSystem.onDeviceChange OnDeviceChange; } void OnDeviceChange(InputDevice device, InputDeviceChange change) { switch (change) { case InputDeviceChange.Added: Debug.Log($Device added: {device.name}); // 新设备如手柄连接可以更新UI提示图标 break; case InputDeviceChange.Removed: Debug.Log($Device removed: {device.name}); // 设备断开如果当前玩家在用这个设备可能需要暂停游戏或切换输入方案 break; case InputDeviceChange.ConfigurationChanged: // 设备配置改变如手柄从蓝牙模式切换到有线模式 break; } } // 在需要的时候可以获取当前所有可用设备 var gamepads Gamepad.all; // 所有连接的手柄 var keyboard Keyboard.current; // 当前键盘通常只有一个 var mouse Mouse.current; // 当前鼠标 if (keyboard ! null keyboard.spaceKey.wasPressedThisFrame) { // 使用设备特定的API更底层通常不如动作抽象好用 }4.3 使用“处理器”优化输入体验在绑定Binding层级添加处理器可以极大地改善手感。除了前面提到的Stick Deadzone还有几个非常实用的Invert(反转)常用于飞行游戏中的反转Y轴视角。直接勾选绑定属性中的Invert复选框即可。Scale(缩放)对输入值乘以一个系数。例如将鼠标delta乘以0.5来降低鼠标灵敏度。Normalize(标准化)对于2D向量如WASD确保对角线方向的速度不会快于轴向即向量长度最大为1。WASD绑定默认就带这个处理器。Clamp(钳制)将输入值限制在最小/最大值之间。自定义处理器你甚至可以继承InputProcessorT类编写自己的处理器实现更特殊的逻辑如输入平滑、加速度模拟等。4.4 在UI与游戏逻辑间切换输入地图这是避免“菜单打开时角色还能动”问题的标准做法。public class InputMapManager : MonoBehaviour { public PlayerInput playerInput; void Start() { // 假设初始启用Gameplay地图 SwitchToActionMap(Gameplay); } public void OpenPauseMenu() { // 打开暂停菜单时切换到UI地图禁用Gameplay SwitchToActionMap(UI); Time.timeScale 0f; // 暂停游戏逻辑 } public void ClosePauseMenu() { // 关闭菜单切回Gameplay SwitchToActionMap(Gameplay); Time.timeScale 1f; } private void SwitchToActionMap(string mapName) { // 禁用所有其他地图启用目标地图 playerInput.actions.Disable(); playerInput.actions.FindActionMap(mapName)?.Enable(); // 也可以使用PlayerInput组件的方法 // playerInput.SwitchCurrentActionMap(mapName); } }将PlayerInput组件的Default Action Map设置为Gameplay然后在UI菜单按钮的事件中调用OpenPauseMenu()和ClosePauseMenu()即可。5. 常见问题、性能优化与避坑指南在实际项目中踩过不少坑这里总结一下最常见的问题和解决方案。5.1 安装与基础问题Q1安装Input System后旧的Input.代码全部报错“找不到类型或命名空间”。A1这是因为项目设置中的Active Input Handling仍为“Old”或“Both (Old)”。你需要将其改为“Input System Package (New)”。如果改为“Both”则旧的UnityEngine.Input和新系统的UnityEngine.InputSystem.InputSystem可以共存但通常建议新项目直接迁移到“New”。Q2.inputactions 文件在Inspector里显示为“TextAsset”无法双击打开可视化编辑器。A2可能是Unity版本或Input System包版本问题。尝试重启Unity或者通过Window Analysis Input Debugger打开调试器在调试器窗口的“Options”菜单里选择“Open Input Actions Asset Editor”来打开。确保你安装的是正式版非Preview的Input System包。Q3为什么我的按钮事件如Jump在按住时会持续触发performed而不是只触发一次A3这是对动作交互Interaction的误解。默认的按钮绑定使用的是“Press”交互它有三个阶段Started按下瞬间、Performed按下后如果满足触发条件如长按、Canceled松开。对于普通的“按下即触发”你应该监听started事件。如果你希望“按下触发一次按住不重复触发”可以使用“Tap”交互轻触或者在代码中通过context.phase来判断是否为InputActionPhase.Started。5.2 性能与最佳实践1. 缓存InputAction引用不要在每次需要读取输入时都去playerInput.actions.FindAction(“xxx”)这会产生字符串查找开销。像前面的示例一样在Awake或Start中缓存引用。2. 合理使用轮询与事件对于连续变化的值如移动、视角在Update中使用ReadValueT()轮询是标准且高效的做法。对于离散的、一次性的动作如跳跃、开枪使用started/performed事件回调更合适可以精确响应触发时机。3. 注意动作的启用状态确保动作在需要时启用在不需要时禁用例如当角色死亡或UI打开时。禁用未使用的动作可以减少不必要的输入事件处理。PlayerInput组件在激活时会自动启用其默认动作地图但手动管理更精细。4. 谨慎使用onActionTriggered事件PlayerInput的onActionTriggered是一个全局事件任何动作触发都会调用它。你需要在里面用if(context.action.name “MyAction”)来判断这在大规模项目中效率较低且难以维护。优先使用每个动作单独的事件订阅或Unity Events。5. 输入调试利器Input DebuggerUnity Editor菜单Window Analysis Input Debugger。这个窗口可以实时显示所有连接的设备、原始输入数据、激活的动作地图和动作状态。当输入不按预期工作时这是你排查问题的第一站。5.3 特定平台与构建注意事项WebGL新输入系统对WebGL的支持很好。但要注意浏览器环境下的输入获取可能需要用户先与页面交互如点击画布。确保游戏启动时有明确的“点击开始”提示。移动端iOS/Android触摸输入被抽象为Touchscreen设备。你可以绑定Touchscreen/primaryTouch/tap来响应点击或者绑定Touchscreen/primaryTouch/position来获取触摸位置。对于虚拟摇杆通常需要自己用UI实现然后将其输出值模拟成一个Vector2输入再通过代码动态绑定到你的“Move”动作上。构建后输入无效确保.inputactions文件被包含在构建中。检查该文件的 Inspector确保其 “Include in Build” 选项是勾选的默认是勾选的。如果使用AssetBundle需要将其正确打包和加载。从老系统迁移过来初期肯定会有些不适应总觉得新系统“杀鸡用牛刀”。但一旦你熟悉了它的工作流尤其是在处理多平台、复杂交互和需要用户自定义控制时你会深刻体会到它带来的秩序和便捷。我的建议是在新项目中果断采用在老项目中可以逐步迁移关键模块。配置过程看似步骤多但绝大部分工作都在可视化的编辑器中完成实际上比在老系统里写一堆杂乱的平台判断代码要清晰和高效得多。