2014年9月23日 星期二

Unity:設計靈活的無縫場景轉換機制

剛開始學習使用 Unity 時應該會發現到,Unity 的儲存並非只有儲存專案(Save Project),較常使用到的是儲存場景(Save Scene),在整個遊戲專案的製作過程中,除非將全部的遊戲畫面都放在同一個場景中處理,否則我們將會儲存多個場景,雖然,Unity 的 Build Settings 中的 Scenes In Build 可以很方便的指定遊戲將會使用到哪幾個場景,而在我們設計的遊戲程式碼之中,也可以很方便地使用 Unity API 中的 Application.LoadLevel() 來載入場景,但這些都只是進行場景轉換的基本動作,在整個遊戲開發上,因應實際需求及考量上,很有必要另行設計一個方便變換及設定的場景轉換機制。


雖然,遊戲可以製作成只有一個場景,然後動態地去載入以及銷毀遊戲物件,但在以前發表過的文章「Unity 卸載未使用的資源」也提到過,在單一場景中,遊戲物件的產生、銷毀、釋放資源等等的動作格外重要,另外,在畫面、關卡的編輯上以及多人開發上,動作流程也會變得更複雜些,為了效能、風險以及開發效率上的考量,通常就會選擇多場景轉換的方式來製作遊戲,但是,遊戲的製作在一開始至少會有幾個部分必須要事先考慮到,就是遊戲資料、狀態的存取以及初始化的進行,而 Application.LoadLevel() 只是很單純的載入指定的場景以及釋放掉目前的場景,所以我們必須自行處理 A 場景中的遊戲資料、狀態能延續到 B 場景繼續使用的這部分行為,另外就是遊戲必須是相當注重使用者體驗的,我們通常不希望場景轉換時,中間會有停頓以及不曉得系統目前正在做什麼事的狀況發生,所以在每個場景轉換期間,不管是載入下個場景畫面資源、從 Server 或 Client 讀取資料、遊戲初始化的準備動作... 等,我們也需要有一套做法使得更換場景能夠更為無縫的進行,並使玩家從載入進度的顯示了解到系統正在做什麼事情。

首先,因為遊戲進行的流程會在多個不同畫面(場景)間持續的不斷互相轉換,而 Unity 在更換場景時,會將原來場景的全部內容通通銷毀,所以我們需要有一個不會被銷毀的遊戲物件來負責作為「場景管理者」,這個時候,可以使用 Object.DontDestroyOnLoad() 來使這個「場景管理者」不會因為更換場景而被銷毀掉。同時,為了使每個場景在開發時都能夠獨立測試,而不是一定要從遊戲主畫面選單一步步選擇進入到該場景的畫面才能測試其內容功能,所以,我們必須讓每個場景內都具備這個「場景管理者」。還有,需要使任何其他的遊戲物件都能夠方便的對它提出請求來讓場景進行轉換,所以,「場景管理者」需要有個 static 成員可以直接調用其功能。於是,可以針對這些需求大致列出如下:
  1. 「場景管理者」遊戲物件不可因更換場景而被銷毀。
  2. 每個獨立場景都必須有「場景管理者」遊戲物件,使其可以獨自執行。
  3. 「場景管理者」的功能必須能直接調用。
由於,希望「場景管理者」不會因為更換場景而被銷毀,但每個獨立場景原本都有自己的「場景管理者」遊戲物件,這將會造成遊戲實際進行時,堆積出多個「場景管理者」遊戲物件,所以也必須針對這部分進行判斷處理,於是,我們有了以下最初版的「場景管理者」Script:

public class SceneManager : MonoBehaviour {

    public static SceneManager ins;

    void Awake(){

        if(ins == null){

            ins = this;
            GameObject.DontDestroyOnLoad(gameObject);

        }else if(ins != this){

            Destroy(gameObject);
        }
    }
}

