很多第三人称角色扮演游戏都有一个很棒的角色摄像机

# 预览

实现后的效果如下:
自由摄像机
使用鼠标即可以移动摄像机,摄像机能智能的检测到碰撞体,并自动修正位置。

# 备注

Camera.fieldOfView 属性可以获取到摄像机的焦距,修改该值,可以实现拉进拉远效果。

# 核心参考

var mouseX = Input.GetAxis("Mouse X");
var mouseY = Input.GetAxis("Mouse Y");
Camera.main.transform.localRotation = Camera.main.transform.localRotation *Quaternion.Euler(-mouseY, mouseX, 0);

以上代码是将鼠标移动和摄像机位置关联起来的关键代码,首先我们取得鼠标的 x 和 y 轴上的 axis 值,然后将摄像机的旋转量 *(-mouseY, mouseX, 0) 构成的四元数。

Quaternion.Euler(vector3)

可以将一个 3 维向量转换为一个四元数。

# 完整代码

using System;
using NaughtyAttributes;
using UnityEngine;
namespace _Scripts
{
    /// <summary>
    /// 摄像机控制器
    /// </summary>
    public class CameraController : MonoBehaviour
    {
        [SerializeField,Header("玩家")] private Transform player;
        [SerializeField,Header("平滑速度")] private float smooth = 10f;
        [SerializeField,Header("h轴偏转速度")] private float hSpeed = 6f;
        [SerializeField,Header("v轴偏转速度")] private float vSpeed = 6f;
        [SerializeField,Header("相机位置偏移")] private Vector3 camOffset;
        [SerializeField,Header("锚点位置偏移")] private Vector3 pivotOffset;
        [MinMaxSlider(-100, 100), SerializeField,Header("偏转角范围")]
        private Vector2 minMaxVAngle;
        private float targetFov;                        // 目标 Fov
        private Vector3 targetPivotOffset;              // 目标锚点 offset
        private Vector3 targetCamOffset;                // 目标相机 offset
        private Transform cam;    
        private Camera myCamera;
        private Vector3 smoothPivotOffset;     // 当前锚点插值
        private Vector3 smoothCamOffset;       // 当前相机位置插值
        private float defaultFov;           // 默认 fov
        private float relCameraPosMag;    // 射线检测长度
        private float angleH, angleV;    
        private void Awake()
        {
            cam = transform;
            myCamera = GetComponent<Camera>();
            defaultFov = myCamera.fieldOfView;
            // 设置相机默认位置
            cam.position = player.position + Quaternion.identity * pivotOffset + Quaternion.identity * camOffset;
            cam.rotation = Quaternion.identity;
            smoothPivotOffset = pivotOffset;
            smoothCamOffset = camOffset;
            angleH = player.eulerAngles.y;
            // 射线检测
            relCameraPosMag = (transform.position - player.position).magnitude - 0.5f;
            // 重置属性
            ResetTargetOffsets ();
            ResetFov ();
        }
        private void Update()
        {
            angleH += Input.GetAxis($"Mouse X") * hSpeed;
            angleV += Input.GetAxis($"Mouse Y") * vSpeed;
            
            // 限制偏转
            angleV = Mathf.Clamp(angleV, minMaxVAngle.x, minMaxVAngle.y);
            var aimRotation = Quaternion.Euler(-angleV, angleH, 0);
            cam.rotation = Quaternion.Euler(-angleV, angleH, 0);
            
            //y 旋转
            var camYRotation = Quaternion.Euler(0, angleH, 0);
            // 焦距
            myCamera.fieldOfView = Mathf.Lerp(myCamera.fieldOfView, targetFov, Time.deltaTime);
            
            // 射线检测动态修正位置
            var baseTempPosition = player.position + camYRotation * targetPivotOffset;
            var noCollisionOffset = targetCamOffset;
            for (var zOffset = targetCamOffset.z; zOffset <= 0; zOffset+=0.5f)
            {
                noCollisionOffset.z = zOffset;
                if (PosCheck(baseTempPosition+aimRotation*noCollisionOffset,Mathf.Abs(zOffset)))
                {
                    break;
                }
            }
            
            // 碰撞修正相机位置
            smoothPivotOffset = Vector3.Lerp(smoothPivotOffset, targetPivotOffset, smooth * Time.deltaTime);
            smoothCamOffset = Vector3.Lerp(smoothCamOffset, noCollisionOffset, smooth * Time.deltaTime);
            cam.position = player.position + camYRotation * smoothPivotOffset + aimRotation * smoothCamOffset;
            
        }
        
        /// <summary>
        /// 重置 offset
        /// </summary>
        public void ResetTargetOffsets()
        {
            targetPivotOffset = pivotOffset;
            targetCamOffset = camOffset;
        }
        /// <summary>
        /// 重置 FOV
        /// </summary>
        public void ResetFov()
        {
            targetFov = defaultFov;
        }
        
        /// <summary>
        /// 检查摄像机位置
        /// </summary>
        /// <param name="checkPos"></param>
        /// <param name="offset"></param>
        /// <returns></returns>
        private bool PosCheck(Vector3 checkPos, float offset)
        {
            // 获得碰撞器高度
            var playerH=player.GetComponent<CapsuleCollider>().height * 0.75f > 0
                ? player.GetComponent<CapsuleCollider>().height * 0.75f
                : player.GetComponent<CharacterController>().height * 0.75f;
            return ViewPosCheck(checkPos, playerH) && ReverseViewPosCheck(checkPos,playerH,offset);
        }
        
        /// <summary>
        /// 碰撞检查
        /// </summary>
        /// <param name="checkPos"></param>
        /// <param name="playerH"></param>
        /// <returns></returns>
        private bool ViewPosCheck(Vector3 checkPos, float playerH)
        {
            var target = player.position + (Vector3.up * playerH);
            if (Physics.SphereCast(checkPos,0.2f,target-checkPos,out  RaycastHit hit,relCameraPosMag))
            {
                if (hit.transform!=player&&!hit.transform.GetComponent<Collider>().isTrigger)
                {
                    return false;
                }
            }
            return true;
        }
        /// <summary>
        /// 反向碰撞检测
        /// </summary>
        /// <param name="checkPos"></param>
        /// <param name="playerH"></param>
        /// <param name="maxDistance"></param>
        /// <returns></returns>
        private bool ReverseViewPosCheck(Vector3 checkPos, float playerH, float maxDistance)
        {
            // Cast origin.
            var origin = player.position + (Vector3.up * playerH);
            if (Physics.SphereCast(origin, 0.2f, checkPos - origin, out RaycastHit hit, maxDistance))
            {
                if(hit.transform != player && hit.transform != transform && !hit.transform.GetComponent<Collider>().isTrigger)
                {
                    return false;
                }
            }
            return true;
        }
        
    }
}