在 Unity 中掌握 2D 相机:游戏开发者教程

已发表: 2022-03-11

对于开发者来说,相机是游戏开发过程的基石之一。 从仅在国际象棋应用程序中显示您的游戏视图到在 3D AAA 游戏中巧妙地引导摄像机移动以获得电影效果,摄像机基本上用于任何制作过的视频游戏,甚至在实际被称为“摄像机”之前。

在这篇文章中,我将解释如何为 2D 游戏设计一个摄像头系统,我还将解释如何在最流行的游戏引擎之一 Unity 中实现它的一些要点。

从 2D 到 2.5D:可扩展的相机系统

我们将共同设计的相机系统是模块化和可扩展的。 它有一个基本核心,由几个组件组成,这些组件将确保基本功能,然后根据手头的情况可以选择使用的各种组件/效果。

我们这里构建的相机系统是针对2D平台游戏的,但可以很容易地扩展到其他类型的2D游戏、2.5D游戏甚至3D游戏。

在 Unity 中掌握 2D 相机:游戏开发者教程

在 Unity 中掌握 2D 相机:游戏开发者教程
鸣叫

我将把相机功能分为两大类:相机跟踪和相机效果。

追踪

我们将在这里进行的大部分摄像机移动将基于跟踪。 这是一个对象(在本例中为相机)在游戏场景中移动时跟踪其他对象的能力。 我们将实施的跟踪类型将解决 2D 平台游戏中遇到的一些常见场景,但可以通过新类型的跟踪来扩展您可能拥有的其他特定场景。

效果

我们将实现一些很酷的效果,如相机抖动、相机变焦、相机淡入淡出和颜色叠加。

入门

在 Unity 中创建一个新的 2D 项目并导入标准资源,尤其是 RobotBoy 角色。 接下来,创建一个地面盒并添加一个角色实例。 您应该能够在当前场景中与您的角色一起行走和跳跃。 确保相机设置为正交模式(默认设置为透视)。

跟踪目标

以下脚本将为我们的主摄像头添加基本跟踪行为。 该脚本必须作为组件附加到场景中的主摄像机,并且它公开了一个用于指定要跟踪的目标对象的字段。 然后脚本确保相机的 x 和 y 坐标与其跟踪的对象相同。 所有这些处理都是在更新步骤中完成的。

 [SerializeField] protected Transform trackingTarget; // ... void Update() { transform.position = new Vector3(trackingTarget.position.x, trackingTarget.position.y, transform.position.z); }

将 RobotBoy 角色从场景层次结构中拖到我们以下行为所显示的“跟踪目标”字段上,以启用对主角的跟踪。

添加偏移量

一切都很好,但我们可以直接看到一个限制:角色总是在我们场景的中心。 我们可以看到角色背后的很多东西,这通常是我们不感兴趣的东西,而我们看到的角色前面的东西太少了,这可能对游戏玩法不利。

为了解决这个问题,我们在脚本中添加了一些新字段,允许将相机定位在距目标偏移的位置。

 [SerializeField] float xOffset; [SerializeField] float yOffset; // ... void Update() { transform.position = new Vector3(trackingTarget.position.x + xOffset, trackingTarget.position.y + yOffset, transform.position.z); }

您可以在下面看到两个新字段的可能配置:

平滑事物

摄像机的移动非常僵硬,并且还会因不断感知环境的移动而使某些玩家感到头晕。 为了解决这个问题,我们将使用线性插值在摄像机跟踪中添加一些延迟,并使用一个新字段来控制角色开始改变其位置后摄像机进入到位的速度。

 [SerializeField] protected float followSpeed; // ... protected override void Update() { float xTarget = trackingTarget.position.x + xOffset; float yTarget = trackingTarget.position.y + yOffset; float xNew = Mathf.Lerp(transform.position.x, xTarget, Time.deltaTime * followSpeed); float yNew = Mathf.Lerp(transform.position.y, yTarget, Time.deltaTime * followSpeed); transform.position = new Vector3(xNew, yNew, transform.position.z); } 

停止头晕:轴锁定

由于您的大脑不喜欢一直看着镜头随着角色上下移动,因此我们引入了轴锁定。 这意味着我们可以将跟踪限制在一个轴上。 然后我们将我们的跟踪代码分离为独立于轴的跟踪,我们将考虑新的锁定标志。

 [SerializeField] protected bool isXLocked = false; [SerializeField] protected bool isYLocked = false; // ... float xNew = transform.position.x; if (!isXLocked) { xNew = Mathf.Lerp(transform.position.x, xTarget, Time.deltaTime * followSpeed); } float yNew = transform.position.y; if (!isYLocked) { yNew = Mathf.Lerp(transform.position.y, yTarget, Time.deltaTime * followSpeed); } 

