配置方法
创建一个空对象挂载相机控制脚本(在代码中让该对象与角色的位置重合),相机作为其子物体
将相机相对于父物体的距离设定为理想的 相机-人物 距离
指定环绕中心和相机
原理
想要实现环绕,首先就需要获取环绕点和相机的Transform属性。
想要表示相机相对于环绕点的方位,你可能会觉得使用相对位置描述更为方便,其实不然,实际上使用两个欧拉角来描述比使用位置描述更为方便,因为我们后面需要对相机的旋转过程插值,使用位置来描述显然是不便于插值的。
这样,我们就有了相机相对于环绕点的角度,加上距离就可以精确描述相机的位置了。
线性插值
如果要对一个变量进行插值,首先要明确插值的实现。什么是线性插值?
给定两个数,a、b,对a、b进行一次线性插值,再给一个参数t,代表插值的位置。
比如对0、1插值,参数为0.1,那么进行一次插值得到的结果就是0.1。
线性插值的公式是 a + (b-a) * t 。
那么插值有哪些实际应用呢?
- 平滑运动中的相机与角色的距离
- 平滑旋转中的相机与角色的角度,并实现一定时长的惯性旋转效果。
为什么插值会产生延时呢?
因为插值有两个数,一个是当前值,一个是目标值,另外还有一个参数是百分比。
当目标值不再变化之后,当前值逼近目标值需要一定的时间。
两个数之间的差距随着时间的变化曲线,类似于一个方向向下的抛物线。
周围障碍检测
对角色前后左右四个方向进行射线检测,获取返回的距离,取得一个最小值。
如果想要更精确的结果,可以将正交的方向向量分别两两相加,得到四个斜的方向向量,这样就可以做八个方向的距离检测。
相机被遮挡处理
如果相机被遮挡,就将相机距离调整到小于 距离角色水平方向最近的障碍距离 。
在手动调整距离时,记录偏好距离。
如果不再被遮挡,就恢复偏好距离。
代码
using UnityEngine;
using System.Collections;
public class OrbitCamera : MonoBehaviour
{
public Transform pivot;
public Transform camera;
private float distance;
void Start()
{
targetSideRotation = transform.eulerAngles.y;
currentSideRotation = transform.eulerAngles.y;
targetUpRotation = transform.eulerAngles.x;
currentUpRotation = transform.eulerAngles.x;
distance = -camera.localPosition.z; //相机局部坐标z值为-1.8,那么相机与距离人物为1.8
}
void Update()
{
if (!pivot) return;
Follow();
DragRotate();
ScrollScale();
OcclusionJudge();
}
void Follow()
{
transform.position = Vector3.Lerp(transform.position, pivot.position, Time.deltaTime * 5); //相机跟随角色的插值,相机当前帧的实际位置为它们中间10%的位置
camera.localPosition = -Vector3.forward * distance; //仅z有值,并且方向为负
}
float MinimumDegree = 0;
float MaximumDegree = 60;
private float targetSideRotation;
private float targetUpRotation;
private float currentSideRotation;
private float currentUpRotation;
void DragRotate()
{
if (Input.GetMouseButton(0))
{
targetSideRotation += Input.GetAxis("Mouse X") * 5;
targetUpRotation -= Input.GetAxis("Mouse Y") * 5;
}
targetUpRotation = Mathf.Clamp(targetUpRotation, MinimumDegree, MaximumDegree);
currentSideRotation = Mathf.LerpAngle(currentSideRotation, targetSideRotation, Time.deltaTime * 5);
currentUpRotation = Mathf.Lerp(currentUpRotation, targetUpRotation, Time.deltaTime * 5);
transform.rotation = Quaternion.Euler(currentUpRotation, currentSideRotation, 0);
}
float MinimumDistance = 1;
float MaximumDistance = 4;
void ScrollScale()
{
distance *= (1 - Input.GetAxis("Mouse ScrollWheel") * 0.2f); //在原值的基础上调整为原值的百分比
distance = Mathf.Clamp(distance, MinimumDistance, MaximumDistance);
if(Input.GetAxis("Mouse ScrollWheel")!=0)
preferdDistance = distance;
}
float preferdDistance = 1;
bool resumable = false;
void OcclusionJudge()
{
Vector3 perferdCamPos = transform.position - camera.forward * preferdDistance;
Debug.DrawLine(perferdCamPos, pivot.position, Color.red);
if (Physics.Raycast(camera.position,camera.forward,distance - MinimumDistance))
{
resumable = true;
distance = NearestObstacleDistance(pivot);
while (Physics.Raycast(camera.position, camera.forward, distance - MinimumDistance) && distance > MinimumDistance)
{
distance *= 0.99f;
distance = Mathf.Clamp(distance, MinimumDistance, MaximumDistance);
float dist = Mathf.Lerp(-camera.transform.localPosition.z, distance, 1f);
camera.localPosition = -Vector3.forward * dist;
}
}
if (!resumable) return;
if (resumable && !Physics.Raycast(perferdCamPos, camera.forward, preferdDistance - MinimumDistance))
{
distance = preferdDistance;
float dist = Mathf.Lerp(-camera.transform.localPosition.z, distance, 1f);
camera.localPosition = -Vector3.forward * distance;
resumable = false;
}
}
float NearestObstacleDistance(Transform start)
{
float dis = float.MaxValue;
RaycastHit hit;
Physics.Raycast(start.position, start.forward, out hit);
if(hit.distance!=0) dis = Mathf.Min(dis, hit.distance);
Physics.Raycast(start.position, -start.forward, out hit);
if (hit.distance != 0) dis = Mathf.Min(dis, hit.distance);
Physics.Raycast(start.position, start.right, out hit);
if (hit.distance != 0) dis = Mathf.Min(dis, hit.distance);
Physics.Raycast(start.position, -start.right, out hit);
if (hit.distance != 0) dis = Mathf.Min(dis, hit.distance);
return dis;
}
}
效果就不演示了,比之前写的那个要好不少
|