我们已经介绍过Unity新一代的输入系统。本文,我们将使用Unity 2019.2开发可以移动、缩放和旋转的可配置摄像机。这种设计方法适用于不需要额外附带一个第一或第三人称摄像机,而是可以让游戏视角在场景自由移动的游戏。

摄像机的配置功能包括:

  • 摄像机角度
  • 最大和最小缩放设置
  • 默认缩放设置
  • 视线偏移(设定摄像机在Y轴上的观察位置)
  • 旋转速度

unity更改摄影机可以拍摄的图 unity添加摄像机_unity更改摄影机可以拍摄的图

学习目标

  • 了解Unity新一代输入系统的重要概念。
  • 获得可根据游戏进行自定义的可配置摄像机。

学习准备

你需要有使用Unity的基础知识。本文将不会介绍基础知识,包括:游戏对象和组件的概念、何时调用Start方法等。

本文中的项目使用了Low Poly: Free Pack资源:

https://assetstore.unity.com/packages/3d/environments/polyworks-free-pack-sample-58821

你可以复制本文的代码库,获得学习时用到的初始项目:

https://github.com/Unity-Technologies/InputSystem

安装新一代输入系统

Unity不断对输入系统进行全面的改进,以便使新一代输入系统更加强大而稳定,可以更好地适用于多种平台和设备配置。我们可以轻松配置该系统,使其能够处理多个本地玩家的输入。

请注意:新一代输入系统仍在不断完善开发中,处于预览阶段。

安装新一代输入系统,请通过资源包管理器安装Input System资源包,请按照以下步骤操作:

  • 依次点击Window > Package Manager
  • 选择Advanced > Show Preview Packages,显示预览版资源包。
  • 在搜索栏输入“Input System”,寻找该资源包。
  • 选中Input System资源包,单击Install按钮。

通用渲染管线等Unity特定功能需要使用旧的输入系统。因此,我们最好确保项目设置中的Active Input Handling属性设为Both。这意味着我们可以在游戏中使用两种输入系统,但在本文中,我们只会使用新一代输入系统。

我们可以访问下面的设置,确定是否已经设置好该属性:依次点击Edit > Project Settings > Player > Configuration。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity 获取鼠标点击位置_02

设置新一代输入系统

新一代输入系统比原有系统更为复杂。虽然初次学习难度更高,但会带来很好的回报。新系统更加强大稳定,在正确设置时,使用新系统所需的工作量会更少。

首先,我们要创建Input Controls输入控制资源,在项目窗口单击右键: 

  • 选择Create > Input Actions
  • 将新文件命名为PlayerInputMapping
  • 双击打开文件的编辑窗口。

配置输入时,有四个概念需要了解:

  • 控制方案(Control Scheme):用于设置必须满足的设备要求,从而使输入绑定变得可用。这是可选设置,我们可以把它保留原样,即不设定要求。
  • 动作导图(Action Maps):这是可以批量启用或禁用的动作组。
  • 动作(Action):能够分组到特定动作下的一组输入绑定,例如:“开火”或“移动”等动作。
  • 输入绑定(Input Bindings):用于指定要监视的设备输入,例如:手柄上的按键、鼠标按钮或键盘按键。

例如:在把动作设为多个输入绑定映射时,我们使用了“开火”动作,该动作会关联到手柄的特定按键,如果是键盘鼠标的设置方案,则会关联到鼠标右键。

动作导图、动作和输入绑定都有各自的属性。我们将在本文中详细介绍这些属性。

定义控制方式

输入方案将设计用于带有键盘和鼠标的设备,但如果需要,我们也可以轻松扩展到其它输入方式。总的而言,我们会有一个控制方案、一个动作导图、四个动作和五个输入绑定。

我们的设置如下图所示。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity 3d水的资源包_03