车道系统

现在相机只水平跟踪玩家,我们被限制在一个屏幕的高度。 如果角色爬了一些梯子或跳得比这更高,我们必须跟随。 我们这样做的方式是使用车道系统。

想象以下场景:

角色最初位于下车道。 当角色保持在该车道的边界内时,相机将仅在我们可以设置的车道特定高度偏移上水平移动。

一旦角色进入另一条车道,摄像机将过渡到该车道并继续从那里水平移动,直到发生下一个车道变换。

必须注意车道设计,以防止在跳跃等动作期间快速切换车道,这会给玩家造成混乱。 仅当玩家的角色要在其上停留一段时间时才应更改车道。

车道的关卡可以根据设计师的特定需求在整个游戏关卡中发生变化,或者可以完全中断,而另一个摄像头跟踪系统可以代替它们。 因此,我们需要一些限制器来指定车道区域。

执行

一种可能的实现是将车道添加为场景中的简单对象。 我们将在上面的跟踪脚本中使用它们的 Y 位置坐标和 Y 偏移来实现系统。 因此,它们在 X 和 Z 坐标上的定位无关紧要。

将 LaneSystem 类与跟踪类一起添加到相机,并将车道对象分配给提供的数组。 还将玩家角色分配给参考字段。 由于参考位于一条车道和另一条车道之间,因此两者中较低的一个将用于定位相机。

LaneSystem 类负责根据参考位置在车道之间移动相机。 此处再次使用followSpeed进行位置插值,防止车道切换过于突然:

 [SerializeField] Transform reference; [SerializeField] List<Transform> lanes; [SerializeField] float followSpeed = 5f; // ... void Update() { float targetYCoord = transform.position.y; if (lanes.Count > 1) { int i = 0; for (i = 0; i < lanes.Count - 1; ++i) { if ((reference.position.y > lanes[i].position.y) && (reference.position.y <= lanes[i + 1].position.y)) { targetYCoord = lanes[i].position.y; break; } } if (i == lanes.Count - 1) targetYCoord = lanes[lanes.Count - 1].position.y; } else { targetYCoord = lanes[0].position.y; } float yCoord = Mathf.Lerp(transform.position.y, targetYCoord, Time.deltaTime * followSpeed); transform.position = new Vector3(transform.position.x, yCoord, transform.position.z); }

这个实现不是所见即所得的,而是留给读者作为练习。

锁节点系统

让摄像头在车道上移动很棒,但有时我们需要将摄像头锁定在游戏场景中的某个兴趣点 (POI) 上。

这可以通过在场景中配置此类 POI 并将触发对撞机附加到它们来实现。 每当角色进入触发对撞机时,我们都会移动相机并停留在 POI 上。 随着角色移动然后离开 POI 的触发对撞机,我们回到另一种类型的跟踪,通常是标准的跟随行为。

摄像机跟踪到锁定节点和返回的切换可以通过简单的开关或堆栈系统完成,在该堆栈系统上推送和弹出跟踪模式。

执行

为了配置一个锁定节点,只需创建一个对象(可以是空的或像下面的屏幕截图中那样,一个精灵)并将一个大的 Circle Collider 2D 组件附加到它上,这样它就可以标记玩家在相机将要进入的区域聚焦节点。 您可以选择任何类型的对撞机,我在这里选择 Circle 作为示例。 还要创建一个您可以轻松检查的标签,例如“CameraNode”并将其分配给该对象。

将以下属性添加到相机上的跟踪脚本中:

 public Transform TrackingTarget { get { return trackingTarget; } set { trackingTarget = value; } }

