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 就再也沒有迭代下去了?

有待更詳細的測試和驗證。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM