很多第三人称角色扮演游戏都有一个很棒的角色摄像机
# 预览
实现后的效果如下:
使用鼠标即可以移动摄像机,摄像机能智能的检测到碰撞体,并自动修正位置。
# 备注
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; | |
} | |
} | |
} |