1. 先弄明白新旧两套输入系统并存的原因
Unity 的 Input System 从 2019 版本正式进入可生产环境之后,就一直处在"用了就回不去、但迁移有点疼"的状态。很多项目组还停在老 Input Manager 上,不是因为不知道新系统存在,而是怕一切换就引来一堆历史遗留问题。我自己的看法是:如果新项目是围绕多平台发布、手柄和键盘鼠标都要支持、操作交互有一定复杂度,那就尽早切到新输入系统;如果是纯休闲玩法的原型、只跑 PC 鼠标点击,那可以继续用旧的那套,别折腾。
但凡是需要认真对待输出配置的游戏,Input System 的价值会非常明显。它的名字容易让新手误以为只是"把老 API 换了个写法",实际它是两代完全不同的输入处理架构:旧的 Input Manager 面向轮询式读取,Update 里调 Input.GetAxis 或 Input.GetKeyDown 拿结果;新的 Input System 是事件驱动加数据流处理的组合体,输入动作先落到核心的 InputActionAsset 资产里,再以事件形式发给你的业务代码。事件驱动意味着你不用在每个 Update 里反复检测状态,系统会在按键按下、抬起、值变化这些时间点主动通知你。
另外一个很现实的问题是设备数量。老系统里键盘、鼠标、手柄、触屏的接口是分裂的,你要分别用不同的 API 去监听,然后自己汇总成一套"玩家操作意图",新系统把这个统一成了 Action 层面的东西。比如"移动"这个动作,不管玩家是用键盘 WASD、手柄摇杆还是触摸板,最终都是一个 Vector2 值发送给你的角色控制器。这种抽象对做多平台项目的开发来说,省掉的不是多少代码量,而是大量重复的判断逻辑。
适合阅读这篇的人,我建议有三种:一是刚从老输入系统迁移过来、准备学习新架构的整体思路;二是已经装了 Input System 包、但只是用了 PlayerInput 组件最简单的一层,想深入了解 Action 配置和代码生成关系;三是团队里要写操作模块封装、为主程或工具向的开发者做技术选型参考。这篇不会去讲每个类的完整 API 列表,而是从项目怎么用这个系统、配置资产怎么搭、事件怎么接、坑在哪里这四条线展开。
提示:写这篇时 Unity 版本默认围绕内置了 Input System 1.4.x 以上的环境。如果你项目里用的是 2020 LTS 或者更老版本,包版本不同会导致部分菜单和生成的代码结构存在差异,思路不受影响。
2. 拆开看新输入系统的四个核心概念
Input System 的中枢不是某个脚本类,而是一个可编辑资产:InputActionAsset,也就是你在工程里看到的 .inputactions 文件。理解了这个文件,就理解了新系统一半的运作方式。它不承担执行逻辑,只负责定义输入行为规格。比如:玩家可执行哪些动作、每个动作可以绑定哪些物理按键、这些绑定属于哪个控制方案、动作发出的值类型是什么。
这个文件主要由 Action Map、Action、Binding、Control Scheme 四层组成,我平时给团队交接时习惯把它们比喻成项目仓库里的目录结构:地图是几个大仓库,动作是仓库里的货架,绑定是货架上的具体货物,控制方案是物流配送规则。
2.1 行动作(Action)和行动作地图(Action Map)的关系
Action 是你要读取的最小逻辑单位,比如 Jump、Move、Fire。它本身不关心物理设备,只定义"我能发出什么类型的值",类型有 Button(布尔按下状态)、Value(连续值,比如 Vector2 轴量)、Pass-Through(透传原始数值,不做中间过滤)。Action Map 则是 Action 的分组容器,项目里很常见的分法是 Gameplay、UI、Menu。切换地图就可以快速切换整套操作模式,比如打开暂停菜单时从 Gameplay 切到 UI Map,原先绑定的移动、射击全部失效,只保留菜单操作和关闭菜单键。
这种分组设计和老系统相比,最大好处是你不需要在各个系统去写"当前是否在弹窗中""当前输入是否应该响应移动"的状态判断。在开菜单的瞬间,一行代码切掉 Action Map,所有游戏行为相关的动作自然不响应,UIMap 里的功能才会生效。对中大型项目来说,这种集中式的状态切换可以避免很多因为忘记管理输入状态而出现的幽灵按键。
2.2 绑定(Binding)、控制方案(Control Scheme)与设备(Device)
Binding 把动作和具体的物理信号直接做映射。比如给 Jump 动作添加一个 Binding,按键是 Keyboard 的 Space;再添加一个 Binding,按键是 Gamepad 的 ButtonSouth。一个 Action 可以同时挂非常多 Binding,运行时会根据当前控制方案和已连接设备动态选择匹配的那个。
Control Scheme 就是你设计的“操作配置模板”。它本质是一组设备需求说明,比如 KeyboardMouse 方案要求设备列表中包含键盘、鼠标中至少一个;Gamepad 方案要求包含手柄。PlayerInput 组件在运行时通过 Auto-Switch 机制,会依据已插入的设备自动切换 Control Scheme。实际跑起来的效果就是,玩家用键盘操作时左上角提示显示“按空格跳跃”,拔掉键盘拿起手柄,提示自动变为“按手柄跳跃”。做到这步的前提是你在编辑器里把 Binding Group 划分清楚。
设备(InputDevice)本身是一个底层概念,代表物理设备在 Input System 中的抽象。日常开发中大多数时候不会直接碰 InputDevice,但调试硬件识别问题时会用到,比如手柄插上没有反应,就要打开 Input Debugger 查看设备是否已经被系统枚举成功。
2.3 玩家输入组件(Player Input)与生成代码的协作
PlayerInput 是属于 MonoBehaviour 层面的东西,它会挂钩一个 InputActionAsset,并且自动监听其中的 Action Map,还可以在设备切换时派发事件。组件本身提供了多种事件处理模式,常见的有 Send Messages、Broadcast Messages 和 Unity Events。Send Messages 是按事件名把消息发到同一个游戏物体上挂载的脚本方法,比较省事;Unity Events 则在 Inspector 面板里可视化地配置回调,适合非程序员或快速搭建。
更精致的做法是用 C# 类的事件方式。点击 .inputactions 资产,在 Inspector 里勾选 Generate C# Class,Unity 会生成一份强类型代码,里面把每个 Action Map、Action 都包装成可直接订阅的 C# 事件。这样写业务代码时就可以这样操作:构造生成的类实例,调用 Enable(),然后给 specificAction.performed 挂上方法。好处是代码中不再出现字符串查找、枚举判断、强制类型转换,全部由生成代码覆盖,编译期就能发现错误。
3. 从零搭建输入映射的实际操作
这里用一个具体案例来讲,做一个简单的横版跳跃 Demo:玩家用 WASD 控制左右移动,空格跳跃,E 键交互物品,ESC 键呼出菜单。多平台同时支持 PC 键盘鼠标和 Xbox 手柄。这个案例比较经典,覆盖了连续值动作、按钮动作、UI切换动作三种最常见的需求,而且规模小、好复现。
3.1 安装包并切换输入后端
如果你的项目是全新创建的,Unity 2021+ 默认情况下可能两种模式都没开启,有个选项让你选择 Active Input Handling。如果项目已经写了不少用 Input.GetAxis 的代码,直接切到新系统会导致这些老 API 全部失效,编辑器控制台刷一堆 MissingMethodException 或冲突警告。正确做法是先在 Edit > Project Settings > Player > Other Settings 里找到 Active Input Handling,把它设为 Input System Package (New)。切完以后 Unity 会提示要重启编辑器。
如果项目里老代码数量比较多,临时又没时间重构,可以选 Both 模式让新旧两套同时跑。但 Both 不是长久之计,它会导致每帧对同一份原生输入数据进行两次处理,移动端的触摸事件、延迟、CPU 占用都会受影响。我先前的经验是,Both 只作为过渡阶段使用,迁移完成后立即改回新系统模式,同时全局搜索 Input.GetAxis、Input.GetKeyDown、Input.touches 这些旧 API,能清就清掉。
安装新包的方式有两种:如果你用的是 2021 之后的版本,Package Manager 里搜索 Input System,直接 Install;老版本还要先去 Player Settings 启用新输入后端,包才可以添加成功。装完包后 ProjectSettings 下会多出 Active Input Handling 选项,注意如果你把包移除,这个设置项还会存在,不手动切回来后期可能会有警告。
3.2 创建 InputAction 资产并配置动作、绑定、分组
在 Project 窗口中右键选择 Create > Input Actions,命名为 PlatformerControls,Unity 会自动打开一个专用的输入动作编辑器窗口。第一次打开会看到默认生成的两个 Action Map 和一个叫 "New action" 的空白动作,这些模板可以直接删掉重来。
第一步新建名为 Gameplay 的 Action Map,添加动作时给它起名 Move,动作类型选择 Value,控制类型选 Vector2。Value 类型会持续输出值,适合移动轴。Jump 和 Interact 选 Button,控制类型区域里有个默认的 Button 类型,这个不用改。还需要给 Gameplay 再加一个名为 Pause 的动作,类型 Button,用于呼出菜单。
第二步去 Binding 面板给 Move 添加绑定。Move 动作默认会生成一个绑定,你可以改它的 "Path" 为 "Gamepad/LeftStick",这个会映射手柄左摇杆。额外添加两条绑定,一个是 "Keyboard/W" 和 "Keyboard/S" 组合的差值绑定、一个是 "Keyboard/A" 和 "Keyboard/D" 组合的差值绑定。差值绑定需要你在 Binding 下拉菜单里选 "2D Vector (Composite)",然后在子绑定里设置负向和正向按键。这样就能在键盘上实现以两个按键组成左右矢量轴。如果想让鼠标 WASD 完整方向移动,添加两条复合绑定分别是 WASD 的四个方向。
Jump 动作添加两条绑定:Keyboard/Space 和 Gamepad/ButtonSouth。Interact 添加 Keyboard/E 和 Gamepad/ButtonWest。Pause 添加 Keyboard/Escape 和 Gamepad/Start。
第三步进入 Control Schemes 区域,新建 KeyboardMouse 方案,设备需求勾选 Keyboard 和 Mouse;新建 Gamepad 方案,设备需求勾选 Gamepad。这一步是后面自动切换控制方案的前提,千万别省略。保存完资产后选中该文件,在 Inspector 勾上 Generate C# Class,Apply 保存,Unity 就会生成一个同名 C# 脚本。
注意:生成 C# Class 时,建议把资产文件放在单独的 Editor 资源目录或者代码可访问的目录,避免它在 Build 过程中被打进最终包里。生成的代码本身会被编译进程序集,不需要额外的资源引用。
3.3 挂载组件并生成对应 C# 代码
把生成的 PlatformerControls 类实例化有两种方式:第一种是不挂 PlayerInput 组件,直接在某个全局管理脚本里 new 一个 PlatformerControls 实例,在 OnEnable 里调用 Enable,在 OnDisable 里调用 Disable。这种方式比较干净,适合纯代码驱动项目,缺点是没有可视化的 Inspector 配置。第二种是挂 PlayerInput 组件,在 Actions 属性里拖入 .inputactions 资产,同时 Behavior 选 C# Events,运行时会自动管理 Action Map 的启停。
我个人的项目都采用第一种纯代码方式。原因是 PlayerInput 组件和 C# 事件在多数场景下会重复铺代码,而且组件方式会让你把输入逻辑和游戏对象生命周期绑在一起,多人协作时职责不清晰。纯代码方式更简单:一个 GameInputManager 单例负责创建、启用、暴露公共事件供各个系统订阅,别的地方不需要直接访问输入类。如果你做的是小型 Game Jam 或快速原型,用 PlayerInput 组件配 Unity Events 反而更快,适可而止即可。
代码结构大致是:
using UnityEngine; using UnityEngine.InputSystem; public class GameInputManager : MonoBehaviour { private PlatformerControls _controls; public Vector2 MoveInput { get; private set; } public bool JumpPressed { get; private set; } public bool InteractPressed { get; private set; } public bool PausePressed { get; private set; } private void OnEnable() { _controls = new PlatformerControls(); _controls.Enable(); _controls.Gameplay.Move.performed += OnMove; _controls.Gameplay.Move.canceled += OnMoveCanceled; _controls.Gameplay.Jump.performed += OnJump; _controls.Gameplay.Interact.performed += OnInteract; _controls.Gameplay.Pause.performed += OnPause; } private void OnDisable() { _controls.Gameplay.Move.performed -= OnMove; _controls.Gameplay.Move.canceled -= OnMoveCanceled; _controls.Gameplay.Jump.performed -= OnJump; _controls.Gameplay.Interact.performed -= OnInteract; _controls.Gameplay.Pause.performed -= OnPause; _controls.Disable(); } private void OnMove(InputAction.CallbackContext context) { MoveInput = context.ReadValue<Vector2>(); } private void OnMoveCanceled(InputAction.CallbackContext context) { MoveInput = Vector2.zero; } private void OnJump(InputAction.CallbackContext context) { JumpPressed = true; } private void OnInteract(InputAction.CallbackContext context) { InteractPressed = true; } private void OnPause(InputAction.CallbackContext context) { PausePressed = true; } private void LateUpdate() { JumpPressed = false; InteractPressed = false; PausePressed = false; } }其它业务脚本只要轮询这个管理类的属性,或者在事件触发的那一帧消费值即可。例如角色移动脚本在 Update 中读取 MoveInput 做横向速度设置,而不是自己去订阅输入事件,这样天然避免了你需要在多个系统里重复处理输入。
3.4 事件式读取与轮询式判断的实际取舍
新输入系统主推事件式,但我们要清醒一点:事件式并非所有场景的银弹。连续轴值(比如移动、视角旋转)如果在 Update 中通过 performed 事件逐帧读取,代码会变得繁琐,因为每次值变化都会触发事件。像 Move 这种高频的连续值,我建议在管理类中用一个属性保存 latest value,业务脚本轮询这个属性即可,此所谓"事件写入、帧间读取",综合两种模式的优势。
按钮的按下和抬起则相反,适合直接用事件消费。比如跳跃在 performed 事件里记录一次 press 标记,在管理类 LateUpdate 中清空标记。这样做的核心考量是:跳跃动作是瞬时的,如果把判断放到 Update 里轮询,当帧率波动时用户按下的持续时间可能横跨多个帧,同一个跳跃动作可能被消费两次,出现空中二段跳 bug。事件订阅天然保证只触发一次。
还有一个关键点:如果同一动作绑定了键盘和手柄,事件只在设备匹配的控制方案激活时才会触发。默认情况下如果 Control Scheme 没有设置 devices 匹配,一个动作的多个 Binding 都能同时响应,这可能导致键盘按下时手柄 Binding 也触发(不同设备路径各自独立,不会因为值相同而合并)。实际项目里,你有需要区分设备场景(比如手柄震动反馈要跟踪设备身份)就需要在 InputActionAsset 的 Binding 组里勾选合适的 Control Scheme,我这里简述,后面专门篇再展开。
4. 常见坑与排查技巧实录
这部分比较重要。Input System 从架构设计上是合理的,但实际项目里几乎每个组都会碰到同一批奇怪问题,大部分不是代码逻辑错,而是配置和使用姿势问题。写成速查表,希望你能少踩几个。
4.1 “我订阅了事件,为什么就是不触发” 的排查顺序
这类问题在群里被问烂了,每次我都建议先查三件事:第一,你的 InputActionAsset 是否被 Enable 了。生成代码里的 Enable() 方法包含所有 ActionMap 的 SetActive,如果一口气没调好,比如只 Enable 了某个 Map 而事件订阅的是另一个 Map,就会完全没反应。
第二,检查是否在同一个实例上订阅了事件。C# 生成类每次 new 出来的实例是独立的,如果你在某处 new 了 PlatformerControls,另一处也 new 了一个并启用自己的事件,它们的输入状态互不干扰。常见错误是 Awake 里订阅了_controls.Gameplay.Jump.performed,然后在 Start 里直接new PlatformerControls()覆盖了_controls,导致之前订阅的实例立刻失去引用。
第三,查 Player Settings 的 Active Input Handling 是否真的切到了新后端。如果设成了 Both,新系统能正常运行;如果还是旧的 Input Manager (Old),那安装的包永远在运行时报 “InvalidOperationException: You are trying to read Input using the UnityEngine.Input class, but you have switched active Input handling to Input System package in Player Settings.” 消息,API 层面直接崩溃。
这三条按顺序排查完,大多数“不触发”问题都能解决。还有个小坑是你在某个禁用状态的 Action 上订阅,但忘了在 OnDisable 里退订,这一般不产生错误,但会造成重复订阅导致事件连续调用两次,值被第二次覆盖,逻辑上表现得像输入没生效。调试间断性输入问题,多想想订阅方退订是否完整。
4.2 键盘鼠标对 UI 操作的影响
新输入系统跟 UGUI 的交互要求同时安装并启用 InputSystemUIInputModule 组件,否则 Event System 只响应鼠标点击,键盘导航和手柄操作全部无效。做法是在 EventSystem 的物体上移除旧的 StandaloneInputModule,挂上 InputSystemUIInputModule(新版本包内自带该组件,可以直接 Add Component 搜索到)。否则在新输入模式下,UI 点击表现充满随机性,有时候鼠标没点也触发按钮效果,因为旧的 StandaloneInputModule 用的输入读取方式本身已经不工作了。
有一个典型案例:手指在手机上点按钮,第一次点击没反应,第二次才有。这不是双点击,而是第一次点击被用于“激活 UI”,第二次才被当作选中按钮执行事件。通常是因为 Canvas 上的 GraphicRaycaster 配置不对,或者 EventSystem 中 InputSystemUIInputModule 的 Default Move Focus、Click Action 绑定了不合适的按键。
另外一个常见问题是鼠标点击 UI 和游戏操作的冲突。假设进入暂停菜单后,你不小心让 Gameplay 的 Move 动作仍处于启用状态,玩家在菜单里移动摇杆,角色在地图里乱跑。如果 UI 正确挂了 InputSystemUIInputModule,菜单焦点一般会拦截部分键盘事件,但游戏动作仍会被输入系统处理,因为它是独立的 Action Map。需要在打开菜单时调用 Gameplay 的 Disable 或 Action Map 的 Disable,关闭菜单时再 Enable,这跟前面 Action Map 分组设计直接相关。不要把"UI是否显示"和"输入映射是否启用"两件事混在一起,这是 Design 上的误区,可能来自旧系统时代的事件拦截习惯。
4.3 手柄识别不到或按键错位
我在 Windows 平台踩过最多的坑,是插着手柄但 InputAction 里 Gamepad 方案就是没激活,后来打开 Input Debugger(菜单 Window > Analysis > Input Debugger)发现手柄确实在里面,但设备名显示为 HID-compliant game controller,而且没有正确映射为 Gamepad 设备类型,于是 Binding 完全无法命中。
这个问题的根源常常是驱动或者老旧设备的 HID 描述不规范。Unity 对 Xbox 手柄的支持比较完整,但很多第三方手柄通过无线的模式会被系统枚举成原始 HID 设备。解决办法有几种:推荐在设备接入后打开 Input Debugger 点击设备上的 “Copy device layout” 手动建立一个 custom layout,映射所有按钮和轴,完成后绑定路径选择该自定义设备路径。这个算高阶操作,大部分情况下项目代码不需要,但作为技术负责人你要能定位原因。
设备映射的另一类问题是绑定路径不匹配。比如你的 Gamepad 绑定路径写的 “Gamepad/ButtonSouth”,而实际设备 layout 中该键叫 “Gamepad/button0” 或 “joystick button 0”,保存资产不会提示错误,运行时不命中也不报警。所以在绑定绑定路径时一定要通过输入动作编辑器的下拉菜单选择设备路径,别手敲。
还有一种 Windows 下的显示 bug:Unity 编辑器下测试手柄正常,打包后插手柄没反应。这大概率是因为在 Game View 的 “Simulate Touch Input from Mouse or Pen” 设置处于默认状态,游戏窗口没有获得焦点,手柄设备和鼠标焦点不匹配。打包后的实际运行设备默认使用系统焦点,这个问题不常见,但窗口化游戏要格外注意输入焦点。
4.4 EnhancedTouch 与 UGUI 的手势冲突
新版 Input System 把触屏封装为EnhancedTouch(UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport),当你需要原始多点触控、滑动距离检测这类底层逻辑时,直接用 EnhancedTouch 比一次性处理 Touchscreen.current 更省事。但它和 UGUI 的交互是两套东西,如果不做隔离,可能出现“UGUI 按钮既收到点击动作,EnhancedTouch 又检测到触摸开始”的双重触发,轻则声音重复,重则逻辑状态错乱。
我的方案是把触摸逻辑做成一个全局 TouchManager,把所有触摸原始数据统一收集起来,映射为“是否在UI元素上”这个布尔字段,业务手势逻辑在检查到触摸起点在 UI 上时直接忽略。判断方法:在 UI 点击事件OnPointerDown里打一个标记,触屏手势开始时从 EventSystem.current.IsPointerOverGameObject(touchId) 取值。注意旧的重载 IsPointerOverGameObject() 不带参数时,在移动端上作用不稳定,必须传 fingerId。
这个坑的难点在于,EnhancedTouch 的触摸事件每帧都触发,频率远高于 UI 点击。如果业务流程在多次触摸中只执行一次,那么还得在事件回调里做时间窗口过滤,否则轻即一个触摸序列被拆成三四个 “触摸开始” 事件(因为每一帧都 delivered)。新版 EnhancedTouch 里已经改善了这个问题,旧版本上的做法是只在触摸开始阶段取第一个触点,后面触点直接丢弃,用状态机管理,不推荐在 every touch 里处理逻辑。
5. 配置细节和项目落地时的决策建议
上面已经把基础搭建和代码接法讲清楚了,这里延展几个项目里常碰到的策略问题。这些内容不是教程书上会写的那种规范,但真实项目中它们决策起来很难,没有唯一正确答案。
5.1 什么时候应该硬着头皮迁移
如果你手头项目是动作游戏、射击游戏、体育竞技游戏、RPG 装备快捷键组合这类操作绑定的复杂度高的,值得把输入模块拆出来重做。这些品类里输入不是"读取按键"这么简单,它牵扯到键位映射、UI 提示切换、玩家自定义按键、设备热插拔、以及云存档同步用户偏好。这些功能老系统做起来要手写很多结构,新系统虽然也麻烦,但自带了一半。特别是自定义按键绑定,InputActionRebindingExtensions 能直接生成重绑定 UI 流程,不用自己解析 HID 报文。
还有两个特征适合立刻迁移:一是项目选择多平台发布,包含主机或移动端;二是团队里已经有同事在研究输入管线,可以牵头做模块化封装。如果不是这些情况,而项目里已经跑了很多年了,我建议别跟自己的旧代码过不去,老系统的成熟稳定代码远比新系统的便利值钱。
5.2 老项目迁移的正确姿势
假设你要在老项目里切到新输入系统,最稳妥的姿势是“分阶段替换,而不是一刀切”。第一阶段把 Player Settings 切到 Both,让新旧两套并行,老代码继续跑,新输入系统开始逐步接入手柄和 UI 部分。第二阶段重构核心玩法控制,把角色的移动、跳跃、攻击输入从 Update 里的旧轮询改为新事件驱动。第三阶段扫尾,把旧输入调用清除干净,切到 New only,遍历所有事件系统组件换成 InputSystemUIInputModule。
这个过程唯一的坑是 Both 模式下,旧输入系统的默认轴可能和新系统的“动作”都向同一份逻辑写入变量,逻辑被触发两次。所以在过渡期你要给新系统处理过的动作加一个“来源标记”,比如只在新系统事件触发时设置值,否则某些代码会觉得自己在双键盘响应。我经历过一次魔性的 bug:玩家按住空格,角色每帧跳跃两次,就是因为老代码和新代码同时朝 Jump 状态里写 true,而角色控制器在一个 Update 里消费了这个状态。
如果迁移动机只是因为某个新设备不兼容,那更简单的办法是先用中间层把设备数据清洗成统一输入状态,再逐步替换调用点。不要天真地认为用搜索替换 Input.GetKeyDown => newInputSystem 就结束了,旧代码里藏在协程、动画事件、插件回调里的间接依赖往往才是主要工作量。
5.3 InputActionAsset 的版本管理和多人协作
最后分享一个团队协作的细节。.inputactions 文件本质是 JSON,如果多个成员同时打开并编辑同一个资产,PN 合并冲突非常丑,可读性极差,经常把 Binding ID 弄乱。我建议:把输入配置资产当作核心主资产,由一个人统一维护,代码其他部分永远引用自动生成的 C# 类而非直接读取 JSON。这样既避免 Number 冲突,也保证代码逻辑不跟着配置随意变。
如果项目依赖非常多的控制方案(比如 PC、WebGL、iOS、Android、Xbox、PlayStation 都各自差异),建议把输入配置拆成多个 .inputactions 文件,按平台或按功能模块划分,而不是堆在一个文件里。输入动作生成 C# 类后,若你有多个文件,会给每个文件生成不同的类,使用时稍微多几条名字空间前缀,但维护起来省心得多。只有极小型项目才推荐单文件一把梭到底。
另外,我会在 CI 流程里加一个验证步骤,检测 .inputactions 文件是否为合法的 JSON,且所有控制方案的设备要求不为空。省得某次打包上线后才发现手柄方案没有设备要求、导致全部设备乱跳,这种问题在编辑器里不报警,打包后才暴露。输入系统这类底层模块,最怕的就是“启动不报错、运行静默错”。
6. 收个尾:我自己的使用体会
跟踪这套系统这几年来,我对它总体是满意的。旧的 Input Manager 最大的问题是“操作意图和物理输入混在一起”,每加一种设备就多一层 if-else,项目越到后期越不敢动输入模块。Input System 把输入行为抽象成 Action 层,代码里接触的永远是“移动、跳跃、交互”,而不是“键码 32 和左摇杆 X 轴”,这种转化对多人协作和功能扩展都有价值。
不过我也得说句公道话:它的学习曲线确实比老系统陡峭,事件驱动、控制方案、Action Map 这些概念对新手不友好,尤其是很多教程上来就贴一串长代码,反而让人学得更懵。真正上手以后会发现,大部分问题不是代码难度,而是配置层的东西没有被很好理解。所以我这篇没有贴一大坨完整项目代码,而是把资产设计的逻辑和代码接缝的方法讲透。
如果你正在规划一个全新项目,我的建议是:别犹豫,直接用新输入系统,但动手前先花几个小时把 Action Asset 的各类配置摸清楚。如果你在维护一个老项目,那就按前面建议的渐进式迁移,别追求一夜重写。后期遇到“某个动作不触发、某台设备不识别、UI 和场景动作打架”之类的问题,回到这篇排查列表逐条对照,多数能省下不少排查时间。
下一期我打算写 InputActionRebindingExtensions 的自定义按键重绑流程,以及 UnityEvents 与 C# 事件两种接入方式的完整对比,如果你正在做支持自定义按键的项目,可以先把这期的基础配置做起来。