博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【Unity Tips】备忘录(扫盲篇)
阅读量:5101 次
发布时间:2019-06-13

本文共 6998 字,大约阅读时间需要 23 分钟。

原文链接:

写在前面

 

虽然是个非常方便的游戏引擎,但还是有一些地方会产生一些让人莫名其妙的问题,而且debug半天也不知道到底哪里错了。往往在经过了大量的log之后,也许我们才顿悟,原来Unity内部是这样做的啊。这里旨在总结这些容易被忽略、但是又经常会给开发造成麻烦的问题,欢迎补充。

随时更新。

 

 

 

备忘录

 

1.  和

 
Coroutine,即协同程序,如果使用的语言是C#,那么我们必须使用StartCoroutine去指明开启一个协同程序。StartCoroutine有两个版本:一种是
StartCoroutine(routine: IEnumerator): ;,另一种是StartCoroutine(methodName: string, value: object = null): ;。
如果你仔细看官方文档,只有第二种,即以string为参数的StartCoroutine才可以使用public void 
StopCoroutine(string methodName);来终止该协同程序。
 
根据网友@Oo幻想oO (多谢!)的提醒,实际上所有的Coroutine都可以用public void 
StopCoroutine(IEnumerator routine);这种传参方式停止。

2. Awake、Start、Update、LateUpdate、FixedUpdate

 

上述函数都是几个最常见的基本函数。但很少有人真正知道它们具体的调用顺序。

 

首先是。当该脚本实例被加载时,Unity会调用它的Awake函数,在该脚本实例的整个生命周期中,它只会被调用一次。具体来说,Unity首先会加载场景中的所有对象,完成后,去扫描这些对象上的MonoBehavior,然后调用它的Awake函数。因此,我们通常使用Awake来初始化各种变量,在其中使用FindWithTag等函数也是没有问题的。Awake函数将早于任何Start函数被调用,但是Awake本身的调用顺序是不定的。

 

然后是函数。很多人认为Awake和Start的区别只在调用顺序上,实际上这是错误的。Start函数真正的调用时间,是在该脚本第一次被enable的那一帧,而且早于它的任何Update调用。那么什么是enable呢?从视觉上来说,就是Inspector面板里该脚本前面的那个勾选框是不是被选中了。我们可以通过*.enable去控制脚本的激活状态。和Awake一样,Start也只会被调用一次,但是和Awake不同的是,除了Awake先于Start外,Awake只要对象被加载就会被调用,而Start需要等到该脚本实例被enable之后才会调用。举例来说,我们没有场景中某对象上的的脚本A前面的勾选框,点击开始后,在某该脚本的Awake还是会被立刻调用,但是Start却没有被调用。这时,不暂停游戏,激活脚本A,就会看到Start函数在勾选框被勾选的那一帧被调用了。

 

接下来是Update函数。Update函数会在每一帧时被调用,但前提是该脚本是enable的。还是上面的例子,如果脚本A一直没有被激活,那么Update函数一直不会被调用。在它被激活的时刻,Update函数在Start函数后面被调用了。

 

下面是。从字面上就说明了它的调用顺序:在Update之后,前提是该脚本是enable的。也就是说,它也是在每一帧时被调用,但是是在所有的Update函数执行完后,再调用的。这种顺序有什么用的?举例来说,我们在Update函数里移动了某个对象的位置,而摄像机是要跟随它的位置的,这时我们就可以在LateUpdate函数里,根据该对象的位置去改变摄像机的位置。和Update类似,如果我们需要知道距离上一次LateUpdate函数的时间,可以使用Time.deltatime。

 

下面是函数。文档中解释,该函数会在一个固定帧率的帧时被调用,但前提是该脚本是enable。固定帧率说明了它和Update函数的不同,也就是说,在每秒内它调用的次数是固定的,这个帧率可以在Project Setting/Time/Fixed Timestep中查看和修改。文档中还指出,当我们需要对刚体(Rigdbody)操作时,如施加力时,应该使用FixedUpdate,而不是Update函数,原因也是因为Update函数调用的时间间隔是不定的,而FixedUpdate是固定的。

 

