新的输入系统
文档用途
Unity 推出了新的输入系统, 已经可以用于生产环境.
新的输入系统基本上全面优于以往的输入系统, 可以完全替代.
这篇文档用于说明如何使用 Input System.
TIP
继续阅读前, 建议先观看20分钟左右的视频 新输入系统 Input System 的基本使用.
优势
相比于旧的输入系统, Input System 的优势在于:
- 将物理输入与逻辑输入区分开,
使得不同的物理输入 (例如鼠标, 触屏, 手柄摇杆) 能够有相同的逻辑输入形态. - 提供强大的
配置界面, 使得输入行为能被统一管理, 细致调整. - API 支持基于 C# 事件的
订阅-发布模式. - 提供更丰富的 API, 包括接近底层的和高度封装的.
- 更容易做自动化测试.
安装与配置
安装与启用
Input System 以 package 的形式发布, 可以在 Package Manager 中找到:

在 Project Settings > Player > Other Settings 中可以找到配置选项.
如下图所示, 如果只想使用新的输入系统, 则应选择 Input System Package (New).
如果想兼容旧的输入系统, 则可以选择 Both.

全局配置
在 Project Settings > Input System Package 中可以对全局参数做配置.
如下图所示, 这些全局配置会被默认应用在具体的输入行为中, 不过也可以 override.
每个选项的作用详见 官方文档.

相关组件
Input Actions asset
在 Project 窗口中鼠标右键空白处, Create > Input Actions
即可创建 Input Actions asset, 图标如下图所示.

所有的输入行为都可以在 Input Actions asset 中定义.
虽然理论上可以在游戏中使用多个 Input Actions asset, 但在实践中, 一个工程只需要一个.
双击后可打开编辑窗口, 如下图所示.Input Actions asset 的配置细节详见 官方文档.

Input Actions asset 的快捷键

可以通过 Input Actions asset 自动生成一个 C# 脚本, 方便在代码中调用.
每当 asset 发生改动时, 对应的 C# 脚本也会自动发生对应的改动.
选中 Input Actions asset 后, 在 Inspector 中勾选 Generate C# Class.
默认情况下自动生成的脚本会与 asset 同名并在相同目录下.

Player Input
Player Input 是个 MonoBehaviours 组件, 用于模拟 "一个玩家".
有的游戏, 例如 "胡闹厨房", 支持一台设备多个玩家同时游戏 (同时输入).
在这种情况下, 场景中就需要多个 Player Input 组件, 数量与玩家数量一致.

WARNING
就笔者试用的情况看, 应该没有必要使用它, 因此不展开说明.
也许 Player Input 组件能发挥作用的情景是:
Behavior 设置为 Invoke Unity Events 时, 在 Inspector 中不通过代码直接调用某些方法.
Input Module
当我们在场景中创建了一个 Canvas 后, 会同时自动创建一个名为 EventSystem 的物体.
挂在在其上的 Standalone Input Module 使用的是旧的输入系统.
点击 Replace with InputSystemUIInputModule 按钮, 即可替换成 Input System 对应的组件.

注意 Actions Asset 所引用的 asset, 默认为 Input System Package 中自带的.
笔者认为应该将其替换成自己创建的 asset, 以保证 Input Actions asset 在工程内唯一,
也方便我们对 UI 的交互行为做自定义.

Cinemachine
Cinemachine 是一套摄像机管理方案.
一些来自 Cinemachine 的组件与输入信息有关.
例如 CinemachineFreeLook 组件.
Cinemachine 默认使用旧的输入系统, 为了使用 Input System, 需要添加一个组件.
下图中的 Cinemachine Input Provider 组件来自于 Cinemachine Package.

上图中的 XY Axis 和 Z Axis 所引用的类型是 InputActionReference, 如下图所示.InputActionReference 就是我们之前编辑的 Input Actions asset 中具体的某个 action.

也可以自己写一小段代码替代 Cinemachine Input Provider 组件.
重点是需要实现 Cinemachine.AxisState.IInputAxisProvider 接口.

