文章

快速上手Unity的新输入系统

通过一个简单的WASD输入检测案例来快速熟悉Unity的新Input System的基本使用方法

快速上手Unity的新输入系统

一、关于旧输入系统

  • Unity的旧Input System通过在Update函数中调用Input.GetKey(KeyCode.W)等静态硬编码API实现,例如下述实现玩家跳跃的简单例子
1
2
3
4
5
6
7
private void Update()
{
    if (Input.GetKeyDown(KeyCode.Space) && player.isGround)
    {
        stateMachine.ChangeState(player.jumpState);
    }
}
  • 新输入系统的使用需要更多的设置,但能更方便地实现跨平台配置,其使用方法详见后文

二、安装新输入系统

  • 在”Package Manager”中的”Unity Registry”类目下搜索”Input System”库进行安装,安装后需重启

InputSystem安装.png

  • 然后我们可以通过在”Project Settings”中修改关于这个输入系统的一些设置

自定义输入设置.png

  • 若在导入该包前你的场景中就使创建了UI游戏对象,那么导入该包并重启后,你需要按照其提醒在自动生成的EventSystem中使用新输入系统模块替换掉旧输入系统模块

三、创建InputActions

  • 在Project资源管理窗口处右键即可创建一个输入检测资产,可用于例如玩家的输入控制

创建InputAction.png

  • 然后通过该资产生成同名脚本,通过该脚本(其内容已经自动生成,一般无需再次编写)即可对该资产对应设置的输入映射进行引用,参考后文代码相关示例

通过InputAction生成脚本.png

四、编辑ActionMaps

  • 在InputActions资产中可以通过”Edit Asset”按钮进入对应窗口,从而进行按键映射管理

编辑InputActionMapsP1.png

  • 下图是角色的前后移动案件绑定WASD示例

WASD的InputBinding设置.png

  • 以下是控制角色视角的绑定按键设置

Camera的InputBinding设置.png

五、输入事件检测

  • 如下脚本就实现了对WASD输入的输入监测读取
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
//Singleton是我自己写的继承自MonoBehaviour的单例基类
public class PlayerInputManager : Singleton<PlayerInputManager>
{
    //Unity新输入系统InputActions资产对应生成的同名脚本类
    private PlayerControls playerControls;

    [Header("Movement")]
    //在对应InputActions资产中设置的ActionMaps中的移动相关输入绑定映射
    [SerializeField] private Vector2 inputMovement;

    [Header("Camera")]
    [SerializeField] private Vector2 inputCamera;

    //当脚本所附加的游戏对象被启用时该函数被调用
    private void OnEnable()
    {
        //初始化角色按键控制监测脚本,并将必要的功能语句订阅到按键输入监测事件上
        if (playerInputActions == null)
        {
            playerInputActions = new PlayerControls();

            #region EventMulticasting
            //当事件performed监测到按键输入发生时,执行对应匿名函数以将对应按键输入值赋予相关变量
            //其中"PlayerBasic"是ActionMaps中的一组控制角色基本操纵的动作按键映射,例如其组内Movement控制角色移动
            //=>运算符用于构造匿名函数,左侧临时变量_i(实际接收的是和事件performed相同的参数)是其传入参数,右侧语句是匿名函数主体
            //_i.ReadValue<Vector2>()相当于读取了performed事件监测接收的InputAction.CallbackContext类型参数中蕴含的Vector2类型输入信息
            //+=运算符执行事件(即委托封装)的多播,此处将匿名函数订阅到performed事件(该事件用于InputSystem监测对应Action绑定按键的输入)
            playerControls.PlayerBasic.Movement.performed += _i => inputMovement = _i.ReadValue<Vector2>();
            playerControls.PlayerBasic.Camera.performed += _i => inputCamera = _i.ReadValue<Vector2>();
            #endregion
        }

        //开启按键事件监测
        playerInputActions.Enable();
    }

    private void OnDisable()
    {
        //关闭按键事件监测
        playerInputActions.Disable();
    }
}
  • 如下图是将上述脚本挂载到GameObject上后监测输入的示例(如果我们绑定的输入是手柄的摇杆的上下左右,那么此处的InputMovement向量的两维度值可为浮点数),我们可以在其它脚本中通过该管理器的实例获取到输入值,从而控制角色移动等行为

展示按压AW时的输入结果抓取.png

  • 然后可以通过下面的函数分别控制人物的移动、摄像机的视角旋转
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//PlayerLocomotionManager.cs
private void UpdateMovement()
{
    //获取视角相机,用于获取相机视角信息
    Camera _cameraObject = PlayerCameraManager.instance.cameraObject;
    Vector2 _inputMovement = PlayerInputManager.instance.GetInputMovement();

    //玩家移动方向与玩家输入以及摄像机朝向有关,且需要进行方向向量归一化
    targetMovementDirection = (_cameraObject.transform.forward * _inputMovement.y 
        + _cameraObject.transform.right * _inputMovement.x).normalized;
    //禁止竖直方向的运动
    targetMovementDirection.y = 0;

    //调用CharacterController类的Move方法,传入参数注意需要乘上单位时间以确保移动速度与帧率无关
    playerManager.characterController.Move(targetMovementDirection * runningSpeed * Time.deltaTime);
}

