2016年12月27日 星期二

Day 28: 排行榜

增加遊戲可玩性的方法有很多種,其中最簡單的方式就是加入排行榜機制,讓玩家有破紀錄、比較成續的快感。


今天我要為遊戲加入一個簡單的排行榜。這個排行榜可紀錄在該裝置中的前三高分,以及前三高分所使用的角色。

 (示意圖)

在Start Scene中建立一個新的Game Object,並為它新增一段名為「ScoreBoardDataControl.cs」的Script。這段Script會比較複雜,因為排行榜機制牽涉到把資料序列化(Serialize)儲存至裝置,以及把裝燈的資料反序列化(Deserialize)變回程式可理解的物件。下圖是該段程式的流程簡圖。


(反序列化是Deserialize,圖片裡忘了改)

馬上進入程式碼:

using UnityEngine;

using System.Collections;

using System;  //Serializable

using System.Runtime.Serialization.Formatters.Binary; //BinaryFormatter

using System.IO;  /FileStream



public class ScoreBoardDataControl : MonoBehaviour 

{
    //Singleton Pattern,可使任何程式呼叫ScoreBoardDataControl.instance取得該物件

    public static ScoreBoardDataControl instance;


    //實際儲存分數資料的物件(ScoreData定義在下方)

    private ScoreData data;


    //要顯示的名次數量(3表示只顯示前三名)

    private const int Places = 3;



    void Awake()
    {
        //Singleton Pattern,只能擁有一個ScoreBoardDataControl物件

        if (instance == null) {

            //轉換場景時不要移除此Game Object

            DontDestroyOnLoad (gameObject);

            //載入排行榜資料

            LoadData ();

            //把instance設好,以供其他地方以ScoreBoardDataControl.instance取得物件

            instance = this;

        } else if (instance != this) {

            //第二次進入開始畫面,把新的ScoreBoardDataControl刪掉

            Destroy (gameObject);

        }

    }



    void LoadData()

    {

        //如果檔案存在(表示不是第一次開啟遊戲)

        if (File.Exists (Application.persistentDataPath + "/scoreInfo.dat")) {

            BinaryFormatter bf = new BinaryFormatter ();

            FileStream file = File.Open (Application.persistentDataPath + "/scoreInfo.dat", FileMode.Open);

            //把裝置中的二進位檔案反序列化,存入data變數中

            data = (ScoreData) bf.Deserialize (file);

            file.Close ();
        } else {     //如果檔案不存在(第一次開啟遊戲)

            InitData (); //初始化資料

            SaveData (); //儲入裝置

        }

    }



    void InitData()

    {
        //初始化data、data的陣列

        data = new ScoreData ();

        data.scores = new int[Places];

        data.playerTypes = new int[Places];



        for (int i = 0; i < Places; i++) {

            data.scores [i] = 0;

            data.playerTypes [i] = -1;

        }

    }



    void SaveData()

    {

        BinaryFormatter bf = new BinaryFormatter ();

        //新增檔案

        FileStream file = File.Create (Application.persistentDataPath + "/scoreInfo.dat");

        //序列化,存入裝置

        bf.Serialize (file, data);

        file.Close ();

    }



    public void NewScore(int playerType, int score)
    {
        //判斷此分數能排在第幾名

        int place = Places - 1;


        while (place >= 0 && score > data.scores [place]) {

            place--;

        }



        place++;

        //無法進入排行榜
        if (place >= Places)

            return;



        //把此分數之後的排名向後挪一名

        for (int i = Places - 2; i >= place; i--) {

            data.scores [i + 1] = data.scores [i];

            data.playerTypes [i + 1] = data.playerTypes [i];

        }
 

        //更新名次

        data.scores [place] = score;

        data.playerTypes [place] = playerType;
      

        //存入裝置

        SaveData ();

    }


    //取得place名次的分數

    public int GetScore(int place)

    {

        return data.scores [place];

    }


    //取得place名次所使用的角色

    public int GetPlayerType(int place)

    {

        return data.playerTypes [place];

    }

}



[Serializable]  //可序列化的類別

class ScoreData
{

    public int[] scores; //儲存前三名的分數

    public int[] playerTypes; //儲存前三名使用的角色

}

大致解說一下上面的程式碼。首先,我使用了類似Singleton Pattern的設計模式(Design Pattern),使整個遊戲只允許一個ScoreBoardDataControl物件存在,並在任何程式碼都可藉ScoreBoardDataControl.instance取得該物件的參考(reference)。

Awake()函數中,我判斷instance是否已經存在,若已經存在,則把新的ScoreBoardDataControl物件刪除,確保只有一個ScoreBoardDataControl物件存在。若不存在,則把自己指定給instance,並以DontDestroyOnLoad()使該物件不會在轉換場景時被移除。

LoadDataSaveData把資料存入或取出裝置中的二進位檔案。其中的Application.persistentDataPath是Unity依照不同作業系統定義的檔案儲存位置。

NewScore(int score)之後會在遊戲結束時被呼叫,用以判斷新的分數是否足以登上排行榜。如果可以,則調整排行榜資料,並存入裝置。

GetScoreGetPlayerType則會在瀏覽排行榜時被呼叫,取得data變數的資料。

最後,[Serializable]屬性(Attribute)讓ScoreData類別可被序列化,存入裝置。

寫完Script後,開個新的Canvas來佈置一下排行榜。



排行榜主要由三個Image和三個Text構成,分別代表前三名所使用的角色和分數。


為每一個Text加上下面的Script。

using UnityEngine;
using System.Collections;

using UnityEngine.UI;



public class ScoreboardScoreText : MonoBehaviour 

{
    //第幾名 (第一名:0,第二名:1,第三名:2)

    public int place;



    void Start()
    {

        //由ScoreBoardDataControl依照名次取得分數

        int score = ScoreBoardDataControl.instance.GetScore (place);

        //若分數不為零,更新分數。為零則不顯示。

        if (score != 0) {
            GetComponent< Text > ().text = score.ToString () + "cm";

        } else {

            gameObject.SetActive (false);

        }

    }

}


為每一個Image加入以下Script。

using UnityEngine;

using System.Collections;

using UnityEngine.UI;



public class ScoreboardIcon : MonoBehaviour 

{
    //名次
    public int place;


    //存儲各角色sprite的陣列

    public Sprite[] characterTypes;



    void Start()

    {
        //依照名次取得使用的角色

        int charType = ScoreBoardDataControl.instance.GetPlayerType (place);

        //如果不為-1,則根據角色更改Sprite。為-1(沒有該名次)則不顯示

        if (charType != -1) {

            GetComponent< Image > ().sprite = characterTypes [charType];
        } else {

            gameObject.SetActive (false);

        }

    }

}

在Inspector把角色的Sprite依照順序拉入Character Types陣列,並把Text和Image的Place變數設定好。

接著在GameOverCanvas.cs的Start()函數加入

ScoreBoardDataControl.instance.NewScore (GameObject.FindObjectOfType< Character > ().charIndex, score);

來儲存該次遊戲結束的分數,排行榜就完成了!


在開始畫面和結束畫面新增一個開啟排行榜的按鈕,玩家就可觀看排行榜的成續了。



待續。

1 則留言 :

  1. 您好,請問我有問題想請教還能找得到您嗎

    回覆刪除