2014年10月11日 星期六

Unity:設計靈活的無縫場景轉換機制-實作測試

繼上次發佈的「Unity:設計靈活的無縫場景轉換機制」一文,獲得一些回應及提問,發現遺漏掉某些資訊未提及,同時,避免內容說明的不易理解,所以這次來舉例實作,並提供實作的專案檔案內容,給有興趣的人當作參考。

首先,要說明的是「Unity:設計靈活的無縫場景轉換機制」文中所使用到的 Application.LoadLevelAsync() 以及 Application.LoadLevelAdditiveAsync() 是需要 Pro 版才能使用,另外官方文件中也有說明到這功能在編輯器中執行的效能不好或是可能有些小問題,所以如果要測試的話,最好是 Build 之後執行看看會比較準確。

在這個實作專案中,已先加入了三個官方範例資源來作為主要的目標場景:Angry BotsUnity Projects: StealthUnity Projects: 2D Platformer,也使用了日本 Unity 製作的 Unity-Chan 的範例資源來製作「初始化場景」及「載入進度場景」。

Angry Bots 場景
Unity Projects: Stealth 場景
Unity Projects: 2D Platformer 場景
使用 Unity-Chan 做初始化場景
使用 Unity-Chan 做載入進度場景
使用 2D 的 Unity-Chan 做另一個載入進度場景
為了使轉換場景的過程中會有畫面轉暗再變亮的效果,最簡單的做法就是在畫面上罩上一個佔滿全畫面的黑色 GUI 貼圖,並動態去改變它的透明度,所以我們可以在除了初始化場景以外的每個場景中,建立一個名為 _SceneFadegameObject,再撰寫含有以下程式碼的 Script 附加上去:

public class SceneFade : MonoBehaviour {

    public static SceneFade ins;

    public Texture fadeImage;
    public Color fadeColor = Color.black;
    public float fadeInSpeed = 1.0f;

    void Awake(){

        ins = this;
    }

    void OnGUI(){

        GUI.color = this.fadeColor;
        if(this.fadeColor.a > 0 && this.fadeImage) GUI.DrawTexture(new Rect(0 , 0 , Screen.width , Screen.height) , this.fadeImage);
    }

    public IEnumerator FadeIn(){

        while(this.fadeColor.a > 0){

            this.fadeColor.a -= Time.deltaTime * this.fadeInSpeed;
            yield return null;
        }

        this.fadeColor.a = 0;
    }

    public IEnumerator FadeOut(){

        float _progress = SceneManager.ins.Progress;

        while(_progress < 1){

            this.fadeColor.a = _progress;
            yield return null;

            _progress = SceneManager.ins.Progress;
        }
    }
}

這時候,在 Hierarchy view 選擇 _SceneFade 後可以在 Inspector view 看到幾個欄位,其中的 Fade Image 欄位,是用來遮擋全畫面的圖,這張圖只需要一張 1 pixel 的白色小圖,Fade Color 欄位預設為黑色,主要是影響畫面變暗時的目標顏色,基本上,保持預設值即可,Fade In Speed 代表的是畫面從暗轉亮時的速度,數值越小會越慢。

_SceneFade gameObject 的欄位設定



接下來,改寫「Unity:設計靈活的無縫場景轉換機制」文中所提到的 SceneLoader class 如下:

public class SceneLoader : MonoBehaviour {

    public enum Scenes{

        AngryBot = 0,
        Stealth,
        Platformer2D
    }

    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);
        StartCoroutine(SceneFade.ins.FadeOut());
    }
}

在這裏,只是改寫了 enum Scenes 裡面定義的列舉來讓之後的程式撰寫及設定上可以更明確指定即將載入哪個場景,以及在 LoadLevel() 裡面多了一句 StartCoroutine(SceneFade.ins.FadeOut()),這樣在載入下個場景時,使畫面有點淡出的效果,使畫面由亮轉暗。完成這個 Script 之後,在目標場景上建立個名為 _SceneLoadergameObject 並將 Script 附加上去,此時在 Hierarchy view 選擇 _SceneLoader 可以在 Inspector view 看到 Own 欄位為下拉式選單,在這裡要為這個欄位指定當前場景是哪個。由於每個實際執行遊戲內容的目標場景都需要這個 _SceneLoader gameObjcet,所以在這裡就可順便將它製作成 Prefab,並依序開啟其他場景,將該 Prefab 拖至場景中並設置其 Own 欄位。

