在 Unity 中为物体旋转提供了各种 API ,例如 Rotate、RotateAround、LookAt 等方法。但为了避免万向节死锁的问题,一般使用四元数来表示物体的旋转。
而接下来的旋转方法我将不再区分Vector和Transform实现的区别,重点来看方法本身。
目录
一、基础旋转
图0-1 物体设置
①Rotate()
public void Rotate (Vector3 eulers, Space relativeTo= Space.Self);
应用一个围绕 Z 轴旋转 eulerAngles.z 度、围绕 X 轴旋转 eulerAngles.x 度、围绕 Y 轴旋转 eulerAngles.y 度(按此顺序)的旋转。
旋转采用欧拉角形式的 Vector3 参数。第二个参数是旋转轴,可以将其设置为本地轴 (Space.Self) 或全局轴 (Space.World)。旋转以欧拉角度数表示。
参数从左到右分别是,Vector类型的旋转角度和物体轴类设置。
知晓参数和使用方法后,我们将场景中的两个物体提前设置好,为了看出轴不同的设置带来的影响,把两个物体旋转一定的角度如图0-1。具体实现效果如图1-1~2,使用如下代码:
[Header("旋转参数")]
public float rotationSpeed;
public GameObject aroundPoint; //围绕中心点
[Header("旋转对象")]
public GameObject rotateObj1;
public GameObject rotateObj2;
void Update()
{
//SelfRotate1();
SelfRotate2();
}
void SelfRotate1()
{
//rotateObj1.transform.Rotate(xAngle, yAngle, zAngle, Space.World); //直接赋值Transform变换
//rotateObj2.transform.Rotate(xAngle, yAngle, zAngle, Space.Self);
rotateObj1.transform.Rotate(Vector3.forward * rotationSpeed * Time.deltaTime, Space.World); //通过V3类特定某个轴
rotateObj2.transform.Rotate(Vector3.forward * rotationSpeed * Time.deltaTime, Space.Self);
}
图1-1 Space.World
Rotate方法中的Space.World坐标轴设置,世界坐标的设置让物体是围绕着世界坐标轴的Z轴旋转。
图1-2 Space.Self
Rotate方法中的Space.Self坐标轴设置,与前一种不同,可以看出来很明显的围绕自己的Z坐标轴旋转。
②RotateAround()
public void RotateAround (Vector3 point, Vector3 axis, float angle);
将变换围绕穿过世界坐标中的 point
的 axis
旋转 angle
度。
这会修改变换的位置和旋转。
参数从左到右分别为应围绕的点,围绕点的哪个轴,旋转多少度。
使用设置好的两个物体和一个围绕中心点看一下效果,见图2-1,及以下代码段:
public GameObject aroundPoint; //围绕中心点
void SelfRotate2()
{
rotateObj1.transform.RotateAround(aroundPoint.transform.position, Vector3.up, rotationSpeed * Time.deltaTime);
rotateObj2.transform.RotateAround(aroundPoint.transform.position, Vector3.up, rotationSpeed * Time.deltaTime);
}
图2-1 RotateAround()
可以看到,两个物体都围绕着设置好的中心点旋转类似星球环绕的效果。
③LookAt()
public void LookAt (Transform target);
public void LookAt (Transform target, Vector3 worldUp= Vector3.up);
旋转变换,使向前矢量指向 target
的当前位置。
然后,它旋转变换以将其向上方向矢量指向 worldUp
矢量暗示的方向。 如果省略 worldUp
参数,该函数将使用世界空间的 Y 轴。 worldUp
只是一个提示矢量。如果向前方向垂直于 worldUp
,则旋转的向上矢量将仅匹配 worldUp
矢量。
参数从左到右分别为看向目标,看向轴向。
我们让物体1以Z轴始终看向物体2,物体2保持环绕旋转,效果见图3-1,及以下代码段。
void SelfRotate3()
{
rotateObj1.transform.LookAt(rotateObj2.transform);
//rotateObj1.transform.LookAt(rotateObj2.transform, rotateObj2.transform.up);
rotateObj2.transform.RotateAround(aroundPoint.transform.position, Vector3.up, rotationSpeed * Time.deltaTime);
}
图3-1 LookAt()
其中注意,无论如何都是物体的Z轴始终指向物体,而具体指向后物体想以怎么样的状态保持指向还需要看第二个参数,可以看到上图3-1中看向后是默认X轴向上的,但如果这里的worldUp设置为rotateObj2.transform.up,则意味着以物体Y轴作为世界Y轴朝向,其他轴向类似见下图3-2。
图3-2 worldUp设置
二、四元数旋转
①Euler()
public static Quaternion Euler (float x, float y, float z);
返回一个围绕 Z 轴旋转 z 度、围绕 X 轴旋转 x 度、围绕 Y 轴旋转 y 度的旋转。
我们将前次课的行为树用上,定义一个物体来回运动。效果见图1-1,及以下代码段。
/// <summary>
/// Euler()
/// </summary>
void QuaternionRotate1()
{
eulerRotate.y += rotationSpeed * Time.deltaTime;
rotateObj1.transform.rotation = Quaternion.Euler(eulerRotate);
rotateObj2.transform.rotation = Quaternion.Euler(eulerRotate);
}
图1-1 Euler()
②Lerp()
public static Quaternion Lerp (Quaternion a, Quaternion b, float t);
在 a
和 b
之间插入 t
,然后对结果进行标准化处理。参数 t
被限制在 [0, 1] 范围内。
该方法比 Slerp 快,但如果旋转相距很远,其视觉效果也更糟糕。
参数从左到右分别为指定Z轴要对齐的物体,世界upwards方向的V3值。
效果见图2-1,及以下代码段。
/// <summary>
/// Lerp()
/// </summary>
void QuaternionRotate2()
{
rotateObj1.transform.rotation = Quaternion.Lerp(from.rotation, to.rotation, rotationSpeed * Time.time);
rotateObj2.transform.rotation = Quaternion.Lerp(from.rotation, to.rotation, rotationSpeed * Time.time);
}
图2-1 Lerp()
③LookRotation()
public static Quaternion LookRotation (Vector3 forward, Vector3 upwards= Vector3.up);
使用指定的 forward
和 upwards
方向创建旋转。
Z 轴将与forward
对齐,X 轴与 forward
和 upwards
之间的差积对齐,Y 轴与 Z 和 X 之间的差积对齐。如果 forward
或 upwards
量值为零,则返回恒等。 如果 forward
和 upwards
共线,则返回恒等。
参数从左到右分别为指定Z轴要对齐的物体,世界upwards方向的V3值。
定义两个物体来回运动,将应对齐的Z轴改为两物体间的位置Z值,upwards就设置默认的Y轴up。效果见图3-1,及以下代码段。
/// <summary>
/// LookRotation()
/// </summary>
void QuaternionRotate3()
{
Vector3 relativePos = from.position - to.position; //相对位置
rotateObj1.transform.rotation = Quaternion.LookRotation(relativePos, Vector3.up);
rotateObj2.transform.rotation = Quaternion.LookRotation(relativePos, Vector3.up);
}
图3-1 LookRotation()
三、应用案例
①第一人称相机旋转
[Header("第一人称")]
public Camera firstCamera;
public float mouseSensitivityX = 5.0f;
public float mouseSensitivityY = 5.0f;
public float minCameraY = -30.0f;
public float maxCameraY = 30.0f;
private float currentRotationY;
private float currentRotationX;
/// <summary>
/// 第一人称角色视角
/// </summary>
private void FirstPlayerCameraRotate() {
currentRotationX += Input.GetAxis("Mouse X") * mouseSensitivityX;
currentRotationY += Input.GetAxis("Mouse Y") * mouseSensitivityY;
currentRotationY = AngleLimit(currentRotationY, minCameraY, maxCameraY);
firstCamera.transform.eulerAngles = new Vector3(-currentRotationY, currentRotationX, 0);
}
/// <summary>
/// 视角角度限制
/// </summary>
/// <param name="rotationLimited">应限制的旋转轴</param>
/// <param name="min">最小值</param>
/// <param name="max">最大值</param>
/// <returns>可到达的角度范围</returns>
private float AngleLimit(float rotationLimited, float min, float max) {
if (currentRotationY < min) {
return min;
}
if (currentRotationY > max) {
return max;
}
return rotationLimited;
}
代码段中可以看到分成了两个函数,第一人称视角主要控制函数FirstPlayerCameraRotate()及AngleLimit()视角角度限制函数。
FirstPlayerCameraRotate()控制函数就比较简单,通过对"Mouse X"和"Mouse Y"轴向的获取,乘以提前设置好的鼠标灵敏度赋值给最新旋转XY。如果好奇这里为什么是对-currentRotationY当作V3的x轴值,在Unity中去试一下Transfom Rotation的值看下效果就明白了。
AngleLimit()视角角度限制函数则是想实现对视角上下有限制,将当前最新旋转值currentRotationY与最大值和最小值比较,若大于最大值则不能继续旋转,小于最小值同样。具体实现效果如下图1
图1 第一人称视角
②第三人称相机环绕
[Header("第三人称")]
public Camera thirdCamera;
public GameObject followObj;
public float rotateSpeedX = 10.0f;
public float rotateSpeedY = 10.0f;
private Quaternion curRotation;
private float currentMouseX = 0.0f;
private float currentMouseY = 0.0f;
void Start()
{
currentRotationX = thirdCamera.transform.eulerAngles.x;
currentRotationY = thirdCamera.transform.eulerAngles.y;
}
/// <summary>
/// 第三人称角色视角
/// </summary>
private void ThirdPlayerCameraRotate()
{
try {
if (followObj != null)
{
currentMouseX += Input.GetAxis("Mouse X") * rotateSpeedX;
currentMouseY += Input.GetAxis("Mouse Y") * rotateSpeedY;
curRotation = Quaternion.Euler(currentMouseY, currentMouseX, 0); //将V3转化为Quaternion
thirdCamera.transform.RotateAround(followObj.transform.position, Vector3.up, currentMouseX);
}
}
catch {
Debug.Log("出了点问题,可能是追随目标没设置");
}
}
代码段中可以看到只有一个函数ThirdPlayerCameraRotate()第三人称控制函数。
首先函数需要对应的跟随物体followObj,其次同样是对"Mouse X"和"Mouse Y"轴向的获取,利用上述Euler函数将V3数值转化为四元素Quaternion数值,后就可以使用RotateAround这一类Transform四元素挂钩的函数了。具体效果见下图2
图2 第三人称环绕旋转
③第三人称平滑旋转缩放与跟随
[Header("相机")]
public Camera thirdCamera;
public GameObject target;
[Header("相机缩放")]
public float zoomSpeed = 2.0f;
public float maxDistance = 15.0f;
public float minDistance = 2.0f;
private float cameraDistance;
private Vector3 cameraPosition;
[Header("相机旋转")]
public float rotateSpeedX;
public float rotateSpeedY;
public float maxRotationY = 60.0f;
public float minRotationY = -10.0f;
private float rotateY;
private float rotateX;
private Quaternion cameraRotation;
[Header("平滑设置")]
public bool isDamping = false;
public float damping = 5.0f;
void Update()
{
ThirdCameraControll();
}
/// <summary>
/// 第三相机控制
/// </summary>
void ThirdCameraControll() {
//旋转
rotateX += Input.GetAxis("Mouse X") * rotateSpeedX * Time.deltaTime;
rotateY += Input.GetAxis("Mouse Y") * rotateSpeedY * Time.deltaTime;
rotateY = AngleLimit(rotateY, minRotationY, maxRotationY);
cameraRotation = Quaternion.Euler(rotateY, rotateX, 0);
//缩放
cameraDistance -= Input.GetAxis("Mouse ScrollWheel") * zoomSpeed;
cameraDistance = Mathf.Clamp(cameraDistance, minDistance, maxDistance);
//位置控制
cameraPosition = cameraRotation * new Vector3(0.0f, 0.0f, -cameraDistance) + target.transform.position;
//平滑处理
if (isDamping)
{
thirdCamera.transform.position = Vector3.Lerp(thirdCamera.transform.position ,cameraPosition, damping * Time.deltaTime);
thirdCamera.transform.rotation = Quaternion.Lerp(thirdCamera.transform.rotation, cameraRotation, damping * Time.deltaTime);
}
else {
thirdCamera.transform.position = cameraPosition;
thirdCamera.transform.rotation = cameraRotation;
}
}
/// <summary>
/// 角度限制
/// </summary>
/// <param name="LimitedValue"></param>
/// <param name="min"></param>
/// <param name="max"></param>
/// <returns></returns>
private float AngleLimit(float LimitedValue, float min, float max) {
if (LimitedValue < -360) return LimitedValue += 360;
if (LimitedValue > 360) return LimitedValue -= 360;
return Mathf.Clamp(LimitedValue, min, max);
}
最终效果入下图3
图3 线性插值和欧拉的处理
至于万向节死锁问题,我这不去讲述了感兴趣的小伙伴们可以自行学习。
喜欢的话还请关注B站账号:波仔羔。好了如果觉得对你有帮助的话不如给个点赞加关注,如果有问题也欢迎评论或私聊我解决哦。