GodZza

导航

Unity3D 中 脚本(MonoBehaviour) 生命周期WaitForEndOfFrame需要注意的地方

首先看看MonoBehaviour的生命周期

先上个图(来源 http://blog.csdn.net/qitian67/article/details/18516503):

 

1.Awake 和 Start的区别

相信很多人都有个类似的疑惑: 在MonoBehaviour中,为什么会有Awake 和 Start 函数? 他们又有何区别?

这个在我初学U3D时也有过的疑惑,但是通过实践得出结论,Awake 会在MonoBehaviour 创建时候即被调用,相当于构造函数。
而Start 函数 会在下一帧更新时 才会被调用到,并仅只调用一次,而Update 函数会在 Start 函数的下一帧开始被调用。

所以有时稍微不注意, 就会出现如下的Bug:

 1 public class MonoTest{
 2     public int testNum = 1;
 3     public void Awake(){
 4         testNum = 2;
 5         print("AWAKE :"+testNumber);
 6     }
 7     public void Start(){
 8         testNum = 3;
 9         print("START :"+testNumber);
10     }
11 }
12 
13 //-----------------------
14 // Create and Invoke
15 public class InvokeTest{
16     MonoTest mt;
17     bool inited = false;
18     void Update(){
19         if(!mt){
20             mt = gameObject.AddCompoment<MonoTest>();
21             //AWAKE: 2
22             //如果在此处修改 mt.testNum 的值, 则将在下一帧会调用MonoTest.Start 被覆盖为3
23         }
24         //mt 刚创建时:
25         //  false  2
26         // Update 在以后调用的输出结果
27         // true 3
28         print(inited+"   "+mt.testNum);
29 
30         inited = true;
31     }
32 }

 2. Destroy 与 DestroyImmediate

Destroy 与 DestroyImmediate 和 Start 与 Awake 其实也一样,不过我是到今天才知道,也是我写下此文的主因。

我第一次用 DestroyImmediate 的时候 是在做编辑器插件(学习 iTweenPath的源码 来做编辑器),当时需要在编辑器下点击一下button 立即删除,代码大概如下:

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class DestroyInEditor : MonoBehaviour {

    public GameObject destroyTarget;
    public bool destroy = false;


    void Update () {
        if(destroy && destroyTarget)
        {
            Destroy(destroyTarget);
            destroy = false;
        }
    }
}

当在Inspector 上将 destroy 设置为true,便会将destroyTarget 销毁。

但是在编辑器模式下会抛出一下异常:

Destroy may not be called from edit mode! Use DestroyImmediate instead.
Also think twice if you really want to destroy something in edit mode. Since this will destroy objects permanently.
UnityEngine.Object:Destroy(Object)

编辑模式下 不能调用Destroy,请使用 DestroyImmediate 。 当时因为英语不好,不明白Immediate 是什么意思,虽然知道是立即,但是并不了解 Destroy 和 DestroyImmediate的区别。

知道今天 使用NGUI.Table ,NGUI.Table 可以将所有子对象进行排版,但是我有一个要求是当点击next 的时候,将Table所有子对象清空,并添加新的子对象s再进行排版。

问题来了,要清空所有子对象,调用Table.RemoveChild()并没有用,原因可以自己查看 NGUI 的源码。

所以我自己写了个函数 将所有子对象清空:

//伪代码
public void Clear(){
   foreach(var c in children){
       Destroy(c.gameObject);
   }
}

并新增 新的子对象

//伪代码
public void Reset(List<Transform> newList){
   Clear();
   foreach(var c in newList){
        // TODO: reset localScale
        c.parent = transform;
    }
    table.Reposition();
}

新问题出现了,排版功能有BUG了。 但是经过我手动 Reposition(Inspector上右击Table 点击Execute) 排版正常了。

而问题就是处在 Destroy 函数.

下面我们进行模拟测试一下,简化这个问题
测试环境

测试代码

using UnityEngine;
using System.Collections;

public class DestroyTest : MonoBehaviour {

    public bool destroy = false;
    

    void Update () {
        if (!destroy)
            return;

        var childCount = transform.childCount;
        print("Destroy Before:" + childCount);

        for(var i = 0; i < childCount; ++i)
        {
            Destroy(transform.GetChild(i).gameObject);
        }

        print("Destroy After:" + transform.childCount);

        destroy = false;

    }
}

测试结果:

Destroy Before:1
Destroy After:1

 也就是说,当我们调用Destroy 时,Unity3D并没有真的将gameObject销毁,而是将gameObject 设置为Destroy标记。待到更新下一帧的间隙时,才真的将gameObject销毁。至于这个间隙,一般是在Render(GPU工作中)时 利用相对空闲CPU 将这些gameObject处理,当然除了处理Destroy Unity3D 还很多其他事。

答案呼之欲出了,将Destroy改为 DestroyImmediate 即可,现在终于明白立即原来是这个意思!

 

 

3.碰撞检测

说到碰撞,先了解下U3D的碰撞组件,看这里:http://www.cnblogs.com/neverdie/p/Unity3D_RigidBody2D_Collider2D.html

我这里先copy两张比较重要的图作备用

如果对一个碰撞器勾选了Is Trigger选项,它就不会与其他没有勾选Is Trigger的碰撞器发生刚体碰撞,而会发生“Trigger 碰撞”,也就是说,这时碰撞时发送的消息是Trigger消息,而不是Collision消息,相应地在脚本中我们要对OnTriggerEnter进行重载,而不是对OnCollisionEnter进行重载。

下图对Collision和Trigger进行了总结,在分别勾选某些属性时,都会发送哪些消息:


 

这里并不是要说碰撞,而是说 FixedUpdate 和 Update, 根据上图我们都知道FixedUpdate 有可能因为更新物理的原因而在一帧内被调用多次,而Update 一帧最多只调用一次。

最初我以为 FixedUpdate 和Update 是多线程同步进行,但其实不是。凡是实际到脚本的代码都只能单线程处理!注:除了WaitForEndOfFrame.

所以, 有时候移植其他游戏的时候发现一些代码会进行比较底层的碰撞检测,例:

public class Player
{
    // player 的当前位置
    int x;
    int y;

    //将player 移动到 y+offsetY 的位置 (
    // 返回值: player 实际移动的位置
    // 如果没有碰撞 则 返回值=offsetY
    // 如果中途产生碰撞 则返回player 下落的位置
    public int MoveToY(int offsetY)
    {
        if(HitTest("block"))
            throw new Exception("已经产生碰撞,不能移动");
        for(var i=0;i<=offsetY;i+=offsetY/10)//有什么余数的暂时不考虑
        {
            y += i;
            if(HitTest("block"))
            {
                y -= offsetY/10;
                return i - offsetY/10; //返回上一次的移动结果
            }
            y -= i;
        }
        y += offsetY;
        return offsetY;
    }

}

但是如果移植到U3D 并使用 U3D自带的物理引擎碰撞系统,就不一定work了。因为你移动的过程中其实并没有将实际的移动位置更新到物理引擎,只是做了个缓存而已,只有在调用FixedUpdate的内部函数(物理引擎处理)时,才会将最新的位置设置到物理引擎上,甚至是渲染引擎也使用最新的位置。

测试代码:

using UnityEngine;
using System.Collections;

public class CollidingTest : MonoBehaviour {
    
    void Update () {
        var p = transform.localPosition;
        //p.x = 1;
        //transform.localPosition = p;
        //p.x = 0;
        //transform.localPosition = p;
        for(var i=0.0f;i<1;i+=0.02f)
        {
            p.x = i;
            transform.localPosition = p;
        }
        p.x = 0;
        transform.localPosition = p;
    }


    private void OnTriggerEnter2D(Collider2D other)
    {
        print("ENTER:"+other);
    }

    //private void OnTriggerStay2D(Collider2D other)
    //{
    //    print("STAY:"+other);
    //}

    private void OnTriggerExit2D(Collider2D other)
    {
        print("EXIT:"+other);
    }
    
}

整个过程中并没有发生碰撞callback,这个是我们需要注意的,至于怎么解决,我还在思考当中。以后会给个答案!

 

 

4.WaitForEndOfFrame

刚才第3点已经提到了WaitForEndOfFrame了,一般是这样使用

public class WaitForEndFrameTest{
    void Awake(){
        StartCoroutine(CallPluginAtEndOfFrames());
    }

     IEnumerator CallPluginAtEndOfFrames(){
        while (true){
            // Wait until all frame rendering is done
            //TODO: Render Plugin but not component
            yield return new WaitForEndOfFrame();
            GL.IssuePluginEvent(1);
        }
     }

}

这是我在写渲染插件的带马上摘抄下来的,。。。

这里我也懵圈了,我印象中 在TODO上是不能修改有关U3D的任何东西的,因为此时是GPU渲染时候,如果中途修改了transform的信息会出BUG。

但是经过测试发现,在 CallPluginAtEndOfFrames中修改 transform的信息并没有真的改变了gameObject 的位置。

using UnityEngine;
using System.Collections;
using System.Threading;
using System.Collections.Generic;

public class WaitForEndFrameTest : MonoBehaviour
{
    //List<int> list = new List<int>();
    void Awake()
    {
        StartCoroutine(CallPluginAtEndOfFrames());
    }

    private void Update()
    {
        var pos = transform.localPosition;
        print("UPDATE " + pos +" "+Thread.CurrentThread.ManagedThreadId);
    }

    IEnumerator CallPluginAtEndOfFrames()
    {
        while (true)
        {
            // Wait until all frame rendering is done
            //TODO: Render but not component
            var pos = transform.localPosition;
            pos.x = 3;
            transform.localPosition = pos;
            yield return new WaitForEndOfFrame();
            pos = transform.localPosition;
            pos.x = 4;
            transform.localPosition = pos;
            GL.IssuePluginEvent(1);

            print("HELLO "+pos+" "+Thread.CurrentThread.ManagedThreadId);
            //list.Add(3);
        }
    }
}

GameObject 加上测试脚本后运行,刚开始 HELLO 和 UPDATE 都有输出,但是一旦在编辑器中 修改了 GameObject的位置, CallPluginAtEndOfFrames 就再也没有迭代下去了?

有待更详细的测试和验证。

posted on 2017-07-04 01:00  GodZza  阅读(5431)  评论(0编辑  收藏  举报