行順序:從原理到實(shí)戰(zhàn)的完整指南)
1. 項(xiàng)目概述為什么腳本執(zhí)行順序如此重要在Unity開(kāi)發(fā)中腳本執(zhí)行順序是一個(gè)看似基礎(chǔ)實(shí)則深刻影響項(xiàng)目穩(wěn)定性和性能的核心機(jī)制。很多開(kāi)發(fā)者尤其是剛接觸Unity的朋友可能會(huì)遇到一些“靈異”問(wèn)題為什么我的UI在Awake里獲取不到另一個(gè)腳本初始化的數(shù)據(jù)為什么物理碰撞檢測(cè)有時(shí)會(huì)失效為什么網(wǎng)絡(luò)消息處理會(huì)亂序這些問(wèn)題十有八九都跟腳本的執(zhí)行順序沒(méi)理清有關(guān)。簡(jiǎn)單來(lái)說(shuō)Unity在每一幀中會(huì)按照一個(gè)既定的順序來(lái)調(diào)用不同腳本的生命周期函數(shù)比如Awake、OnEnable、Start、Update、FixedUpdate等。如果腳本A依賴腳本B的初始化結(jié)果但腳本B的Awake卻在腳本A的Update之后才執(zhí)行那么腳本A自然就拿不到正確數(shù)據(jù)輕則功能異常重則直接報(bào)錯(cuò)崩潰。因此理解并主動(dòng)控制腳本的執(zhí)行順序是構(gòu)建健壯、可維護(hù)的Unity項(xiàng)目的必備技能。這不僅僅是“知道怎么設(shè)置”更是要理解其背后的設(shè)計(jì)哲學(xué)、應(yīng)用場(chǎng)景以及潛在的陷阱。2. 腳本執(zhí)行順序的核心原理與默認(rèn)行為在深入如何設(shè)置之前我們必須先搞清楚Unity默認(rèn)是怎么做的。這能幫你理解為什么有時(shí)候不設(shè)置也能工作而有時(shí)候卻必須手動(dòng)干預(yù)。2.1 Unity默認(rèn)的執(zhí)行順序規(guī)則Unity默認(rèn)的執(zhí)行順序并非完全隨機(jī)它遵循一套內(nèi)部規(guī)則但對(duì)我們開(kāi)發(fā)者而言它表現(xiàn)為“不確定的確定性”。主要規(guī)則如下同類型生命周期函數(shù)的執(zhí)行順序不確定對(duì)于掛載在同一GameObject上的多個(gè)腳本它們的Awake、Start、Update等函數(shù)的調(diào)用順序默認(rèn)是未定義的。你不能假設(shè)腳本A的Awake一定比腳本B的Awake先執(zhí)行。這個(gè)順序可能會(huì)因?yàn)槟_本的編譯順序、項(xiàng)目導(dǎo)入順序等因素而發(fā)生變化因此絕對(duì)不要依賴這種默認(rèn)順序來(lái)編寫(xiě)邏輯。不同生命周期函數(shù)之間有明確的先后關(guān)系這是Unity生命周期模型的基石。對(duì)于同一個(gè)腳本其生命周期函數(shù)的調(diào)用順序是嚴(yán)格固定的Awake僅調(diào)用一次在腳本實(shí)例被加載時(shí)立即調(diào)用早于所有Start函數(shù)。OnEnable在腳本對(duì)象啟用時(shí)調(diào)用例如GameObject被激活或腳本組件被啟用。Start僅調(diào)用一次在Update函數(shù)第一次被調(diào)用之前執(zhí)行。關(guān)鍵點(diǎn)所有腳本的Awake都執(zhí)行完畢后才會(huì)開(kāi)始執(zhí)行任何一個(gè)腳本的Start。FixedUpdate按固定的物理時(shí)間步長(zhǎng)調(diào)用用于物理計(jì)算。Update每幀調(diào)用一次用于游戲邏輯。LateUpdate在Update之后調(diào)用常用于跟隨攝像機(jī)邏輯或需要在所有對(duì)象移動(dòng)后進(jìn)行的操作。OnDisable在腳本對(duì)象禁用時(shí)調(diào)用。OnDestroy在腳本被銷(xiāo)毀前調(diào)用。不同GameObject之間的執(zhí)行順序默認(rèn)情況下不同GameObject上腳本的執(zhí)行順序也是不確定的。一個(gè)場(chǎng)景根節(jié)點(diǎn)下的子物體和父物體上的腳本其執(zhí)行順序沒(méi)有默認(rèn)的父子先后關(guān)系。注意很多初學(xué)者會(huì)誤以為掛載順序或Inspector中的組件排列順序決定了執(zhí)行順序這是錯(cuò)誤的。Inspector中的順序僅影響顯示與運(yùn)行時(shí)執(zhí)行順序無(wú)關(guān)。2.2 依賴管理與執(zhí)行順序的關(guān)聯(lián)當(dāng)腳本之間存在依賴關(guān)系時(shí)默認(rèn)的“不確定”順序就成了問(wèn)題的根源。依賴通常分為兩種數(shù)據(jù)依賴腳本A需要在Start中讀取腳本B在Awake中初始化好的一個(gè)公共變量。邏輯依賴腳本C的Update邏輯必須在腳本D的Update邏輯執(zhí)行完畢后才生效例如先處理輸入再根據(jù)輸入更新角色狀態(tài)。如果依賴方腳本A的執(zhí)行順序先于被依賴方腳本B就會(huì)導(dǎo)致讀取到空值或錯(cuò)誤狀態(tài)。手動(dòng)設(shè)置執(zhí)行順序本質(zhì)上就是在告訴Unity“請(qǐng)確保腳本B的初始化方法如Awake在腳本A的任何依賴方法如Start或Update之前執(zhí)行”從而將“不確定”變?yōu)椤按_定”。3. 如何設(shè)置腳本執(zhí)行順序三種方法詳解Unity提供了幾種方式來(lái)控制執(zhí)行順序每種都有其適用場(chǎng)景和優(yōu)缺點(diǎn)。我將從最常用到最靈活的順序來(lái)介紹。3.1 方法一使用Script Execution Order設(shè)置窗口最直觀這是最常用、最直觀的方法通過(guò)Unity編輯器的圖形化界面進(jìn)行設(shè)置。操作步驟打開(kāi)Unity編輯器點(diǎn)擊頂部菜單欄Edit-Project Settings。在項(xiàng)目設(shè)置窗口中選擇Script Execution Order。窗口下方會(huì)顯示當(dāng)前項(xiàng)目中所有腳本的列表及其當(dāng)前的執(zhí)行順序索引默認(rèn)都是0。點(diǎn)擊號(hào)從彈出窗口中選擇你需要調(diào)整順序的腳本例如PlayerController。在列表中選中該腳本然后在右側(cè)的Execution Order字段中輸入一個(gè)整數(shù)。負(fù)數(shù)表示該腳本的默認(rèn)方法如Update會(huì)更早執(zhí)行。正數(shù)表示該腳本的默認(rèn)方法會(huì)更晚執(zhí)行。0保持默認(rèn)。你可以添加多個(gè)腳本并設(shè)置不同的值。Unity會(huì)按照數(shù)值從小到大的順序執(zhí)行腳本的同一生命周期方法。示例場(chǎng)景假設(shè)我們有三個(gè)腳本GameManager負(fù)責(zé)全局狀態(tài)初始化設(shè)置順序?yàn)?100DataManager負(fù)責(zé)加載配置數(shù)據(jù)設(shè)置順序?yàn)?50PlayerController依賴全局狀態(tài)和數(shù)據(jù)設(shè)置順序?yàn)?默認(rèn)這樣在每一幀中GameManager.Update會(huì)最先執(zhí)行然后是DataManager.Update最后才是PlayerController.Update。它們的Awake和Start函數(shù)也遵循同樣的相對(duì)順序。實(shí)操心得與注意事項(xiàng)范圍管理建議為不同類型的腳本規(guī)劃一個(gè)順序范圍。例如系統(tǒng)框架腳本在-200到-100數(shù)據(jù)管理腳本在-99到-50游戲邏輯腳本在-49到0UI腳本在1到50。這能極大提升項(xiàng)目可維護(hù)性。不要過(guò)度使用只為存在明確依賴關(guān)系的腳本設(shè)置順序。濫用會(huì)導(dǎo)致順序鏈復(fù)雜化難以調(diào)試。對(duì)預(yù)制件的影響這個(gè)設(shè)置是基于腳本類型的而不是基于腳本實(shí)例。一旦為PlayerController設(shè)置了順序場(chǎng)景中所有PlayerController腳本實(shí)例都會(huì)遵循這個(gè)順序。動(dòng)態(tài)加載的腳本對(duì)于運(yùn)行時(shí)通過(guò)Resources.Load或Addressables動(dòng)態(tài)加載并實(shí)例化的GameObject上的腳本此設(shè)置同樣生效。3.2 方法二使用InitializeOnLoad和RuntimeInitializeOnLoadMethod更早的初始化有些代碼需要在所有場(chǎng)景加載前、所有腳本的Awake之前就執(zhí)行比如注冊(cè)自定義序列化類型、初始化靜態(tài)管理器等。這時(shí)就需要用到InitializeOnLoad屬性在編輯器中運(yùn)行和RuntimeInitializeOnLoadMethod屬性在運(yùn)行時(shí)。原理與用法[InitializeOnLoad]這是一個(gè)編輯器屬性。將它放在一個(gè)靜態(tài)類的構(gòu)造函數(shù)上這個(gè)構(gòu)造函數(shù)會(huì)在Unity編輯器啟動(dòng)、或重新編譯腳本后立即自動(dòng)調(diào)用。這早于任何場(chǎng)景加載和游戲運(yùn)行。using UnityEditor; [InitializeOnLoad] public static class EditorInitializer { static EditorInitializer() { Debug.Log(編輯器腳本初始化這個(gè)調(diào)用非常早。); // 通常用于注冊(cè)編輯器菜單、自定義窗口等 } }[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType)]這是一個(gè)運(yùn)行時(shí)屬性。將它標(biāo)記在任何靜態(tài)方法上該方法會(huì)在游戲運(yùn)行時(shí)的特定階段被自動(dòng)調(diào)用。RuntimeInitializeLoadType是一個(gè)枚舉常用值有RuntimeInitializeLoadType.BeforeSceneLoad在場(chǎng)景加載之前調(diào)用。這是最早的可控運(yùn)行時(shí)初始化點(diǎn)適合初始化不依賴場(chǎng)景對(duì)象的全局管理器。RuntimeInitializeLoadType.AfterSceneLoad在場(chǎng)景加載之后所有Awake方法調(diào)用之前調(diào)用。RuntimeInitializeLoadType.SubsystemRegistration在子系統(tǒng)注冊(cè)后調(diào)用用于一些底層系統(tǒng)初始化。using UnityEngine; public class RuntimeInitializer { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void OnBeforeSceneLoad() { Debug.Log(在場(chǎng)景加載前執(zhí)行早于所有Awake。); // 初始化單例、加載配置表等 GameApp.Instance.Initialize(); } }應(yīng)用場(chǎng)景對(duì)比方法執(zhí)行時(shí)機(jī)主要用途是否參與游戲運(yùn)行順序Script Execution Order窗口控制Awake,Start,Update等標(biāo)準(zhǔn)生命周期方法的相對(duì)順序解決腳本間運(yùn)行時(shí)依賴是[RuntimeInitializeOnLoadMethod(BeforeSceneLoad)]場(chǎng)景加載前所有Awake之前全局、靜態(tài)資源的初始化否它是最早的起點(diǎn)[InitializeOnLoad]編輯器啟動(dòng)或重編譯后編輯器擴(kuò)展功能初始化否僅在編輯器中提示RuntimeInitializeOnLoadMethod標(biāo)記的方法執(zhí)行順序在其同類方法之間也是不確定的。如果你有多個(gè)標(biāo)記為BeforeSceneLoad的方法并且它們之間有依賴你需要通過(guò)Script Execution Order窗口為它們所在的類設(shè)置順序或者在一個(gè)主初始化方法中顯式地按順序調(diào)用它們。3.3 方法三通過(guò)腳本動(dòng)態(tài)控制執(zhí)行流最靈活圖形化設(shè)置和屬性標(biāo)記雖然方便但有時(shí)我們需要更動(dòng)態(tài)、更精細(xì)的控制。例如根據(jù)游戲模式?jīng)Q定先執(zhí)行哪個(gè)系統(tǒng)或者管理一批運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建的物體。這時(shí)就需要在代碼層面設(shè)計(jì)執(zhí)行流。核心思路不依賴Unity底層的順序調(diào)用而是自己建立一個(gè)“管理器”或“調(diào)度器”顯式地控制不同模塊的初始化、更新順序。示例一個(gè)簡(jiǎn)單的自定義更新管理器using System.Collections.Generic; using UnityEngine; public interface ICustomUpdater { void OnCustomUpdate(float deltaTime); int UpdateOrder { get; } // 用于排序的優(yōu)先級(jí) } public class CustomUpdateManager : MonoBehaviour { private static CustomUpdateManager _instance; private ListICustomUpdater _updaters new ListICustomUpdater(); public static void RegisterUpdater(ICustomUpdater updater) { if (_instance null) { GameObject go new GameObject(CustomUpdateManager); _instance go.AddComponentCustomUpdateManager(); DontDestroyOnLoad(go); } _instance._updaters.Add(updater); // 注冊(cè)時(shí)根據(jù)優(yōu)先級(jí)排序 _instance._updaters.Sort((a, b) a.UpdateOrder.CompareTo(b.UpdateOrder)); } public static void UnregisterUpdater(ICustomUpdater updater) { if (_instance ! null) { _instance._updaters.Remove(updater); } } private void Update() { float dt Time.deltaTime; // 按照注冊(cè)時(shí)排好的順序顯式調(diào)用每個(gè)更新器的更新方法 for (int i 0; i _updaters.Count; i) { _updaters[i].OnCustomUpdate(dt); } } } // 使用示例一個(gè)輸入處理器 public class InputHandler : MonoBehaviour, ICustomUpdater { public int UpdateOrder -100; // 設(shè)置高優(yōu)先級(jí)希望先處理輸入 private void OnEnable() { CustomUpdateManager.RegisterUpdater(this); } private void OnDisable() { CustomUpdateManager.UnregisterUpdater(this); } public void OnCustomUpdate(float deltaTime) { // 處理輸入邏輯 if (Input.GetKeyDown(KeyCode.Space)) { Debug.Log(Input handled first!); } } }這種方法的優(yōu)勢(shì)極度靈活可以隨時(shí)注冊(cè)、注銷(xiāo)、調(diào)整順序。邏輯清晰執(zhí)行流完全由你的代碼控制一目了然。性能可控可以輕松實(shí)現(xiàn)分幀更新、按需更新避免每幀遍歷所有GameObject。注意事項(xiàng)增加復(fù)雜度引入了新的管理器和接口對(duì)小型項(xiàng)目可能過(guò)于繁重。內(nèi)存與生命周期管理需要小心處理對(duì)象的注冊(cè)與注銷(xiāo)避免內(nèi)存泄漏如上面的OnEnable/OnDisable配對(duì)使用。與Unity原生生命周期并存使用這種模式后腳本可能同時(shí)有Update和OnCustomUpdate需要明確分工避免重復(fù)計(jì)算。4. 高級(jí)應(yīng)用場(chǎng)景與架構(gòu)設(shè)計(jì)理解了基礎(chǔ)設(shè)置方法后我們來(lái)看看在更復(fù)雜的項(xiàng)目中如何利用執(zhí)行順序來(lái)構(gòu)建穩(wěn)健的架構(gòu)。4.1 單例模式與執(zhí)行順序的陷阱單例是Unity中最常用的設(shè)計(jì)模式之一但也是最容易因執(zhí)行順序問(wèn)題而出錯(cuò)的模式。典型問(wèn)題場(chǎng)景// GameManager.cs public class GameManager : MonoBehaviour { public static GameManager Instance; public int GlobalScore 100; private void Awake() { if (Instance null) Instance this; else Destroy(gameObject); DontDestroyOnLoad(gameObject); } } // Player.cs - 掛載在玩家角色上 public class Player : MonoBehaviour { private void Start() { // 嘗試在Start中訪問(wèn)單例 int score GameManager.Instance.GlobalScore; // 這里可能拋出NullReferenceException! Debug.Log(score); } }如果Player的Start在GameManager的Awake之前執(zhí)行那么GameManager.Instance就還是null。解決方案使用Script Execution Order將GameManager腳本的執(zhí)行順序設(shè)置為一個(gè)較小的負(fù)數(shù)如-100確保其Awake最先執(zhí)行。使用RuntimeInitializeOnLoadMethod在GameManager類中創(chuàng)建一個(gè)靜態(tài)初始化方法在場(chǎng)景加載前就創(chuàng)建實(shí)例。public class GameManager : MonoBehaviour { public static GameManager Instance; public int GlobalScore 100; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void InitBeforeScene() { if (Instance null) { GameObject go new GameObject(GameManager); Instance go.AddComponentGameManager(); DontDestroyOnLoad(go); } } // Awake方法可以留空或用于其他初始化 private void Awake() { } }使用惰性初始化或訪問(wèn)器在訪問(wèn)Instance屬性時(shí)才嘗試創(chuàng)建或查找實(shí)例。這種方法能避免空引用但可能無(wú)法保證在Start時(shí)數(shù)據(jù)已準(zhǔn)備好。public static GameManager Instance { get { if (_instance null) { _instance FindObjectOfTypeGameManager(); if (_instance null) { GameObject go new GameObject(GameManager); _instance go.AddComponentGameManager(); } } return _instance; } } private static GameManager _instance;4.2 網(wǎng)絡(luò)消息處理與狀態(tài)同步的順序控制在網(wǎng)絡(luò)游戲中消息處理的順序至關(guān)重要。例如必須先處理“玩家加入”消息才能處理該玩家的“移動(dòng)”消息必須先應(yīng)用服務(wù)器的狀態(tài)同步本地才能進(jìn)行預(yù)測(cè)和渲染。架構(gòu)設(shè)計(jì)建議分層處理設(shè)計(jì)一個(gè)網(wǎng)絡(luò)消息分發(fā)器NetworkManager設(shè)置較高的執(zhí)行順序如-150。它的Update或LateUpdate負(fù)責(zé)從網(wǎng)絡(luò)接收原始數(shù)據(jù)包。消息隊(duì)列與排序在NetworkManager內(nèi)部將接收到的消息根據(jù)類型和時(shí)序放入不同的優(yōu)先級(jí)隊(duì)列。例如系統(tǒng)控制消息如匹配開(kāi)始優(yōu)先級(jí)最高實(shí)體狀態(tài)同步消息次之聊天消息優(yōu)先級(jí)最低。分幀處理在NetworkManager的LateUpdate中按照優(yōu)先級(jí)從高到低的順序處理一定數(shù)量的消息避免單幀卡頓然后將處理結(jié)果事件發(fā)布出去。邏輯系統(tǒng)訂閱游戲邏輯系統(tǒng)如PlayerSystem,BattleSystem以較低的腳本執(zhí)行順序運(yùn)行如0或正數(shù)。它們?cè)诟髯缘腢pdate中訂閱并響應(yīng)NetworkManager發(fā)布的事件。這樣就保證了“收包 - 解包 - 排序 - 發(fā)布 - 響應(yīng)”的固定執(zhí)行流。4.3 UI系統(tǒng)與數(shù)據(jù)綁定的初始化順序現(xiàn)代UI框架如自研的MVC/MVP框架或使用Unity的UI Toolkit經(jīng)常遇到數(shù)據(jù)綁定問(wèn)題。比如一個(gè)角色信息面板需要顯示PlayerData中的數(shù)據(jù)但面板的Start可能先于PlayerData的初始化執(zhí)行。解決方案模式事件驅(qū)動(dòng)初始化UI控件不在Start中直接綁定數(shù)據(jù)而是監(jiān)聽(tīng)一個(gè)“數(shù)據(jù)就緒”事件。public class PlayerInfoUI : MonoBehaviour { public Text playerNameText; private void OnEnable() { // 訂閱事件而不是在Start中直接獲取 PlayerData.OnDataInitialized RefreshUI; // 同時(shí)如果數(shù)據(jù)已經(jīng)初始化立即刷新一次 if (PlayerData.IsInitialized) RefreshUI(); } private void OnDisable() { PlayerData.OnDataInitialized - RefreshUI; } private void RefreshUI() { playerNameText.text PlayerData.Instance.Name; } }設(shè)置明確的腳本順序?qū)layerData腳本順序設(shè)為-50將PlayerInfoUI腳本順序設(shè)為50。確保數(shù)據(jù)先初始化。使用UI框架的生命周期如果使用UI Toolkit可以利用其VisualElement的OnInitialization等回調(diào)這些回調(diào)的執(zhí)行時(shí)機(jī)可能與MonoBehaviour生命周期不同需要結(jié)合框架文檔進(jìn)行設(shè)計(jì)。5. 常見(jiàn)問(wèn)題排查與性能優(yōu)化即使設(shè)置了執(zhí)行順序開(kāi)發(fā)中仍會(huì)遇到各種奇怪的問(wèn)題。這里記錄一些我踩過(guò)的坑和排查技巧。5.1 執(zhí)行順序設(shè)置了但無(wú)效檢查腳本編譯錯(cuò)誤如果腳本有編譯錯(cuò)誤Unity可能會(huì)忽略其執(zhí)行順序設(shè)置。確保控制臺(tái)沒(méi)有報(bào)錯(cuò)。確認(rèn)腳本名稱和類名一致在Script Execution Order窗口中顯示的是腳本文件名.cs文件但它綁定的是文件中的類名。如果修改了類名但沒(méi)改文件名或者反之可能會(huì)導(dǎo)致設(shè)置失效。確保兩者一致。檢查是否為抽象類或泛型類Script Execution Order窗口無(wú)法設(shè)置抽象類或泛型類的順序。你需要為其具體的實(shí)現(xiàn)類設(shè)置順序。動(dòng)態(tài)腳本加載對(duì)于通過(guò)Assembly.Load等方式在運(yùn)行時(shí)動(dòng)態(tài)加載的程序集中的腳本編輯器的Script Execution Order設(shè)置是無(wú)效的。這類腳本需要完全依靠動(dòng)態(tài)代碼控制執(zhí)行流如第3.3節(jié)所述。5.2 如何調(diào)試腳本執(zhí)行順序使用簡(jiǎn)單的Debug.Log在每個(gè)腳本的Awake和Start開(kāi)頭打印日志并包含時(shí)間戳Time.time和腳本名稱。運(yùn)行游戲后查看控制臺(tái)輸出順序。private void Awake() { Debug.Log($[{Time.time:F4}] Awake called on {gameObject.name}.{this.GetType().Name}); }使用Unity Profiler的Deep Profiling打開(kāi)Profiler窗口啟用Deep Profiling。在CPU使用率圖表中你可以看到每一幀所有腳本方法的詳細(xì)調(diào)用堆棧和時(shí)間并能清晰看到它們的調(diào)用順序。這是最強(qiáng)大的調(diào)試工具。自定義性能分析工具可以編寫(xiě)一個(gè)簡(jiǎn)單的編輯器工具在運(yùn)行時(shí)收集所有MonoBehaviour及其生命周期函數(shù)的調(diào)用順序并生成報(bào)告。5.3 執(zhí)行順序?qū)π阅艿挠绊懖缓侠淼膱?zhí)行順序設(shè)置本身不會(huì)直接導(dǎo)致性能下降但它可能間接引發(fā)問(wèn)題阻塞式初始化如果一個(gè)高優(yōu)先級(jí)腳本的Awake或Start執(zhí)行了非常耗時(shí)的操作如同步加載大量資源、復(fù)雜計(jì)算它會(huì)阻塞后面所有腳本的初始化導(dǎo)致游戲卡在加載界面。解決方案將耗時(shí)的初始化操作協(xié)程化StartCoroutine或異步化讓出執(zhí)行權(quán)。Update循環(huán)中的計(jì)算密集操作如果多個(gè)高優(yōu)先級(jí)腳本的Update都執(zhí)行重計(jì)算會(huì)導(dǎo)致幀率波動(dòng)。解決方案使用自定義更新管理器如3.3節(jié)將非實(shí)時(shí)性要求的計(jì)算分散到多幀執(zhí)行或者根據(jù)距離、重要性進(jìn)行裁剪。過(guò)深的順序依賴鏈腳本A依賴BB依賴CC依賴D……形成一個(gè)長(zhǎng)鏈。這會(huì)使系統(tǒng)變得脆弱難以理解和維護(hù)。解決方案重構(gòu)代碼引入事件總線Event Bus或消息系統(tǒng)來(lái)解耦模塊間的直接依賴。模塊間通過(guò)發(fā)布/訂閱事件通信而非直接調(diào)用這樣它們的執(zhí)行順序就變得不那么關(guān)鍵了。5.4 多場(chǎng)景加載Additive Loading下的執(zhí)行順序當(dāng)使用SceneManager.LoadScene的LoadSceneMode.Additive加載多個(gè)場(chǎng)景時(shí)新加載場(chǎng)景中腳本的Awake和Start調(diào)用時(shí)機(jī)需要特別注意。Awake在新場(chǎng)景加載完成、對(duì)象被實(shí)例化后立即調(diào)用發(fā)生在本幀內(nèi)。Start在所有場(chǎng)景包括原有場(chǎng)景和新場(chǎng)景中所有腳本的Awake都執(zhí)行完畢后在下一幀開(kāi)始之前統(tǒng)一調(diào)用所有腳本的Start。這意味著即使你為主場(chǎng)景的GameManager設(shè)置了很高的執(zhí)行順序新加載場(chǎng)景中某個(gè)腳本的Awake也可能在GameManager的Start之前執(zhí)行。如果你的邏輯依賴Start中的初始化就可能出問(wèn)題。最佳實(shí)踐對(duì)于跨場(chǎng)景的全局初始化盡量放在Awake中完成或者使用RuntimeInitializeOnLoadMethod(BeforeSceneLoad)。對(duì)于場(chǎng)景內(nèi)的本地初始化可以使用Start但要清楚其執(zhí)行時(shí)機(jī)。