在 _SceneLoader gameObject 的欄位指定當前場景為何
完成主要場景的需求之後,同樣改寫「Unity:設計靈活的無縫場景轉換機制」文中的 LoadingScene class 給「載入進度場景」使用:

public class LoadingScene : MonoBehaviour {

    public Texture barImage;
    public Color barColor = Color.white;
    public Rect barPosition = new Rect(100 , 200 , 300 , 25);
    public float barMargin = 5.0f;
    public bool showBar;

    IEnumerator Start () {

        yield return StartCoroutine(SceneFade.ins.FadeIn());
        this.showBar = true;
        SceneManager.ins.StartLoadTargetScene();
    }

    void OnGUI(){

        if(!this.showBar) return;

        this.barPosition.center = new Vector2(Screen.width / 2 , Screen.height / 2);

        GUI.Box(this.barPosition , string.Empty);

        GUI.color = this.barColor;

        GUI.DrawTexture(new Rect(this.barPosition.x + this.barMargin , this.barPosition.y + this.barMargin , (this.barPosition.width - this.barMargin * 2) * SceneManager.ins.Progress , this.barPosition.height - this.barMargin * 2) , this.barImage);
    }
}

主要動作是希望在「載入進度場景」開始時一樣會淡入,畫面由暗轉亮之後才開始顯示進度條,完成這個 Script 之後,開啟要作為「載入進度場景」的場景,建立一個名為 _LoadingScenegameObject 並將這個 Script 附加上去;在 Hierarchy view 選擇 _LoadingScene 可以在 Inspector view 看到幾個欄位,其中的 Bar Image 與前面的 SceneFade 相同,只需要提供 1 pixel 的小白圖即可,其餘的欄位,除了將 Bar Color 改成想要的進度條顏色之外,基本上都可維持不變使用預設值。

_LoadingScene gameObject 的欄位設定
最後,別忘了還有初始化場景,同樣的,改寫「Unity:設計靈活的無縫場景轉換機制」文中的 SceneInitializer class 給「初始化場景」使用:

public class SceneInitializer : MonoBehaviour {

    public Texture barImage;
    public Color barColor = Color.white;
    public Rect barPosition = new Rect(100 , 200 , 300 , 25);
    public float barMargin = 5.0f;

    public float[] initTimes;
    private float propress;

    IEnumerator Start () {

        yield return StartCoroutine(this.WaitInitialization());
        SceneManager.ins.LoadOwnScene();
    }

    void OnGUI(){

        this.barPosition.center = new Vector2(Screen.width / 2 , Screen.height / 2);

        GUI.Box(this.barPosition , string.Empty);

        GUI.color = this.barColor;

        GUI.DrawTexture(new Rect(this.barPosition.x + this.barMargin , this.barPosition.y + this.barMargin , (this.barPosition.width - this.barMargin * 2) * this.propress , this.barPosition.height - this.barMargin * 2) , this.barImage);
    }

    private IEnumerator WaitInitialization(){

        int currentInitStep = 0;
        float currentInitTime = 0;

        while(currentInitStep < this.initTimes.Length){

            currentInitTime += Time.deltaTime;

            if(currentInitTime >= this.initTimes[currentInitStep]){

                StartCoroutine(this.BarToPropress(this.propress + 1.0f / this.initTimes.Length));
                currentInitTime = 0;
                currentInitStep++;
            }

            yield return null;
        }
    }

    private IEnumerator BarToPropress(float target){

        while(this.propress < target){

            this.propress += Time.deltaTime;
            if(this.propress > target) this.propress = target;

            yield return null;
        }
    }
}

主要動作也是顯示初始化的進度條,不過由於這裡只是場景轉換的測試用而已,並沒有實際開發遊戲專案時的初始化動作(如連接伺服器或遊戲初始化設置、驗證、資源檢查...等等),所以另外多了設定時間區段的數值來用於模擬初始化進度;同樣的,完成這個 Script 之後,開啟要作為「初始化場景」的場景,建立一個名為 _SceneInitializergameObject 並將這個 Script 附加上去;在 Hierarchy view 選擇 _SceneInitializer 可以在 Inspector view 看到幾個欄位,其中除了 Init Times 之外,都與前面的 LoadingScene 相同,而 Init Times 欄位,就是要用來模擬初始化進度的時間值,如果指定五個時間長度,之後執行時的初始化進度條則會分五段完成。