虽然上图看起来复杂,但创建该布局的方法其实很简单。打开PlayerInputMapping资源,创建一个新的动作导图

  • 单击Action Maps旁边的+图标,命名为Player。这会自动创建空白的Action部分和Input Binding节点。
  • Action部分重命名为Camera_Move。设置以下属性:Action Type设为Value。Control Type设为Vector 2。

我们使用2D Vector Composite绑定节点,而不是使用默认创建的节点。每次按下W、S、A或D键时,该节点会告诉输入系统发送2D Vector数值。目前该部分不会起到任何作用,在把动作关联到摄像机后,我们会使用到该数值。

  • 在空白的Binding部分单击右键,选择Delete,删除该节点
  • 右键单击Camera_Move动作,选择Add 2D Vector Composite,把新建的Binding部分命名为WASD
  • 选择名称有“Up: ”的部分,把Path设为W [Keyboard]
  • 对名称为Down、Left和Right的部分重复这些操作,把它们的Path设为对应的按键。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity更改摄影机可以拍摄的图_04

对方向键执行相同的操作。添加新的2D Vector Composite,命名为Arrows。设置每项映射到对应的方向键,现在我们会看到下图的设置。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity tooltip_05

我们现在需要设置剩余的动作和绑定:

  • 添加新动作,命名为Camera_Rotate
  • 把Action Type设为Value,Control Type设为Vector 2
  • 单击绑定部分,把它的Path设为Delta (Mouse)

接下来,我们要设置Camera_Rotate_Toggle的动作和绑定

  • 添加新动作,命名为Camera_Rotate_Toggle。 
  • Action Type设为Button
  • 单击绑定部分,把Path设为Right Button [Mouse]

最后,我们要设置Camera_Zoom动作和绑定:

  • 添加新动作,命名为Camera_Zoom
  • Action Type设为Value,Control Type设为Vector 2
  • 单击绑定部分,把Path设为Scroll [Mouse]

点击Save Asset保存改动。我们的导图画面如下图所示。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity 3d水的资源包_06

设置并移动摄像机

我们会使用两个游戏对象:CameraRig和Main Camera对象

  • 在场景中,创建空白游戏对象,命名为CameraRig
  • Main Camera对象设为CameraRig对象的子对象
  • 创建新脚本,命名为CameraController,把该脚本添加到CameraRig游戏对象。

CameraRig对象的作用是处理在场景中的移动和旋转。通过把这项功能作为单独的游戏对象来使用,我们可以随意在正向轴或右轴(Forward/Right axis)上移动,不必担心摄像机朝着哪个方向。

Main Camera对象会在开始时使用自定义属性来配置,确保它在世界空间中朝着正确方向。该对象也会处理缩放过程。

由于摄像机将是可配置的,因此我们首先定义可以在检视窗口设置的变量。

public class CameraController : MonoBehaviour

{
   [Header("Configurable Properties")]
   [Tooltip("This is the Y offset of our focal point. 0 Means we're looking at the ground.")]    public float LookOffset;
   [Tooltip("The angle that we want the camera to be at.")]    public float CameraAngle;
   [Tooltip("The default amount the player is zoomed into the game world.")]    public float DefaultZoom;
   [Tooltip("The most a player can zoom in to the game world.")]    public float ZoomMax;
   [Tooltip("The furthest point a player can zoom back from the game world.")]    public float ZoomMin;
   [Tooltip("How fast the camera rotates")]    public float RotationSpeed;
}

我们将把摄像机角度设为45度,在地上1米的位置进行观察,并且把缩放大小限制在2-10米。检视窗口中可以设置的所有属性如下图所示。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity更改摄影机可以拍摄的图_07