然后将以下脚本附加到播放器,这将允许它临时将相机的目标切换到您设置的锁定节点。 该脚本还将记住其先前的目标,以便当玩家离开触发区域时它可以返回到它。 如果需要,您可以继续将其转换为一个完整的堆栈,但出于我们的目的,因为我们不重叠多个锁节点,这将是可行的。 另外请注意,您可以调整 Circle Collider 2D 的位置,或者再次添加任何其他类型的对撞机来触发相机锁定,这只是一个示例。

 public class LockBehavior : MonoBehaviour { #region Public Fields [SerializeField] Camera camera; [SerializeField] string tag; #endregion #region Private private Transform previousTarget; private TrackingBehavior trackingBehavior; private bool isLocked = false; #endregion // Use this for initialization void Start() { trackingBehavior = camera.GetComponent<TrackingBehavior>(); } void OnTriggerEnter2D(Collider2D other) { if (other.tag == tag && !isLocked) { isLocked = true; PushTarget(other.transform); } } void OnTriggerExit2D(Collider2D other) { if (other.tag == tag && isLocked) { isLocked = false; PopTarget(); } } private void PushTarget(Transform newTarget) { previousTarget = trackingBehavior.TrackingTarget; trackingBehavior.TrackingTarget = newTarget; } private void PopTarget() { trackingBehavior.TrackingTarget = previousTarget; } } 

相机变焦

摄像机缩放可以在用户输入时执行,也可以在我们想要关注诸如 POI 或关卡中更紧凑的区域之类的东西时作为动画执行。

Unity 3D 中的 2D 相机缩放可以通过操纵相机的 orthographicSize 来实现。 将下一个脚本作为组件附加到相机并使用 SetZoom 方法更改缩放因子将产生所需的效果。 1.0 表示不缩放,0.5 表示放大两次,2 表示缩小两次,以此类推。

 [SerializeField] float zoomFactor = 1.0f; [SerializeField] float zoomSpeed = 5.0f; private float originalSize = 0f; private Camera thisCamera; // Use this for initialization void Start() { thisCamera = GetComponent<Camera>(); originalSize = thisCamera.orthographicSize; } // Update is called once per frame void Update() { float targetSize = originalSize * zoomFactor; if (targetSize != thisCamera.orthographicSize) { thisCamera.orthographicSize = Mathf.Lerp(thisCamera.orthographicSize, targetSize, Time.deltaTime * zoomSpeed); } } void SetZoom(float zoomFactor) { this.zoomFactor = zoomFactor; }

屏幕抖动

每当我们需要在游戏中显示地震、爆炸或任何其他效果时,相机抖动效果都会派上用场。

GitHub 上提供了如何执行此操作的示例实现:gist.github.com/ftvs/5822103。 实现相当简单。 与我们迄今为止介绍的其他效果不同,它依赖于一点随机性。

淡入淡出和叠加

当我们的关卡开始或结束时,淡入或淡出效果很好。 我们可以通过在整个屏幕上延伸的面板中添加不可交互的 UI 纹理来实现这一点。 最初是透明的,我们可以用任何颜色和不透明度填充它,或者对其进行动画处理以达到我们想要的效果。

这是该配置的示例,请注意 UI 面板对象被分配给主相机对象的“相机叠加”子对象。 Camera Overlay 公开了一个名为 Overlay 的脚本,该脚本具有以下功能:

 [SerializeField] Image overlay; // ... public void SetOverlayColor(Color color) { overlay.color = color; } 

为了获得淡入效果,通过在下一个脚本中使用 SetOverlayColor 设置的目标颜色添加插值来更改覆盖脚本,并将面板的初始颜色设置为黑色(或白色)和目标颜色到叠加层的最终颜色。 您可以将 fadeSpeed 更改为适合您需要的任何值,我认为 0.8 对于初学者来说是一个不错的选择。 fadeSpeed 的值用作时间修饰符。 1.0 意味着它将发生在多个帧上,但在 1 秒的时间范围内。 0.8 意味着它实际上需要 1/0.8 = 1.25 秒才能完成。

 public class Overlay : MonoBehaviour { #region Fields [SerializeField] Image overlay; [SerializeField] float fadeSpeed = 5f; [SerializeField] Color targetColor; #endregion void Update() { if (overlay.color != targetColor) { overlay.color = Color.Lerp(overlay.color, targetColor, Time.deltaTime * fadeSpeed); } } #region Public public void SetOverlayColor(Color color) { targetColor = color; } #endregion }

包起来

在本文中,我试图展示为您的游戏配备模块化 2D 相机系统所需的基本组件,以及设计它所需的思维定势。 当然,所有游戏都有其特定的需求,但是通过这里描述的基本跟踪和简单效果,您可以获得很长的路要走,并且还可以拥有实现自己效果的蓝图。 然后,您可以更进一步,将所有内容打包到一个可重复使用的 Unity 3D 包中,您也可以将其转移到其他项目中。

摄像头系统对于为您的玩家传达正确的氛围非常重要。 当您想到古典戏剧和电影之间的区别时,我喜欢使用一个很好的比较。 相机和胶卷本身为场景带来了如此多的可能性,以至于它最终独立发展成为一种艺术,所以如果你不打算实现另一个“乒乓”游戏,那么在任何游戏项目中,高级相机都应该是你的首选工具。 '从现在开始承担。

相关:与 MVC 的统一:如何升级您的游戏开发