《土豆荣耀》重构笔记(十)实现摄像机跟随角色移动的功能


前言

  当角色在场景中移动时,为了更好地观察角色,我们需要让摄像机跟随角色移动。又因为场景的大小一般是有限的,为了避免穿帮,我们还需要限制摄像机移动的范围。也就是说,我们需要让摄像机在可移动的范围内跟随角色进行移动。


跟随角色移动

  首先,我们在Assets\Scripts下创建一个名为Utility的文件夹,并在Utility下创建一个名为CameraFollow的C#脚本。然后我们将CameraFollow.cs添加到Main Camera上。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraFollow : MonoBehaviour {
    [Tooltip("水平方向上最大偏移量")]
    public float HorizontalMargin = 2f;
    [Tooltip("竖直方向上最大偏移量")]
    public float VerticalMargin = 2f;
    [Tooltip("水平方向上跟随角色的速度")]
    public float HorizontalFollowSpeed = 2f;
    [Tooltip("竖直方向上跟随角色的速度")]
    public float VerticalFollowSpeed = 2f;

    // 角色Transform组件的引用
    private Transform m_Player;

    private void Awake() {
        // 获取引用
        m_Player = GameObject.FindGameObjectWithTag("Player").transform;

        if(m_Player == null) {
            Debug.LogError("请添加Tag为Player的GameObject");
        }
    }

    private void LateUpdate() {
        TrackPlayer();
    }

    // 跟随玩家
    private void TrackPlayer() {
        float targetX = transform.position.x;
        float targetY = transform.position.y;

        // 如果超出了偏移量,计算摄像机跟随后的位置
        if (CheckHorizontalMargin()) {
            targetX = Mathf.Lerp(transform.position.x, m_Player.position.x, HorizontalFollowSpeed * Time.deltaTime);
        }

        if (CheckVerticalMargin()) {
            targetY = Mathf.Lerp(transform.position.y, m_Player.position.y, VerticalFollowSpeed * Time.deltaTime);
        }

        // 更新摄像机的位置
        transform.position = new Vector3(targetX, targetY, transform.position.z);
    }

    // 判断水平方向上是否超出了最大偏移量
    private bool CheckHorizontalMargin() {
        return Mathf.Abs(transform.position.x - m_Player.position.x) > HorizontalMargin;
    }

    // 判断竖直方向上是否超出了最大偏移量
    private bool CheckVerticalMargin() {
        return Mathf.Abs(transform.position.y - m_Player.position.y) > VerticalMargin;
    }
}

代码说明:
  GameObject.FindGameObjectWithTag("Player")表示获取场景中Tag为Player的GameObject的引用,如果场景中没有Tag为Player的GameObject,那么它会返回null。这里,我们使用GameObject.FindGameObjectWithTag("Player").transform来获取角色的Transform组件的引用。

  编辑完CameraFollow.cs之后,我们设置Player的Tag为Player,并将修改Apply到Player的Prefab上。接着,我们运行游戏,可以看到摄像机已经可以跟随角色进行移动。

修改Player的Tag


获取摄像机的视口大小

  在Unity里,正交摄像机(Orthographic Camera)的视口高度是固定的,大小为Camera的Size属性的值 * 2个Unity基本单位。而视口的宽度则是根据输出画面的高度分辨率和宽度分辨率的比例,乘上视口的高度得到的。在CameraFollow.cs加入以下代码。

public class CameraFollow : MonoBehaviour {
    ...


