写在前面:
写本系列(自用)的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。主要是C#代码部分。
六、四元数
欧拉角具有旋转约定,也就是说,无论你调整角度的顺序是什么,最终物体旋转的顺序都会是约定的旋转序列。最常见用约定是Y-X-Z约定,也就是物体按Y-X-Z的方向进行旋转。
无论你如何更改角度,系统都会重新从初始状态按顺序Y-X-Z进行调整,这和万向锁相似。也就是说,先转动的轴会带动后转动的轴转动,但后转动的轴不能影响先转动的轴。欧拉角会带来万向节死锁的问题,导致在特定情况下丢失一个自由度。
此外,同样的角度能使用不同的欧拉角角度来描述,因此,引入四元数来解决欧拉角的这两个问题。
1、四元数
一个四元数包含一个标量和一个3D向量
[w, v],w为标量,v为3D向量,也就是[w, (x, y, z)],对于给定的任意一个四元数,表示3D空间中的一个旋转量。
(1)四元数的公式
假设绕着n轴旋转β度,n轴为(x,y,z),那么可以构成四元数为:
四元数Q = [cos(β/2),sin(β/2)n] = [cos(β/2),sin(β/2)x,sin(β/2)y,sin(β/2)z]
四元数Q表示绕着n轴,旋转β度的旋转量。
2、Unity中的四元数
可以有两种初始化方法:
1、Quaternion q = new Quaternion(sin(β/2)x,sin(β/2)y,sin(β/2)z, cos(β/2));
2、 Quaternion q2 = Quaternion.AngleAxis(旋转度数, 旋转轴);
一般使用第二种初始化方法:
void Start()
{//绕着x轴转60度Quaternion q = new Quaternion(Mathf.Sin(Mathf.Deg2Rad), 0, 0, Mathf.Cos(30*Mathf.Deg2Rad));GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);obj.transform.rotation = q;Quaternion q2 = Quaternion.AngleAxis(60, Vector3.right);
}
(1)四元数和欧拉角转换
欧拉角转为四元数使用的API是:Quaternion.Euler(),括号内传入需要转化的欧拉角,比如Quaternion.Euler(60, 0, 0),转化出来的四元数是绕x轴转60度的四元数。
四元数转欧拉角使用的API是:.eulerAngles
void Start()
{//欧拉角转四元数,这个就是绕着x轴转60度Quaternion q3 = Quaternion.Euler(60, 0, 0);//四元数转欧拉角print(q2.eulerAngles);
}
最后,需要注意的是,四元数的角度区间是-180~180,因此四元数表示的角度是唯一的。
3、四元数的常用方法
(1)单位四元数
单位四元数表示没有旋转量(角位移),当角度为0或者360度时,对于给定轴都会得到单位四元数。
例如[1,(0, 0, 0)]和[-1, (0, 0, 0)]都是单位四元数,表示没有旋转量。原因是,由四元数Q = [cos(β/2),sin(β/2)n] = [cos(β/2),sin(β/2)x,sin(β/2)y,sin(β/2)z],角度为0时,cos0 = 1, sin0均为0;或是cos180度等于-1,sin180都等于0。
在Unity中提供了表示单位四元数的API:Quaternion.identity
void Start()
{print(Quaternion.identity);testObj.rotation = Quaternion.identity;
}
单位四元数可用于初始化。初始化在0,0,0点,方向为0, 0, 0的物体:
Instantiate(testObj, Vector3.zero, Quaternion.identity);
(2)四元数插值运算
四元数中的Lerp和Slerp只有一些细微差别,由于算法不同,Slerp的效果会好一些,Lerp的效果相比Slerp更快,但当旋转范围较大时,效果不太好,所以建议使用Slerp进行插值运算。与之前的插值运算区别不大,只不过这里插值的是角度,同样演示了两种方法:
public Transform target;
public Transform A;
public Transform B;private Quaternion start;
private float time = 0;void Start()
{start = B.rotation;
}void Update()
{A.rotation = Quaternion.Slerp(A.rotation, target.rotation, Time.deltaTime);time+= Time.deltaTime;B.rotation = Quaternion.Slerp(start,target.rotation, time);
}
(3)向量指向转四元数
向量指向转四元数的API是:Quaternion.LookRotation(向量)
这个方法可以将传入的面朝向量转换为对应四元数角度信息。例如,物体A想要看向物体B,就可以将向量AB传入。这个方法会返回转为AB方向的四元数,这时候将物体A的角度信息改为该四元数即可实现A看向B。
public Transform LookA;
public Transform LookB;void Start()
{Quaternion q = Quaternion.LookRotation(LookB.position - LookA.position);LookA.rotation = q;
}
4、四元数计算
(1)四元数相乘
两个四元数相乘得到一个新的四元数,代表两个旋转量的叠加。旋转相对的坐标系是物体自身的坐标系。
void Start(){Quaternion q = Quaternion.AngleAxis(20, Vector3.up);this.transform.rotation *= q;this.transform.rotation *= q;}
(2)四元数乘向量
四元数乘向量返回一个新向量,相当于将这个向量旋转了对应四元数的旋转量。
void Start(){Vector3 v = Vector3.forward;print(v);//四元数×向量顺序不能改变v = Quaternion.AngleAxis(45, Vector3.up) * v;print(v);}
七、延迟函数
延迟函数就是会延时执行的函数,我们可以自己设定延时要执行的函数和具体延时时间。
1、延迟函数
延迟函数使用的API是:Invoke(),里面传入两个参数,第一个参数是需要延迟执行的函数的名字的字符串,第二个参数是延迟几秒钟执行。
void Start()
{Invoke("DelayDoSomething", 5);
}private void DelayDoSomething()
{print("执行");
}
延迟函数无法调用有参数的函数,需要进行包裹。函数名必须是该脚本上声明的函数。只能和有参函数一样进行包裹。例如下例,只有这样才能执行有参数的延迟函数。
void Start()
{Invoke("DelayDoSomething", 5);}private void DelayDoSomething()
{print("执行");TestFunc(2);
}private void TestFunc(int i)
{print("执行" + i);
}
2、延迟重复执行函数
延迟函数可以一直重复执行,使用的API是:InvokeRepeating(),括号内传入三个参数,第一个参数是重复执行的函数名的字符串,第二个参数是第一次执行延迟多少秒,第三个参数是之后执行延迟多少秒。如下例函数,会在第5秒时执行一次,之后每隔1秒执行一次。
void Start()
{InvokeRepeating("DelayRe", 5, 1);
}private void DelayRe()
{print("重复执行");
}
3、取消延迟函数
(1)取消所有
使用CancelInvoke()后会取消所有延迟函数。
void Start()
{//取消所有CancelInvoke();
}
(2)指定函数名取消
也可以通过传入函数名的字符串指定取消延迟函数。只要取消了指定延迟,不管之前函数开启了多少次,延迟执行都会统一取消。此外,如果没有该延迟函数也不会报错。
void Start()
{CancelInvoke("DelayRe");
}
4、判断是否有延迟函数
使用IsInvoking()可以检查是否存在延迟函数,同样的,也可以传入参数判断是否有指定延迟函数。
void Start()
{if(IsInvoking()){print("存在延迟函数");}if(IsInvoking("DelayDoSomething")){print("存在延迟函数DelayDoSomething");}
}
5、延迟函数受对象失活的影响
脚本挂载的对象失活或是脚本失活,延迟函数只要开启了,就依然可以执行。脚本或者挂载的对象删除了或是脚本销毁了,延迟函数就会失效。
八、协同程序
1、Unity的多线程
Unity是支持多线程的,需要引用命名空间using System.Threading。创建一个新线程使用的是:Thread t = new Thread(),需要注意的是,括号内需要传入一个委托函数,该函数会在新线程中执行。传入函数后,t.Start()即可开启线程。
线程与编辑器的生命周期一样长,要记得手动关闭多线程。可以在对象销毁时运行的生命周期函数OnDestroy()中关闭线程。线程的关闭使用t.Abort()即可。
Thread t;void Start()
{t = new Thread(Test);t.Start();
}private void Test()
{while (true){Thread.Sleep(1000);print("新开线程");}
}private void OnDestroy()
{t.Abort();t = null;
}
此外,需要注意的是,新开线程无法访问Unity相关对象的内容,也就是无法控制场景中的对象,这些对象的逻辑只能在主线程中控制。
2、协同程序
(1)概念
协同程序简称协程,它是假的多线程。它将代码分时执行,不卡主线程,也就是它把可能会让主线程卡顿的耗时的逻辑分时分布执行。主要使用的场景有:异步加载文件,异步下载文件,场景异步加载,批量创建时防止卡顿。
(2)协程和线程的区别
新开一个线程是独立的一个管道,和主线程并行执行;新开协程是在原线程之上开启,进行逻辑分时分步执行。
3、协程的使用
(1)声明
协同程序函数的返回值必须是 IEnumerator或者继承了它的类型。协程函数当中必须使用yield return进行返回。例如:
IEnumerator MyCoroutine(int i, string str)
{print(i);yield return new WaitForSeconds(5f);print(str);
}
该协程函数中 yield return new WaitForSeconds(5f);指的是第二部分延迟5s执行。也就是说在print(i)过后会延迟5s再执行print(str)。
此外,yield return可以写多个,可以把代码分成几个部分来执行。
IEnumerator MyCoroutine(int i, string str)
{print(i);yield return new WaitForSeconds(5f);print(str);yield return new WaitForSeconds(1f);print("end");
}
(2)开启协程
协程函数不能直接调用。有两种调用方式,第一种先创建一个 IEnumerator变量接收返回值,再将返回值传入函数StartCoroutine(返回值)。第二种,直接使用StartCoroutine(函数名)。
void Start()
{IEnumerator ie = MyCoroutine(1, "123");StartCoroutine(ie);StartCoroutine(MyCoroutine(1, "123"));
}
(3)关闭协程
有两种关闭协程的方式:第一种方式为关闭所有协程StopAllCoroutines(),它会关闭你开启的所有协程。第二种方式为关闭指定协程,可以创建Coroutine c1来存储协程返回的变量,而后使用StopCoroutine(c1);关闭指定协程。
void Start(){Coroutine c1 = StartCoroutine(MyCoroutine(1, "123"));Coroutine c2 = StartCoroutine(MyCoroutine(1, "123"));Coroutine c3 = StartCoroutine(MyCoroutine(1, "123"));StopCoroutine(c1);StopAllCoroutines();}
4、yield return
1、下一帧执行:yield return 数字、yield return null。过了一帧后,下一次执行会在Update和LateUpdate之间执行。
2、等待指定秒后执行:yield return new WaitForSeconds(秒)。等待指定秒数后,下一次执行会在Update和LateUpdate之间执行。
3、等待下一个固定物理帧更新时执行:yield return new WaitForFixedUpdate()。等待固定物理帧后,会在FixedUpdate和碰撞检测相关函数之后执行。
4、等待摄像机和GUI渲染完成后执行:yield return newWaitForEndOfFrame()。在LateUpdate之后的渲染相关处理完毕之后执行。
5、一些特殊类型的对象,比如异步加载相关函数返回的对象,后续介绍。一般在Update和LateUpdate之间执行
6、跳出协程 yield break;
5、协程受对象和组件失活销毁的影响
协程开启后,组件和物体销毁,协程不执行;物体失活协程不执行,组件失活协程执行。
6、协同程序原理
(1)协程的本质
协程可以分成两部分,协程函数本体和协程函数调度器。协程函数本体是一个能够中间暂停返回的函数,协程调度器是Unity内部实现的,会在对应时机 帮助我们继续执行协程函数。
(2)协程函数本体
创建一个迭代器IEnumerator ie来接受协程函数,利用迭代器的ie.MoveNext()方法就可以依次调度到下一个yield return为止的函数。利用ie.Current可以获得yield return中传入的值。如下:
public class TestClass
{public int time;public TestClass(int time){this.time = time; }
}IEnumerator Test()
{print("第一次执行");yield return 1;print("第二次执行");yield return 2;print("第三次执行");yield return "123";print("第四次执行");yield return new TestClass(10);
}void Start()
{IEnumerator ie = Test();ie.MoveNext();print(ie.Current);ie.MoveNext();print(ie.Current);ie.MoveNext();print(ie.Current);ie.MoveNext();TestClass ts = ie.Current as TestClass;print(ts.time);
}
可见,协程的本质就是一个迭代器,传入Unity的协程调度器后,Unity自动执行协程。