//PlayerLocomotionManager.cs
private void UpdateRotation()
{
    Camera _cameraObject = PlayerCameraManager.instance.cameraObject;
    Vector2 _inputMovement = PlayerInputManager.instance.GetInputMovement();

    //旋转朝向同理与玩家输入以及摄像机朝向有关,进行归一化后消除竖直方向运动的可能性
    targetRotationDirection = (_cameraObject.transform.forward * _inputMovement.y
        + _cameraObject.transform.right * _inputMovement.x).normalized;
    targetRotationDirection.y = 0;
    //如果玩家无移动输入,那么维持原有方向
    if (targetRotationDirection == Vector3.zero)
        targetRotationDirection = transform.forward;

    #region ApplyPlayerRotation
    //根据传入的目标方向创建新旋转状态,LookRotation函数返回一个使对象的forward方向对齐目标方向的四元数
    Quaternion _targetRotation = Quaternion.LookRotation(targetRotationDirection);
    //Slerp即球面线性插值,其能创建返回一个以一定旋转速度从当前旋转状态平滑过渡到目标旋转状态的旋转操作四元数
    //传入始末旋转状态四元数,创建对应的旋转操作四元数,并通过对当前旋转状态直接赋值以应用该旋转操作以变换到目标旋转状态
    transform.rotation = Quaternion.Slerp(transform.rotation, _targetRotation, rotatingSpeed * Time.deltaTime);
    #endregion
}

//PlayerCameraManager.cs
private void UpdateRotatingAroundPlayer()
{
    Vector2 _inputCamera = PlayerInputManager.instance.GetInputCamera();

    //计算左右旋转(注意是加号)的角度,并乘上单位时间确保相机视角旋转速率与帧率无关
    leftRightLookAngle += _inputCamera.x * leftRightRotatingSpeed * Time.deltaTime;
    //计算上下旋转(注意是减号)的角度,并限制上下视角的边界角度
    upDownLookAngle -= _inputCamera.y * upDownRotatingSpeed * Time.deltaTime;
    upDownLookAngle = Mathf.Clamp(upDownLookAngle, minPivotAngleBound, maxPivotAngleBound);

    #region ApplyCameraRotation
    Vector3 _cameraRotation = Vector3.zero;

    //旋转Hierachy中最高层级相机对象,控制y轴偏航角(Yaw)即左右视角旋转
    _cameraRotation.y = leftRightLookAngle;
    transform.rotation = Quaternion.Euler(_cameraRotation);

    //旋转Hierachy中第二层级相机对象,控制x轴俯仰角(Pitch)即上下视角旋转
    _cameraRotation = Vector3.zero;
    _cameraRotation.x = upDownLookAngle;
    //注意localRotation是局部旋转,不会带动其父对象旋转
    cameraPivot.localRotation = Quaternion.Euler(_cameraRotation);
    #endregion
}

六、限定可控场景

  • 当游戏处于特定的场景时,可能已经生成了玩家对象预制体,但我们不希望此时的输入能够对玩家进行操控,那么我们就需要判断当处于角色非可控场景内时隐藏附着PlayerInputManager这类脚本的GameObject,可以参考以下的简单实现(在上述例子脚本中添加如下内容)
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
public class PlayerInputManager : Singleton<PlayerInputManager>
{
    //...

    private void Start()
    {
        //当前单例初始化时为场景切换订阅一个委托,用于检测那些无权控制角色的场景,以隐藏该单例附着的GameObject来关闭输入检测
        SceneManager.activeSceneChanged += OnSceneChanged;

        //初始不启用而切换场景时再判断启用与否;DontDestroyOnLoad(gameObject);语句以及委托多播都需在该句之前写出才有作用
        instance.enabled = false;
    }

    private void OnDestroy()
    {
        //析构时取消订阅,防止内存泄漏
        SceneManager.activeSceneChanged -= OnSceneChanged;
    }

    private void OnSceneChanged(Scene _oldScene, Scene _newScene)
    {
        //若新场景的buildIndex(即在BuildSettings中的顺序索引)属于世界场景,那么允许玩家控制角色
        if (SavesManager.instance.IsWorldSceneIndex(_newScene.buildIndex))
            instance.enabled = true;
        else
            instance.enabled = false;
    }

    //...
}
本文由作者按照 CC BY-NC-SA 4.0 进行授权