但是!!!在实际编程中,有一个特例。举一个例子,我们在脚本A里定义了自己的函数function0,脚本A被添加到一个prefab上。当我们在脚本B中使用Instantiate函数实例化了该prefab,并且立刻调用了function0,那么是Awake、Start、Update、function0调用顺序是什么?按我最初的想法,从先到后分别是Awake、Start、Update、function0。

 

[csharp]
   
  1. GameObject obj = Instantiate(prefab) as GameObject;  
  2. obj.GetComponent<A>().function0();  
脚本A如下:

 

[csharp]
   
  1. public int a = 0;  
  2.   
  3. void Awake() {  
  4.     Debug.Log("Awake " + a);  
  5.     a = 1;  
  6. }  
  7.   
  8. void Start() {  
  9.     Debug.Log("Start " + a);  
  10.     a = 2;  
  11. }  
  12.   
  13. void Update() {  
  14.     Debug.Log("Update " + a);  
  15. }  
  16.   
  17. public void function0() {  
  18.     Debug.Log("function0 " + a);  
  19.     a = 3;  
  20. }  
结果如下:

 

从上图可以看出,我们自定义的函数是先于Start和Update函数的。这有什么影响呢?如果你在function0中修改了某些变量,在Start函数里对它们进行了赋值,那么function0所做的更改就会消失;又或者某些变量在Start函数里初始化,那么调用funciton0的时候就会错空指针错误!因此,由于Unity的脚本仅自动生成了Start函数,而没有Awake函数,有些人就喜欢在Start函数里对GameObject等类型的对象初始化,这往往会造成错误。因此,建议就是,请尽量在Awake函数里进行对象初始化!当然,也建议Unity可以自动生成Awake函数,这样也许偷懒的人就会更少啦~

 

 

3. 和Update、FixedUpdate

 

基本大家知道,一般实现游戏暂停的方法就是令Time.timeScale = 0。但是我们经常会发现一些匪夷所思的问题,而很多时候是何Update函数有关。

 

根据官方文档可知,设置Time.timeScale为0将回暂停所有和帧率无关的事情。这些主要是指所有的物理事件和依赖时间的函数、刚体力和速度等,而且FixedUpdate会被暂停(不是Update)。但是,动画(Animations)和任何你放到Update中的通过Time.deltaTime来控制的位移也会属于和帧率无关的一类:不管你的帧率具体是多少,它们总是在相同的时间旋转或平移。

 

但是,Update函数本身的执行是不会受Time.timeScale的影响的。根据上一点我们知道,Update是依赖你的机器的,它的调用次数和你的机器渲染一样快慢(一些特殊情况除外);性能高的机器,帧率高,Update函数执行次数也就多。因此,当使用Time.timeScale = 0时,游戏看起来是被冻结了,这是因为所有和时间有关的事情都被暂停了。但是,我们的游戏仍在渲染,也就是说Update函数仍在执行。

 

还有一点,Time.timeScale为0时,Time.deltaTime将为0。这意味着,如果你使用Time.deltaTime来控制旋转和位移等,那么Time.timeScale = 0也将使这些物体停止运动。

 

参考:

 

那么,正确的暂停游戏的方法是什么呢?详见。 

 

 

4. 让Input能够正确工作

 

一些人总觉得自己按照文档里去读取Input,但总是没有响应似的,苦苦找不到原因。其实,大部分这类原因是因为这里面涉及到了物理运算。官网上写到,物理运算要放到FixedUpdate里面进行处理,那么如果需要用Input控制物理运动,就把Input也放到FixedUpdate里面不就可以了吗?这是错的!这是因为,Update是真正和我们机器相关,帧率受不同机器性能影响的函数,而FixedUpdate则是一个Unity自行封装定义的函数,它的默认值一般是0.02m,也就是50fps,我们可以通过Edit -> Project Setting -> Physics来改变调用间隔时间。但要注意,这个值只是一个请求,你的机子可能不能达到这个速度。

 