接下來,可以開始思考遊戲的實際運作過程中,當每個畫面都是獨立場景時,第一個場景載入前必定需要一個負責一些如資料建置、玩家資料載入、Server 端下載資源... 等等的初始化工作的場景,並且顯示出這些工作進度的畫面讓玩家了解到目前系統正在做什麼事情,另外,遊戲場景間轉換時也需要在下個場景完成載入前,為其做準備以及顯示其準備進度的畫面場景,也就是「載入進度場景」,而這個場景在實際遊戲中又並非每個畫面的變更都需要,也不一定每個場景轉換間的「載入進度場景」都是相同的;同時,還要考慮到某些情況下的載入進度畫面可能是在目前場景不被清除的狀態下直接疊在目前畫面上,而這就是需要直接使用 Application.LoadLevelAdditive() 或是 Application.LoadLevelAdditiveAsync() 來載入場景,所以針對這種某些時候需要、某些不需要、某些時候直接載入到當前場景的情況,就需要設計一個設置的機制,使其更為靈活且可運用於其他的遊戲中,而不是每次開發遊戲又要再寫一次相關的程式碼;關於設置的部分,許多人會選擇使用自定格式或是XML格式的純文字檔來讓程式讀取,但在 Unity 中則可以選擇繼承 ScriptableObject 來製作資料資源檔來進行設置,這樣的好處可以方便調用內容、將來擴充或變更內容,以及為其直接在 Unity 中製作設置專用的 Editor,詳細說明可參考以前的文章「Unity 3D : 腳本化物件 ScriptableObject 設置資料成為 AssetBundle」。大致整理一下以上想法,列出如下:
  1. 需要一個「遊戲初始化場景」。
  2. 遊戲場景變換過程,可能需要「載入進度場景」。
  3. 每個場景轉換間的「載入進度場景」可能不同。
  4. 「載入進度場景」可能直接疊在目前場景之上。
  5. 需要設計設置機制。
為了達成場景轉換的彈性設定機制,則需要利用 ScriptableObject 撰寫一個負責設定「場景流」的 Script 並使其依據「Unity 3D : 腳本化物件 ScriptableObject 設置資料成為 AssetBundle」文章中提到的方法製作資料資源檔:

public class SceneNames : ScriptableObject {

    public string initScene;
    public SceneNameHolder[] scenes;
}

[System.Serializable]
public class SceneNameHolder {

    public string own;
    public string loading;
    public bool isAdditiveLoading;
}

製作成資料資源檔,就可以在 Inspector view 的欄位直接設置

在製作成資料資源檔之後,就可以在 Project view 選擇該檔案並在 Inspector view 直接設置場景名稱,當然,必須是在 Build Settings 中有設置的場景名稱才有效,否則可能會在載入時發生錯誤;那麼,這個資料資源檔將為我們帶來以下的功能:
  1. 在 Init Scene 的欄位指定「遊戲初始化場景」名稱,如果保留空白不輸入名稱,則不需要載入「遊戲初始化場景」。
  2. 在 Scenes 的 Size 欄位輸入大於 0 的整數則出現每個場景的設定欄位,並可以開始為各場景設置載入方式。
  3. 在 Own 欄位指定該場景的名稱,如果保留空白不輸入名稱,則跳過而不載入該場景。
  4. 在 Loading 欄位指定該場景載入之前的「載入進度場景」名稱,如果保留空白不輸入名稱,則跳過「載入進度場景」而直接載入該場景。
  5. 如果有勾選 Is Additive Loading 的話,則讓「載入進度場景」直接載入疊在目前場景畫面上,待該場景被載入後才清除目前場景及載入進度畫面。
