Notifications
Article
使用Unity新一代输入系统实现可配置摄像机
Updated 23 days ago
1.3 K
6
本文,我们将使用Unity 2019.2开发可以移动、缩放和旋转的可配置摄像机。
我们已经介绍过Unity新一代的输入系统。本文,我们将使用Unity 2019.2开发可以移动、缩放和旋转的可配置摄像机。这种设计方法适用于不需要额外附带一个第一或第三人称摄像机,而是可以让游戏视角在场景自由移动的游戏。
摄像机的配置功能包括:
  • 摄像机角度
  • 最大和最小缩放设置
  • 默认缩放设置
  • 线偏移(设定摄像机在Y轴上的观察位置)
  • 旋转速度

学习目标

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

学习准备

你需要有使用Unity的基础知识。本文将不会介绍基础知识,包括:游戏对象和组件的概念、何时调用Start方法等。
本文中的项目使用了Low Poly: Free Pack资源。
你可以复制本文的代码库,获得学习时用到的初始项目: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。

设置新一代输入系统

新一代输入系统比原有系统更为复杂。虽然初次学习难度更高,但会带来很好的回报。新系统更加强大稳定,在正确设置时,使用新系统所需的工作量会更少。
首先,我们要创建Input Controls输入控制资源,在项目窗口单击右键:
  • 选择Create > Input Actions。
  • 将新文件命名为PlayerInputMapping。
  • 双击打开文件的编辑窗口。
配置输入时,有四个概念需要了解:
  • 控制方案(Control Scheme):用于设置必须满足的设备要求,从而使输入绑定变得可用。这是可选设置,我们可以把它保留原样,即不设定要求。
  • 动作导图(Action Maps):这是可以批量启用或禁用的动作组。
  • 动作(Action):能够分组到特定动作下的一组输入绑定,例如:“开火”或“移动”等动作。
  • 输入绑定(Input Bindings):用于指定要监视的设备输入,例如:手柄上的按键、鼠标按钮或键盘按键。
例如:在把动作设为多个输入绑定映射时,我们使用了“开火”动作,该动作会关联到手柄的特定按键,如果是键盘鼠标的设置方案,则会关联到鼠标右键。
动作导图、动作和输入绑定都有各自的属性。我们将在本文中详细介绍这些属性。
定义控制方式
输入方案将设计用于带有键盘和鼠标的设备,但如果需要,我们也可以轻松扩展到其它输入方式。总的而言,我们会有一个控制方案、一个动作导图、四个动作和五个输入绑定。
我们的设置如下图所示。
虽然上图看起来复杂,但创建该布局的方法其实很简单。打开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设为对应的按键。
对方向键执行相同的操作。添加新的2D Vector Composite,命名为Arrows。设置每项映射到对应的方向键,现在我们会看到下图的设置。
我们现在需要设置剩余的动作和绑定:
  • 添加新动作,命名为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保存改动。我们的导图画面如下图所示。

设置并移动摄像机

我们会使用两个游戏对象: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米。检视窗口中可以设置的所有属性如下图所示。
接下来,基于设定的属性,配置摄像机的起始点。添加以下全局变量和Start()方法到脚本中。
//摄像机专用变量 private Camera _actualCamera; private Vector3 _cameraPositionTarget; void Start() { //存储对Camera Rig的引用 _actualCamera = GetComponentInChildren<Camera>(); //基于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; /// <summary> /// 基于玩家提供的输入,设置移动方向。 /// </summary> public void OnMove(InputAction.CallbackContext context) { //读取输入系统发送的输入数值。 Vector2 value = context.ReadValue<Vector2>(); //把数值存为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<Vector2>()来存储玩家输入数值。由于在使用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()。
我们现在可以进入运行模式,然后移动摄像机。
虽然我们使用的是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<Vector2>(); //把数值存为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; }
进入运行模式后,我们的摄像机移动过程变得非常流畅,而且可以很好地处理方向变化,如下图所示。

添加缩放行为

