#Overview

本教程极大程度的参考了 Unity Learn 上的官方教程[^1],但并不是其翻译版本,而是根据我的学习过程进行相应增删改。
完整工程见:xuejiaW/InputSystemSample: A minimum unity project to illustrate how to use Unity new input system. (github.com)

Unity 有内建的 Input Manager 机制,这一套机制存在了非常久的时间。针对于传统的键盘,鼠标等输入,内置的 Input Manager 可以健壮的处理,但 Input Manager 的可拓展性不高。也因此随着输入设备种类的增多(如各类 XR 设备),Input Manager 无法优雅的解决这些输入。

Input System 是 Unity 为了解决 Input Manager 的上述问题,提供的高可拓展性,高自由配置的新输入解决方案。

对于新工程,Unity 官方都推荐使用 Input System 作为输入的解决方案,但 Input Manager 并不会短期内被废弃,因为历史包袱过重[^2]。

Input System 依赖 Unity 2019.1 及以上版本,本文档基于 Unity 2022.3.15f1 + Input System 1.7 编写

#Get Started

#安装 Input System

首先通过 Unity Package Manager 安装 Input System:

在安装 Package 后,会自动弹出如下窗口,该窗口表示 Input System 启用后需要重启 Editor Backend 才能正常使用,点击 Yes 启用 Input System,此时 Unity Editor 会自动重启:
启用 Input System

当 Unity 重启后,根据 Unity 版本的不同,可能内置的 Input Manager 会被关闭,如果要重新启用,可以在 Edit -> Project Settings -> Player -> Other Settings -> Active Input Handling 中选择 Both
Player Settings 中切换输出方式

至此 Input System 已经被正确安装。

为方便后续的调试,Demo 工程中预先引入了 URP 和一个最简的测试场景,此时的工程的见:A
xuejiaW/InputSystemSample at 7d041a5604c71096b0147cd0ae672557eee517c8 (github.com)

#创建 Input System Assets

为了使用 Input System,推荐先创建 Settings Asset,你需要在 Project Settings -> Input System Package 中创建相关资源:

Create Input System

该 Settings Asset 作为 Input System 全局的配置,但其并不是必须项。如果未创建该文件 Unity 会使用默认的 Input System 配置。

当点击创建后,会在工程的根目录创建出一个 InputSystem.inputsettings 文件,该文件即是 Input System 的总配置文件。同时原 Input System Package 页面也会包含有一系列的针对于 Input System 的配置项:
Input System Settings

选择创建出来的 InputSystem.inputsettings 文件,在 Inspector 中会出现直接跳转到 Input System Settings 的按钮,点击该按钮,同样会跳转到上述 Input System Settings 的配置页面:
Asset Inspector | 400

你可以随意修改 InputSystem.inputsettings 的位置,并不要求该文件必须在工程根目录下。

此时的工程状态见:
xuejiaW/InputSystemSample at 62aff15fcf5c3479e5a3073af7646a6c2775e043 (github.com)

#查看 Input Debugger 窗口

你可以在 Window -> Analysis -> Input Debugger 中打开 Input Debugger 窗口,该窗口中可以显示当前连接的输入设备:
Input Debugger 窗口

#使用 Input System

#创建 Input Action Asset

如场景中有如下小球:
Sphere

为了让其移动,可以为它添加一个 Player Input 组件,在 Player Input 组件上,选择 Create Action 创建出一个 Input Action 资源:

当点击后,会需要你选择保存的路径,选择后,会在该路径下创建出一个 Input Action 资源( Input System.inputactions),并自动打开该资源的配置窗口,窗口如下所示:
Input Action Window

创建出来的 Input Action Asset 资源如下所示,当点击该资源上的 Edit asset 按钮或双击该资源,都将打开上述的窗口:
Input Action Asset

具体查看 Input Action Asset 中的 Move Action,可以看到其中通过定义了可以通过键盘的 WASD上下左右 触发:

此时按下 WASD上下左右,会发现小球还 不能 移动,因为此时小球只是 获取 到了输入信息,但还是没有 处理 这些输入信息。

此时工程状态见:
xuejiaW/InputSystemSample at ed81be6f2efcf89c72f287240c9b56ea80a24094 (github.com)

#使用代码控制小球

为了处理 Input System 的输入信息,可以添加 PlayerController 脚本,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
private Rigidbody m_Rb;
private float m_MovementX;
private float m_MovementY;
[SerializeField] private float m_Speed = 5;

private void Start() { m_Rb = GetComponent<Rigidbody>(); }