目前為止,完成了以上的準備工作,接下來在實作「場景管理者」之前,需要再思考一些問題,由於希望能夠無縫的進行場景轉換,所以將不再使用一般常用的 Application.LoadLevel() 來載入場景,而改使用 Application.LoadLevelAsync() 來非同步載入場景,它將回傳一個 AsyncOperation,能夠從中得知場景的載入進度並可在後續利用它的資料來顯示於畫面上,另外,在畫面的處理上,通常也會讓畫面因為轉換場景而有些漸漸變暗或淡出的效果,在目前場景到「載入進度場景」被載入完成前,利用 AsyncOperation 提供的進度來呈現變暗及淡出的效果,然後在「載入進度場景」被載入完成時,也可在場景最開頭簡單製作個漸漸變亮或淡入的效果,如此就能更完美的使場景間無縫轉換,而在真正的下個遊戲場景完成載入之前,也由於這個回傳的 AsyncOperation 使得「載入進度場景」可以正確的顯示遊戲場景載入的進度,在遊戲場景載入完成後,「載入進度場景」將會被直接釋放掉,因此,就可使場景間的轉換變得更為滑順,同時也可讓玩家很清楚地了解到系統上的處理進度。

接下來,先來定義一些「場景管理者」的一些進度狀態,所以直接定義一些狀態並宣告私有欄位來存取當前的狀態,所以在以上的 SceneManager class 加入以下程式碼:

private enum Status{

    None,
    Prepare,
    Start,
    Loading,
    Complete
}

private Status status;

由於需要存取到前面製作的場景設定資料資源檔,所以需要宣告個形態為 SceneNames 的 public 欄位讓 Inspector view 出現用來放入設定資料的欄位,如此只要將該場景設定的資料資源檔直接拉給這個欄位,那麼 SceneManager class 中的程式碼就能直接取得設定的內容;在這裏,應該不難發現到這種做法的便利性,就是將來只要複製資源檔修改其內容,重新拉至該欄位即可換成另一組設定值,如果遊戲需要輸出各種不同設定的版本,只要抽換設定檔即可,相當便於編輯。現在,將 SceneManager class 加入這一行:

public SceneNames sceneNames;

在載入場景的過程也需要加入以下程式碼來獲得目前載入進度:

private AsyncOperation loadOperation;

public float Progress{
    get{
        if(this.loadOperation == null){
            return 0;
        }else{
            return this.loadOperation.progress;
        }
    }
}

現在,真正開始載入場景的部分,首先,當第一個場景被載入時,必須先讓流程轉到載入「遊戲初始化場景」去進行遊戲初始化的動作;前面有提到過必須讓每個場景都能獨立執行,所以假設有 A、B、C 三個場景,我們必須當其中任一個場景是第一個被執行的場景時,都能夠進行遊戲初始化的動作,如果沒設定「遊戲初始化場景」名稱則不需載入「遊戲初始化場景」,所以必須將 SceneManager class 的 Awake() 修改成以下這樣:

void Awake(){

    if(ins == null){

        ins = this;
        GameObject.DontDestroyOnLoad(gameObject);

        if(!string.IsNullOrEmpty(this.sceneNames.initScene)){
            Application.LoadLevel(this.sceneNames.initScene);
        }
    }else if(ins != this){
        Destroy(gameObject);
    }
}

接下來,我們依照前面所提到的需求在 SceneManager class 使用加入幾個方法(method)作為主要的非同步載入的流程控制功能:

public void LoadScene(int idx){

    if(this.status != Status.None && this.status != Status.Complete) return;

    if(idx >= this.sceneNames.scenes.Length) return;

    StartCoroutine(this.AsyncLoadScene(this.sceneNames.scenes[idx]));
}

private IEnumerator AsyncLoadScene(SceneNameHolder sceneName){

    yield return StartCoroutine(this.LoadLoadingScene(sceneName));

    yield return StartCoroutine(this.LoadTargetScene(sceneName));
}

private IEnumerator LoadLoadingScene(SceneNameHolder sceneName){

    if(string.IsNullOrEmpty(sceneName.loading)) yield break;

    this.status = Status.Prepare;

    this.loadOperation = sceneName.isAdditiveLoading ? Application.LoadLevelAdditiveAsync(sceneName.loading) : Application.LoadLevelAsync(sceneName.loading);

    yield return this.loadOperation;

    while(this.status == Status.Prepare){

        yield return null;
    }
}

