1. 项目概述告别旧Input拥抱新系统如果你是从Unity的旧Input Manager就是那个在Edit - Project Settings - Input Manager里配置一堆Axis的地方转过来的或者刚接触Unity 2023准备开始一个新项目那么直接上手新的Input System绝对是明智之选。旧系统虽然简单直接但它的硬伤也很明显代码里到处都是Input.GetKey、Input.GetAxis这种“硬编码”设备支持有限跨平台输入处理繁琐而且很难做复杂的输入组合比如“冲刺跳跃”同时按下触发特殊动作。新的Input System就是为了解决这些问题而生的它采用基于事件的、数据驱动的架构把输入抽象成“动作”Actions让你可以集中管理所有输入配置并且原生支持手柄、触屏、XR设备等各种输入源。简单来说这个项目要做的就是利用Unity 2023内置的Input System包从零开始配置一套控制逻辑并编写代码实现一个游戏角色比如一个胶囊体在场景中平滑、可配置地移动。这听起来基础但却是理解新Input System工作流的核心。整个过程会涉及创建Input Actions资源、配置动作映射、编写基于PlayerInput组件或直接C#事件的响应代码以及处理移动逻辑。我会带你走一遍我实际项目中最常用、最稳妥的路径避开一些官方文档里没明说的“坑”。2. 环境准备与Input System安装在开始之前确保你的Unity版本是2023.1或更高。虽然Input System在更早的版本如2020.3就以包的形式提供了但2023版对其集成和支持更好。我们首先需要把Input System包安装到项目中。2.1 通过Package Manager安装打开Unity顶部菜单栏选择Window - Package Manager。在Package Manager窗口左上角确保来源Sources是Unity Registry。然后在列表中找到Input System包。如果没找到可以点击左上角的“”号选择“Add package by name...”输入com.unity.inputsystem。找到后点击右下角的Install按钮。这里会弹出一个警告窗口提示“启用新的输入后端将禁用旧的输入APIInput Manager”。这是正常的也是我们想要的。直接点击“Yes”确认。安装完成后可能需要重启Unity编辑器。注意安装后旧Input Manager的API如Input.GetKey在代码中虽然还能编译但运行时将不再响应输入。所有输入逻辑都需要迁移到新系统。2.2 项目设置与后端确认安装完成后最好检查一下项目设置。点击Edit - Project Settings在左侧选择Player然后在右侧找到Other Settings区域。向下滚动找到Configuration下的Active Input Handling选项。这里应该已经自动被设置成了Input System Package (New)。如果还是“Both”你可以手动改为“Input System Package (New)”这表示我们只使用新的Input System。这个设置很重要它决定了Unity运行时使用哪套输入处理后端。选择“Both”理论上可以新旧并存但在实际开发中容易引起混淆和冲突尤其是处理UI输入时。对于新项目我强烈建议只使用新的Input System。3. 核心资产创建Input Actions配置详解Input System的核心是一个叫做Input Action Asset的配置文件.inputactions后缀。你可以把它理解为一个输入控制的“总菜单”里面定义了所有玩家可以执行的操作Actions以及这些操作由哪些具体的按键、摇杆等控件Bindings来触发。3.1 创建与组织Input Action Asset在Project窗口右键选择Create - Input Actions。我给这个文件起名为PlayerControls。双击这个文件会打开一个专门的配置编辑器窗口。这个编辑器窗口分为三列Action Maps动作映射集用于将动作分组。比如你可以有“Gameplay”移动、跳跃、攻击和“UI”菜单导航、确认两个不同的映射集根据游戏状态切换避免输入冲突。Actions动作定义具体的玩家操作如“Move”、“Jump”、“Fire”。**Properties属性**和Bindings绑定选中一个Action后在这里设置其类型和具体的按键绑定。对于人物移动我们首先创建一个Action Map。点击Action Maps右边的“”号命名为Player。然后在这个Player映射集下我们来创建移动所需的Action。3.2 定义“Move”动作及其绑定在Actions栏下点击“”号创建一个新Action命名为Move。这是最关键的一步。1. Action Type动作类型选择Value值类型用于返回一个连续值的输入比如摇杆的二维坐标、鼠标位置、扳机键的压力值。人物移动通常使用这个类型因为它能给我们一个表示方向的向量。Button按钮类型用于返回按下/松开状态的输入如键盘按键、手柄按钮。返回bool值。Pass-Through直通类型较少用它会收集所有绑定控件的输入不做处理。所以为Move选择Value类型。在Control Type控件类型中选择Vector2因为我们的移动是在2D平面XZ平面上需要一个二维向量来表示方向。2. 添加绑定Bindings选中Move动作在右边的Bindings栏点击“”号选择“Add Binding”。这里会出现一个Path字段点击右边的监听按钮一个小点然后按下你想要的键比如键盘的W键。系统会自动生成路径Keyboard/w。 但这样只绑定了W键只能得到一个向上的向量。对于四方向移动我们需要复合绑定。更高效的方法是使用“Up/Down/Left/Right Composite”。在Bindings栏不要直接添加简单绑定而是点击“”号旁边的下拉箭头选择“2D Vector”。这会自动创建四个子绑定Up、Down、Left、Right。点击Up旁边的“”号路径选择Keyboard/w。点击Down旁边的“”号路径选择Keyboard/s。点击Left旁边的“”号路径选择Keyboard/a。点击Right旁边的“”号路径选择Keyboard/d。现在当你同时按下W和DInput System会自动为你计算出一个(1, 1)的标准化向量这正是我们需要的。3. 为手柄左摇杆添加绑定为了让游戏支持手柄我们可以在同一个Move动作下添加另一个绑定。在Bindings栏再次点击“”号这次直接“Add Binding”。在Path中选择Gamepad/leftStick。这样Move动作现在有两个绑定源键盘WASD和手柄左摇杆。Input System会自动处理多设备输入优先使用最后产生输入的那个设备或者你可以自己写逻辑处理。实操心得在绑定摇杆时建议点开绑定条目右边的齿轮图标看看“Processors”处理器。对于摇杆默认可能已经添加了“Stick Deadzone”摇杆死区。死区可以过滤掉摇杆微小的中心漂移。默认值0.125通常够用但如果你的手柄比较旧漂移严重可以稍微调大比如0.2。但别调太大否则轻微推动摇杆会没反应。4. 代码实现三种驱动方式剖析配置好Input Actions后接下来就是如何在代码中获取这些输入。新Input System提供了几种不同的驱动模式这里我详细分析最常用的三种并给出移动逻辑的完整代码。4.1 方式一使用PlayerInput组件快速原型这是最简单快捷的方式适合原型开发和小型项目。给你的玩家角色GameObject比如一个Capsule添加PlayerInput组件。在组件中将Actions属性拖入我们创建的PlayerControls.inputactions文件。将Default Action Map设置为Player我们之前创建的映射集名。Behavior行为模式选择Invoke Unity Events。这时组件下方会出现一个Events折叠栏里面列出了Player映射集里所有的Action比如Move。你可以像配置UI按钮事件一样为每个Action添加响应函数。创建响应脚本using UnityEngine; using UnityEngine.InputSystem; // 必须引用新的命名空间 public class PlayerMovement : MonoBehaviour { [Header(移动参数)] public float moveSpeed 5f; public float rotationSpeed 720f; private Vector2 _inputVector; // 存储从Input System获取的输入方向 private Rigidbody _rb; void Start() { _rb GetComponentRigidbody(); } void Update() { // 移动逻辑放在FixedUpdate中更佳这里为了演示放在Update MoveCharacter(); } // 这个方法将被PlayerInput组件的Move事件调用 public void OnMove(InputAction.CallbackContext context) { // 从回调中读取ValueVector2()这就是我们配置的2D向量输入 _inputVector context.ReadValueVector2(); } private void MoveCharacter() { // 将2D输入向量转换为3D世界空间方向 Vector3 direction new Vector3(_inputVector.x, 0, _inputVector.y).normalized; if (direction.magnitude 0.1f) { // 计算目标朝向 Quaternion targetRotation Quaternion.LookRotation(direction, Vector3.up); // 平滑旋转到目标朝向 transform.rotation Quaternion.RotateTowards(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); // 移动角色使用Rigidbody保证物理交互 Vector3 moveVelocity direction * moveSpeed; // 保持现有的Y轴速度用于重力或跳跃只改变XZ平面速度 moveVelocity.y _rb.velocity.y; _rb.velocity moveVelocity; } else { // 没有输入时停止水平移动但保持垂直速度如下落 _rb.velocity new Vector3(0, _rb.velocity.y, 0); } } }然后将这个脚本挂到玩家对象上在PlayerInput组件的Move事件中点击“”号将游戏对象拖进去在下拉列表中选择PlayerMovement - OnMove。优点配置可视化无需手动启用/禁用Action Map事件绑定直观。缺点逻辑分散响应函数通过反射调用性能稍弱不适合大型项目或需要复杂输入状态管理的场景。4.2 方式二使用C#事件监听推荐这是我最推荐也是在实际项目中用得最多的方式。它直接在代码中创建Input Actions的实例并订阅事件控制力强性能好。首先我们需要让Unity为我们基于PlayerControls.inputactions文件生成一个C#脚本。选中这个文件在Inspector窗口中勾选“Generate C# Class”然后点击“Apply”。这会在与该asset相同的目录下生成一个名为PlayerControls.cs的脚本。这个脚本包含了一个与配置对应的类里面有所有Action Map和Action的定义。修改后的移动脚本using UnityEngine; using UnityEngine.InputSystem; // 注意这里不需要手动引用生成的PlayerControls因为它在同一命名空间或通过using引入 public class PlayerMovementAdvanced : MonoBehaviour { public float moveSpeed 8f; public float rotationSharpness 15f; // 旋转平滑度值越大转向越快 private PlayerControls _controls; // 生成的输入控制类实例 private Rigidbody _rb; private Vector2 _moveInput; private Vector3 _smoothedMoveDirection; // 用于平滑移动方向 void Awake() { _rb GetComponentRigidbody(); _controls new PlayerControls(); // 实例化输入控制 // 初始化平滑方向 _smoothedMoveDirection transform.forward; } void OnEnable() { // 启用Player这个Action Map _controls.Player.Enable(); // 订阅Move动作的performed进行中和canceled取消事件 _controls.Player.Move.performed OnMovePerformed; _controls.Player.Move.canceled OnMoveCanceled; } void OnDisable() { // 非常重要禁用时取消订阅防止内存泄漏 _controls.Player.Move.performed - OnMovePerformed; _controls.Player.Move.canceled - OnMoveCanceled; _controls.Player.Disable(); } void FixedUpdate() { // 移动逻辑放在FixedUpdate中与物理更新同步 HandleMovement(); } private void OnMovePerformed(InputAction.CallbackContext context) { // 当有输入时按下按键或推动摇杆更新输入向量 _moveInput context.ReadValueVector2(); } private void OnMoveCanceled(InputAction.CallbackContext context) { // 当输入取消时松开所有按键或摇杆回中将输入向量归零 _moveInput Vector2.zero; } private void HandleMovement() { // 将2D输入转换为3D世界方向假设摄像机朝向为前向 Vector3 cameraForward Camera.main.transform.forward; Vector3 cameraRight Camera.main.transform.right; cameraForward.y 0; cameraRight.y 0; cameraForward.Normalize(); cameraRight.Normalize(); // 计算相对于摄像机的移动方向 Vector3 desiredDirection (cameraForward * _moveInput.y cameraRight * _moveInput.x).normalized; // 使用Lerp平滑移动方向避免瞬间转向带来的生硬感 _smoothedMoveDirection Vector3.Lerp(_smoothedMoveDirection, desiredDirection, Time.deltaTime * rotationSharpness); if (desiredDirection.magnitude 0.01f) // 有有效输入 { // 只有当有输入时才更新朝向避免在停止时角色乱转 Quaternion targetRotation Quaternion.LookRotation(_smoothedMoveDirection, Vector3.up); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotationSharpness); // 计算目标速度 Vector3 targetVelocity _smoothedMoveDirection * moveSpeed; // 使用Rigidbody的MovePosition或velocity进行移动。这里用velocity更简单。 // 注意直接设置velocity会覆盖物理效果对于有复杂物理交互的场景需谨慎。 // 另一种方法是使用AddForce手感更“重”更真实。 Vector3 currentVelocity _rb.velocity; Vector3 velocityChange targetVelocity - currentVelocity; velocityChange.y 0; // 不改变垂直方向的速度由重力或跳跃控制 _rb.AddForce(velocityChange, ForceMode.VelocityChange); } else { // 无输入时施加一个反向力来模拟摩擦力使角色更快停止 Vector3 horizontalVelocity new Vector3(_rb.velocity.x, 0, _rb.velocity.z); _rb.AddForce(-horizontalVelocity * 10f, ForceMode.Acceleration); } } }关键点解析事件订阅我们订阅了Move动作的performed和canceled事件。performed在输入值改变时触发如按下键、摇杆推动canceled在输入值归零时触发如松开键、摇杆回中。started事件动作首次启动对于按钮更有用对于连续值移动我们更关心进行中和取消。基于摄像机的相对移动HandleMovement方法中将输入向量与摄像机方向结合实现了“按W永远向屏幕上方摄像机前方移动”的典型第三人称/第一人称移动效果。这是3D游戏移动的标配逻辑。平滑处理使用Vector3.Lerp和Quaternion.Slerp对移动方向和旋转进行插值让运动看起来更平滑自然避免因键盘按键的瞬时开关导致角色运动生硬。物理移动使用Rigidbody.AddForce并采用VelocityChange模式来改变速度。这种方式比直接设置_rb.velocity更好因为它在一定程度上尊重了物理质量并且更容易与其他力如跳跃力、爆炸冲击力叠加。同时在无输入时施加反向力模拟摩擦力比直接速度归零手感更好。4.3 方式三在Update中轮询这种方式类似于旧Input Manager在Update中直接读取动作的当前值。它简单但失去了事件驱动的灵活性通常不推荐作为主要方式但可以用于一些特殊情况。void Update() { // 直接读取Move动作的当前值 Vector2 moveInput _controls.Player.Move.ReadValueVector2(); // ... 后续移动逻辑与方式二类似 }在OnEnable中只需要启用Action Map不需要订阅事件。这种方式代码量最少但你需要自己管理输入状态并且ReadValue在每一帧都会被调用。5. 高级配置与输入处理技巧掌握了基础移动后我们来看看如何让输入控制更精细、更专业。5.1 使用Processors处理器和Interactions交互这是新Input System的强大之处可以在绑定层面预处理输入数据。Processors处理器对原始输入值进行加工。例如Stick Deadzone为摇杆设置死区过滤中心漂移。NormalizeVector2将非标准化的向量如键盘对角线输入(1,1)标准化为长度1。对于键盘WASD移动强烈建议加上这个处理器否则同时按两个方向键时向量长度会是√2导致移动速度比单方向快。ScaleVector2缩放输入值。InvertVector2反转轴。如何添加在Input Actions编辑器中选中一个绑定比如键盘WASD的复合绑定下的Up在右边Properties的“Processors”栏点击“Add Processor”选择NormalizeVector2。这样无论按一个键还是两个键输出的向量长度都是1。Interactions交互定义输入如何被触发。例如Tap点击、Hold长按、SlowTap慢按、MultiTap多次点击。Press可以配置按压点Press Point比如设置扳机键按下一半就算触发。比如你可以为“Jump”动作添加一个Hold交互设置按住0.5秒后触发用于实现“蓄力跳”。交互会改变回调的phase阶段你可以在代码中通过context.phase来判断是开始、进行中还是取消。5.2 处理多设备与输入控制切换新Input System自动支持多设备。PlayerInput组件有一个SwitchCurrentControlScheme方法可以用来动态切换控制方案如从键鼠切换到游戏手柄。在生成的C#代码方式中你可以通过监听InputSystem.onDeviceChange事件来检测设备插拔并相应地调整UI提示如显示“按A键”还是“按空格键”。一个常见的技巧是在获取输入提示时不要硬编码“按E键”而是使用InputControlPath.ToHumanReadableString方法将绑定路径转换为当前活动设备上可读的字符串。5.3 移动逻辑的进阶优化加速度与减速度不要直接给角色一个恒定速度。引入加速度加速到最大速度的时间和减速度停止所需的时间参数使用Mathf.SmoothDamp或Vector3.SmoothDamp来平滑速度变化手感会好很多。斜坡处理如果你的角色需要走斜坡直接使用transform.Translate或设置velocity可能会打滑或卡住。使用CharacterController组件配合SimpleMove或Move方法会自带斜坡处理和台阶偏移。如果坚持用Rigidbody可以考虑将移动力施加在XZ平面并利用射线检测地面法线来调整移动方向使其贴合斜坡。动画融合将_moveInput向量的magnitude长度作为Blend Tree的参数可以平滑地在待机、行走、奔跑动画间过渡。向量方向也可以用来决定转向动画。6. 常见问题与调试技巧实录在实际使用中你肯定会遇到一些坑。这里记录了几个最常见的问题和我的解决方法。6.1 Input System不响应输入这是新手最常遇到的问题。检查1Active Input Handling设置确保Project Settings - Player - Active Input Handling 是 “Input System Package (New)” 或 “Both”。如果选的是“Input Manager (Old)”新系统当然没反应。检查2Action Map是否启用如果你用代码控制确保在OnEnable中调用了_controls.Player.Enable()并且在OnDisable中正确禁用。一个常见的错误是只在Start或Awake中启用一次但在场景切换或对象禁用/启用时没处理好。检查3UI是否拦截了输入如果场景中有EventSystem和使用了Input System的UI组件如Button键盘/手柄的导航输入可能会被UI优先捕获。检查UI元素的Navigation设置或者确保在游戏模式下UI不是激活状态。检查4编辑器输入焦点Unity编辑器运行时如果你的鼠标点击了Scene视图或InspectorGame视图可能会失去输入焦点。确保Game视图是激活的或者勾选Edit - Project Settings - Editor - Lock Game View to Device并将Device设置为“None”。6.2 手柄摇杆输入不精确或漂移调整死区在摇杆绑定的Processors中添加或调整Stick Deadzone。Min和Max定义了死区范围和最大值范围。通常Min设为0.125-0.2Max设为0.925-0.95可以过滤中心漂移并确保最大输入值。使用轴向死区Axis Deadzone处理器对单个轴如扳机键更有效。代码中二次处理如果处理器还不够可以在代码中读取到_moveInput后手动判断其长度magnitude如果小于某个阈值如0.1就将其置为Vector2.zero。6.3 移动方向与摄像机方向不对应这个问题在实现第三人称摄像机跟随时非常典型。确保计算正确如4.2节代码所示必须用摄像机的前向(forward)和右向(right)向量来转换输入。切记要将这两个向量的Y分量归零并重新标准化否则摄像机的俯仰角会影响水平移动方向。摄像机参考确保你获取的是正确的摄像机。如果使用多摄像机或摄像机切换需要一个稳定的引用来获取当前主摄像机的Transform。6.4 输入延迟或感觉“不跟手”事件模式选择PlayerInput组件的Update Mode属性在Inspector中默认为Process Events In Dynamic Update这会在Update循环中处理输入。对于要求极高反应速度的游戏如格斗可以尝试改为Process Events In Fixed Update让输入处理与物理更新同步但这可能增加固定帧率下的输入延迟。通常Dynamic Update就够了。移动逻辑的执行时机将移动计算特别是基于物理的移动放在FixedUpdate中而不是Update中。因为FixedUpdate的调用频率是固定的默认0.02秒一次而Update受帧率影响。在Update中读取输入在FixedUpdate中应用移动是一种常见做法但要注意输入状态在这之间的传递。避免在渲染循环中进行复杂计算如果Update中做了太多事情可能会阻塞输入事件的派发。使用Profiler查看性能瓶颈。6.5 生成的C#类找不到或编译错误确保生成选中.inputactions文件Inspector中必须勾选“Generate C# Class”并点击Apply。检查命名空间生成的类默认可能在全局命名空间或者在一个以asset文件名命名的命名空间里。在你的脚本中要么使用完全限定名要么在文件顶部添加对应的using语句。如果asset文件名为PlayerControls生成的类名通常就是PlayerControls。重新生成如果你在生成C#类之后又修改了Input Actions asset比如重命名了Action Map需要再次点击Apply来重新生成脚本否则旧的C#类定义可能与asset不匹配导致运行时错误。最后调试输入的一个宝贵工具是Input Debugger。通过菜单Window - Analysis - Input Debugger打开。在这里你可以实时看到所有连接的设备、它们的输入状态、以及触发的动作事件是排查输入问题不可或缺的利器。