    private void Start() {
        Camera camera = this.GetComponent<Camera>();

        // 获取视口右上角对应的世界坐标
        Vector3 cornerPos = camera.ViewportToWorldPoint(new Vector3(1f, 1f, Mathf.Abs(transform.position.z)));

        // 此时,摄像机的世界坐标就是视口中心点的世界坐标
        // 计算摄像机视口的宽度
        float cameraWidth = 2 * (cornerPos.x - transform.position.x);
        // 计算视口的高度
        float cameraHeight = 2 * (cornerPos.y - transform.position.y);

        Debug.Log("Width: " + cameraWidth);
        Debug.Log("Height: " + cameraHeight);
    &#125;

    ...
&#125;

  修改完成后,运行游戏,因为此时摄像机的Size为11,设置的屏幕分辨率为1920 * 1080,所以输出的Height应为22,输出的Weight应为1920 / 1080 * 22 = 39.11,也就是我们计算得到的结果正确。

计算结果


定义摄像机的移动范围

  接下来,我们来限制摄像机的移动范围。因为摄像机的移动范围大小和场景的大小有关,为了能清晰地在游戏场景里看到我们当前编辑的边界大小,我们不使用数值来定义摄像机移动的范围,我们使用Box Collider2D组件来定义摄像机的移动范围

  首先,我们在Hierarchy中创建一个名为SceneBounds的Empty GameObject,并Reset它的Transform组件。接着,我们给SceneBounds添加一个Box Collider2D组件,并调整Box Collider2D组件的OffsetSize属性,直到Box Collider2D覆盖整个游戏场景为止。

Box Collider2D的属性为:

  • Offset: (0, 0)
  • Size: (48, 27)

  调整完毕之后,为了避免SceneBoundsBox Collider2D组件对游戏场景里的其他物体产生影响,我们需要新建一个名为Setting的Layer,将SceneBounds的Layer设置为Setting之后,在Layer Collision Matrix里设置Setting与所有其他Layer不产生任何交互。

Layer Collision Matrix


限制摄像机的移动

  最后,我们在CameraFollow.cs加入以下代码。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraFollow : MonoBehaviour &#123;
    ...
    [Tooltip("摄像机可移动的范围")]
    public BoxCollider2D Region;

    // 摄像机中心点在水平方向上可移动的范围
    private Vector2 m_HorizontalRegion;
    // 摄像机中心点在竖直方向上可移动的范围
    private Vector2 m_VerticalRegion;

    ...

    private void Start() &#123;
        ...

        // 计算Box Collider2D中心点的世界坐标
        Vector2 regionPosition = new Vector2(
            Region.transform.position.x + Region.offset.x,
            Region.transform.position.y + Region.offset.y
        );

        // 计算Box Collider2D和摄像机视口的宽度差的一半
        float halfDeltaWidth = (Region.size.x - cameraWidth) / 2;
        // 计算Box Collider2D和摄像机视口的高度差的一半
        float halfDeltaHeight = (Region.size.y - cameraHeight) / 2;

        if(halfDeltaWidth < 0) &#123;
            Debug.LogError("Box Collider2D的宽度小于摄像机视口的宽度");
        &#125;

        if(halfDeltaHeight < 0) &#123;
            Debug.LogError("Box Collider2D的高度小于摄像机视口的高度");
        &#125;

        // 计算摄像机中心点水平方向上可移动的范围
        m_HorizontalRegion = new Vector2(
            regionPosition.x - halfDeltaWidth,
            regionPosition.x + halfDeltaWidth
        );

        // 计算摄像机中心点竖直方向上可移动的范围
        m_VerticalRegion = new Vector2(
            regionPosition.y - halfDeltaHeight,
            regionPosition.y + halfDeltaHeight
        );
    &#125;

    ...

    // 跟随玩家
    private void TrackPlayer() &#123;
        ...

        targetX = Mathf.Clamp(targetX, m_HorizontalRegion.x, m_HorizontalRegion.y);
        targetY = Mathf.Clamp(targetY, m_VerticalRegion.x, m_VerticalRegion.y);

        // 更新摄像机的位置
        transform.position = new Vector3(targetX, targetY, transform.position.z);
    &#125;
&#125;

代码说明:
  Box Collider2D组件的Offset属性表示Box Collider2D中心点和Transform组件中心点的偏移量。我们想获得Box Collider2D组件的中心点,就需要在Transform组件的Position属性的值上加上Box Collider2D组件的Offset属性的值。

  编辑完CameraFollow.cs之后,我们将SceneBounds拖拽到Region属性的赋值框,运行游戏,可以看到摄像机被正确限制在Box Collider2D组件定义的范围内。

设置Region


完整代码

  本篇文章涉及到的所有完整代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraFollow : MonoBehaviour &#123;
    [Tooltip("水平方向上最大偏移量")]
    public float HorizontalMargin = 2f;
    [Tooltip("竖直方向上最大偏移量")]
    public float VerticalMargin = 2f;
    [Tooltip("水平方向上跟随角色的速度")]
    public float HorizontalFollowSpeed = 2f;
    [Tooltip("竖直方向上跟随角色的速度")]
    public float VerticalFollowSpeed = 2f;
    [Tooltip("摄像机可移动的范围")]
    public BoxCollider2D Region;

    // 摄像机中心点在水平方向上可移动的范围
    private Vector2 m_HorizontalRegion;
    // 摄像机中心点在竖直方向上可移动的范围
    private Vector2 m_VerticalRegion;

    // 角色Transform组件的引用
    private Transform m_Player;

    private void Awake() &#123;
        // 获取引用
        m_Player = GameObject.FindGameObjectWithTag("Player").transform;

        if(m_Player == null) &#123;
            Debug.LogError("请添加Tag为Player的GameObject");
        &#125;
    &#125;

    private void Start() &#123;
        Camera camera = this.GetComponent<Camera>();

        // 获取视口右上角对应的世界坐标
        Vector3 cornerPos = camera.ViewportToWorldPoint(new Vector3(1f, 1f, Mathf.Abs(transform.position.z)));

        // 此时,摄像机的世界坐标就是视口中心点的世界坐标
        // 计算摄像机视口的宽度
        float cameraWidth = 2 * (cornerPos.x - transform.position.x);
        // 计算视口的高度
        float cameraHeight = 2 * (cornerPos.y - transform.position.y);


        // 计算Box Collider2D中心点的世界坐标
        Vector2 regionPosition = new Vector2(
            Region.transform.position.x + Region.offset.x,
            Region.transform.position.y + Region.offset.y
        );

        Debug.Log(regionPosition.x);
        Debug.Log(regionPosition.y);

        // 计算Box Collider2D和摄像机视口的宽度差的一半
        float halfDeltaWidth = (Region.size.x - cameraWidth) / 2;
        // 计算Box Collider2D和摄像机视口的高度差的一半
        float halfDeltaHeight = (Region.size.y - cameraHeight) / 2;

        if(halfDeltaWidth < 0) &#123;
            Debug.LogError("Box Collider2D的宽度小于摄像机视口的宽度");
        &#125;

        if(halfDeltaHeight < 0) &#123;
            Debug.LogError("Box Collider2D的高度小于摄像机视口的高度");
        &#125;

        // 计算摄像机中心点水平方向上可移动的范围
        m_HorizontalRegion = new Vector2(
            regionPosition.x - halfDeltaWidth,
            regionPosition.x + halfDeltaWidth
        );

        // 计算摄像机中心点竖直方向上可移动的范围
        m_VerticalRegion = new Vector2(
            regionPosition.y - halfDeltaHeight,
            regionPosition.y + halfDeltaHeight
        );
    &#125;

    private void LateUpdate() &#123;
        TrackPlayer();
    &#125;

    // 跟随玩家
    private void TrackPlayer() &#123;
        float targetX = transform.position.x;
        float targetY = transform.position.y;

        // 如果超出了偏移量,计算摄像机跟随后的位置
        if (CheckHorizontalMargin()) &#123;
            targetX = Mathf.Lerp(transform.position.x, m_Player.position.x, HorizontalFollowSpeed * Time.deltaTime);
        &#125;

        if (CheckVerticalMargin()) &#123;
            targetY = Mathf.Lerp(transform.position.y, m_Player.position.y, VerticalFollowSpeed * Time.deltaTime);
        &#125;

        targetX = Mathf.Clamp(targetX, m_HorizontalRegion.x, m_HorizontalRegion.y);
        targetY = Mathf.Clamp(targetY, m_VerticalRegion.x, m_VerticalRegion.y);

        // 更新摄像机的位置
        transform.position = new Vector3(targetX, targetY, transform.position.z);
    &#125;

    // 判断水平方向上是否超出了最大偏移量
    private bool CheckHorizontalMargin() &#123;
        return Mathf.Abs(transform.position.x - m_Player.position.x) > HorizontalMargin;
    &#125;

    // 判断竖直方向上是否超出了最大偏移量
    private bool CheckVerticalMargin() &#123;
        return Mathf.Abs(transform.position.y - m_Player.position.y) > VerticalMargin;
    &#125;
&#125;

后言

  至此,我们已实现了摄像机跟随角色移动的功能,摄像机跟随的速度、摄像机与角色的偏移量以及摄像机可移动的范围可以根据自己的喜好进行设置。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay8分支下看到,读者可以clone这个仓库到本地进行查看。


参考链接

  1. Unity的Camera

文章作者: RainbowCyan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 RainbowCyan !
 上一篇
《土豆荣耀》重构笔记(十一)实现发射导弹的功能 《土豆荣耀》重构笔记(十一)实现发射导弹的功能
前言  在实现了怪物攻击角色的功能之后,我们接下来需要实现玩家攻击怪物的功能。玩家攻击怪物的方式有发射导弹和放置炸弹。导弹在场景中会以恒定速率飞行,碰到物体时爆炸,若碰到怪物则对怪物造成伤害。接下来,我们开始制作能伤害怪
下一篇 
《土豆荣耀》重构笔记(九)实现角色的血量控制功能 《土豆荣耀》重构笔记(九)实现角色的血量控制功能
前言  本篇文章的内容是实现实现角色的血量控制功能,在开始实现之前,我们需要知道角色血量控制功能的需求是什么。 角色血量控制功能的需求 角色头上需要显示一个跟随角色移动的血量条,实时显示角色当前的血量 角色的最大血
  目录