重要 API
简单示例
在写代码之前, 应确保已经配置好了 Input Actions asset.
如下图所示, 通过代码和备注可以清晰地看明白大概应该怎么使用这些重要的 API.
using UnityEngine;
using UnityEngine.InputSystem;
public class InputTest : MonoBehaviour
{
// MyInputActions 是自动生成的脚本
// 在 Input Action Asset 面板中勾选 Generate C# Class 就会自动生成同名脚本
private MyInputActions _myInputActions;
private MyInputActions myInputActions => _myInputActions ??= new MyInputActions();
private void Awake()
{
// 形式为 <Name>.<Action Maps>.<Actions>.performed += xxx;
myInputActions.Player.Move.performed += MovePerformed;
// 也可以直接使用 Lambda 表达式
myInputActions.Player.Move.performed += context => Debug.Log(context.ReadValue<Vector2>());
// 当不需要获取输入的数值时,可以使用弃元符号,即 _
myInputActions.Player.Fire.performed += _ => Debug.Log("Fire");
}
// 需要调用 Enable() 方法使得输入生效
private void OnEnable() => myInputActions.Enable();
private void OnDisable() => myInputActions.Disable();
private void MovePerformed(InputAction.CallbackContext context)
{
// 可以通过 ReadValue<TValue>() 获得输入值
Debug.Log(context.ReadValue<Vector2>());
}
}上述写法主要是为了简单易懂, 在实践中应该会有所不同:
- 应确保
MyInputActions在运行时中只有一个实例. myInputActions.Enable()和myInputActions.Disable()的调用应该与游戏的生命周期同步.
也就是说: 在游戏启动时调用
Enable(), 在游戏退出时调用Disable().
- 应确保任意一个方法都
不会重复订阅某个 performed 事件.
在上述代码中, 如果连续写了两句:
myInputActions.Player.Move.performed += MovePerformed;
myInputActions.Player.Move.performed += MovePerformed;
那么当 performed 事件被触发一次时,MovePerformed()就会被调用两次, 有问题.
- 应确保失效的订阅及时被取消.
举个例子, 对应到上述代码, 则需要在恰当的时候及时执行这条语句:
myInputActions.Player.Move.performed -= MovePerformed;
- 当与 Entities Package 结合使用时, 用法可能有明显的不同.
Entities 中的
系统处理逻辑的机制接近于轮询, 而不是订阅-发布.
好在 Input System 也提供了用于主动检测输入事件API.
主动检测事件
如果下图所示, 使用 triggered 来判断输入行为是否触发.

获取硬件输入
如下图所示, 我们依旧可以使用类似于旧的输入系统的 API 来检测输入:
这里使用 Keyboard 举例. 输入硬件的种类很多 (例如 Mouse, Touchscreen), 写法类似.

游戏运行时, 按住键盘空格键一小会儿后再放开, 会在 Console 窗口中看到:

使用这种 API 的好处在于比较方便, 不需要配置 Input Actions asset, 直接写代码就好.
performed 前后
除了之前前面出现的 performed 事件, 还有两个事件: started 和 canceled.
如下图, 三个事件被定义在 InputAction 类中, UnityEngine.InputSystem 命名空间.

实际应用的效果如下图所示:

触屏与模拟
- 比较底层的触屏 API 被定义在
TouchScreen中,UnityEngine.InputSystem命名空间下. - 比较高级的触屏 (也是官方推荐开发者使用的) API 被定义在
Touch中,UnityEngine.InputSystem.EnhancedTouch命名空间下. - 用于在开发阶段模拟触屏操作的 API 被定义在
TouchSimulation中,UnityEngine.InputSystem命名空间下.
示例代码如下.
using UnityEngine;
using UnityEngine.InputSystem.EnhancedTouch;
using UnityEngine.InputSystem.Controls;
// 因为 UnityEngine 命名空间下也定义了 Touch (旧的输入系统)
// 所以在这里需要通过别名来指定 Touch 是具体指谁
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
public class TouchInputTest : MonoBehaviour
{
TouchControl simulatedTouch =>
TouchSimulation.instance.simulatedTouchscreen.primaryTouch;
private void Update()
{
// 用于检测是否存在模拟触屏事件, 也就是检测鼠标有没有按下
if (simulatedTouch.isInProgress)
{
// 这里读取的实际上是鼠标位置, 因为在 Unity Editor 中使用鼠标模拟触屏
Debug.Log(simulatedTouch.position.ReadValue());
}
// 获取当前屏幕上所有的触摸状态 (多指同时接触屏幕)
foreach (var touch in Touch.activeTouches)
Debug.Log($"{touch.touchId}: {touch.screenPosition}, {touch.phase}");
}
private void OnEnable()
{
// 如果想启用 EnhancedTouch, 则需要调用以下方法
EnhancedTouchSupport.Enable();
// 如果想用用鼠标来模拟触屏操作, 则需要使用 TouchSimulation
TouchSimulation.Enable();
}
private void OnDisable()
{
EnhancedTouchSupport.Disable();
TouchSimulation.Disable();
}
}需要注意, 如果在 Unity Editor 中运行与触屏相关的代码可能会有报错,
这是因为在编辑器中运行时 缺少触屏输入设备, 针对这个问题的解决方案是: 开启下图中选项.
通过菜单栏 Window > Analysis > Input Debug 打开窗口.
点击Options, 开启Simulate Touch Input From Mouse or Pen.