private void OnMove(InputValue value)
{
var inputVector = value.Get<Vector2>();
m_MovementX = inputVector.x;
m_MovementY = inputVector.y;
}

private void FixedUpdate()
{
Vector3 movement = new(m_MovementX, 0.0f, m_MovementY);
m_Rb.AddForce(movement * m_Speed);
}
}

其中的 OnMove 对应 创建 Input Action Asset 后 Asset 中的 Move Action:

对于 Asset 中任意名称的 Action,都可以通过 On<ActionName> 监听到。
如果 Action 叫做 AAA,则可以定义 OnAAA 函数监听。

将该脚本挂载在 Player 上,如下所示:
挂载 PlayerController | 500

此时小球就可以通过键盘的 WASD上下左右 移动:
控制小球

此时的工程状态见:
xuejiaW/InputSystemSample at 9cb75b9a8719cc8e695ac32ad786adcc007494d8 (github.com)

#自定义 Action Asset

在上 创建 Input Action Asset 步骤中,创建出来的 Input Actions 是 Unity 默认实现的,即适合于 Player Input 组件的 Actions 资源。

Player Input 组件也是 Unity 内建的读取 Assets 资源的脚本

在这一节,会自定义 Actions 资源,并自定义使用该 Actions 资源的脚本。

#创建自定义 Action Asset

在 Project 面板中,空白处右键选择 Create -> Input Actions,创建出一个新的 Input Actions 资源:
New Input Actions Assets

双击创建的资源(本例中为 BallControls.inputactions ) 后会打开空白的 Input Actions 窗口:
Empty Input Assets

此时点击画面左侧的 + 号可以创建出 Input Action Map,我们将新增的 Input Action Map 命名为 BallPlayer

在窗口中间,可以为这个 Input Action Map 创建一些 Input Action,如下过程,创建了 Buttons 这个 Input Action:

在窗口的右侧,可以为 Input Action 创建一系列 Input Binding,如下步骤分别绑定了 GamePadEast ButtonWest Button

GamePadEastWest Button,在 Xbox 控制器上分别对应 X 键和 B

你也可以继续为 Buttons Action 绑定 Keyboard 的 F1F2 按键,步骤如上,当绑定完成后,整个 Buttons Action 如下所示:

进一步创建 Move Input Action ,与 Buttons 不同是,Move 需要将 Action Type 设置为 Value,且 Control Type 为 Vector2,这表示 Action 会返回 Vector2 数据,即用于平面移动的上下左右数据:

如之前步骤一样,为该 Move Input Action 绑定 Left Stick,绑定后结果如下:
左摇杆

也可以将键盘上的按键通过 组合绑定(Composite Bindings) 至 Move Input Action,如下所示,其逻辑为使用四个按键分别表示 Vector2 四个方向(+x,x,+y,y+x,-x,+y,-y):
四按键组合绑定

分别为上下左右四个方向设定四个按键 K,J,H,L,结果如下所示:
绑定后的四个按键

至此,自定义的 Action Asset 创建完成,其中定义了使用 GamePadKeyboard 两种输入设备,分别控制 ButtonsMove 两个 Action。将新建的 BallControls.inputactions 替换掉 Player Input 组件中的 Actions,即可使用新的 Action Asset:
使用新 Asset

此时运行游戏,可以发现通过手柄的左摇杆和 HJKL 都可以控制小球的移动,而 WASD 则不行了。

这是因为 PlayerController 脚本监听的 Motion 事件在 BallControls.inputactions 中也存在,因此我们定义的左摇杆和 HJKL 四个按键都能响应,即使不修改 PlayerController 也可以正常运行。而原 PlayerInput.inputactions 中的 WASD 我们并没有绑定,所以无法相应。

此时的工程状态见:
xuejiaW/InputSystemSample at 8d994e47fbf7c766c87aa62ce517e7e5bdda031b (github.com)

#创建自定义 Player Input

#手动解析 Actions Asset

