前言
现在很多游戏都会在主页中显示一个场景,然后玩家的人物会在场景当中。这个时候往往会需要一些旋转功能,来方便玩家查看人物的各个角度。
一般的做法就是人物自身转动,摄像机不动,这样用户无法用其他视角观察到整个场景。另一种是人物不动,相机围绕人物,或者某个点转动,这种做法的缺点在于要么人物在屏幕中央(看向人物),要么会在屏幕左右乱跑(看向人物边上某个点),会局限UI的设计。
因此最好的方法便是可以让人物在屏幕中的任意位置,同时旋转起来的时候,转动相机,同时人物相对屏幕的位置也保持不变,如图:
思路
因为相机看向的点(这里我们叫他LookedAt点),永远在屏幕的中央。假设我们的人物放在UI的左侧,那么我们要保证旋转相机的时候,人物的点(这里我们叫他Target点)在相机Camera看来始终在LookedAt点的左边。
我们可以把LookedAt点,Target点,和Camera,三个点想象成一个三角形,然后这个三角形围绕着Target点旋转,也就是LookedAt点和Camera同时以相同速度(角度/每秒)围绕Target点公转。即可实现我们要的效果
视角俯视或仰视的话,只需修改Camera的高度,其他两个点不动。
视角拉近或拉远,则需要同比例修改LookedAt点到Target点的距离和Camera到Target点的距离,Target点不动,即可保持三角形的三个角的角度不变。
代码实现
首先我们先搭建一个简单的场景,如图,Target点在LookAt点的左侧
我们可以先通过一些参数来初始化Camera的位置,然后此时Camera,LookedAt和Target三个点形成的三角形即我们到时候要旋转的样式。
然后获取变成和角度等,来计算旋转后的偏移值等等。具体注释见下面代码:
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class CameraAndLookAtRotateWithTarget : MonoBehaviour
{
[SerializeField] Transform m_target;//需要被观察的点
[SerializeField] Transform m_lookedAt;//摄像机lookat的点
[SerializeField] bool m_isTargetLeft;//相机看到的,m_target是否在m_lookedAt的左边
[SerializeField] float m_defaultCameraAngleX = 180;//相机初始位置相对m_lookedAt.forward的水平角度偏移
[SerializeField] float m_defaultCameraAngleY = 5;//相机初始位置相对法线为m_lookedAt.up的平面的角度偏移,锐角
[SerializeField] float m_defaultCameraDistance = 8;//相机初始位置相对m_lookedAt的距离
#if UNITY_EDITOR
[SerializeField] bool m_isChangeDefaultValue = false;//修改默认值之后设为true来重新设置摄像机初始位置,方便调试
#endif
//相机上下的最大最小偏移角度
[SerializeField] float m_cameraMaxAngleY = 60;
[SerializeField] float m_cameraMinAngleY = 0;
//相机距离m_target的最大最小距离
[SerializeField] float m_cameraMaxDistance = 20;
[SerializeField] float m_cameraMinDistance = 5;
[SerializeField] float m_cameraCurrentDistance = 0;//显示当前相机距离m_target的距离,方便调试
Transform m_transform;
const float ANGLE_CONVERTER = Mathf.PI / 180;//弧度,用于Mathf.Sin,Mathf.Cos的计算
//m_target到m_lookedAt的距离,m_target到相机距离
float m_targetToLookedAtDistance, m_targetToCameraDistance;
//m_lookedAt和相机到m_target的连线,x为相对m_target.forward的角度,y为相对法线为m_target.up的平面的角度
float m_lookedAtAngleX, m_lookedAtAngleY, m_cameraAngleX, m_cameraAngleY;
///m_lookedAt和相机的位置相对m_target位置的偏移
Vector3 m_lookedAtOffset, m_cameraOffset;
//是否旋转或拉近拉远了
bool m_isChange = false;
//距离每次变化的比例
float m_changeDistanceRatio = 0.05f;
void Awake()
{
m_transform = transform;
}
void Start()
{
SetCameraDefaultPosition();
GetInitialData();
}
//设置摄像机的初始位置
void SetCameraDefaultPosition()
{
Vector3 offset = CalculateOffset(m_defaultCameraDistance, m_defaultCameraAngleX, m_defaultCameraAngleY);
m_transform.position = m_lookedAt.position + offset;
m_transform.LookAt(m_lookedAt);
}
void GetInitialData()
{
//获取距离
m_targetToLookedAtDistance = Vector3.Distance(m_lookedAt.position, m_target.position);
m_targetToCameraDistance = Vector3.Distance(m_transform.position, m_target.position);
//获取角度
Vector3 targetToLookedAt = m_lookedAt.position - m_target.position;
m_lookedAtAngleX = Vector3.Angle(Vector3.ProjectOnPlane(targetToLookedAt, m_target.up), m_target.forward);
m_lookedAtAngleY = Vector3.Angle(Vector3.ProjectOnPlane(targetToLookedAt, m_target.up), targetToLookedAt);
//取锐角
if ((m_isTargetLeft && targetToLookedAt.x < 0) || (!m_isTargetLeft && targetToLookedAt.x > 0))
{
m_lookedAtAngleY = 180 - m_lookedAtAngleY;
}
Vector3 targetToCamera = m_transform.position - m_target.position;
m_cameraAngleX = Vector3.Angle(Vector3.ProjectOnPlane(targetToCamera, m_target.up), m_target.forward);
m_cameraAngleY = Vector3.Angle(Vector3.ProjectOnPlane(targetToCamera, m_target.up), targetToCamera);
if ((m_isTargetLeft && targetToCamera.x < 0) || (!m_isTargetLeft && targetToCamera.x > 0))
{
m_cameraAngleY = 180 - m_cameraAngleY;
}
}
#if UNITY_EDITOR
void Update()
{
if (m_isChangeDefaultValue)
{
SetCameraDefaultPosition();
m_isChangeDefaultValue = false;
}
}
#endif
void LateUpdate()
{
if (m_isChange)
{
//更新位置
CalculateLookedAtAndCameraOffset();
m_isChange = false;
m_lookedAt.position = m_target.position + m_lookedAtOffset;
m_transform.position = m_target.position + m_cameraOffset;
m_transform.LookAt(m_lookedAt);
}
}
//根据角度计算偏移相机和m_lookedAt相对m_target的偏移
void CalculateLookedAtAndCameraOffset()
{
m_lookedAtOffset = CalculateOffset(m_targetToLookedAtDistance, m_lookedAtAngleX, m_lookedAtAngleY);
m_cameraOffset = CalculateOffset(m_targetToCameraDistance, m_cameraAngleX, m_cameraAngleY);
if (!m_isTargetLeft)
{
m_lookedAtOffset.x = -m_lookedAtOffset.x;
m_cameraOffset.x = -m_cameraOffset.x;
}
}
//可以想象成在一个球面上运动,根据xy的角度和球的半径,计算球面上的一个点相对球心的偏移
//先计算出高度y,然后就是在一个圆面上去计算xz
Vector3 CalculateOffset(float distance, float x, float y)
{
Vector3 offset;
offset.y = distance * Mathf.Sin(y * ANGLE_CONVERTER);
float newRadius = distance * Mathf.Cos(y * ANGLE_CONVERTER);
offset.x = newRadius * Mathf.Sin(x * ANGLE_CONVERTER);
offset.z = newRadius * Mathf.Cos(x * ANGLE_CONVERTER);
return offset;
}
//水平方向同时修改m_lookedAtAngleX和m_cameraAngleX,即m_lookAt和相机一同绕着m_target公转
//垂直方向,只修改m_cameraAngleY,即只有相机会有高低变化
public void Rotate(Vector2 v)
{
if (v.x != 0)
{
if (m_isTargetLeft)
{
m_lookedAtAngleX -= v.x;
m_cameraAngleX -= v.x;
}
else
{
m_lookedAtAngleX += v.x;
m_cameraAngleX += v.x;
}
}
if (v.y != 0)
{
m_cameraAngleY += v.y;
m_cameraAngleY = m_cameraAngleY > m_cameraMaxAngleY ? m_cameraMaxAngleY : m_cameraAngleY;
m_cameraAngleY = m_cameraAngleY < m_cameraMinAngleY ? m_cameraMinAngleY : m_cameraAngleY;
}
m_isChange = true;
}
//同时缩放m_target到相机和m_target到m_lookedAt的距离
public void ChangeDistance(float value)
{
if (value > 0)
{
if (m_targetToCameraDistance < m_cameraMaxDistance)
{
m_targetToCameraDistance += m_targetToCameraDistance * m_changeDistanceRatio;
m_targetToLookedAtDistance += m_targetToLookedAtDistance * m_changeDistanceRatio;
}
}
else
{
if (m_targetToCameraDistance > m_cameraMinDistance)
{
m_targetToCameraDistance -= m_targetToCameraDistance * m_changeDistanceRatio;
m_targetToLookedAtDistance -= m_targetToLookedAtDistance * m_changeDistanceRatio;
}
}
m_isChange = true;
}
}
然后我们将这个组件挂载在场景的Camera上,同时写个组件来控制UI来调用Camera的旋转和距离缩放
using UnityEngine;
using UnityEngine.EventSystems;
public class UIRotate : MonoBehaviour, IBeginDragHandler, IDragHandler
{
[SerializeField]CameraAndLookAtRotateWithTarget m_controller;
Vector2 m_lastPosition;
public void OnBeginDrag(PointerEventData eventData)
{
m_lastPosition = eventData.position;
}
public void OnDrag(PointerEventData eventData)
{
//屏幕拖动
m_controller.Rotate(m_lastPosition - eventData.position);
m_lastPosition = eventData.position;
}
void Update()
{
//鼠标滚轮
if (Input.GetAxis("Mouse ScrollWheel") < 0)
{
m_controller.ChangeDistance(1);
}
if (Input.GetAxis("Mouse ScrollWheel") > 0)
{
m_controller.ChangeDistance(-1);
}
}
}
挂载在UIPanel上即可。