接下来,基于设定的属性,配置摄像机的起始点。添加以下全局变量和Start()方法到脚本中。

    //摄像机专用变量

    private Camera _actualCamera;

    private Vector3 _cameraPositionTarget;

    void Start()

    {

        //存储对Camera Rig的引用

        _actualCamera = GetComponentInChildren();

        //基于CameraAngle属性设置摄像机的旋转

        _actualCamera.transform.rotation = Quaternion.AngleAxis(CameraAngle, Vector3.right);

        //基于Look Offset、Camera Angle和Default Zoom属性,设置摄像机的位置。这会确保我们观察正确的焦点。

        _cameraPositionTarget = (Vector3.up * LookOffset) + (Quaternion.AngleAxis(CameraAngle, Vector3.right) * Vector3.back) * DefaultZoom;

        _actualCamera.transform.position = _cameraPositionTarget;

    }

最好存储Main Camera游戏对象的引用,而不是调用Camera.main。在Unity没有存储Main Camera对象的引用时,直接调用Camera.main会产生明显的性能影响,并在每次调用时遍历场景层级和组件。

添加移动行为

添加移动行为到摄像机时,我们需要多个全局变量,LateUpdate()中的调用和新的OnMove()方法

    //移动变量

    private const float InternalMoveTargetSpeed = 8;

    private const float InternalMoveSpeed = 4;

    private Vector3 _moveTarget;

    private Vector3 _moveDirection;

    ///

    /// 基于玩家提供的输入,设置移动方向。

    ///

    public void OnMove(InputAction.CallbackContext context)

    {

        //读取输入系统发送的输入数值。

        Vector2 value = context.ReadValue();

        //把数值存为Vector3类型,确保在Z轴上移动Y输入。

        _moveDirection = new Vector3(value.x, 0, value.y);

        //增加摄像机的新移动目标位置。

        _moveTarget += (transform.forward * _moveDirection.z + transform.right * _moveDirection.x) * Time.fixedDeltaTime * InternalMoveTargetSpeed;

    }

    private void LateUpdate()

    {

        //把摄像机插补到新的移动目标位置。

        transform.position = Vector3.Lerp(transform.position, _moveTarget, Time.deltaTime * InternalMoveSpeed);

    }

OnMove()会通过调用context.ReadValue()来存储玩家输入数值。由于在使用Vector 2合成绑定,根据不同输入,我们会看到相应的X值和Y值:

  • Up: 0, 1
  • Down: 0, -1
  • Right: 1, 0
  • Left: -1, 0

将输入系统关联到代码

有了初始代码后,我们要进行测试运行,查看它的使用效果。为此,我们需要告诉输入系统什么时候发送动作。

我们添加Player Input组件到场景的游戏对象:

  • 创建新游戏对象,命名为GameManager
  • 单击Add Component按钮,搜索Player Input组件。
  • 设置以下属性:
    Actions:设为刚刚配置的PlayerInputMapping资源。 
    Default Map:设为Player。
    Behavior:设为Invoke Unity Events。 
  • 展开Events和Player部分
  • Camera_Move事件下,引用CameraRig对象,把事件设为CameraController.OnMove()。 

unity更改摄影机可以拍摄的图 unity添加摄像机_unity更改摄影机可以拍摄的图_08

我们现在可以进入运行模式,然后移动摄像机。