自定义一个 BallController 脚本,用于解析刚刚创建的 BallControls.inputactions,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class BallController : MonoBehaviour
{
[SerializeField] private float m_Speed = 10;
[SerializeField] private InputActionAsset m_Asset = null;

private Rigidbody m_Rb;
private Vector2 m_Move;

private InputActionMap m_ActionMap = null;
private InputAction m_ButtonsAction = null;
private InputAction m_MoveAction = null;

private void Awake()
{
m_ActionMap = m_Asset.FindActionMap("BallPlayer");
m_ButtonsAction = m_ActionMap.FindAction("Button");
m_MoveAction = m_ActionMap.FindAction("Move");
m_MoveAction.canceled += _ => OnMove(Vector2.zero);

m_ButtonsAction.performed += _ => OnButton();
m_MoveAction.performed += ctx => OnMove(ctx.ReadValue<Vector2>());

m_Rb = GetComponent<Rigidbody>();
}

private void OnEnable() { m_ActionMap.Enable(); }

private void OnButton() { Debug.Log("On Buttons clicked triggered"); }

private void OnMove(Vector2 coordinates)
{
Debug.Log($"On move clicked triggered {coordinates.ToString("f4")}");
m_Move = coordinates;
}

private void FixedUpdate()
{
Vector3 movement = new(m_Move.x, 0.0f, m_Move.y);
m_Rb.AddForce(movement * m_Speed);
}

private void OnDisable() { m_ActionMap.Disable(); }
}

可以看到,该脚本直接引用了之前的 InputActionAsset ,并使用了 InputActionAsset.FindActionMap](https://docs.unity3d.com/Packages/com.unity.inputsystem@1.7/api/UnityEngine.InputSystem.InputActionAsset.html#UnityEngine_InputSystem_InputActionAsset_FindActionMap_System_String_System_Boolean_) 找寻之前创建的 BallPlayer [Input Action Map,并在 OnEnableOnDisable 时启用和禁用该 Input Action Map。

另外脚本中通过 InputActionMap.FindAction](https://docs.unity3d.com/Packages/com.unity.inputsystem@1.7/api/UnityEngine.InputSystem.InputActionMap.html#UnityEngine_InputSystem_InputActionMap_FindAction_System_String_System_Boolean_) 找寻之前创建的 ButtonsMove Action,并监听了 [Input Action 的 performed 事件,触发对应的回调函数 OnButtonOnMove

至此 BallController 脚本已经完全实现了之前 Player Input + PlayerController 的功能,因此在 Player 游戏物体上仅需要 BallController 脚本即可,注意要将之前创建的 BallControls.inputactions 挂载至脚本中:

此时小球可以如同之前一样的通过手柄和键盘控制移动。

此时工程状态见:
xuejiaW/InputSystemSample at f070c14f0671d7c1c702b270f3751aed8c003692 (github.com)

#基于 Actions Asset 自动生成对应类

手动解析 Actions Asset 中需要手动管理 Actions Asset 并从中读取 Input Action Map 和 Input Action。

上述的读取过程,会随着 Action Assets 中的数据变更而出现潜在的失效(如命名错误),因此 Unity 提供了从 Action Assets 中自动创建相应脚本的能力,可以简化上述步骤。

可以选择 Action Assets 便选择,并勾选其中的 Generate C# Class ,选择需要创建的类名称,文件和命名空间,并点击 Apply 正式创建脚本:
Create

此时会创建出对应的脚本:
Automated Create Script

修改之前的 BallController 脚本,以适配自动生成的 BallControls,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using UnityEngine;

public class BallController_AutoScripts : MonoBehaviour
{
[SerializeField] private float m_Speed = 10;

private BallControls m_Controls;

private Rigidbody m_Rb;
private Vector2 m_Move;

private void Awake()
{
m_Controls = new BallControls();
m_Rb = GetComponent<Rigidbody>();

m_Controls.BallPlayer.Button.performed += _ => OnButton();
m_Controls.BallPlayer.Move.performed += ctx => OnMove(ctx.ReadValue<Vector2>());
m_Controls.BallPlayer.Move.canceled += _ => OnMove(Vector2.zero);
}

private void OnEnable() { m_Controls.BallPlayer.Enable(); }

private void OnButton() { Debug.Log("On Buttons clicked triggered"); }

private void OnMove(Vector2 coordinates)
{
Debug.Log($"On move clicked triggered {coordinates.ToString("f4")}");
m_Move = coordinates;
}

private void FixedUpdate()
{
Vector3 movement = new(m_Move.x, 0.0f, m_Move.y);
m_Rb.AddForce(movement * m_Speed);
}

private void OnDisable() { m_Controls.BallPlayer.Disable(); }
}

此时不再需要手动获取 Input Action Map 和 Input Action,只需要使用 <ActionControls>.<ActionMapName>.<Action> 的风格直接访问即可。

此时将之前挂载在 Player 上的的 BallController 脚本换为 BallController_AutoScripts 脚本,并运行,可以看到效果与之前的效果无差别。

此时工程状态见:
xuejiaW/InputSystemSample at a92af1df6ccfb22f2747548d2abf1a08c43a3407 (github.com)

#Reference