private IEnumerator LoadTargetScene(SceneNameHolder sceneName){

    this.status = Status.Loading;

    if(string.IsNullOrEmpty(sceneName.own)){

        this.status = Status.None;
        yield break;
    }

    this.loadOperation = Application.LoadLevelAsync(sceneName.own);
    yield return this.loadOperation;

    this.status = Status.Complete;
}

在這裏,我們讓外部可以呼叫 SceneManager 的 LoadScene() 來指定預計要載入場景資料資源檔中所設置的第幾個場景並開始進行場景的載入,而它透過 StartCoroutine() 執行 AsyncLoadScene() 控制「載入進度場景」以及「目標場景」的載入狀態並使前面宣告的 loadOperation 可以獲得載入進度資訊,在這裡也滿足了一些在設置場景資料資源檔時所提到的一些條件。

有了核心的載入場景功能之後,就可以開始製作「載入進度場景」的部分,但在此之前,如果仔細看前面的程式碼會發現到 LoadLoadingScene() 內的 while 似乎不會結束,那要怎麼執行到 LoadTargetScene() 呢?其實,這裡是在等待 status 被改變來使它進行下一個步驟,而在這邊負責改變它的應該就是「載入進度場景」被載入完成時,需要呼叫某個方法(method)來變更它,為了程式方便閱讀及了解,就直接在 SceneManager class 加入以下這個方法:

public void StartLoadTargetScene(){

    this.status = Status.Start;
}

那麼我們將可簡單的撰寫一個給「載入進度場景」專用的 class:

public class LoadingScene : MonoBehaviour {

   IEnumerator Start () {

        yield return new WaitForSeconds(3);

        SceneManager.ins.StartLoadTargetScene();
    }
}

以上,當「載入進度場景」被載入完成之後,將會等待 3 秒後,變更 SceneManager 的 status 而開始進行目標場景的載入。



到目前為止,我們已經可以直接呼叫 LoadScene() 指定要載入的場景,並會依照我們在「載入場景資料資源檔」中所設定的內容,去決定是否需要「載入進度場景」或「載入進度場景」是否附加在目前場景的畫面之上,但在實作上,我們不應該直接呼叫 LoadScene() 來載入場景,而是另外製作一個「場景載入器」來請求「場景管理者」載入指定的場景,因為,直接呼叫 SceneManager 的 LoadScene() 是給予一個 int 的參數,來告知要載入設定檔第幾個場景,但當我們遊戲有相當多的關卡或流程場景時,我們在整個專案的很多地方可能會看到不少諸如 LoadScene(3)、LoadScene(23)、LoadScene(28)... 這一類的呼叫,在後續的維護上,很難一眼看出此時的 3、23、28.. 究竟是載入什麼場景,而在程式撰寫上也應該要避免出現“不可思議”的值,所以有了 SceneManager class 這個核心的「場景管理者」之後,在不同的遊戲中,我們應該要為個別的遊戲製作「場景載入器」並為其定義每個場景的列舉名稱,這樣在開發及維護時,也方便於 IDE 尋找參照。由於「場景載入器」才是實際上在整個遊戲專案中被呼叫執行載入場景的對象,所以也需要跟「場景管理者」相同不可被銷毀以及能獨立於各場景中,所以他的需求如下:
  1. 「場景載入器」遊戲物件不可因更換場景而被銷毀。
  2. 每個獨立場景都必須有「場景載入器」遊戲物件,使其可以獨自執行。
  3. 「場景載入器」的功能必須能直接調用。
  4. 必須宣告每個場景的列舉名稱。
於是,我們可以撰寫出這樣的程式:

public class SceneLoader : MonoBehaviour {

    public enum Scenes{

        Start = 0,
        Test1,
        Test2,
        Test3
    }

    public static SceneLoader ins;

   void Awake(){

        if(ins == null){

            ins = this;
            GameObject.DontDestroyOnLoad(gameObject);

        }else if(ins != this){

            Destroy(gameObject);
        }
    }

    public void LoadLevel(Scenes target){

        SceneManager.ins.LoadScene((int)target);
    }
}