添加缩放功能时,我们需要调整代码,使代码更简洁。这是因为摄像机需要能够根据当前缩放值,重新计算新的Y值和Z值。
首先,我们添加下列全局变量和UpdateCameraTarget()方法。
//缩放变量 private float _currentZoomAmount; public float CurrentZoom { get => _currentZoomAmount; private set { _currentZoomAmount = value; UpdateCameraTarget(); } } private float _internalZoomSpeed = 4; /// <summary> /// 根据多个属性计算新的位置 /// </summary> private void UpdateCameraTarget() { _cameraPositionTarget = (Vector3.up * LookOffset) + (Quaternion.AngleAxis(CameraAngle, Vector3.right) * Vector3.back) * _currentZoomAmount; }
我们可以更新Start()方法,把CurrentZoom设为DefaultZoom的数值,而不是让脚本计算数值,代码如下所示。
void Start() { //存储对Camera Rig的引用 _actualCamera = GetComponentInChildren<Camera>(); //基于CameraAngle属性设置摄像机的旋转。 _actualCamera.transform.rotation = Quaternion.AngleAxis(CameraAngle, Vector3.right); //基于Look Offset、Camera Angle和Default Zoom属性,设置摄像机的位置。这会确保我们观察正确的焦点。 CurrentZoom = DefaultZoom; _actualCamera.transform.position = _cameraPositionTarget; }
接下来,添加新的OnZoom()方法,更新LateUpdate()方法,使其基于新的缩放系数来移动_actualCamera的本地位置。
/// <summary> /// 设置缩小和放大的逻辑。限制为最小值和最大值。 /// </summary> /// <param name="context"></param> public void OnZoom(InputAction.CallbackContext context) { if (context.phase != InputActionPhase.Performed) { return; } // 根据滚动方向调整当前缩放值,该值的大小限制为最大值和最小值之间。 CurrentZoom = Mathf.Clamp(_currentZoomAmount - context.ReadValue<Vector2>().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
如下图所示,现在我们会看到流畅的滚动行为。

添加旋转行为

摄像机的旋转过程由两步组成。首先,我们需要知道玩家是否在让摄像机旋转。这一步会监视玩家是否按下鼠标右键。如果按下了鼠标右键,我们会获取鼠标位置,告诉游戏应该朝什么方向旋转。
监视按钮操作非常简单,我们只需要读取某个浮点值是0(关闭)还是1(启用)即可。为此,我们要给脚本添加以下全局变量和OnRotateToggle()方法。
//旋转变量 private bool _rightMouseDown = false; private const float InternalRotationSpeed = 4; private Quaternion _rotationTarget; private Vector2 _mouseDelta; /// <summary> /// 设置玩家是否按下鼠标右键。 /// </summary> /// <param name="context"></param> public void OnRotateToggle(InputAction.CallbackContext context) { _rightMouseDown = context.ReadValue<float>() == 1; }
给脚本添加OnRotate()方法,该方法会在按下鼠标右键时,旋转摄像机。
/// <summary> /// 如果玩家按下鼠标右键并移动鼠标,则设置旋转目标的Quaternion类数值。 /// </summary> /// <param name="context"></param> public void OnRotate(InputAction.CallbackContext context) { // 如果按下鼠标右键,我们会读取鼠标的_mouseDelta值。如果没有按下,我们会清零该值。 // 请注意:清零_mouseDelta值会避免在玩家朝某个方向快速移动鼠标时,发生“Death Spin”情况。 _mouseDelta = _rightMouseDown ? context.ReadValue<Vector2>() : Vector2.zero; _rotationTarget *= Quaternion.AngleAxis(_mouseDelta.x * Time.deltaTime * RotationSpeed, Vector3.up); }
最后,给LateUpdate()方法和Start()方法添加逻辑,让它们旋转摄像机。
void Start() { //存储对Camera Rig的引用 _actualCamera = GetComponentInChildren<Camera>(); //基于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()事件。
此外在一帧中,随着每次事件触发而发送的鼠标增量会逐渐增长。考虑到这一点,我们最好每帧应用一次最终增量值。
为此,把_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官方论坛:https://forum.unity.com/forums/new-input-system.103
更多Unity精彩内容,请搜索“Unity官方平台”关注Unity官方微信公众号。
开发过程中遇到问题?请在App”群聊“ - “技术交流”中提问。
Tags:
Unity开发者原创
17
Comments
Tonycenter
16 days ago
unity版本更新太快了。。。。
0
h
hua2
Staff
22 days ago
title
收藏🙂
1
小木
22 days ago
收藏
0
夜翼
23 days ago
看见了英文我就两个字,卧槽😱😱😱😱😱
1
闫学峰
23 days ago
刚好回家
0