尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity新输入系统(Input System)实战:从配置到多状态管理

Unity新输入系统(Input System)实战:从配置到多状态管理 1. 项目概述为什么Unity新输入系统是必选项如果你还在用老旧的Input.GetKeyDown和Input.GetAxis来写角色控制那感觉就像是在用算盘处理Excel表格。按键打架、手柄适配噩梦、多玩家输入冲突、菜单和游戏状态切换时输入逻辑乱成一锅粥——这些痛点但凡做过几个Unity项目的开发者都深有体会。Unity的新输入系统Input System就是为了彻底解决这些问题而生的它不是对旧系统的简单修补而是一次从底层逻辑到上层API的彻底重构。我最初接触Input System时也被它看似复杂的Action Maps、Bindings、Processors等概念吓退过觉得用回老方法更“快”。但真正在一个需要支持PC键盘鼠标、Xbox手柄、PS手柄以及未来可能移动触屏的跨平台项目里踩过坑后我才明白前期花时间配置Input System后期节省的是海量的调试、适配和重构时间。它把输入从“事件响应”提升到了“资源管理”的层面。简单来说旧系统是你手动去监听每一个物理按键新系统则是你定义好“跳跃”、“移动”、“攻击”这些逻辑动作Actions然后系统帮你处理不同设备上具体哪个物理按键Bindings触发这个动作甚至还能处理摇杆死区、按键组合、长按双击等复杂逻辑。这次我就以一个实战项目为例带你从零开始手把手完成Input System的保姆级配置。我们不仅会配置一套基础的第三人称角色控制还会实现游戏内菜单、暂停界面、多场景切换时输入系统的无缝衔接与状态管理并附上可直接复用的核心代码。目标是让你看完就能在自己的项目里用起来彻底告别输入逻辑的混乱。2. 环境准备与Input System基础概念扫盲2.1 安装与项目设置首先确保你的Unity版本在2019.4 LTS或更新版本。Input System作为一个独立的包需要通过Package Manager安装。打开Unity进入菜单栏Window Package Manager。在Package Manager窗口左上角将视图从“In Project”切换到“Unity Registry”。在列表中找到“Input System”点击右下角的“Install”按钮。注意安装过程中Unity会提示你“启用新的输入后端”。这里一定要点“Yes”。这意味着项目将禁用旧的UnityEngine.InputAPI虽然仍可通过兼容层访问但不推荐混用。如果你的项目严重依赖旧输入代码建议在新项目或分支中尝试。安装完成后你会在Project Settings Player Other Settings Configuration中看到Active Input Handling选项被设置为Input System Package (New)。这就表示新输入系统已激活。2.2 核心概念理解Actions、Action Maps、Bindings、Control Schemes这是理解Input System的四个基石务必先搞懂Control控件最底层的物理输入单元例如键盘上的“W”键、Xbox手柄的“A”按钮、鼠标左键、手柄左摇杆的X轴。它由输入设备如Keyboard、Gamepad定义。Binding绑定一座桥梁它将一个具体的物理控件Control关联到一个逻辑动作Action上。例如将键盘“空格键”和手柄“South按钮”A键都绑定到“跳跃”动作上。Action动作你游戏中的逻辑输入指令例如“移动”、“跳跃”、“攻击”、“暂停”。Action有类型Value Button PassThrough决定了它如何处理输入数据。Action Map动作映射集一组相关Action的集合。这是实现多场景、多状态输入切换的关键。通常你会创建诸如“Player”控制角色、“UI”控制菜单、“Vehicle”控制载具等不同的Action Map。在同一时间通常只有一个Action Map是“活跃”的。Control Scheme控制方案一组绑定的集合定义了特定类型设备的完整控制方式。例如你可以定义一个“KeyboardMouse”方案包含所有键盘鼠标的绑定再定义一个“Gamepad”方案包含所有手柄绑定。Input System能自动检测玩家使用的设备并切换到对应的Control Scheme这是实现跨平台无缝适配的核心。你可以把整个系统想象成一个图书馆图书馆Input System管理一切。书架Action Map用来分类书籍比如“科幻小说架”、“历史文献架”。书籍Action就是具体的《三体》、《明朝那些事儿》。书的索引号Binding告诉你这本书具体在哪个位置。阅读方式Control Scheme可以是“纸质阅读”键盘鼠标或“电子阅读”手柄内容故事是一样的但载体不同。3. 创建与配置你的第一个Input Actions Asset理论讲完我们开始动手。所有输入配置的核心都是一个.inputactions资产文件。在Project窗口右键选择Create Input Actions。我将其命名为PlayerControls。双击这个文件会打开Input Actions Editor窗口。这是我们的主战场。3.1 创建Action Maps与Actions我们首先创建两个最常用的Action MapPlayer和UI。点击号添加Action Map命名为Player。在这个Map下我们添加以下ActionsMove(类型:Value 控制类型:Vector2): 用于角色移动将绑定到键盘WASD和手柄左摇杆。Look(类型:Value 控制类型:Vector2): 用于视角旋转绑定到鼠标Delta和手柄右摇杆。Jump(类型:Button): 跳跃。Sprint(类型:Button): 冲刺。Interact(类型:Button): 交互。Attack(类型:Button): 攻击。再添加一个Action Map命名为UI。在这个Map下添加Navigate(类型:Value 控制类型:Vector2): 在UI菜单项间导航。Submit(类型:Button): 确认/选择。Cancel(类型:Button): 取消/返回。Pause(类型:Button): 暂停游戏这个键也可能在Player Map里看设计。3.2 详细配置Binding与Control Scheme以PlayerMap下的Move动作为例演示如何配置多设备绑定点击Move动作右边的号选择Add Binding。新创建的Binding会显示在Move下方。在右侧Path处点击搜索图标。为了兼容性我们先为它设置一个“通用”绑定。在搜索框输入Stick选择Gamepad/leftStick。这样我们就为Move添加了手柄左摇杆的绑定。但这还不够我们还需要键盘绑定。注意看Move动作或刚创建的Binding条目右边有一个很小的图标点击它选择Add Up/Down/Left/Right Composite。神奇的事情发生了你会看到Move下自动生成了Up、Down、Left、Right四个子绑定。这就是Composite复合绑定它允许我们将多个一维输入组合成一个二维向量Vector2。分别点击这四个子绑定为它们设置路径Up:Keyboard/wDown:Keyboard/sLeft:Keyboard/aRight:Keyboard/d现在Move动作就同时拥有了手柄摇杆和键盘WASD两种输入方式。实操心得在配置Binding时善用右侧的“Listen”功能。点击“Listen”按钮然后操作你的手柄或键盘系统会自动捕获你按下的键位这对于配置不熟悉的设备路径特别有用。3.3 为Binding添加Processors处理器Processors可以在输入数据到达你的代码之前进行预处理比如标准化、平滑、取反等。这是提升手感的关键。选择PlayerMap下的Look动作。为它添加一个绑定到Mouse/delta鼠标移动增量。在Look动作的右侧面板找到Processors折叠栏点击Add Processor。添加一个Scale Processor并将Factor设置为0.1到0.5之间的值例如0.2。这是因为鼠标Delta的原始值很大直接用于旋转会过于敏感需要用系数缩放。再为Look添加一个绑定到Gamepad/rightStick。对于这个手柄绑定我们通常需要添加Stick Deadzone处理器。同样在Processors栏添加可以保留默认值min0.125, max0.925或根据手感微调。死区处理器可以消除摇杆微小回中不准带来的抖动。3.4 创建与分配Control Schemes我们希望系统能自动区分玩家用的是键鼠还是手柄并可能应用不同的UI提示图标。在Input Actions Editor的左上角找到Control Schemes点击创建两个方案KeyboardMouse和Gamepad。创建后你会发现每个Binding条目的最左侧多了一个小图标点击可以为其分配所属的Control Scheme。例如将Move下键盘WASD的复合绑定分配到KeyboardMouse方案将手柄左摇杆的绑定分配到Gamepad方案。Look、Jump等动作也依此处理。对于UIMap下的Navigate动作也需要分别绑定键盘方向键分配到KeyboardMouse手柄方向键D-Pad和左摇杆分配到Gamepad。配置完成后你的PlayerControls.inputactions资产应该结构清晰包含了两个Action Map每个Action下有针对不同Control Scheme的绑定和相应的处理器。保存资产基础配置就完成了。4. 在代码中读取与使用输入配置是基础如何在脚本中使用这些输入才是目的。Input System提供了几种读取方式这里介绍最推荐、最灵活的两种通过PlayerInput组件回调以及直接通过InputAction的C#事件。4.1 方法一使用PlayerInput组件与消息发送这是快速原型和简单场景的利器。给你需要接收输入的游戏对象例如Player角色添加PlayerInput组件。在Actions属性上拖入我们创建好的PlayerControls.inputactions资产。在Default Action Map中选择Player。Behavior选项选择Send Messages或Invoke Unity Events。Send Messages 要求该游戏对象上挂载的脚本拥有特定名称的方法如OnMove,OnLook,OnJump。系统会自动调用它们。Invoke Unity Events 更直观会在组件上为每个Action生成一个UnityEvent你可以像配置UI按钮一样在Inspector里拖拽指定响应函数。示例代码Send Messages方式using UnityEngine; using UnityEngine.InputSystem; // 必须引用这个命名空间 public class PlayerController : MonoBehaviour { private Vector2 m_MoveInput; private Vector2 m_LookInput; private bool m_IsSprinting; // 方法名必须为 OnAction名驼峰命名。参数类型由Action类型决定。 public void OnMove(InputValue value) { m_MoveInput value.GetVector2(); // Debug.Log($Move Input: {m_MoveInput}); } public void OnLook(InputValue value) { m_LookInput value.GetVector2(); } // Button类型动作Getfloat() 0 表示按下 public void OnJump(InputValue value) { if (value.isPressed) { // 执行跳跃逻辑 Debug.Log(Jump!); } } // Button类型也可以这样处理用于持续按下的状态 public void OnSprint(InputValue value) { m_IsSprinting value.isPressed; } void Update() { // 在Update中使用缓存下来的输入值 if (m_MoveInput.magnitude 0.1f) { Vector3 moveDirection new Vector3(m_MoveInput.x, 0, m_MoveInput.y); // ... 将moveDirection转换到世界空间并应用移动 } // ... 处理 m_LookInput 旋转视角 } }这种方式简单直接但耦合度较高且不适合复杂的输入逻辑组合。4.2 方法二直接引用InputAction推荐更强大、更灵活的方式是在脚本中直接获取和操作InputAction对象。我们可以通过InputActionAsset来找到它们。首先在脚本中创建一个PlayerControls类的实例。这个类在你创建.inputactions文件时由Unity自动生成需要开启Assets目录下的“Generate C# Class”选项在Input Actions Editor的右上角。初始化这个类并启用需要的Action。示例代码using UnityEngine; using UnityEngine.InputSystem; // 这个类名 PlayerControls 对应你的 .inputactions 文件名 [RequireComponent(typeof(CharacterController))] public class AdvancedPlayerController : MonoBehaviour { private PlayerControls m_PlayerControls; // 自动生成的C#类 private CharacterController m_CharacterController; private InputAction m_MoveAction; private InputAction m_LookAction; private InputAction m_JumpAction; private InputAction m_SprintAction; private Vector2 m_CurrentMoveInput; private bool m_IsSprinting; private void Awake() { m_CharacterController GetComponentCharacterController(); // 1. 实例化Input Action Asset m_PlayerControls new PlayerControls(); } private void OnEnable() { // 2. 获取具体的Action引用 m_MoveAction m_PlayerControls.Player.Move; // Player是Action Map名Move是Action名 m_LookAction m_PlayerControls.Player.Look; m_JumpAction m_PlayerControls.Player.Jump; m_SprintAction m_PlayerControls.Player.Sprint; // 3. 为Action订阅回调函数方式一事件 m_JumpAction.performed OnJumpPerformed; m_SprintAction.performed OnSprintPerformed; m_SprintAction.canceled OnSprintCanceled; // 4. 启用这个Action Map启用后其中的Action才会开始监听输入 m_PlayerControls.Player.Enable(); } private void OnDisable() { // 重要在禁用时取消订阅防止内存泄漏和空引用 m_JumpAction.performed - OnJumpPerformed; m_SprintAction.performed - OnSprintPerformed; m_SprintAction.canceled - OnSprintCanceled; // 禁用Action Map m_PlayerControls.Player.Disable(); } private void Update() { // 方式二在Update中直接读取Value类型Action的当前值适合连续输入如移动、视角 m_CurrentMoveInput m_MoveAction.ReadValueVector2(); Vector2 lookDelta m_LookAction.ReadValueVector2(); // 应用移动和旋转逻辑... HandleMovement(m_CurrentMoveInput); HandleRotation(lookDelta); } private void OnJumpPerformed(InputAction.CallbackContext context) { // performed 事件在按钮被按下时触发一次 Debug.Log(Jump Performed!); // 执行跳跃力计算等 } private void OnSprintPerformed(InputAction.CallbackContext context) { m_IsSprinting true; } private void OnSprintCanceled(InputAction.CallbackContext context) { m_IsSprinting false; } private void HandleMovement(Vector2 input) { if (input.magnitude 0.1f) return; Vector3 move new Vector3(input.x, 0, input.y); move transform.TransformDirection(move); // 将输入从本地空间转到世界空间 move * m_IsSprinting ? sprintSpeed : walkSpeed; // 应用重力等... move.y Physics.gravity.y * Time.deltaTime; m_CharacterController.Move(move * Time.deltaTime); } private void HandleRotation(Vector2 delta) { // 处理鼠标或摇杆输入旋转角色和摄像机 // 注意这里需要根据输入设备类型调整灵敏度 } }这种方式将输入逻辑完全掌控在代码中可以轻松实现如“按下瞬间”、“持续按住”、“松开瞬间”的不同响应也便于进行输入组合如“冲刺翻滚”的判断。5. 实现多场景与多状态输入切换这是Input System真正发挥威力的地方。游戏通常有多个状态主菜单、游戏进行中、暂停菜单、过场动画、对话等。每个状态需要激活不同的输入映射。5.1 使用PlayerInput组件切换Action Map如果你的项目使用了PlayerInput组件切换非常简单public class GameManager : MonoBehaviour { public PlayerInput playerInput; // 拖入挂载了PlayerInput组件的对象 void EnterPauseMenu() { // 从Player Map切换到UI Map playerInput.SwitchCurrentActionMap(UI); // 同时你可能需要禁用玩家角色的移动脚本 // playerCharacterController.enabled false; Time.timeScale 0f; // 暂停游戏逻辑 } void ResumeGame() { // 从UI Map切换回Player Map playerInput.SwitchCurrentActionMap(Player); // playerCharacterController.enabled true; Time.timeScale 1f; } }5.2 在纯代码无PlayerInput模式下管理多Map更常见的做法是我们有一个全局的输入管理器它持有PlayerControls实例并对外提供安全的接口来启用/禁用特定的Action Map。using UnityEngine; using UnityEngine.InputSystem; public class InputManager : MonoBehaviour { public static InputManager Instance { get; private set; } private PlayerControls m_Controls; // 对外暴露的只读访问器方便其他脚本获取Action引用 public PlayerControls.PlayerActions Player m_Controls.Player; public PlayerControls.UIActions UI m_Controls.UI; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); // 输入管理器常驻跨场景 m_Controls new PlayerControls(); // 默认情况下不启用任何Map由游戏状态决定 DisableAllInput(); } public void EnablePlayerInput() { m_Controls.UI.Disable(); m_Controls.Player.Enable(); Debug.Log(Player Input Enabled.); } public void EnableUIInput() { m_Controls.Player.Disable(); m_Controls.UI.Enable(); Debug.Log(UI Input Enabled.); } public void DisableAllInput() { m_Controls.Player.Disable(); m_Controls.UI.Disable(); Debug.Log(All Input Disabled.); } // 示例在场景加载时自动切换输入 private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (scene.name MainMenu) { EnableUIInput(); } else if (scene.name Gameplay) { EnablePlayerInput(); } } private void OnEnable() { SceneManager.sceneLoaded OnSceneLoaded; } private void OnDisable() { SceneManager.sceneLoaded - OnSceneLoaded; m_Controls?.Dispose(); // 重要释放资源 } }其他脚本可以通过InputManager.Instance.Player.Move.ReadValueVector2()来获取输入或者订阅InputManager.Instance.Player.Jump.performed事件。输入管理器确保了同一时间只有一套输入规则生效彻底杜绝了“在菜单里角色还在跑”的尴尬情况。5.3 处理设备变更与多玩家输入Input System能自动检测设备的插拔。你可以监听InputSystem.onDeviceChange事件。private void Start() { InputSystem.onDeviceChange OnDeviceChanged; } private void OnDeviceChanged(InputDevice device, InputDeviceChange change) { switch (change) { case InputDeviceChange.Added: Debug.Log($设备已连接: {device.name}); // 新设备连接可以更新UI提示图标 break; case InputDeviceChange.Removed: Debug.Log($设备已断开: {device.name}); // 设备断开如果当前玩家用的这个设备可能需要暂停游戏或提示 break; case InputDeviceChange.ConfigurationChanged: // 设备配置改变如手柄从蓝牙模式切到有线模式 break; } }对于本地多人游戏你可以实例化多个PlayerInput组件并通过playerInput.Join()让它们分别配对到不同的输入设备上。Input System会自动处理输入的分流。6. 实战集成UI输入与运行时按键重绑定6.1 让Event System支持Input System新输入系统需要与Unity的UI事件系统EventSystem配合工作。删除场景中默认的EventSystem游戏对象。在GameObject UI Event System菜单下你会发现一个新的选项Input System UI Input Module。选择它Unity会创建一个包含必要组件的新EventSystem对象。检查这个新对象上的Input System UI Input Module组件。你需要将PlayerControls.inputactions资产拖到Actions Asset栏位。在Default Actions下点击Point、Left Click等Action栏位右边的圆圈选择我们资产中UIAction Map下对应的Action例如UI/Point,UI/Click。现在你的UI按钮、滑块就可以响应通过Input System配置的键盘、手柄导航和确认取消了。6.2 实现运行时按键重绑定这是一个能极大提升游戏体验的功能。核心是使用InputActionRebindingExtensions.PerformInteractiveRebinding方法。using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; public class RebindUI : MonoBehaviour { [SerializeField] private InputActionReference m_TargetAction; // 在Inspector中绑定要重映射的Action如“Player/Jump” [SerializeField] private Text m_BindingText; // 显示当前绑定键位的Text UI [SerializeField] private GameObject m_WaitingForInputPanel; // 提示“等待输入”的面板 private InputActionRebindingExtensions.RebindingOperation m_RebindOperation; public void StartRebinding() { if (m_TargetAction null || m_TargetAction.action null) return; // 1. 禁用目标Action防止重绑定过程中触发它 m_TargetAction.action.Disable(); // 2. 显示“等待输入”提示 m_WaitingForInputPanel.SetActive(true); m_BindingText.text Press any key...; // 3. 开始交互式重绑定 m_RebindOperation m_TargetAction.action .PerformInteractiveRebinding() // 可选排除鼠标位置等不需要的控件 .WithControlsExcluding(Mouse/position) .WithControlsExcluding(Mouse/delta) .WithControlsExcluding(Pointer/position) .WithControlsExcluding(Pointer/delta) // 可选只允许特定类型的设备如只允许键盘 // .WithExpectedControlType(Button) .OnMatchWaitForAnother(0.1f) // 等待0.1秒看是否有组合键 .OnComplete(operation RebindComplete()) .Start(); } private void RebindComplete() { // 4. 操作完成 m_RebindOperation.Dispose(); m_RebindOperation null; // 5. 更新UI文本显示新的绑定 UpdateBindingDisplay(); // 6. 隐藏提示面板重新启用Action m_WaitingForInputPanel.SetActive(false); m_TargetAction.action.Enable(); // 7. 重要保存绑定到PlayerPrefs或配置文件 SaveBindings(); } private void UpdateBindingDisplay() { if (m_TargetAction null || m_BindingText null) return; // 获取当前生效的绑定索引对于复合绑定可能需要更复杂的逻辑 int bindingIndex m_TargetAction.action.GetBindingIndexForControl(m_TargetAction.action.controls[0]); // 获取绑定路径的显示字符串如“W”、“Gamepad Button South” m_BindingText.text InputControlPath.ToHumanReadableString( m_TargetAction.action.bindings[bindingIndex].effectivePath, InputControlPath.HumanReadableStringOptions.OmitDevice); } private void SaveBindings() { // 将整个Input Action Asset的绑定覆盖信息保存为字符串 string rebinds m_TargetAction.action.actionMap.asset.SaveBindingOverridesAsJson(); PlayerPrefs.SetString(CustomControlBindings, rebinds); PlayerPrefs.Save(); } private void LoadBindings() { string rebinds PlayerPrefs.GetString(CustomControlBindings); if (!string.IsNullOrEmpty(rebinds)) { m_TargetAction.action.actionMap.asset.LoadBindingOverridesFromJson(rebinds); UpdateBindingDisplay(); } } private void Start() { LoadBindings(); // 游戏开始时加载自定义绑定 } }将此脚本挂载到你的重绑定按钮上将按钮的OnClick事件关联到StartRebinding方法。这样玩家就可以在游戏内自由定制按键了。7. 常见问题、性能优化与避坑指南7.1 常见问题速查表问题现象可能原因解决方案输入无响应1. Action Map未启用。2.PlayerInput组件未关联Actions资产或Behavior设置错误。3. 脚本中未启用Action或未订阅事件。4. 有多个EventSystem或Input Module冲突。1. 检查代码中是否调用了xxxActionMap.Enable()。2. 检查Inspector配置。3. 检查OnEnable中的订阅和启用代码。4. 确保场景中只有一个活动的Input System UI EventSystem。手柄摇杆有微小抖动未设置死区Deadzone。在Input Actions Editor中为手柄摇杆的Binding添加Stick DeadzoneProcessor。鼠标视角旋转过快/过慢鼠标Delta值未经过缩放。为鼠标Delta的Binding添加Scale Processor并调整Factor通常0.1-0.5。UI导航同时控制了角色多个Action Map同时启用。确保在启用UI Map时禁用Player Map反之亦然。使用中央输入管理器。重绑定后设置丢失未保存绑定覆盖信息。使用SaveBindingOverridesAsJson()和LoadBindingOverridesFromJson()持久化数据。在构建Build后输入失效.inputactions资产未包含在构建中。确保该资产在Resources文件夹下或通过“Addressables”或“AssetBundle”系统加载。最简单的方法是将其放在Resources文件夹内代码中使用Resources.Load。7.2 性能优化与最佳实践缓存InputAction引用不要在Update里反复使用m_Controls.Player.Move来获取Action而应该在Awake或OnEnable中缓存它。合理使用Action类型Value用于连续变化的输入如摇杆、鼠标移动。在Update中ReadValue。Button用于离散动作。优先使用performed和canceled事件而非在Update中检查IsPressed。PassThrough直通类型不进行状态检查所有输入直接传递。用于需要原始输入流的场景如日志记录、输入回放。减少垃圾回收GCReadValueT()通常不会产生GC。但频繁创建CallbackContext结构体是安全的。主要注意不要在每帧都进行字符串操作如将绑定路径转为显示名而应缓存结果。为移动平台优化移动触摸屏输入本质上是模拟的指针和触控。Input System提供了TouchSimulation在编辑器中模拟触摸。在真机上确保你的UI和虚拟摇杆通过Input System UI Input Module正确关联到UIAction Map下的Point、Click、Touch等Action。调试利器使用Input Debugger窗口Window Analysis Input Debugger。它可以实时显示所有设备、Action的状态和数值是排查输入问题的终极工具。从旧输入系统迁移到新输入系统初期会有一个学习曲线但一旦掌握其带来的代码清晰度、可维护性和跨平台能力的提升是巨大的。它迫使你以更架构化的方式思考输入逻辑将“物理输入”与“游戏逻辑”解耦。我自己的项目在切换后处理多玩家、多场景的输入混乱问题减少了90%以上菜单和游戏状态切换也变得异常清爽。花一个下午时间按照这份指南配置一遍你就能亲身体会到这种顺畅。
返回列表