這個 SceneLoader class 使用 enum 宣告列舉了 4 個場景名稱,之後在專案的其他地方,就可以像這樣 SceneLoader.ins.LoadLevel(SceneLoader.Scenes.Test1) 來請求載入場景,這樣語義上是不是更清楚些呢!如果覺得這個參數 SceneLoader.Scenes.Test1 好像太長了,也可以不必將 enum 宣告在 SceneLoader class 裏面,那麼就變成 SceneLoader.ins.LoadLevel(Scenes.Test1),這樣變短一點,一樣是很明確,或是在 SceneLoader class 裡面多寫幾個方法,讓外部的程式直接呼叫,例如:

public void LoadSceneByTest1(){

    SceneLoader.ins.LoadLevel(SceneLoader.Scenes.Test1);
}

這樣做的話,可以在這個方法(method)裡面準備載入某些場景之前先做些特定驗證或判斷。

以上,完成了可以讓每個遊戲專案共用的「場景管理者」以及「場景設置資料資源檔」,還有不同遊戲專案依據實際需求客製化的「場景載入器」,但是還有一個東西遺漏掉了,就是「初始化場景」,由於期待不管是直接開啟哪個場景(「載入進度場景」除外)直接執行遊戲都能正常運作,所以如前面 SceneManager class 的內容寫到的,在開始執行時,如果有設置「初始化場景」名稱,將會載入轉換到「初始化場景」,等待做完我們自己設計的初始化的工作之後再回到剛剛開啟的場景,為了讓「初始化場景」能順利回到前一個場景,我們必須在 SceneLoader class 再宣告一個欄位,讓場景編製內容時可以指定目前場景為何並在開始執行的 Awake() 將此欄位指定並記錄到 SceneManager class,而 SceneManager class 也需提供一個方法使「初始化場景」可以直觀又方便的呼叫並返回到原本的場景中,相關的程式修改如下:

1. SceneLoader class 增加場景編製時用來指定當前場景的欄位:

public Scenes own;

2. SceneManager class 增加儲存當前場景索引的私有欄位及用於儲存驗證的屬性(property):

private int own;

public int OwnSecne{
    set{
        this.own  = Mathf.Clamp(value , 0 , this.sceneNames.scenes.Length - 1);
    }
}

3. 修改 SceneLoader class 的 Awake():

void Awake(){

    if(ins == null){

        ins = this;
        GameObject.DontDestroyOnLoad(gameObject);
        SceneManager.ins.OwnSecne = (int)this.own;

    }else if(ins != this){

        Destroy(gameObject);
    }
}

4. SceneManager class 需提供讓「初始化場景」返回原場景的方法:

public void LoadOwnScene(){

    this.LoadScene(this.own);
}

5. 建立 SceneInitializer class 供「初始化場景」使用:

public class SceneInitializer : MonoBehaviour {

   IEnumerator Start () {

        yield return new WaitForSeconds(3);

        SceneManager.ins.LoadOwnScene();
    }
}

完成了這幾個步驟的程式,如果「場景設置資料資源檔」有設定「初始化場景」名稱的話,不管我們開啟哪個場景(「載入進度場景」除外),都應該會先轉換至「初始化場景」執行完初始化的動作之後再回到原場景中,由於以上的 SceneInitializer class 內容只是測試用的,所以只是讓它等待 3 秒鐘即回到原本的場景;至於為何要在 SceneLoader class 中指定場景本身為何,而不直接在 SceneManager class 中指定,主要是 SceneLoader class 列舉了場景名,這在 Inspector view 設置欄位內容會更明確些,如果直接在 SceneManager class 設置則必須輸入整數值,在編輯上既不方便又容易發生人為疏失,如果將場景名的列舉用在 SceneManager class,那麼不同的遊戲專案,就可能都要來修改 SceneManager class 的內容,而這也是我們所不希望的。

前面寫了一大堆,似乎蠻混亂的,重新將程式整理如下:

public class SceneManager : MonoBehaviour {

    private enum Status{

        None,
        Prepare,
        Start,
        Loading,
        Complete
    }

    public static SceneManager ins;

    public SceneNames sceneNames;

