2018年1月21日 星期日

隨手札記:Unity 之 你不該相信的 Awake() / OnEnable() / Start()



警告:本篇含有大量不雅字眼。
以圖片的形式(?)
不喜者請自行斟酌喔!




愉快的隨手札記時間!

隨著某獨立遊戲專案的規模越來越大,做為主程式的我也來到需要設計「跨Scene儲存/保留/更新Player資料」功能的時候了。

配合胡亂說・隨便寫大大提供的無縫場景轉換機制教學文,現在我已經有一個簡單的GameSystemManager了。保持靜態的結構如下:

public class GameSystemManager : MonoBehaviour
{
public static GameSystemManager exist;
    //this is used to check whether there's already one manager.
    
    private void Awake()
    {
        //here, do the "only-one" process.
        if(exist == null)
        {
            exist = this;
            DontDestroyOnLoad(gameObject);
        }
        else if(exist != this)
        {
            //update Game and Scene Data
            //.......
//update Game and Scene Data finished. Delete the later one (this)
            Destroy(gameObject);
        }
    }
}

接下來,我要完成「把Player資料存取到GameSystemManager」的功能:

  • 基本倉儲:當GSM裡面沒有Player資料時,存入這個Player資料。(用在第一個場景)
  • 更新資訊:如果GSM裡面存在同一個Player的資料,更新它。
  • 獨一性:類似前面的GSM--當更新完資訊之後,多餘的Player必須爆炸銷毀。
  • 可存取性:規範Manager族群(包含GSM本身)在載入Player資料之後才取用這份資料清單
當然,這些都是建構在GSM必須預先載入完畢(更新 exist 內容)的前提下。

精美的Unity官方手冊表示,做為工程師的你有一個現成的 Awake() -> OnEnable() -> Start() 函式順序可以使用。
Unity官方手冊函式執行流程圖。
建議點進網站裡看大圖。

於是我們心中就可以勾勒出一份美好的藍圖:

看起來很美好、簡單,對吧?

很可惜,事情總不如想像中那麼順利。

剛寫完之後立刻來跑跑看,卻發現Player仍然跳警告、表示找不到GSM。

然後測試之後發現、原來是調用順序出了問題啊。

每個訊息都是照字面上去呼叫的。
懶得看圖片的話,以下是文字說明:
若 Scene 同時含有以下GameObject / 腳本:

  • Player: 擁有OnEnable()與Start()
  • GSM: 擁有Awake()
  • LM: 擁有Awake()與Start()
則場景執行腳本的順序可能是:(注意,只是可能)


  1. Player(2) 的 OnEnable()
  2. LM、GSM 的 Awake()
  3. Player(1) 的 OnEnable()
  4. Player(2)、LM、Player(1)的Start()

所以實測發現:在 Unity 中, Awake()->OnEnable() 的順序並不總是正確
更精確來說,在同一個Component中、Awake()確實早於OnEnable();然而在多個Component的執行過程中,同個Component的Awake()與OnEnable()是綁在一起執行的

....真是莫名其妙,那這樣幹嘛分兩個部分?

「您的疑惑我聽見了~就讓 Unity 小精靈來回答您吧!」
->如果仔細讀取上面那份Unity手冊,就可以發現:

  • Awake()會在Prefab生成(instantiated)的時候就開始執行。(不過呢,如果GameObject在一開始是Inactive狀態就不會執行,等到active後才會執行)
  • OnEnable()是在GameObject啟動(enabled)的下一瞬間開始執行。(一樣要求active)

....乾,感覺起來還是差不多啊= ="
於是我又再稍微鑽研了一下:Awake會在GameObject打勾/啟用(become "active")的時候執行,而OnEnable則是在Component本身打勾/啟用(become "enabled")的時候執行。

簡而言之:如果你只啟動了GameObject卻沒有啟動Component,才有可能保證先跑Awake()再跑OnEnable().....因為系統也只會跑Awake()。
(參見這篇博客

.....然後這個Component的OnEnable()就和Start()沒有差別了。
而且硬要用的話,我還得寫腳本去開啟Component,根本是多此一舉、浪費資源。
這設定根本是僅供Editor逐步測試程式碼用的吧......

================================

那麼,該怎麼樣才能完成一開始提到的儲存資料功能呢?

雖然就我所知,目前的Unity Editor(2017.2.1)調用函式順序仍然是「逐個Component的(Awake&OnEnable) -> 逐個Component的Start」,然而可以透過Editor的內建設定來調整Component的(AO套組)執行順序。

只要到 File -> ProjectSettings -> Script Execution Order、然後再放上特定的腳本並排序,他們的AO套組就會被強制排序執行。以前面的例子來說是這樣的畫面:

執行之後就會變得像是我們想要的了:

裁剪相片時邊界各種亂跳、結果就切邊了。偉哉Win 10。
文字敘述是這樣:
若 Scene 套用前述SEO設定,並同時含有以下GameObject / 腳本:

  • Player: 擁有OnEnable()與Start()
  • GSM: 擁有Awake()
  • LM: 擁有Awake()與Start()

則腳本執行順序大概會是這樣:
  1. GSM 的 Awake()
  2. Player(2)、Player(1)的 OnEnable()
  3. LM 的 Awake()
  4. Player(2)、Player(1)、LM的Start()
(注意:只是「大概」。同種腳本、不同物件的執行順序在某些情況下仍然可能互換)

順帶一提,流程圖變成了這樣:


.....某種程度上來說也是達成了最初的執行順序需求啦。

可是瑞凡,我就是想要這個Awake()後面接那個OnEnable()

有辦法辦到嗎?有的!
透過協程強制等待(Coroutine / yield return ... )、或者強行委派(Delegate / Invoke())就可以辦到囉。

協程(概念):我就等你跑完、我才開始跑。
委派(概念):我跑完之後,趕緊交棒給你。

....不過兩種寫法都太冗長、又太針對了,我目前還不打算使用就是。
有興趣的人可以自行參考這篇Unity問答,Bilelmnasser大大的講解與範例應該可以提供足夠的教學。

1 則留言: