Unity笔记-29-ARPG游戏项目-01-第三人称相机
相机观测实现
核心思路
通过Yaw
和Pitch
轴控制相机旋转
private Vector3 myDafaultDir;//默认方向
[SerializeField]
public Vector3 myCurrentDir;//当前方向
private Transform myPlayerTransform;//玩家Transform
private Vector3 myRotateValue;//控制器存储旋转值
private Vector3 myPitchRotateAxis;//俯仰方向旋转轴
private Vector3 myYawRotateAxis;//左右横向方向旋转轴
[Header("相机观测距离")]
public float distance = 4f;//相机观测距离
[Header("相机观测距离限制")]
public Vector2 distanceLimit = new Vector2(1f, 4f);
[Header("相机旋转速度")]
public float moveSpeed = 120f;//相机旋转速度
[Header("观测目标偏移量")]
public Vector3 offset = new Vector3(0f,1.5f,0f);//观测目标偏移量
[Header("视距调整速度")]
[Range(0,1)]
public float stadiaAdjustmentSpeed;
[Header("pitch反转")]
public bool invertPitch=true;//反转pitch方向相机滑动
[Header("pitch角度限制")]
public Vector2 pitchLimit = new Vector2(-40f, 70f);//pitch方向角度约束
private void OnEnable()
{
myCurrentDir = myDafaultDir;
myCurrentDistance = distance;
//世界坐标系:y轴方向
Vector3 upAxis = -Physics.gravity.normalized;
//找到玩家Transform
myPlayerTransform = GameObject.FindGameObjectWithTag(GameConst.PLAYER).transform;
//通过摄像机与玩家的方向向量投影在地面上的单位向量-获得初始方向
//地面使用地面的法向量
myDafaultDir = Vector3.ProjectOnPlane((transform.position-myPlayerTransform.position),upAxis).normalized;
//横向旋转轴
myYawRotateAxis = upAxis;
//俯仰旋转轴,使用摄像机朝向与世界y轴方向的叉积得到
myPitchRotateAxis = Vector3.Cross(upAxis, Vector3.ProjectOnPlane(transform.forward, upAxis));
}
初始化操作
upAxis
:通过物理重力轴获得世界y轴朝向,作为世界地面的法向量,后续用于投影;
myPlayerTransform
:通过标签获得观测对象Transform
myDafaultDir
:默认方向,通过由角色指向摄像机方向向量于地面上的投影的单位向量表示
Yas&Pitch
:前者为世界y轴,后者为摄像机朝向与地面上的投影向量和世界y轴的叉积向量,相机观测仅需要这两个自由度,无需Roll
轴
myRotateValue
:作为旋转量的临时存储容器
Update实现
通过虚拟轴输入
Vector2 inputDelta = new Vector2(Input.GetAxis(GameConst.MOUSEX_AXIS),Input.GetAxis(GameConst.MOUSEY_AXIS));
更新旋转值
//更新横向旋转值
myRotateValue.x += inputDelta.x * moveSpeed * Time.smoothDeltaTime;
myRotateValue.x = AngleCorrection(myRotateValue.x);
//更新纵向旋转值
#region 俯仰角度约束
myRotateValue.y += inputDelta.y * moveSpeed * (invertPitch ? -1 : 1) * Time.smoothDeltaTime;
myRotateValue.y = AngleCorrection(myRotateValue.y);
myRotateValue.y = Mathf.Clamp(myRotateValue.y, pitchLimit.x, pitchLimit.y);
#endregion
存储临时旋转值——>欧拉角修正——>俯仰角限制
不断叠加输入的旋转值到旋转值临时存储容器里,通过欧拉角修正,把角度控制在0-360度
叠加四元数旋转
//构建角轴四元数,通过四元数方法:AngleAxis(角度,旋转轴)获得角轴旋转
Quaternion horizontalQuat = Quaternion.AngleAxis(myRotateValue.x, myYawRotateAxis);//横向
Quaternion verticalQuat = Quaternion.AngleAxis(myRotateValue.y, myPitchRotateAxis);//俯仰
//注:第三人称控制只需要两个自由度,不需要Roll
Vector3 finalDir = horizontalQuat * verticalQuat * myDafaultDir;//叠加四元数旋转获得最终方向
myCurrentDir = finalDir;
将旋转容器里的旋转值通过四元数角轴获得对应轴体四元数(旋转),将旋转叠加给默认方向,即可获得当前方向
//Vector3 from = myPlayerTransform.TransformPoint(offset);//获得偏移后的伪相机位置
Vector3 from = myPlayerTransform.localToWorldMatrix.MultiplyPoint3x4(offset);//获得偏移后的伪相机位置
Vector3 to = from + finalDir * distance;//添加观测距离后的相机最终位置
通过角色局部坐标转化,获得偏移的伪相机坐标,将伪相机坐标叠加观测距离后即可后的相机的最终坐标,
叠加观测距离:把相机从伪相机坐标朝着最终方向位移观测距离即伪相机的最终位置
transform.LookAt(from);
最终调整摄像机朝向,让摄像机看向伪相机坐标,也就是角色偏移的观测位置;
相机与墙体
穿模修正
private Vector3 ObstacleProcess(Vector3 from,Vector3 to)
{
//获得从伪相机位置到相机最终位置的方向向量
Vector3 dir = (to - from).normalized;
//检查最初位置是否会检测到障碍物,若检测到则报错
//if (Physics.CheckSphere(from, obstacleSphereRadius, obstacleLayerMask))
// Debug.Log("错误!障碍物检测球体半径应小于角色胶囊");
RaycastHit hit = default;
//使用圆形检测是否有障碍物,如果有则将射线检测位置的负方向圆形半径位置赋值
if(Physics.SphereCast(new Ray(from,dir),obstacleSphereRadius,out hit, distance, obstacleLayerMask))
{
return hit.point + (-dir * obstacleSphereRadius);
}
return to;
}
核心思想
获取伪相机位置from
,以及相机本来的最终位置to
计算由from
射向to
的单位方向向量dir
,从from
发射朝向dir
的球体射线检测是否存在物体,如果检测到物体,那么则返回检测点加上负dir
方向乘以球体半径的位置,作为真正的最终位置,
相机位置插值恢复
在因为碰撞体导致摄像机距离被拉近后,当视角拉到无碰撞体的位置,相机位置会恢复,但是不能立刻恢复,否则感觉十分突兀,需要通过插值平滑过渡
private void CameraMoveDelay(Vector3 from, Vector3 to)
{
//临时位置存储
Vector3 exceptTo = ObstacleProcess(from, to);
//获取移动距离
float expectDistance = Vector3.Distance(exceptTo, from);
if (expectDistance < myCurrentDistance)//如果拉近,则重置延迟
{
//拉近瞬时拉近
myCurrentDistance = expectDistance;
myDistanceRecoveryDelayCounter = distanceRecoveryDelay;
}
else//如果为拉远,延迟一段时间逐渐拉远
{
if (myDistanceRecoveryDelayCounter > 0)
myDistanceRecoveryDelayCounter -= Time.deltaTime;
else
myCurrentDistance = Mathf.Lerp(myCurrentDistance, expectDistance, Time.smoothDeltaTime * distanceRecoverySpeed);
}
}
相机延迟恢复,首先使用穿模修正获得真正的相机位置,然后计算真正位置与伪相机位置的距离,判断当前十分需要拉近操作,如果小于当前距离,那么则将距离存储给当前距离,并重置拉远延迟计数器;否则,即为拉远距离,通过计数器延迟拉远,并通过插值平滑拉远速度
Update实现补充
//省略前部分代码
Vector3 finalDir = horizontalQuat * verticalQuat * myDafaultDir;//叠加四元数旋转获得最终方向
myCurrentDir = finalDir;
//Vector3 from = myPlayerTransform.TransformPoint(offset);//获得偏移后的伪相机位置
Vector3 from = myPlayerTransform.localToWorldMatrix.MultiplyPoint3x4(offset);//获得偏移后的伪相机位置
Vector3 to = from + finalDir * distance;//添加观测距离后的相机最终位置
//在此处添加插值恢复检测即可
CameraMoveDelay(from, to);
transform.position = from + finalDir * myCurrentDistance;
transform.LookAt(from);
#endregion
欧拉角修正
private float AngleCorrection(float value)
{
float angle = value - 180;
if (angle > 0)
{
return angle - 180;
}
if (value == 0) return 0;
return value;
}