Unity 3D 场景简单镜头行为两例

前言:这里展示了两种镜头行为,作为 3D 场景中跟随角色的摄像机移动脚本。


1 可缩放的俯视角镜头

行为: 类似 DOTA-like 类游戏的视角。镜头为不同高度的俯视角,可以使用滚轮控制观察距离,镜头朝向固定。

原理: 通过维持与观察目标的相对坐标保证镜头位移正确,镜头在预设的轨道上移动。简单起见轨道表示为3个锚点间的两条连线,通过线性插值函数控制镜头在轨道上的位置。

脚本代码:

using UnityEngine;

public class SimpleOverlookCamera : MonoBehaviour
{
    /*
     * 相机动作表示,这里设置了三个机位顶点
     * 相机可以在这三个顶点构成的两条连线上移动
     * 鼠标滚轮控制相机移动,实现镜头移动和人物缩放效果
     */

    [Header("__顶、中及底部机位,按相对位置__")]
    public Vector3 topPivot;  // 最上方机位点
    public Vector3 middlePivot;  // 居中机位点
    public Vector3 bottomPivot;  // 下方机位点
    Vector3 cameraPos;  // 当前相机机位

    Vector3 dTopPivot;
    Vector3 dMiddlePivot;
    Vector3 dBottomPivot;

    [Header("__机位移动的上下限__")]
    [Range(1f, 2f)]
    public float posTopLimit = 2;
    [Range(0, 1f)]
    public float posBottomLimit = 0;
    [Header("__当前机位或开始状态的机位点__")]
    [Range(0, 2f)]
    public float posPercent = 1.01f;  // 作为机位默认值

    [SerializeField, Range(0, 1f)]
    private float followSpeed = 0.5f;

    [SerializeField, Range(0, 1f)]
    private float seekSpeed = 0.33f;

    Camera selfCamera;
    public bool scanMain = true;

    public Transform seekTrans;
    public Transform followTrans;

    // Use this for initialization
    void Start()
    {
        if (scanMain) { selfCamera = Camera.main; }
        else { selfCamera = GetComponent<Camera>(); }

        if (seekTrans != null && followTrans != null)
        {
            dTopPivot = topPivot;
            dMiddlePivot = middlePivot;
            dBottomPivot = bottomPivot;
        }
        else
        {
            // 抛出一条错误
            Debug.Log(string.Format("<!> seekTrans or followTrans is null!"));
        }

        _SetCameraPos();
    }

    // Update is called once per frame
    void LateUpdate()
    {
        var dVar = Input.GetAxis("Mouse ScrollWheel");

        if (posPercent > posBottomLimit && dVar < 0)
        {
            posPercent += dVar;
        }
        if (posPercent < posTopLimit && dVar > 0)
        {
            posPercent += dVar;
        }

        _SetCameraPos();
    }

    void _SetCameraPos()
    {
        PivotsFollow();

        if (posPercent > 1) { cameraPos = Vector3.Lerp(middlePivot, topPivot, posPercent - 1); }
        if (posPercent < 1) { cameraPos = Vector3.Lerp(bottomPivot, middlePivot, posPercent); }
        posPercent = (posPercent == 1) ? posPercent + 0.001f : posPercent;
        //selfCamera.transform.position = cameraPos;
        selfCamera.transform.position = Vector3.Lerp(selfCamera.transform.position, cameraPos, followSpeed);
        //selfCamera.transform.LookAt(seekTrans);
        var lookRotation = Quaternion.LookRotation(seekTrans.position - transform.position);
        transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, seekSpeed);
    }

    void PivotsFollow()
    {
        topPivot = followTrans.position + dTopPivot;
        middlePivot = followTrans.position + dMiddlePivot;
        bottomPivot = followTrans.position + dBottomPivot;
    }
}

备注: 使用时将脚本挂载到需要跟随角色的相机上,设定镜头朝向的节点 seekTrans 和镜头跟随的节点 followTrans,并需要设置锚点位置 (远、中、近) 才能正常工作 (对人形角色,参考位置为远:(0, 7, -5),中:(0, 4, -4),近:(0, 1, -1))。锚点设置过远或远中、近中两条连线角度过小可能在调节视角时产生“梯度感”。由于大多数人物模型的锚点在模型脚下,需要为人物模型增加一个空节点为 seekTrans 赋值,通常来说这个点应当在人形角色的躯干部正中位置。


2 随鼠标移动的自由视角镜头

行为: 类似《黑暗之魂》的视角。镜头与角色的距离固定并总是保持角色在画面中央,移动鼠标将移动镜头朝向,通过地形或镜头位置过低碰到地面时镜头会调节距离确保角色不会被挡住。