    private Status status;
    private AsyncOperation loadOperation;
    private int own;

    public float Progress{
        get{
            if(this.loadOperation == null){
                return 0;
            }else{
                return this.loadOperation.progress;
            }
        }
    }

    public int OwnSecne{
        set{
            this.own  = Mathf.Clamp(value , 0 , this.sceneNames.scenes.Length - 1);
        }
    }

    void Awake(){

        if(ins == null){

            ins = this;
            GameObject.DontDestroyOnLoad(gameObject);

            if(!string.IsNullOrEmpty(this.sceneNames.initScene)){
                Application.LoadLevel(this.sceneNames.initScene);
            }

        }else if(ins != this){

            Destroy(gameObject);
        }
    }

    public void LoadScene(int idx){

        if(this.status != Status.None && this.status != Status.Complete) return;

        if(idx >= this.sceneNames.scenes.Length) return;

        StartCoroutine(this.AsyncLoadScene(this.sceneNames.scenes[idx]));
    }

    public void LoadOwnScene(){

        this.LoadScene(this.own);
    }

    public void StartLoadTargetScene(){

        this.status = Status.Start;
    }

    private IEnumerator AsyncLoadScene(SceneNameHolder sceneName){

        yield return StartCoroutine(this.LoadLoadingScene(sceneName));

        yield return StartCoroutine(this.LoadTargetScene(sceneName));
    }

    private IEnumerator LoadLoadingScene(SceneNameHolder sceneName){

        if(string.IsNullOrEmpty(sceneName.loading)) yield break;

        this.status = Status.Prepare;

        this.loadOperation = sceneName.isAdditiveLoading ? Application.LoadLevelAdditiveAsync(sceneName.loading) : Application.LoadLevelAsync(sceneName.loading);

        yield return this.loadOperation;

        while(this.status == Status.Prepare){

            yield return null;
        }
    }

    private IEnumerator LoadTargetScene(SceneNameHolder sceneName){

        this.status = Status.Loading;

        if(string.IsNullOrEmpty(sceneName.own)){

            this.status = Status.None;
            yield break;
        }

        this.loadOperation = Application.LoadLevelAsync(sceneName.own);
        yield return this.loadOperation;

        this.status = Status.Complete;
    }
}

public class SceneLoader : MonoBehaviour {

    public enum Scenes{
        Start = 0,
        Test1,
        Test2,
        Test3
    }

    public static SceneLoader ins;

    public Scenes own;

    void Awake(){
        if(ins == null){

            ins = this;
            GameObject.DontDestroyOnLoad(gameObject);
            SceneManager.ins.OwnSecne = (int)this.own;

        }else if(ins != this){

            Destroy(gameObject);
        }
    }

    public void LoadLevel(Scenes target){

        SceneManager.ins.LoadScene((int)target);
    }
}

public class LoadingScene : MonoBehaviour {

    IEnumerator Start () {

        yield return new WaitForSeconds(3);

        SceneManager.ins.StartLoadTargetScene();
    }
}

public class SceneInitializer : MonoBehaviour {

    IEnumerator Start () {

    yield return new WaitForSeconds(3);

        SceneManager.ins.LoadOwnScene();
    }
}

public class SceneNames : ScriptableObject {

    public string initScene;
    public SceneNameHolder[] scenes;
}

[System.Serializable]
public class SceneNameHolder {

    public string own;
    public string loading;
    public bool isAdditiveLoading;
}

完成了以上的程式撰寫以及建立「場景設置資料資源檔」之後,就可以開始配置這個場景轉換機制:
1. 使用 GameObject > Create Empty 建立一個空的 GameObject,賦予它 SceneManager。
2. 選擇這個 SceneManager 的 GameObject,並將「場景設置資料資源檔」從 Project view 中拖曳至 Inspector view 中的 Scene Names 欄位。