_SceneInitializer gameObject 的欄位設定
完成以上動作之後,我們可以寫一個簡單的 Script 來做為測試用:

public class SceneTester : MonoBehaviour {

    private bool showGUI;

    void Start () {
        StartCoroutine(SceneFade.ins.FadeIn());
    }

    void Update(){
        if(Input.GetKeyDown(KeyCode.Escape)){
            this.showGUI = !this.showGUI;
            Screen.showCursor = this.showGUI;
        }
    }

    void OnGUI(){

        if(!this.showGUI) return;

        if(GUILayout.Button(SceneLoader.Scenes.AngryBot.ToString())) SceneLoader.ins.LoadLevel(SceneLoader.Scenes.AngryBot);
        if(GUILayout.Button(SceneLoader.Scenes.Stealth.ToString())) SceneLoader.ins.LoadLevel(SceneLoader.Scenes.Stealth);
        if(GUILayout.Button(SceneLoader.Scenes.Platformer2D.ToString())) SceneLoader.ins.LoadLevel(SceneLoader.Scenes.Platformer2D);
    }
}

這個 Script 其實也就如同正式開發遊戲專案中的每個場景中的 Game Manager 一樣,只是在這裡只做測試場景轉換使用,所以只有簡單的內容,主要功能是希望場景開始時有點淡入效果,讓畫面由暗轉亮,以及幾個 GUI 按鈕來用於切換場景使用;
由於,這個實作中所使用到的幾個官方範例場景,在遊戲進行中會將滑鼠指標隱藏起來,所以另外加入按下鍵盤的 ESC 鍵才顯示出鼠標以及 GUI 按鈕的功能。完成這個 Script 之後,在目標場景上建立個名為 _SceneTestergameObject 並將 Script 附加上去。由於每個實際執行遊戲內容的目標場景都需要這個 _SceneTester gameObjcet,所以在這裡可以將它製作成 Prefab,並依序開啟其他場景,將該 Prefab 拖至場景中。

最後,別忘了放置「Unity:設計靈活的無縫場景轉換機制」文中所製作的 SceneManager 至每個場景中。

各目標場景都會有這四個 GameObject
「載入進度場景」會有這三個 GameObject
「初始化場景」會有這二個 GameObject
全部的工作都完成了之後,我們可以參考「Unity:設計靈活的無縫場景轉換機制」中的流程設置好「場景設置資料資源檔」以及 Build Settings,就可以開始測試結果了。

以上,寫了一堆,可能還是無法完整表達做法,所以在此提供範例檔案給大家下載回去參考:點此下載



範例中的 Assets/Settings/SceneNameSettings 為「場景設置資料資源檔」,主要為提供給 SceneManagerScene Names 欄位設置使用,而 Assets/Settings/SceneSettings 算是「場景設置資料資源檔」的設定工具,它使用了自定義編輯器的技巧重新編寫了 Inspector view 上的功能,在需要設置場景轉換流程時,於 Project view 選取 Assets/Settings/SceneSettings 之後,即可直接從 Project view 拖曳場景檔到 Inspector view 中的欄位中,此時會同時更新 Assets/Settings/SceneNameSettings 中的場景名稱以及 Build Settings 的 Scenes In Build 欄位內容,可以節省許多設定場景流程資料時的動作以及避免不必要的輸入錯誤。

Assets/Settings/SceneSettings 可直接拉進場景檔案到欄位裡
Assets/Settings/SceneNameSettings 隨著 SceneSettings 內容的變更也跟隨改變內容
另外,範例中也另外提供一個 LoadingSceneAdditive 的場景檔,其製作方式與 LoadingScene 大同小異,當有需要勾選「場景設置資料資源檔」中的 Is Additive Loading 時,可使用此場景替換掉 LoadingScene,它可使轉換場景時,直接在當前場景顯示載入下個場景的進度條並開始載入下個場景。

整個範例的專案目錄下有個 BuildFiles 資料夾中也提供了已經 Build 好的 PC 及 Mac 的執行檔,可直接執行測試一下結果。

這個無縫場景轉換機制主要為表達非同步載入場景、附加場景載入、初始化場景的準備以及場景間插入顯示進度的場景.. 等這些轉換場景概念,同時利用設定檔的方式來設定場景轉換流程的更換,以盡可能的避免寫死在程式碼中,而使其在設置上更為彈性,但此做法並非唯一且最完美的解決方案,還有許多待改進空間,等待大家自行去進化它。

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