因此,Update和FixedUpdate其实是完全独立的两个函数。当Update执行了一次时,FixedUpdate可能已经执行了两次、三次甚至一次都没有执行过。下面的例子就说明了为什么把Input放到FixedUpdate中可能会无法响应。在这个例子里,Update的速率是100fps,而FixedUpdate是50fps。(来源)

 

也就是说,下面的写法是错误的:

 

[csharp]
   
  1. using UnityEngine;  
  2.    
  3. public class Test:MonoBehaviour{  
  4.     void FixedUpdate(){  
  5.         if(Input.GetKeyDown(KeyCode.Space)){  
  6.            //Action  
  7.         }  
  8.     }  
  9. }  
正确的写法是:
[csharp]
   
  1. using UnityEngine;  
  2.    
  3. public class Test:MonoBehaviour{  
  4.     bool action = false;  
  5.     void Update(){  
  6.          if(Input.GetKeyDown(KeyCode.Space)){  
  7.              action = true;  
  8.          }  
  9.     }  
  10.     void FixedUpdate(){  
  11.        if(action){  
  12.            //Action  
  13.            action = false;  
  14.         }  
  15.     }  
  16. }  
上面,我们用一个boolean值来控制开关。由于Update每一帧都会调用,因此可以保证所有的输入都可以被响应。

 

5. 使用Quaternions来修改物体的rotation

 

 

一个四元数(quaternion)的x、y、z、w参数和你在Unity面板中看到的物体的rotation没有任何关系。x、y、z、w参数不是按度数存储的,而是一些角度的sin值和cos值。按度数存储的rotation则是存储在.eulerAngles。

你不应该修改四元数的x、y、z、w,除非你知道你正在干什么。如果你想要使用度数来改变物体的rotation,那么你应该使用 .eulerAngles。

如果你想知道四元数应该怎么用,你可以看这篇。

 

想要在代码里设置物体面板中的rotation,你应该像下面这样:

 

[csharp]
   
  1. transform.rotation.eulerAngles = new Vector3(100,0,100);  

6. 从Javascript里访问C#,或者从C#里访问Javascript

 

你只可以从一个方向实现这种访问,也就是说,从C#文件中访问Javascript,或者反过来。如果存在两种语言,你不可能从两个方向实现这种互相访问。
如果你使用一个完全独立的第三库,只要你把它们放在下面其中的位置(这些位置的脚本将首先被编译),那么它们是使用什么语言的不重要 —— Standard Assets, Pro Standard Assets 或者 Plugins。你可以在其他不在这些位置的代码中访问它们。

和C#不会被编译到同一个汇编中,因此不能只简单地从一个里面访问另一个。而任何在Plugins, Standard Assets 或者 Pro Standard Assets将会首先被编译,因此不在这些文件夹下的代码可以访问这些代码。 

记住,如果Javascript已经在其中一个文件夹中,那么它就不可以再访问任何C#代码,即便这些代码在上述特殊的文件夹中。反之亦然。

关于脚本的编译顺序,可以看 。

 

7.  Unity中的Javascript是什么

Unity支持的语言之一Javascript,和Jave没有什么关系,它只在一些high level上有些许类似。当初Javascript出现时,实际是被称为LiveScript。而那时Java已经非常流行了,于是它就被重命名来使得听起来好像有某种联系。
如果你之前是一个Java程序员,那么你会觉得C#比Unity的Javascript更接近Java。网上有很多从Java转换到C#的教程,这对于Unity中的C#也适用。
Unity中的Javascript不是网页中经常见的Javascript。我们更愿意叫它Unity Script来区分它们。Unity中的Javascript是一种.NET语言,跟Javascript相比,它和Action Script关系更密切。它的确有一些Javascript的特性,但很多都没有保留。例如,它使用经典的继承而非原型继承。你不可以在编译后给对象添加函数,等等等等。
任何情况下,如果你学习真正的Javascript书籍或者教程,然后再应用到Unity中,最后,你只会变得更加困惑。

 

