文章目录
简介:我是一名Unity
游戏开发工程师,皮皮是我养的猫,会讲人话,它接到了喵星的特殊任务:学习编程,学习Unity
游戏开发。
于是,发生了一系列有趣的故事。
7.1 超过光速的移动
我:“皮皮,你知道真空光速是多少吗?”
皮皮:“你以为我是百科全书呀?不过我知道,目前所知的物理定律里,光速是无法超越的,连我们猫族也无法超越这个速度。”
我:“真空中的光速是299792458
米/秒,大约300000
米/毫秒。在Unity
中,你信不信我可以超越光速?”
皮皮:“真的假的?”
我打开Unity
,说:“真的,不信我可以证明给你看。”
首先,创建一个Cube
。
接着,创建一个脚本TransformTest.cs
。
代码如下:
using System.Diagnostics;
using UnityEngine;
public class TransformTest : MonoBehaviour
{
void Update()
{
// 检测空白键按下
if (Input.GetKeyDown(KeyCode.Space))
{
// 使用Stopwatch测量运行时间
Stopwatch sw = new Stopwatch();
// 时间测量开始
sw.Start();
// 执行设置坐标
SetPos();
// 时间测量结束
sw.Stop();
// 输出日志
UnityEngine.Debug.Log("总耗时: " + sw.ElapsedMilliseconds);
}
}
/// <summary>
/// 设置坐标
/// </summary>
void SetPos()
{
// 临时缓存transform对象
Transform selfTransform = transform;
// 循环执行10000次,看总耗时,在计算单次的运行时间
for (int i = 0; i < 100000; ++i)
{
selfTransform.position = Vector3.one * i;
}
}
}
注:上面代码中,我用到了
Stopwatch
这个类,用它可以精确测量函数的运行时间。
将TransformTest
脚本挂到Cube
上。
运行Unity
,按下空白键,可以在console
窗口中看到日志输出。
由此可得,执行10000
次的position
操作,耗时8
毫秒,折算一下,1
毫秒可以执行position
操作1250
次,而真空光速是约300000
米/毫秒,300000
除以1250
等于240
,也就是说,只要一次position
操作的距离超过240
即可超过光速啦,如下:
// 设置初始坐标为Vector3.zero,即(0, 0, 0)
transform.position = Vector3.zero;
//从坐标(0, 0, 0)移动到(241, 0, 0),根据上面的计算,这个移动已经超过光速了
transform.position = new Vector3(241, 0, 0);
皮皮:“真是不可思议呀,电脑的运行速度这么快。”
我:“不过这是虚拟世界里的速度,真实的物理世界,光速还是无法超越的,另外需要注意一点,不同计算机的CPU
主频不同,运行速度不同,比如上面执行10000
次的position
操作耗时8
毫秒,可能别的性能差一点的电脑就要耗时14
毫秒,那么最后算出值就不同了。”
皮皮:“刚刚看代码,里面设置position
坐标要通过Transform
对象,这个Transform
是什么呀?”
7.2 初识Transform类
我:“Transform
是Unity
中非常非常重要的一个类,所有的GameObject
对象都有一个Transform
组件,你创建一个空物体的时候,就会看到它就已经自带了一个Transform
组件”
我指着Inspector
视图中的Transform
组件,“你猜猜这个Transform
组件到底是用来干嘛的?”
皮皮:“我看出来了,它是用来设置坐标、旋转角度和缩放的。”
我:“没错,一个GameObject
必然会有一个坐标、旋转角度和缩放,所以Transform
组件是必须的。不过呢,后面Unity
官方引入了ECS
框架,在ECS
框架中,一个实体Entity
可以没有Transform
。现在你只需记住,Transform
组件可以用来设置游戏对象的坐标、旋转角度和缩放。”
更深入一些,我们看一下Transform
常用的属性和函数吧。
7.3 Transform的属性
属性 | 数据类型 | 描述 |
---|---|---|
position | Vector3 | 在世界空间中的坐标 |
localPosition | Vector3 | 相对于父节点的局部坐标,如果没有父节点,则localPosition等于position |
eulerAngles | Vector3 | 世界坐标系中的旋转(欧拉角) |
localEulerAngles | Vector3 | 相对于父节点的局部旋转(欧拉角),如果没有父节点,则localEulerAngles等于eulerAngles |
rotation | Quaternion | 世界坐标系中的旋转(四元数) |
localRotation | Quaternion | 相对于父节点的旋转(四元数),如果没有父节点,则localRotation等于rotation |
right | Vector3 | 局部坐标系的x轴方向向量 |
up | Vector3 | 局部坐标系的y轴方向向量 |
forward | Vector3 | 局部坐标系的z轴方向向量 |
localScale | Vector3 | 相对于父节点的缩放比例 |
parent | Transform | 父节点的Transform组件 |
root | Transform | 根节点的Transform组件 |
childCount | int | 子节点数量 |
lossyScale | Vector3 | 全局缩放比例(只读) |
worldToLocalMatrix | Matrix4x4 | 矩阵变换的点从世界坐标转为自身坐标(只读) |
localToWorldMatrix | Matrix4x4 | 矩阵变换的点从自身坐标转为世界坐标(只读) |
皮皮:“这么多我该怎么记呀?”
我:“多写多练,用多了你就记住啦。我给你示范几个例子吧。”
7.3.1 设置坐标
设置世界坐标:position
// 设置世界坐标
transform.position = new Vector3(100, 0, 0);
设置局部坐标:localPosition
// 设置局部坐标
transform.localPosition = new Vector3(100, 0, 0);
皮皮:“什么是世界坐标和局部坐标呀?”
我:“世界坐标就是以世界坐标系为参考的坐标,局部坐标(或叫本地坐标)就是以本地坐标系为参考的坐标。三维坐标系由x、y、z
个轴组成,我们一般分为左手坐标系和右手坐标系,老皮,你看看Unity
采用的是左手坐标系还是右手坐标系呢?”
皮皮举起了它的爪子,分不清左右。
我:“哈哈哈,我直接告诉你吧,Unity
采用的是左手坐标系。不管是世界坐标系还是局部坐标系,它们都是左手坐标系。通过坐标系,我们就可以使用坐标值表示任意一个位置。世界有一个坐标系,游戏对象本身也有一个坐标系,游戏对象可以嵌套形成父子节点关系,当游戏对象有父节点的时候,相对父节点的坐标就是局部坐标localPosition
,它相对世界坐标系的坐标就是世界坐标position
。当游戏对象没有父节点的时候,或者可以理解为它此时的父节点就是世界,此时局部坐标localPosition
就会等于世界坐标position
。”
皮皮指着Inspector
视图问:“那Inspector
视图中的Position
到底是世界坐标还是局部坐标呀?”
我:“Inspector
视图中的Position
显示的是局部坐标,如果游戏对象没有父节点,那么局部坐标就会等于世界坐标,此时Position
显示的既是局部坐标也是世界坐标。”
皮皮:“那Rotation
和Scale
也是同理吗?”
我:“是的,你已经学会触类旁通了呀,不错不错。”
7.3.2 设置旋转角度
设置旋转角度有两种方式,一种是欧拉角,一种是四元数。
设置局部欧拉角旋转:localEulerAngles
// 设置局部欧拉角
transform.localEulerAngles = new Vector3(100, 0, 0);
设置局部四元数旋转:localRotation
// 设置局部四元数旋转
transform.localRotation = Quaternion.Euler(new Vector3(100, 0, 0));
皮皮:“为什么要弄两套旋转方法呢?”
我:“这里就要理解欧拉角的原理以及它的问题,它的主要问题会引发万向锁问题,还有,它做差值运算不合理。为了解决这些问题,人们发明了四元数来表示旋转。后面我再单独讲讲这部分的内容,这里你只需看懂如何通过代码设置旋转角度即可。”
7.3.3 设置缩放
设置缩放,为原始的2
倍
transform.localScale = Vector3.one * 2;
皮皮:“我看到缩放还有一个叫lossyScale
,它与localScale
有什么关系呢?”
我:“localScale
是本地坐标系中的缩放,lossyScale
是世界坐标系中的缩放,类似于localPosition
与position
的关系。不过lossyScale
是只读的,我们不能对它进行赋值,实际项目中lossyScale
比较少用到呢。”
7.3.4 设置朝向
设置物体的x
轴与向量(1, 1, 0)
朝向一致。
transform.right = new Vector3(1, 1, 0);
设置物体的y
轴与世界坐标系的y
轴朝向一致。
transform.up = Vector3.up;
设置物体的z
轴与主摄像机的z
轴反方向。
transform.forward = -Camera.main.transform.forward;
7.3.5 设置父节点
// 创建父游戏对象 parentGo
GameObject parentGo = new GameObject("parentGo");
// 创建子游戏对象 childGo
GameObject childGo = new GameObject("childGo");
// 设置 childGo 的父对象为 parentGo
childGo.transform.parent = parentGo.transform;
注意,parent
是一个Transform
,不是GameObject
哦。
7.4 Transform的函数
函数 | 说明 |
---|---|
Translate | 用来移动物体的函数 |
Rotate | 用来旋转物体的函数 |
RotateAround | 让物体以某一点为轴心成圆周运动 |
LookAt | 让物体的z轴看向目标物体 |
TransformDirection | 从本地坐标到世界坐标变换方向 |
InverseTransformDirection | 从世界坐标到本地坐标变换方向,与TransformDirection相反 |
TransformPoint | 将基于当前游戏对象的局部坐标转化为基于世界坐标系的坐标 |
InverseTransformPoint | 将基于世界坐标系的坐标转换为基于当前对象的局部坐标 |
DetachChildren | 分离子物体,所有子物体解除父子关系 |
Find | 通过名字查找子物体并返回它 |
SetParent | 设置父节点 |
IsChildOf | 判断自身是否是某个Transform的子节点 |
皮皮:“按照惯例,show me code
。”
7.4.1 移动物体
函数原型:
public void Translate(float x, float y, float z);
public void Translate(float x, float y, float z, [DefaultValue("Space.Self")] Space relativeTo);
public void Translate(Vector3 translation);
public void Translate(Vector3 translation, [DefaultValue("Space.Self")] Space relativeTo);
public void Translate(float x, float y, float z, Transform relativeTo);
public void Translate(Vector3 translation, Transform relativeTo);
示例:
向本地坐标系的x
轴正方向移动1
米
m_selfTrans.Translate(1, 0, 0, Space.Self);
注:Unity中,坐标轴上的1个单位长度的距离表示真实世界中的1米距离
皮皮:“举手提问,这个Translate
移动与直接设置localPosition
坐标有什么区别呢?”
我:“设置localPosition
是直接设置最终的目标位置,而Translate
方法相当于是一个增量操作,是基于当前坐标进行一个增量移动。”
皮皮:“提问,参数Space relativeTo
是什么意思呀?”
我:“这个是参考系,Space
是一个枚举,很好理解,Space.World
是以世界坐标系为参考,Space.Self
是以本地坐标系为参考。”
public enum Space
{
World = 0,
Self = 1
}
我:“皮皮,现在你猜猜,下面这个重载函数的第二个参数Transform relativeTo
是表示什么?”
public void Translate(Vector3 translation, Transform relativeTo);
皮皮:“不用猜,参考系,以它的坐标系为参考。”
我:“厉害哦,变通能力越来越强了。”
7.4.2 旋转物体
函数原型:
public void Rotate(float xAngle, float yAngle, float zAngle);
public void Rotate(Vector3 eulers, [DefaultValue("Space.Self")] Space relativeTo);
public void Rotate(Vector3 eulers);
public void Rotate(float xAngle, float yAngle, float zAngle, [DefaultValue("Space.Self")] Space relativeTo);
public void Rotate(Vector3 axis, float angle, [DefaultValue("Space.Self")] Space relativeTo);
public void Rotate(Vector3 axis, float angle);
示例:
围绕本地坐标系的y
轴顺时针旋转1
度
transform.Rotate(0, 1, 0);
我:“老皮,考考你,这个Rotate
方法与直接设置localEulerAngles
有什么区别?”
皮皮:“你这么问,我就知道了,Rotate
方法是增量操作,上面讲Translate
的时候说过。”
我:“看来我不用多解释啦,聪明聪明。”
皮皮:“我看到还有一个RotateAround
方法,它与Rotate
有什么区别呢?”
函数原型:
public void RotateAround(Vector3 point, Vector3 axis, float angle);
public void RotateAround(Vector3 axis, float angle);
我:“你知道地球自转和公转吗?”
皮皮:“知道呀,我们古老的喵星也有自转和公转,在遥远的拉姆达星系,喵星围绕着巨大的鲁特恒星旋转,在大约4000千万年前… … ”
皮皮突然捂住自己的嘴,“糟了,泄露机密了。”
我:“哈哈哈,不要怕,我不会出卖你们的,喵星人早已经是人类的好朋友了,而且你也没说你们星系的具体坐标呢。”
皮皮:“打住,回归正题!”
我:“嘛,这个RotateAround
就是类似公转的效果。”
RotateAround
也是一个增量操作,参数point
是围绕的坐标点,参数axis
是旋转的轴,angle
是旋转的角度。
例:
using UnityEngine;
public class TransformTest : MonoBehaviour
{
private Transform m_selfTrans;
void Awake()
{
// 缓存transform
m_selfTrans = transform;
}
void Update()
{
// 围绕中心点Vector3.zero, 绕着轴Vector3.up旋转,旋转角度1度
m_selfTrans.RotateAround(Vector3.zero, Vector3.up, 1);
}
}
运行效果:
7.4.3 方向转换计算
本地方向向量 转 世界方向向量
函数原型:
public Vector3 TransformDirection(float x, float y, float z);
public Vector3 TransformDirection(Vector3 direction);
示例:
计算本地坐标下的forward
向量(即本地坐标系的z
轴的正方向向量)在世界坐标系下的向量
Vector3 worldObjForward = transform.TransformDirection(transform.forward);
世界方向向量 转 本地方向向量
函数原型:
public Vector3 InverseTransformDirection(Vector3 direction);
public Vector3 InverseTransformDirection(float x, float y, float z);
示例:
计算世界坐标系下的forward
向量在本地坐标系下的向量
Vector3 localForward = transform.InverseTransformDirection(Vector3.forward);
7.4.4 坐标转换计算
本地坐标 转 世界坐标
函数原型:
public Vector3 TransformPoint(float x, float y, float z);
public Vector3 TransformPoint(Vector3 position);
示例:
计算局部坐标(10, 0, 0)
在世界坐标系下的坐标
Vector3 worldPos = transform.TransformPoint(10, 0, 0);
世界坐标 转 本地坐标
函数原型:
public Vector3 InverseTransformPoint(float x, float y, float z);
public Vector3 InverseTransformPoint(Vector3 position);
示例:
计算世界坐标系下的(10, 0, 0)
在本地坐标系下的坐标
Vector3 localPos = transform.InverseTransformPoint(10, 0, 0);
7.4.5 查找子物体
函数原型:
public Transform Find(string n);
示例:
创建节点,节点结构如下
root
节点挂TransformTest
脚本,脚本代码如下
using UnityEngine;
public class TransformTest : MonoBehaviour
{
Transform m_selfTrans;
void Awake()
{
// 缓存自身的transform
m_selfTrans = transform;
}
void Start()
{
// 查找a节点
var a = m_selfTrans.Find("a");
// 查找c节点
var c = m_selfTrans.Find("a/b/c");
//查找e节点
var d = m_selfTrans.Find("d");
}
}
7.4.6 判断是否是子节点
函数原型:
public bool IsChildOf([NotNull] Transform parent);
示例:
GameObject a = new GameObject("a");
GameObject b = new GameObject("b");
b.transform.parent = a.transform;
// 设置父节点也可以用SetParent方法
// b.transform.SetParent(a.transform, false);
if(b.transform.IsChildOf(a.transform))
{
Debug.Log("a 是 b 的子节点");
}