传感器
移动设备相比于电脑, 主机等平台而言, 有着丰富的传感器, 用好了效果会很棒. 示例代码如下.
using UnityEngine;
using UnityEngine.InputSystem;
// 由于 UnityEngine 和 UnityEngine.InputSystem 两个命名空间下都定义了 Gyroscope
// 为了避免歧义,在这里使用别名来指定其为 UnityEngine.InputSystem.Gyroscope
using Gyroscope = UnityEngine.InputSystem.Gyroscope;
public class SensorInputTest : MonoBehaviour
{
/// <summary> 用于判断是否为移动平台 </summary>
private bool isOnMobilePlatform =>
(Application.platform == RuntimePlatform.Android) ||
(Application.platform == RuntimePlatform.IPhonePlayer);
private void OnEnable()
{
if (isOnMobilePlatform)
{
// 启用陀螺仪传感器
InputSystem.EnableDevice(Gyroscope.current);
// 设置采样频率
Gyroscope.current.samplingFrequency = 60;
}
}
private void OnDisable()
{
if (isOnMobilePlatform)
InputSystem.DisableDevice(Gyroscope.current);
}
}上面只是用陀螺仪传感器 (Gyroscope) 举了个例子, 可使用的全部传感器详见 官方文档.
这里记录可以同时在 Android 和 iOS 设备中使用的传感器.
| Device | Android | iOS | Control | Type |
|---|---|---|---|---|
| Accelerometer | Yes | Yes | acceleration | Vector3Control |
| Gyroscope | Yes | Yes | angularVelocity | Vector3Control |
| GravitySensor | Yes | Yes | gravity | Vector3Control |
| AttitudeSensor | Yes | Yes | attitude | QuaternionControl |
| LinearAccelerationSensor | Yes | Yes | acceleration | Vector3Control |
| StepCounter | Yes | Yes | stepCounter | IntegerControl |
- Accelerometer 加速度传感器, 以三维向量表示加速度.
- Gyroscope, 陀螺仪传感器, 以三维向量表示角速度.
- GravitySensor, 重力传感器, 以三维向量表示重力方向.
- AttitudeSensor, 姿态传感器, 以四元数表示设备在空间中的姿态, 即 rotation.
- LinearAcceleationSensor, 线性加速度传感器, 以三维向量表示去除重力影响后的加速度.
另外需要注意, 对于移动设备而言, 有时候玩家会切出游戏回到系统桌面 (或其他 APP),
此时如果再次回到游戏, 会发现传感器停止了工作.
为了避免这种情况, 需要调用 OnApplicationFocus(bool focusStatus).
拿姿态传感器 (AttitudeSensor) 举个例子:
private void OnApplicationFocus(bool focusStatus)
{
if (focusStatus)
InputSystem.EnableDevice(AttitudeSensor.current);
else
InputSystem.DisableDevice(AttitudeSensor.current);
}自动化测试
简述
关于自动化测试的基础知识, 详见 Unity 中的自动化测试 文档.
Input System 提供了 API 用于自动化测试, 可以用代码完全模拟真机输入行为.
准备工作
要想测试 InputSystem, 则一定要在测试程序集中添加下图中的程序集引用:

还有一个 非常非常重要 却容易被忽视的一点: 如果希望测试来自于 package 的代码,
那么就需要在 Packages/manifest.json 文件中把目标 package 的名字列在 "testables" 中:
{
// ...
"testables": [
"com.unity.inputsystem",
]
}添加 "testables" 相关内容后, 需要重启 Unity 使其生效.
如何检验 "testables" 相关配置是否已生效
如下图所示, 当 Input System package 能被测试时,
可以看到 Test Runner 窗口中 PlayMode 页面下, 新增了一个测试程序集.

代码示例
模拟键盘空格键被按下的输入操作:

如果想模拟 Input Actions asset 中定义的输入行为,
例如下图 Fire 这个输入行为绑定了两个输入方式, 一个需要鼠标, 一个需要触屏.

对应的自动化测试代码如下:

辅助 API
这里必须要着重说明 InputTestFixture, 其作用主要体现在:
- 提供丰富的方法用来模拟输入行为.
- 提供独立的 Input System, 专门用于测试.
- Input System 的初始状态中欠缺的仅仅是输入设备, 因此需要在测试用例中指明.
- 为每个测试用例提供已初始化处于默认状态的 Input System,
并在测试用例执行完成后将 Input System 再自动恢复成一开始的初始化状态.
因此在对 Input System 做自动化测试时, 一定要使用 InputTestFixture.