虽然我们使用的是Invoke Unity Events,即调用Unity事件通知行为,但也要了解不同选项及其作用:

  • Send Messages(发送信息):该选项会发送信息到该对象上的所有脚本。
  • Broadcast Messages(广播信息):除了把输入信息发送到同一对象上的组件外,该选项还会把信息发送到子对象层级。
  • Invoke Unity Events(调用Unity事件):该选项会为每种类型的信息调用UnityEvent。UI可用于设置回调方法。
  • Invoke C Sharp Events(调用C#事件):该选项类似Invoke Unity Events,但是会调用C#事件,这些事件必须通过脚本的回调来注册。

了解不同事件类型及其设置方式:

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Components.html

修复摄像机移动

我们还未实现想要的行为,玩家应该能够按住按键,观察摄像机朝着相应方向持续移动的过程。

输入系统只会在按键按下时发送一次事件,输入系统没有简单的方法来监视按住按键的行为,因此我们需要自己解决该问题。

输入绑定有交互的概念,其中一项交互叫“Hold”。这项交互的作用是在按住按键的特定持续时间后触发动作,而在按住按钮时,它不会持续触发动作。

了解交互的更多内容:

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Interactions.html#predefined-interactions

这个问题的解决方法很简单,我们只要把最后一行代码从OnMove()移动到FixedUpdate()中

我们的代码如下所示:

    public void OnMove(InputAction.CallbackContext context)

    {

        //读取输入系统发送的输入数值。

        Vector2 value = context.ReadValue();

        //把数值存为Vector3类型,确保在Z轴上移动Y输入。

        _moveDirection = new Vector3(value.x, 0, value.y);

    }

    private void FixedUpdate()

    {

        //根据移动方向设置移动目标位置,该操作必须在此完成,因为输入系统没有逻辑来计算按住输入按键的事件。

        _moveTarget += (transform.forward * _moveDirection.z + transform.right * _moveDirection.x) * Time.fixedDeltaTime * InternalMoveTargetSpeed;

    }

进入运行模式后,我们的摄像机移动过程变得非常流畅,而且可以很好地处理方向变化,如下图所示。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity 3d水的资源包_09

添加缩放行为

添加缩放功能时,我们需要调整代码,使代码更简洁。这是因为摄像机需要能够根据当前缩放值,重新计算新的Y值和Z值。

首先,我们添加下列全局变量和UpdateCameraTarget()方法

    //缩放变量

    private float _currentZoomAmount;

    public float CurrentZoom

    {

        get => _currentZoomAmount;

        private set

        {

            _currentZoomAmount = value;

            UpdateCameraTarget();

        }

    }

    private float _internalZoomSpeed = 4;

    ///

    /// 根据多个属性计算新的位置

    ///

    private void UpdateCameraTarget()

    {

        _cameraPositionTarget = (Vector3.up * LookOffset) + (Quaternion.AngleAxis(CameraAngle, Vector3.right) * Vector3.back) * _currentZoomAmount;

    }

我们可以更新Start()方法,把CurrentZoom设为DefaultZoom的数值,而不是让脚本计算数值,代码如下所示。

    void Start()

    {

        //存储对Camera Rig的引用

        _actualCamera = GetComponentInChildren();

        //基于CameraAngle属性设置摄像机的旋转。

        _actualCamera.transform.rotation = Quaternion.AngleAxis(CameraAngle, Vector3.right);

        //基于Look Offset、Camera Angle和Default Zoom属性,设置摄像机的位置。这会确保我们观察正确的焦点。

        CurrentZoom = DefaultZoom;

        _actualCamera.transform.position = _cameraPositionTarget;

    }

接下来,添加新的OnZoom()方法,更新LateUpdate()方法,使其基于新的缩放系数来移动_actualCamera的本地位置。

        ///

        /// 设置缩小和放大的逻辑。限制为最小值和最大值。

        ///

        ///

        public void OnZoom(InputAction.CallbackContext context)

        {

            if (context.phase != InputActionPhase.Performed)

            {

                return;

            }

            // 根据滚动方向调整当前缩放值,该值的大小限制为最大值和最小值之间。

            CurrentZoom = Mathf.Clamp(_currentZoomAmount - context.ReadValue().y, ZoomMax, ZoomMin);

        }

    private void LateUpdate()

    {

        //把摄像机插补到新的移动目标位置。

        transform.position = Vector3.Lerp(transform.position, _moveTarget, Time.deltaTime * InternalMoveSpeed);

        //根据新的缩放系数,移动_actualCamera的本地位置。

        _actualCamera.transform.localPosition = Vector3.Lerp(_actualCamera.transform.localPosition, _cameraPositionTarget, Time.deltaTime * _internalZoomSpeed);

    }       

根据输入阶段的不同,事件的多个实例会以不同的状态发送。对于OnZoom(),如果处于Performed状态,我们只想处理读取数值的部分,因为这会确保我们不会得到扰乱逻辑的数值。如果没有这项检查,我们会在Started状态和Canceled状态处理两个以上的调用。

了解输入动作状态的更多内容:

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/UnityEngine.InputSystem.InputActionPhase.html

现在我们要进行测试。通过关联Move事件的方法,把逻辑关联到输入系统:

  • Camera_Zoom事件下,引用CameraController游戏对象,把事件设为CameraController.OnZoom
  • 运行项目,然后滚动鼠标滚轮。

我们发现,缩放值会在设置的最大缩放值和最小缩放值之间切换,而不是逐渐递增。这是因为滚动鼠标滚轮时,发送的输入值太大,每次滚动发出的Vector 2值都会是(0, 120)或(0, -120)。

为了实现缓慢地逐渐递增,我们的逻辑需要把数值归一化为(0, 1)或(0, -1)。为此,我们进行以下操作:

  • 打开PlayerInputMapping资源,选中Camera_Zoom动作下的Scroll [Mouse]绑定
  • 在属性面板,单击Processors部分下的+按钮,选择Normalize Vector 2
  • 保存文件

我们有许多实用的处理器可以应用到动作、控制和绑定,包括:为手柄输入指定空白区域数值。

了解更多不同事件类型及设置方法的内容:

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Processors.html

unity更改摄影机可以拍摄的图 unity添加摄像机_unity 输入管理器怎么添加shift_10

 如下图所示,现在我们会看到流畅的滚动行为。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity 3d水的资源包_11

添加旋转行为

摄像机的旋转过程由两步组成。首先,我们需要知道玩家是否在让摄像机旋转。这一步会监视玩家是否按下鼠标右键。如果按下了鼠标右键,我们会获取鼠标位置,告诉游戏应该朝什么方向旋转。

监视按钮操作非常简单,我们只需要读取某个浮点值是0(关闭)还是1(启用)即可。为此,我们要给脚本添加以下全局变量和OnRotateToggle()方法。

    //旋转变量

    private bool _rightMouseDown = false;

    private const float InternalRotationSpeed = 4;

    private Quaternion _rotationTarget;

    private Vector2 _mouseDelta;

    ///

    /// 设置玩家是否按下鼠标右键。

    ///

    ///

    public void OnRotateToggle(InputAction.CallbackContext context)

    {

        _rightMouseDown = context.ReadValue() == 1;

    }

给脚本添加OnRotate()方法,该方法会在按下鼠标右键时,旋转摄像机。

    ///

    /// 如果玩家按下鼠标右键并移动鼠标,则设置旋转目标的Quaternion类数值。

    ///

    ///

    public void OnRotate(InputAction.CallbackContext context)

    {

        // 如果按下鼠标右键,我们会读取鼠标的_mouseDelta值。如果没有按下,我们会清零该值。

        // 请注意:清零_mouseDelta值会避免在玩家朝某个方向快速移动鼠标时,发生“Death Spin”情况。

        _mouseDelta = _rightMouseDown ? context.ReadValue() : Vector2.zero;

        _rotationTarget *= Quaternion.AngleAxis(_mouseDelta.x * Time.deltaTime * RotationSpeed, Vector3.up);

    }

最后,给LateUpdate()方法和Start()方法添加逻辑,让它们旋转摄像机。

    void Start()

    {

         //存储对Camera Rig的引用

        _actualCamera = GetComponentInChildren();

        //基于CameraAngle属性设置摄像机的旋转。

        _actualCamera.transform.rotation = Quaternion.AngleAxis(CameraAngle, Vector3.right);

        //基于Look Offset、Camera Angle和Default Zoom属性,设置摄像机的位置。这会确保我们观察正确的焦点。

        CurrentZoom = DefaultZoom;

        _actualCamera.transform.position = _cameraPositionTarget;

        //设置初始旋转值。

        _rotationTarget = transform.rotation;

    }

    private void LateUpdate()

    {

        //把Camera Rig插值到新的移动目标位置

        transform.position = Vector3.Lerp(transform.position, _moveTarget, Time.deltaTime * InternalMoveSpeed);

        //根据新的缩放系数,移动_actualCamera的本地位置。

        _actualCamera.transform.localPosition = Vector3.Lerp(_actualCamera.transform.localPosition, _cameraPositionTarget, Time.deltaTime * _internalZoomSpeed);

       //根据新的目标,对Camera Rig的旋转进行球面插值。

        transform.rotation = Quaternion.Slerp(transform.rotation, _rotationTarget, Time.deltaTime * InternalRotationSpeed);

    }

根据新的方法,把逻辑关联到输入系统:

  • Camera_Rotate事件下,引用CameraController游戏对象,把事件设为CameraController.OnRotate
  • Camera_Rotate_Toggle事件下,引用CameraController游戏对象,把事件设为CameraController.OnRotateToggle
  • 运行项目,按住鼠标右键并移动鼠标。

虽然此时看似正常运行,但我们为了更新旋转状态使用了过多的不必要调用。为了更好了解情况,我们要知道OnRotate()输入事件每帧会进行多少次调用。

我们会加入一些临时代码来展示次数。

// 创建新的全局变量。

private float _eventCounter;

// 添加以下代码到OnRotate方法的结尾。

// 该代码会在每次事件调用时,递增eventCounter值。

eventCounter += _rightMouseDown ? 1 : 0;

// 添加下面代码到LateUpdate方法的结尾。

// 由于LateUpdate方法会在每帧运行一次,因此它会记录事件在每帧调用的总次数,然后在下次检查时清空结果。

Debug.Log(eventCounter);

eventCounter = 0;

在运行代码并旋转摄像机时,我们可以看到每一帧都多次触发OnRotate()事件。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity 输入管理器怎么添加shift_12

此外在一帧中,随着每次事件触发而发送的鼠标增量会逐渐增长。考虑到这一点,我们最好每帧应用一次最终增量值。

为此,把_rotationTarget *= Quaternion.AngleAxis(_mouseDelta.x * Time.deltaTime * RotationSpeed, Vector3.up); 代码从OnRotate()方法移动到LateUpdate()方法中。

 private void LateUpdate()

        {          

            //把Camera Rig插值到新的移动目标位置

            transform.position = Vector3.Lerp(transform.position, _moveTarget, Time.deltaTime * InternalMoveSpeed);

            //根据新的缩放系数,移动_actualCamera的本地位置。

            _actualCamera.transform.localPosition = Vector3.Lerp(_actualCamera.transform.localPosition, _cameraPositionTarget, Time.deltaTime * _internalZoomSpeed);

            //根据鼠标增量位置和旋转速度,设置目标旋转。

            _rotationTarget *= Quaternion.AngleAxis(_mouseDelta.x * Time.deltaTime * RotationSpeed, Vector3.up);

            //根据新的目标,对Camera Rig的旋转进行球面插值。

            transform.rotation = Quaternion.Slerp(transform.rotation, _rotationTarget, Time.deltaTime * InternalRotationSpeed);

        }

大功告成,我们现在拥有了功能完善的摄像机,它能够在场景中通过使用新一代输入系统进行旋转、缩放和移动。我们可以在检视窗口通过调整RotationSpeed变量来增加速度。

unity更改摄影机可以拍摄的图 unity添加摄像机_unity 3d水的资源包_13

小结

通过本文的学习,我们希望开发者能够熟练掌握好Unity新一代的输入系统。如果你有任何反馈,请访问Unity官方论坛:

https://forum.unity.com/forums/new-input-system.103

下载Unity Connect APP,请点击此处。 观看更多Unity官方精彩视频,请关注“Unity官方”B站账户。

你可以访问Unity答疑专区留下你的问题,Unity社区和官方团队帮你解答:

Connect.unity.com/g/discussion