原理: 通过维持与观察目标的相对坐标保证镜头位移正确,镜头在一个半径既定的球面上移动,球心为观察点位置。脚本内使用球面坐标表示镜头的相对位置,并换算到空间直角坐标系内使坐标系与世界坐标一致。

脚本代码:

using UnityEngine;

public class SimpleFreeLookCamera : MonoBehaviour
{
    float alpha;  // 横向角, 规定为与 x 轴的夹角
    float sigma;  // 纵向角,规定为与 y 轴的夹角
    float radius;  // 相机跟随人物的实际半径
    public float initRadius = 10;
    Vector3 orderTrans;  // 保存镜头目标位置
    Vector3 dir = Vector3.back;  // 从被观察点到镜头的矢量
    public bool isControlable = true;

    [Header("_依次为观察目标的中心点、横向和纵向旋转速度(表示为角度)_")]
    public Transform center;
    public float alphaSpeed = 360;
    public float sigmaSpeed = 360;
    float _alphaSpeed;
    float _sigmaSpeed;

    [Header("_分别为镜头与 y 轴夹角的最大值和最小值_")]
    public float sigmaMax = 120;
    public float sigmaMin = 15;
    float _sigmaMax = 3.14f;
    float _sigmaMin = 0;

    [Header("_表示移动和旋转的缓冲程度,值越大越不明显_")]
    [SerializeField, Range(0, 1f)] float lerpFollow = 0.25f;
    [SerializeField, Range(0, 1f)] float slerpRotate = 0.25f;

    [Header("_勾选此项时镜头将随地形调整位置,可设置镜头的最短距离_")]
    public bool suitLandform = true;
    public float minRadius = 2;
    public LayerMask raycastLayer;  // 选取希望被射线检测的层

    [Header("_显示鼠标_")]
    [SerializeField] bool _showCursur = true;
    public bool ShowCursur
    {
        get
        {
            return Cursor.visible;
        }
        set
        {
            _showCursur = value;
            Cursor.visible = _showCursur;
        }
    }

    // Use this for initialization
    void Start()
    {
        Init();
        _alphaSpeed = alphaSpeed / 180 * Mathf.PI;
        _sigmaSpeed = sigmaSpeed / 180 * Mathf.PI;
        _sigmaMax = sigmaMax / 180 * Mathf.PI;
        _sigmaMin = sigmaMin / 180 * Mathf.PI;
        radius = initRadius;

        ShowCursur = _showCursur;
    }

    private void Update()
    {
        var h = -Input.GetAxis("Mouse X");
        var v = Input.GetAxis("Mouse Y");

        if (!isControlable)
        {
            h = 0;
            v = 0;
        }

        alpha += h * _alphaSpeed * Time.deltaTime;
        sigma += v * _sigmaSpeed * Time.deltaTime;
        if (sigma > Mathf.PI) { sigma = Mathf.PI; }
        if (sigma < _sigmaMin) { sigma = _sigmaMin; }
        if (sigma > _sigmaMax) { sigma = _sigmaMax; }
        dir = new Vector3(Mathf.Cos(alpha) * Mathf.Sin(sigma),
                                 Mathf.Cos(sigma),
                                 Mathf.Sin(alpha) * Mathf.Sin(sigma));
    }

    private void FixedUpdate()
    {
        if (!suitLandform)
        {
            radius = initRadius;
            return;
        }
        RaycastHit hit;
        Ray ray = new Ray(center.position + dir * minRadius, transform.position - center.position);
        if (Physics.Raycast(ray, out hit, initRadius, raycastLayer))
        {
            radius = Mathf.Min(Vector3.Distance(transform.position, hit.point), initRadius);
        }        
    }

    private void LateUpdate()
    {
        // 位置跟随
        orderTrans = center.position + dir * radius;
        transform.position = Vector3.Lerp(transform.position, orderTrans, lerpFollow);
        // 视角跟随
        var newForward = Quaternion.LookRotation(center.position - transform.position);
        transform.rotation = Quaternion.Slerp(transform.rotation, newForward, slerpRotate);
    }

    public void Init()
    {
        orderTrans = center.position + dir * initRadius;
        transform.position = orderTrans;
        alpha = -Mathf.PI / 2;
        sigma = Mathf.PI / 2;
    }
}

备注: 使用时将脚本挂载到跟随角色的相机上,并为 center 字段赋值。一般来说人形角色的模型锚点在角色脚下,需要为角色增加一个空节点作为 center 的位置,就人形模型而言这个点一般在角色头部或躯干部正中偏上。

此例脚本中相机和人物间距需要预设,根据设计需求调节即可。也可以增加一个函数控制相机和人物之间的距离,只需要控制 radius 字段的值即可。