Scene Names 放入「場景設置資料資源檔」
3. 將這個 SceneManager 的 GameObject 直接拖曳至 Project view 製作成 Prefab。
4. Ctrl+S (command+S for Mac) 儲存場景作為基礎 Scene 檔。
5. 在 Project view 中選擇剛剛儲存的 Scene 檔,Ctrl+D (command+D for Mac)直接複製並開啟複製出來的 Scene 檔。
6. 使用 GameObject > Create Empty 建立一個空的 GameObject,賦予它 SceneLoader。
7. 選擇這個 SceneLoader 的 GameObject 並從 Inspector view 中的 Own 欄位(下拉選單)指定目前這個場景的列舉場景名,並儲存此場景。

在 Own 欄位指定目前場景
8. 在 Project view 中選擇剛剛儲存的 Scene 檔,預計有多少個目標場景即複製幾個 Scene 檔,個別開啟依照步驟 7 設置 Own 欄位並儲存。
9. 複製步驟 1~4 所儲存的 Scene 檔並開啟,使用 GameObject > Create Empty 建立一個空的 GameObject,賦予它 LoadingScene 並儲存。

「載入進度場景」的內容
10. 複製步驟 1~4 所儲存的 Scene 檔並開啟,使用 GameObject > Create Empty 建立一個空的 GameObject,賦予它 SceneInitializer 並儲存。

「初始化場景」的內容
12. 在「場景設置資料資源檔」設置場景名稱。

在資源檔設置好名稱
13. 開啟 Build Settings 並將使用到的 Scene 檔拖曳至 Scenes In Build 欄位中。

在 Build Settings 設置
14. Edit > Project Settings > Script Execution Order 開啟 MonoManager,在 Inspector view 添加 SceneManager 和 SceneLoader 並設置在 Default Time 之上(注意:SceneManager 必須設置在 SceneLoader 之上)。

設置 Script 執行順序
完成了以上設置後,我們可以寫一個簡單的程式來測試場景轉換的情形:

public class SceneTester : MonoBehaviour {

    void OnGUI(){
        if(GUILayout.Button("Load Start scene")) SceneLoader.ins.LoadLevel(SceneLoader.Scenes.Start);
        if(GUILayout.Button("Load Test1 scene")) SceneLoader.ins.LoadLevel(SceneLoader.Scenes.Test1);
        if(GUILayout.Button("Load Test2 scene")) SceneLoader.ins.LoadLevel(SceneLoader.Scenes.Test2);
        if(GUILayout.Button("Load Test3 scene")) SceneLoader.ins.LoadLevel(SceneLoader.Scenes.Test3);

        GUILayout.Box(SceneManager.ins.Progress.ToString());
    }
}

將這個 SceneTester 掛到前面步驟 3 所製作的 Prefab,即可開始執行任何一個已經設置好的目標場景,點擊畫面的按鈕,可以測試場景的轉換並在按鈕下方的 Box 查看載入進度的數值,不過請記得在每個不同場景添加不同的物件,以方便辨識場景是否正常轉換。

到目前為止,這個場景轉換機制算是已經完成了,但是使用時也許會發現到要設置「場景設置資料資源檔」的場景名稱,又要另外設置 Build Settings 的 Scenes In Build,似乎有點麻煩,如果場景名稱輸入錯字,也容易有系統發生錯誤的情形,其實這部分可以為「場景設置資料資源檔」另行製作編輯器介面來解決,讓設置場景名稱改成直接拖曳 Scene 檔至欄位上即完成設置,在「場景設置資料資源檔」中有任何欄位內容變更即自動更新 Build Settings 的 Scenes In Build 的內容,而不用另外開啟 Build Settings 重新配置。不過,自定義編輯器界面相關的製作是另一個廣泛的課題,就不在此詳細討論,將來有機會再來分享編輯器的製作經驗。
自製編輯器可使工作更便利
最後,提醒大家注意,每當修改過「場景設置資料資源檔」或 Prefab 內容時,最好能習慣性的 File > Save Project 儲存專案比較保險,否則光是 Ctrl+S (command+S for Mac)只是儲存場景,可能並未儲存到 Project view 裡面資源相關檔案的變更。

P.S. 目前使用 Unity 版本為 4.5.4f1。