8. 为什么Coroutine在wait和yield后没有执行完

 

大多数原因是因为你disable掉了它所在的脚本,或者销毁了所在的对象。比如,你想要在通过Coroutine播放一个死亡动画后再销毁,或者更新完分数后销毁。

Coroutines将会立即执行到第一个yield语句。因此,它的第一个部分总会被执行。如果你这时disable了你的脚本,或者销毁了你的对象,那么剩下的routine就永远也不会被执行了。
如果你使用WaitForSeconds,那么Time.timeScale必须大于0,否则你的yield永远不会返回。记住,WaitForSeconds使用受timeScale影响的游戏时间。
Coroutines将会执行在调用StartCoroutine的对象上。因此,如果你想在另一个的对象上开始一个coroutine,那么你就在该对象上使用StartCoroutine,而不是你当前正在处理的对象。例如,otherScript.StartCoroutine(otherScript.SomeFunction());

下面是写coroutine一个错误的写法:

 

[csharp]
   
  1. void Update() {  
  2.     if(health < 0) {   
  3.            StartCoroutine(Die());   
  4.            Destroy(gameObject); //or enabled = false;   
  5.     }   
  6. }   
  7.    
  8. IEnumerator Die() {   
  9.        animation.Play("wobble");   
  10.        yield return new WaitForSeconds(3);   
  11.        //This will never be called   
  12.        animation.Play("die");   
  13. }  
上面的代码会造成,在执行完播放“wobble”动画后,该对象立即被销毁,因此不会再播放“die”动画。

正确的写法是:

 

[csharp]
   
  1. bool dying;   
  2. void Update() {   
  3.      if(dying) return;   
  4.      if(health < 0) {   
  5.          StartCoroutine(Die());   
  6.      }   
  7. }   
  8.    
  9. IEnumerator Die() {   
  10.       dying = true;   
  11.       animation.Play("wobble");   
  12.       yield return new WaitForSeconds(3);   
  13.       animation.Play("die");   
  14.       yield return new WaitForSeconds(3);   
  15.       Destroy(gameObject);   
  16. }  
boolean值dying将保证只会执行一次Die()函数。

 

9. Unity的坐标系问题

 

Unity使用的是左手坐标系,因此在进行旋转等工作时需要注意它的方向问题。

 

10. Shader中的向量和矩阵

 

Unity Shader使用的是CG语言,CG中的vector是row vector,因此,在定义类似float3x3这种矩阵的时候,初始化列表是按行来填充的。例如:

 

[cpp]
   
  1. float3x3 m = float3x3(  
  2.    1.1, 1.2, 1.3, // first row (not column as in GLSL!)  
  3.    2.1, 2.2, 2.3, // second row  
  4.    3.1, 3.2, 3.3  // third row  
  5. );  
参考:

转载于:https://www.cnblogs.com/android-blogs/p/6424238.html

你可能感兴趣的文章
SequenceFile介绍
查看>>
安卓 代码混淆与打包
查看>>
AT&T汇编语言及其寻址方式
查看>>
ubuntu下 java、mysql、tomcat(ssl认证) 配置
查看>>
linux命名详解及其软件安装实例
查看>>
查看iOS沙盒(SanBox)文件
查看>>
顺时针打印矩阵
查看>>
[转载]Chrome 与 Chrome OS 各版本下载集合
查看>>
面试微软前必须要读的十本书
查看>>
妙味——操作元素属性的几种方法
查看>>
Ring 0 Inline Hook
查看>>
Linux man C++ 库函数
查看>>
PE结构对照表
查看>>
复杂性渐近阶的重要性
查看>>
js数组创建两种方法
查看>>
IOS自得其乐系列(一)-------------------加载动态图片
查看>>
Function Spec
查看>>
关于我 Jake Lin
查看>>
hue简单介绍
查看>>
现代服务业是什么?
查看>>