前言
現在很多遊戲都會在主頁中顯示一個場景,然後玩家的人物會在場景當中。這個時候往往會需要一些旋轉功能,來方便玩家查看人物的各個角度。
一般的做法就是人物自身轉動,攝像機不動,這樣用戶無法用其他視角觀察到整個場景。另一種是人物不動,相機圍繞人物,或者某個點轉動,這種做法的缺點在於要麼人物在屏幕中央(看向人物),要麼會在屏幕左右亂跑(看向人物邊上某個點),會侷限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上即可。