微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

《学Unity的猫》——第七章:Transform的魔力,超越光速的移动

简介:我是一名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)
transform.position = Vector3.zero;
//从坐标(0,0)移动到(241,0),根据上面的计算,这个移动已经超过光速了
transform.position = new Vector3(241, 0, 0);

皮皮:“真是不可思议呀,电脑的运行速度这么快。”
我:“不过这是虚拟世界里的速度,真实的物理世界,光速还是无法超越的,另外需要注意一点,不同计算机的cpu主频不同,运行速度不同,比如上面执行10000次的position操作耗时8毫秒,可能别的性能差一点的电脑就要耗时14毫秒,那么最后算出值就不同了。”
皮皮:“刚刚看代码,里面设置position坐标要通过Transform对象,这个Transform是什么呀?”

7.2 初识Transform类

我:“TransformUnity中非常非常重要的一个类,所有的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);

设置局部坐标:localPosition

// 设置局部坐标
transform.localPosition = new Vector3(100, 0);

皮皮:“什么是世界坐标和局部坐标呀?”
我:“世界坐标就是以世界坐标系为参考的坐标,局部坐标(或叫本地坐标)就是以本地坐标系为参考的坐标。三维坐标系由x、y、z个轴组成,我们一般分为左手坐标系和右手坐标系,老皮,你看看Unity采用的是左手坐标系还是右手坐标系呢?”

在这里插入图片描述


皮皮举起了它的爪子,分不清左右。
我:“哈哈哈,我直接告诉你吧,Unity采用的是左手坐标系。不管是世界坐标系还是局部坐标系,它们都是左手坐标系。通过坐标系,我们就可以使用坐标值表示任意一个位置。世界有一个坐标系,游戏对象本身也有一个坐标系,游戏对象可以嵌套形成父子节点关系,当游戏对象有父节点的时候,相对父节点的坐标就是局部坐标localPosition,它相对世界坐标系的坐标就是世界坐标position。当游戏对象没有父节点的时候,或者可以理解为它此时的父节点就是世界,此时局部坐标localPosition就会等于世界坐标position。”
皮皮指着Inspector视图问:“那Inspector视图中的Position到底是世界坐标还是局部坐标呀?”

在这里插入图片描述


我:“Inspector视图中的Position显示的是局部坐标,如果游戏对象没有父节点,那么局部坐标就会等于世界坐标,此时Position显示的既是局部坐标也是世界坐标。”
皮皮:“那RotationScale也是同理吗?”
我:“是的,你已经学会触类旁通了呀,不错不错。”

7.3.2 设置旋转角度

设置旋转角度有两种方式,一种是欧拉角,一种是四元数。
设置局部欧拉角旋转:localEulerAngles

// 设置局部欧拉角
transform.localEulerAngles = new Vector3(100, 0);

设置局部四元数旋转:localRotation

// 设置局部四元数旋转
transform.localRotation = Quaternion.Euler(new Vector3(100, 0));

皮皮:“为什么要弄两套旋转方法呢?”
我:“这里就要理解欧拉角的原理以及它的问题,它的主要问题会引发万向锁问题,还有,它做差值运算不合理。为了解决这些问题,人们发明了四元数来表示旋转。后面我再单独讲讲这部分的内容,这里你只需看懂如何通过代码设置旋转角度即可。”

7.3.3 设置缩放

设置缩放,为原始的2

transform.localScale = Vector3.one * 2;

皮皮:“我看到缩放还有一个lossyScale,它与localScale有什么关系呢?”
我:“localScale是本地坐标系中的缩放,lossyScale是世界坐标系中的缩放,类似于localPositionposition的关系。不过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 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, Transform relativeto);
public void Translate(Vector3 translation, Transform relativeto);

示例:
向本地坐标系的x轴正方向移动1

m_selfTrans.Translate(1, 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 zAngle, [DefaultValue("Space.Self")] Space relativeto);
public void Rotate(Vector3 axis, float angle, float angle);

示例:
围绕本地坐标系的y轴顺时针旋转1

transform.Rotate(0, 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 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 z);

示例:
计算世界坐标系下的forward向量在本地坐标系下的向量

Vector3 localForward = transform.InverseTransformDirection(Vector3.forward);
7.4.4 坐标转换计算

本地坐标 转 世界坐标
函数原型:

public Vector3 TransformPoint(float x, float z);
public Vector3 TransformPoint(Vector3 position);

示例:
计算局部坐标(10,0)在世界坐标系下的坐标

Vector3 worldPos = transform.TransformPoint(10, 0);

世界坐标 转 本地坐标
函数原型:

public Vector3 InverseTransformPoint(float x, float z);
public Vector3 InverseTransformPoint(Vector3 position);

示例:
计算世界坐标系下的(10,0)在本地坐标系下的坐标

Vector3 localPos = transform.InverseTransformPoint(10, 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 的子节点");
}

《学Unity的猫》——第八章:Unity预设文件,无限纸团喷射机

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