ゲームデータについて(9)ジェネリックの利用

これまで紹介したサンプルプログラムは固定データ・可変データを1種類ずつしか持っていませんが、大抵のゲームでは多数用意することになると思います。
今のままでは各クラス間で重複するコードが多く、データの種類が追加される度にこれらを全て書く必要があります。
C#ジェネリック(C++のテンプレート)を利用することでコード量の削減が見込めます。

固定データクラスのジェネリック

Assets/GameData/FixedData/FixedDataManager.csを修正

using UnityEngine;
using UnityEngine.Assertions;
using System; //この行を追加.
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

public static class FixedDataManager
{
    // ここからを追加.
    [Serializable]
    public class FixedDataList<T> where T : class
    {
        private List<T> _list = new List<T>();
        public List<T> list
        {
            get { return _list; }
        }

        public int GetDataCount()
        {
            return list.Count;
        }

        public bool CheckValidIndex(int index)
        {
            return (index >= 0) && (index < GetDataCount());
        }
        public T GetData(int index)
        {
            if (!CheckValidIndex(index))
            {
                Assert.IsTrue(false);
                return null;
            }
            return list[index];
        }
        public void Add(T data)
        {
            list.Add(data);
        }
    }

    private static FixedDataList<FixedMonsterData> _monsterList = null;
    public static FixedDataList<FixedMonsterData> monsterList
    {
        get { return _monsterList; }
    }
    // ここまでを追加.
    // ここからを削除.
    //private static List<FixedMonsterData> _monsterList = null;
    //public static List<FixedMonsterData> monsterList
    //{
    //    get { return _monsterList; }
    //}
    // ここまでを削除.

(中略)

    private static void Initialize()
    {
        // この行を下の行に差し替え.
        //_monsterList = new List<FixedMonsterData>();
        _monsterList = new FixedDataList<FixedMonsterData>(); 
    }

(中略)

    public static void Serialize()
    {
        // この行を下の行に差し替え.
        //_monsterList = ReadSerializedData<List<FixedMonsterData>>("FixedData/FixedMonsterData");
        _monsterList = ReadSerializedData<FixedDataList<FixedMonsterData>>("FixedData/FixedMonsterData"); 
    }

    // ここからを削除.
    //public static int GetMonsterDataCount()
    //{
    //    return _monsterList.Count;
    //}
    //public static bool CheckValidMonsterIndex(int index)
    //{
    //    return (index >= 0) && (index < GetMonsterDataCount());
    //}
    //public static FixedMonsterData GetMonsterData(int index)
    //{
    //    if (!CheckValidMonsterIndex(index))
    //    {
    //        Assert.IsTrue(false);
    //        return null;
    //    }
    //    return _monsterList[index];
    //}
    // ここまでを削除.
}

Assets/Editor/FixedDataConverter.csを修正

            // この行を下の行に差し替え.
            //List<FixedMonsterData> monsterList = FixedDataManager.monsterList;
            FixedDataManager.FixedDataList<FixedMonsterData> monsterList = FixedDataManager.monsterList;

Assets/GameData/TextData/TextDataManager.csを修正

            // モンスター名.
            case REPLACE_TYPE.MONSTER:
                {
                    if (counter >= replaceMonsterParams.Count)
                    {
                        Assert.IsTrue(false);
                        break;
                    }
                    int id = replaceMonsterParams[counter];
                    // この行を下の行に差し替え.
                    //strAdd = FixedDataManager.GetMonsterData(id).GetName();
                    strAdd = FixedDataManager.monsterList.GetData(id).GetName();
                }
                break;
可変データクラスのジェネリック

VariableDataManagerクラスのGetCharacterDataFormID(int)関数はCharacterDataクラスのメンバint _idを参照しています。

    public static CharacterData GetCharacterDataFormID(int id)
    {
        if (id < 0)
        {
            Assert.IsTrue(false);
            return null;
        }

        CharacterData characterData = characterList.Find(x => x.id == id); // ここでメンバ_idを参照.
        if (characterData == null)
        {
            Assert.IsTrue(false);
            return null;
        }

        return characterData;
    }

このままではジェネリックを利用できないので、可変データのベースクラスとしてメンバint idのみを持つBaseVariableDataクラスを作成し、ジェネリック型制約(where句)を利用して実装、CharacterDataはBaseVariableDataの派生という形にします。
Assets/GameData/VariableData/BaseVariableData.csを作成

using System;

[Serializable]
public class BaseVariableData
{
    private int _id = -1;
    public int id
    {
        get { return _id; }
        set { _id = value; }
    }

    public void Init(int _id)
    {
        id = _id;
    }

    public void Clear()
    {
        id = -1;
    }

    public void Copy(BaseVariableData data)
    {
        id = data.id;
    }

    public bool IsValid()
    {
        return id != -1;
    }
}

Assets/GameData/VariableData/CharacterData.csを修正

using System;

[Serializable]
// ここからを削除.
//public class CharacterData
//{
//    private int _id = -1;
//    public int id
//    {
//        get { return _id; }
//        set { _id = value; }
//    }
// ここまでを削除.
// ここからを追加.
public class CharacterData : BaseVariableData
{
// ここまでを追加.

(中略)

    public void Init( int _id, int _level, int _hp, int _mp, int _attack, int _defense, int _agility )
    {
        // この行を下の行に差し替え.
        //id = _id;
        base.Init(_id);
        level = _level;
        hp = _hp;
        mp = _mp;
        attack = _attack;
        defense = _defense;
        agility = _agility;
    }

    // ここからを削除.
    //public void Clear()
    //{
    //    id = -1;
    // ここまでを削除.
    // ここからを追加.
    new public void Clear()
    {
        base.Clear();
    // ここまでを追加.
        level = 0;
        hp = 0;
        mp = 0;
        attack = 0;
        defense = 0;
        agility = 0;
    }    
    
    public void Copy( CharacterData data )
    {
        // この行を下の行に差し替え.
        //id = data.id;
        base.Copy(data); 
        level = data.level;
        hp = data.hp;
        mp = data.mp;
        attack = data.attack;
        defense = data.defense;
        agility = data.agility;
    }

    // ここからを削除.
    //public bool IsValid()
    //{
    //    return id != -1;
    //}
    // ここまでを削除.
    // ここからを追加.
    new public bool IsValid()    
    {
        return base.IsValid();
    }
    // ここまでを追加.

Assets/GameData/VariableData/VariableDataManager.csの修正

using System; // この行を追加.
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine.Assertions;

public static class VariableDataManager
{
    // ここからを追加.
    [Serializable]
    public class VariableDataList<T> where T : BaseVariableData
    {
        private List<T> _list = new List<T>();
        private List<T> list
        {
            get { return _list; }
            set { _list = value; }
        }

        public void Add(T data)
        {
            list.Add(data);
        }

        public void Clear()
        {
            list.Clear();
        }

        public T GetData(int index)
        {
            if (index < 0 || index >= list.Count)
            {
                Assert.IsTrue(false);
                return null;
            }

            return list[index];
        }

        public T GetDataFromID(int id)
        {
            if (id < 0)
            {
                Assert.IsTrue(false);
                return null;
            }

            T data = list.Find(x => x.id == id);
            if (data == null)
            {
                Assert.IsTrue(false);
                return null;
            }

            return data;
        }
        public int GetDataCount()
        {
            return list.Count;
        }
    }

    private static VariableDataList<CharacterData> _characterList = null;
    public static VariableDataList<CharacterData> characterList
    {
        get { return _characterList; }
        set { _characterList = value; }
    }

    // ここまでを追加.
    // ここからを削除.
    //private static List<CharacterData> _characterList = null;
    //private static List<CharacterData> characterList
    //{
    //    get { return _characterList; }
    //    set { _characterList = value; }
    //}

(中略)

    private static void Initialize()
    {
        // この行を下の行に差し替え.
        //characterList = new List<CharacterData>();
        characterList = new VariableDataList<CharacterData>();
    }

(中略)

    public static void LoadData(FileStream fileStream)
    {
        BinaryFormatter binaryFormatter = new BinaryFormatter();
        // この行を下の行に差し替え.
        //characterList = binaryFormatter.Deserialize(fileStream) as List<CharacterData>;
        characterList = binaryFormatter.Deserialize(fileStream) as VariableDataList<CharacterData>;
    }

    // ここからを削除.
    //public static CharacterData GetCharacterData(int index)
    //{
    //    if (index < 0 || index >= characterList.Count)
    //    {
    //        Assert.IsTrue(false);
    //        return null;
    //    }
    //
    //    return characterList[index];
    //}
    //public static CharacterData GetCharacterDataFormID(int id)
    //{
    //    if (id < 0)
    //    {
    //        Assert.IsTrue(false);
    //        return null;
    //    }
    //
    //    CharacterData characterData = characterList.Find(x => x.id == id);
    //    if (characterData == null)
    //    {
    //        Assert.IsTrue(false);
    //        return null;
    //    }
    //
    //    return characterData;
    //}
    //public static int GetCharacterDataNum()
    //{
    //    return characterList.Count;
    //}
    // ここまでを削除.

ゲームデータについて(8)テキストデータの差し替え処理のつづき

前記事のサンプルコードについて簡単に解説し、使用例も紹介します。

差し替えタイプとタグの定義
    // 差し替えタイプ.
    private enum REPLACE_TYPE
    {
        NUMBER = 0, // 数値.
        STRING,     // 文字列.
        MONSTER,    // モンスター名.

        NUM
    };
    // 差し替えタグ.
    private static string[] replaceTags =
    {
        "{NUM}",
        "{STR}",
        "{MONSTER}",
    };

差し替える内容として数値・任意の文字列・モンスター名を差し替えタイプとして定義し、各タイプのタグ文字列を定義します。

差し替えパラメータとカウンタ
    // 差し替えパラメータ.
    private static List<int> replaceNumParams = new List<int>();
    private static List<string> replaceStrParams = new List<string>();
    private static List<int> replaceMonsterParams = new List<int>();

    // 差し替えカウンタ(タイプ毎の配列).
    private static byte[] replaceCount = new byte[(int)REPLACE_TYPE.NUM];

差し替える文字列は差し替えパラメータとして一時的にメンバに格納されます。
数値はint,文字列はstring.モンスター名はint(モンスターID)として一つのテキストにつき複数保持できるようにListにします。
また、処理途中に何番目のパラメータを処理しているかの情報としてカウンタを保持します。

差し替えパラメータ・差し替えカウンタのクリア処理
    public static void ClearReplaceParams()
    {
        replaceNumParams.Clear();
        replaceStrParams.Clear();
        replaceMonsterParams.Clear();
    }

    private static void ClearReplaceCounter()
    {
        for (int i = 0; i < (int)replaceCount.Length; ++i)
        {
            replaceCount[i] = 0;
        }
    }

差し替えパラメータは差し替え処理が終わったら不要になるので差し替え後に必ず呼ばれるようにします。
一方差し替えカウンタは差し替え処理が始まる前にクリアされる必要があるので処理前に呼ばれます。

差し替えパラメータの設定
    public static void SetNumber(List<int> nums)
    {
        replaceNumParams = nums;
    }

    public static void SetString(List<string> strings)
    {
        replaceStrParams = strings;
    }

    public static void SetMonster(List<int> ids)
    {
        replaceMonsterParams = ids;
    }

差し替えパラメータを設定する処理です。
テキストデータを取得する際にGetTextを呼ぶ前に呼ばれる想定です。

文字列の差し替え処理
    private static void ReplaceText(ref string strText)
    private static int ReplaceTextSub(ref string strText, REPLACE_TYPE replaceType, int index)

文字列の差し替え処理としてGetTextの中から呼ばれます。
ReplaceTextで引数として渡された文字列を走査してタグを見つけたらサブルーチンであるReplaceTextSubを呼びます。
ReplaceTextSub内でタグが差し替えパラメータに応じた文字列に差し替えられ、差し替えた文字列の長さを返します。
ReplaceTextはReplaceTextSubが返した文字列数分進めた状態で走査を続行します。

使用例
    // 数値の差し替え.
    TextDataManager.SetNumber(new List<int> { 100, 200 });
    Debug.Log(TextDataManager.GetText(TEXT_LABEL.SYSTEM_OBTAIN_EXP_AND_GOLD));
    // 文字列の差し替え.
    TextDataManager.SetString(new List<string> { "ラダトーム" });
    Debug.Log(TextDataManager.GetText(TEXT_LABEL.SYSTEM_INFORM_LOCATION));
    // モンスター名の差し替え.(モンスターIDを指定する).
    TextDataManager.SetMonster(new List<int> { 0 });
    Debug.Log(TextDataManager.GetText(TEXT_LABEL.SYSTEM_MONSTER_APPEARS));
応用

今回はタグを利用した文字列の差し替えについて説明しましたが、文字色や文字サイズ・太さの変更なども同じ要領で実装ができると思います。

ゲームデータについて(7)テキストデータの差し替え処理

本記事ではこちらの記事で軽く触れた、テキストデータの一部をゲームの状況に応じて特定の文字列に差し替える処理の実装について説明します。

文字列の差し替え処理の実装

テキストデータにタグを埋め込んで、タグの種類に応じた文字列を差し替える処理を実装します。
差し替えられる種類は数値・文字列・モンスター名とし、それぞれにタグを用意します。

Assets/Editor/GameData/textData.xlsm のSystemシートに文字列差し替えタグを含めた文字列を追加します。

Assets/GameData/TextData/TextDataManager.csを下記のように修正します。

using UnityEngine;
using UnityEngine.Assertions;
using System.Collections.Generic;     // これを追加.
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public static class TextDataManager
{
    private static TextData textData = null;

    // ここからを追加.
    // 差し替えタイプ.
    private enum REPLACE_TYPE
    {
        NUMBER = 0, // 数値.
        STRING,     // 文字列.
        MONSTER,    // モンスター名.

        NUM
    };
    // 差し替えタグ.
    private static string[] replaceTags =
    {
        "{NUM}",
        "{STR}",
        "{MONSTER}",
    };

    // 差し替えパラメータ.
    private static List<int> replaceNumParams = new List<int>();
    private static List<string> replaceStrParams = new List<string>();
    private static List<int> replaceMonsterParams = new List<int>();
    // 差し替えカウンタ(タイプ毎の配列).
    private static byte[] replaceCount = new byte[(int)REPLACE_TYPE.NUM];
    // ここまでを追加.

    static TextDataManager()
    {
    }

    public static bool Serialize()
    {
        TextAsset serializeFile = Resources.Load("TextData/TextData") as TextAsset;
        using (MemoryStream memoryStream = new MemoryStream(serializeFile.bytes))
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            textData = binaryFormatter.Deserialize(memoryStream) as TextData;
        }

        return true;
    }

    // ここからを削除.
    //public static string GetText(TEXT_LABEL textLabel)
    //{
    //    Assert.IsTrue(textLabel < TEXT_LABEL.NUM);

    //    // テキストデータ取得.
    //    return textData.TextList[(int)textLabel];
    //}
    // ここまでを削除.

    // ここからを追加.
    public static void ClearReplaceParams()
    {
        replaceNumParams.Clear();
        replaceStrParams.Clear();
        replaceMonsterParams.Clear();
    }

    private static void ClearReplaceCounter()
    {
        for (int i = 0; i < (int)replaceCount.Length; ++i)
        {
            replaceCount[i] = 0;
        }
    }

    public static void SetNumber(List<int> nums)
    {
        replaceNumParams = nums;
    }

    public static void SetString(List<string> strings)
    {
        replaceStrParams = strings;
    }

    public static void SetMonster(List<int> ids)
    {
        replaceMonsterParams = ids;
    }

    /***********************************************************
    文字列の差し替え処理.
    差し替えた文字列をstrTextに返す.
    ***********************************************************/
    private static void ReplaceText(ref string strText)
    {
        // 差し替えカウンタを初期化.
        ClearReplaceCounter();

        int index = 0;

        for (; ; )
        {
            // テキストの最後まで検索した.
            if (index >= strText.Length)
            {
                break;
            }

            // 差し替えタグ文字を探す.
            index = strText.IndexOf('{', index);

            // タグ文字がなかった.
            if (index < 0)
            {
                break;
            }

            int indexEnd = strText.IndexOf('}', index);

            string strTag = strText.Substring(index, indexEnd - index + 1);
            for (int i = 0; i < (int)REPLACE_TYPE.NUM; i++)
            {
                // 差し替えタグ一致チェック.
                if (strTag == replaceTags[i])
                {
                    // 差し替え.
                    int replacedLength = ReplaceTextSub(ref strText, (REPLACE_TYPE)i, index);
                    if (replacedLength <= 0)
                    {
                        // エラー.
                        index = -1;
                    }
                    else
                    {
                        index += replacedLength;
                    }
                    break;
                }
            }

            // 差し替えのエラーによる終了.
            if (index < 0)
            {
                Debug.LogWarning("文字列の差し替えエラー");
                break;
            }
        }
    }

    /***********************************************************
    文字列の差し替え処理のサブルーチン.
    差し替えタグが検出されると呼ばれてタイプに応じたパラメータの値で差し替える.
    差し替えた文字列をstrTextに返す.
    差し替えた文字の長さを返り値とする.
    ***********************************************************/
    private static int ReplaceTextSub(ref string strText, REPLACE_TYPE replaceType, int index)
    {
        Assert.IsNotNull(strText);
        Assert.IsTrue(replaceType < REPLACE_TYPE.NUM);

        // 差し替えタグを削除.
        int removeLength = replaceTags[(int)replaceType].Length;
        string strReplace = strText.Remove(index, removeLength);
        // 差し替え内容.
        string strAdd = "";
        // 差し替えカウンタ.
        int counter = replaceCount[(int)replaceType];

        switch (replaceType)
        {
            // 数値.
            case REPLACE_TYPE.NUMBER:
                {
                    if(counter >= replaceNumParams.Count)
                    {
                        Assert.IsTrue(false);
                        break;
                    }
                    strAdd = replaceNumParams[counter].ToString();
                }
                break;

            // 文字列.
            case REPLACE_TYPE.STRING:
                {
                    if (counter >= replaceStrParams.Count)
                    {
                        Assert.IsTrue(false);
                        break;
                    }
                    strAdd = replaceStrParams[counter];
                }
                break;

            // モンスター名.
            case REPLACE_TYPE.MONSTER:
                {
                    if (counter >= replaceMonsterParams.Count)
                    {
                        Assert.IsTrue(false);
                        break;
                    }
                    int id = replaceMonsterParams[counter];
                    strAdd = FixedDataManager.GetMonsterData(id).GetName();
                }
                break;

            default:
                {
                    return 0;
                }
        }

        // 差し替えの実行.
        if (strAdd != null)
        {
            strReplace = strReplace.Insert(index, strAdd);
        }

        // 元の文字列に代入.
        strText = strReplace;

        // 差し替えカウンタを増やす.
        replaceCount[(int)replaceType]++;

        // 差し替え内容の長さを返す.
        return strAdd.Length;
    }

    public static string GetText(TEXT_LABEL textLabel)
    {
        Assert.IsTrue(textLabel < TEXT_LABEL.NUM);

        // テキストデータ取得.
        string strText = textData.TextList[(int)textLabel];

        // タグによる差し替え処理.
        ReplaceText(ref strText);
        // 差し替えパラメータのクリア.
        ClearReplaceParams();

        return strText;
    }
    // ここまでを追加.
}

次記事でざっくりと解説します。

ゲームデータについて(6)テキストデータのつづき

前記事からの続きです。

ラベルのコンバータを作成

textData.xlsmのB列に記載されたラベル定義をソースコードとして出力するコンバータを作成します。
他のコンバータと同様にUnity Editor上に作りたいところですが、出力されるファイルはプロジェクトの一部になるので、出力に失敗して空ファイルになるとプロジェクトのコンパイルが通らなくなってしまいます。
それを避けるためにExcelVBAマクロでコンバータを作成します。
textData.xlsmに以下のマクロを作成します。

Sub OutputTextLabel()

  '出力先
  StrFileName = ThisWorkbook.Path + "\..\..\GameData\TextData\TextDataLabel.cs"

  'ファイルをオープン
  Dim adoStream As Object
  Set adoStream = CreateObject("ADODB.Stream")

  adoStream.Type = 2
  adoStream.Charset = "UTF-8"
  adoStream.Open

  '書き込み処理
  'ヘッダ書き込み
  adoStream.WriteText "//-----------------------------------------------------------------------------", 1
  adoStream.WriteText "//  note : このファイルはマクロから出力されています。直接編集しないでください。", 1
  adoStream.WriteText "//-----------------------------------------------------------------------------", 1
  adoStream.WriteText "using UnityEngine;" & Chr(13) & Chr(10), 1
  adoStream.WriteText "public enum TEXT_LABEL", 1
  adoStream.WriteText "{", 1

  'ラベル書き込み準備
  Dim strLabel As String
  Dim strIndent As String
  strIndent = Chr(32) & Chr(32) & Chr(32) & Chr(32)
  Dim textNumPerSheet As Long
  textNumPerSheet = 1000
  Dim sheetBegin As Long
  sheetBegin = 1
  Dim rowBegin As Long
  rowBegin = 2
  For sheetCurrent = sheetBegin To ActiveWorkbook.Worksheets.Count

    'シートごとの処理
    Dim sheet As Worksheet
    Set sheet = Worksheets(sheetCurrent)

    Dim row As Long
    Dim rowEnd As Long
    rowEnd = sheet.Range("B2").End(xlDown).row
 
    'シートの最初のラベルの定義
    strLabel = strLabel & strIndent & UCase(sheet.Name) & "_START = " & (sheetCurrent - sheetBegin) * textNumPerSheet & "," & Chr(13) & Chr(10)

    'シートの各ラベル
    For row = rowBegin To rowEnd
       If Len(Application.Trim(sheet.Cells(row, 3))) = 0 Then
            Exit For
       End If
       strLabel = strLabel & strIndent & sheet.Cells(row, 2) & " = " & (sheetCurrent - sheetBegin) * textNumPerSheet + (row - rowBegin) & "," & Chr(13) & Chr(10)
    Next row

    'シートの最後のラベルの定義
    strLabel = strLabel & strIndent & UCase(sheet.Name) & "_END = " & (sheetCurrent - sheetBegin) * textNumPerSheet + (row - rowBegin) & "," & Chr(13) & Chr(10)
 
    'シートが変わるので改行
    strLabel = strLabel & Chr(13) & Chr(10)

  Next sheetCurrent

  'ラベル書き込み
  adoStream.WriteText strLabel, 1
  
  'ファイルフッタ書き込み
  adoStream.WriteText strIndent & "NUM", 1
  adoStream.WriteText "}", 1
  adoStream.WriteText "//-----------------------------------------------------------------------------", 1
  adoStream.WriteText "// EOF", 1
  adoStream.WriteText "//-----------------------------------------------------------------------------", 1

  'ファイルセーブ
  adoStream.SaveToFile StrFileName, 2

  'ファイルを閉じる
  adoStream.Close
  Set adoStream = Nothing

  MsgBox "Succeeded in converting text label file."

End Sub

マクロを作ったら実行してAssets/GameData/TextData/TextDataLabel.csが出力されるのを確認します。

テキストデータ管理クラスを作成

固定データ・可変データと同様にテキストデータを管理するクラスを作成します。 Assets/GameData/TextDataにTextDataManager.csを作成します。

using UnityEngine;
using UnityEngine.Assertions;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public static class TextDataManager
{
    private static TextData textData = null;

    static TextDataManager()
    {
    }

    public static bool Serialize()
    {
        TextAsset serializeFile = Resources.Load("TextData/TextData") as TextAsset;
        using (MemoryStream memoryStream = new MemoryStream(serializeFile.bytes))
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            textData = binaryFormatter.Deserialize(memoryStream) as TextData;
        }

        return true;
    }

    public static string GetText(TEXT_LABEL textLabel)
    {
        Assert.IsTrue(textLabel < TEXT_LABEL.NUM);

        // テキストデータ取得.
        return textData.TextList[(int)textLabel];
    }

}

Assets/GameData/GameDataManager.csを修正します。

    public static void Initialize()
    {
        // 固定データのロード.
        FixedDataManager.Serialize();

        // 可変データの新規作成.
        VariableDataManager.CreateData();

        // ここからを追加.
        // テキストデータのロード.
        TextDataManager.Serialize();
        // ここまでを追加.
    }

適当なところで
TextDataManager.GetText(TEXT_LABEL.SYSTEM_BACK_TO_TITLE);
TextDataManager.GetText(TEXT_LABEL.MONSTER_NAME_0);
等を呼んで文字列を取得できることを確認します。

固定データからテキストデータを参照

固定データのモンスターデータからテキストデータのモンスター名を参照できるようにします。
Assets/Editor/GameData/fixedData.xslxを開いて、Monsterシートに"名前"というカラムを追加しTextDataLabel.csに定義された数値を代入します。

Assets/GameData/FixedData/FixedMonsterData.csを以下のように修正

(略)

    private int _agility = 0;
    public int agilty
    {
        get { return _agility; }
#if UNITY_EDITOR
        set { _agility = value; }
#endif // UNITY_EDITOR
    }

    // ここからを追加.
    private int _name = 0;
    public int name
    {
        get { return _name; }
#if UNITY_EDITOR
        set { _name = value; }
#endif // UNITY_EDITOR
    }
    // ここまでを追加.

    public FixedMonsterData()
    {
    }

    // ここからを追加.
    public string GetName()
    {
        return TextDataManager.GetText((TEXT_LABEL)name);
    }
    // ここまでを追加.

}

Assets/Editor/FixedDataConverter.csに追加されたカラムのコンバート処理を追記

(略)

    private enum MONSTER_COLUMN
    {
        ID,
        COLUMN_B, // 不使用.
        HP,
        MP,
        ATK,
        DEF,
        AGI,
        NAME, // これを追加.
    }

(略)

                        case MONSTER_COLUMN.AGI:
                            {
                                data.agilty = (int)cell.NumericCellValue;
                            }
                            break;
                        // ここからを追加.
                        case MONSTER_COLUMN.NAME:
                            {
                                data.name = (int)cell.NumericCellValue;
                            }
                            break;
                            // ここまでを追加.
                        default:
                            {
                                break;
                            }
                    }
(略)

モンスターデータを再度コンバートして
FixedDataManager.GetMonsterData(int).GetName();
でモンスター名が取得できることを確認します。

ゲームデータについて(5)テキストデータ

本記事ではテキストデータについて説明します。
システムメッセージデータとモンスターの名前データを作成するケースを例にとります。

テキストデータの外部ファイル化

テキストデータも固定データと同様に外部ファイル化します。
Assets/EditorにTextDataというディレクトリを作成してテキストデータを管理するエクセルファイルを用意します。
名前はtextData.xlsmとします。
fixedData.xlsxと異なりマクロありファイルにします。(理由は後述)
システムメッセージテキストをこんな感じで作成します。

シート名は"System"とします。
テキストを呼び出す際のIDとして、内容がわかりやすいように文字列で定義されたものを用意します。(B列)
これをラベルと呼ぶことにします。
モンスター名テキストも同様に作成します。シート名は"MonsterName"とします。

テキストデータクラスの生成

Assets/GameData/にTextDataというディレクトリを作成し、Assets/GameData/TextData/にTextData.csというファイルを作成します。

using System;
using System.Collections.Generic;

[Serializable]
public class TextData
{
    public List<string> TextList = new List<string>();

    public void Clear()
    {
        TextList.Clear();
    }
}
テキストデータのコンバータの作成

Assets/EditorにTextDataConverter.csというスクリプトを作成します。
textData.xlsmには複数のシートがありますが、各シートにつき1000ずつIDを割り振って、空いているID分はダミーデータを保持する形式にします。

using UnityEditor;
using UnityEngine;
using UnityEngine.Assertions;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using NPOI.XSSF.UserModel;
using NPOI.SS.UserModel;

#if UNITY_EDITOR

public class TextDataConverter : EditorWindow
{
    [ MenuItem( "Utility/Convert Text Data" ) ]
    public static void ShowWindow()
    {
        EditorWindow window = EditorWindow.GetWindow( typeof( TextDataConverter ) );
    }

    public void OnGUI()
    {
        if (GUI.Button(new Rect(8, 8, 120, 32), "Text Data"))
        {
            ConvertData();
        }
    }

    // Excelファイルのコンバートを開始/終了するシート.
    private const int SHEET_BEGIN = 0;
    private const int SHEET_END = 1;

    // Excelファイルのヘッダ行数.
    private const int ROW_HEADER_NUM = 1;
    // テキスト列.
    private const int COLUMN_TEXT = 2;
    // シートごとのテキスト数.
    private const int TEXT_NUM_PER_SHEET = 1000;


    public void ConvertData()
    {
        using (FileStream readFileStream = new FileStream("Assets/Editor/GameData/textData.xlsm", FileMode.Open, FileAccess.Read))
        {
            XSSFWorkbook book = new XSSFWorkbook(readFileStream);
            if (book == null)
            {
                Assert.IsTrue(false);
                return;
            }

            TextData data = new TextData();
            data.Clear();

            for (int i = SHEET_BEGIN; i <= SHEET_END; i++)
            {
                ISheet sheet = book.GetSheetAt(i);

                // シートのデータをTextDataに格納する.
                ConvertData(sheet, ref data);

            }

            byte[] serializedData = null;

            using (MemoryStream memoryStream = new MemoryStream())
            {
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(memoryStream, data);
                serializedData = memoryStream.ToArray();
            }

            using (FileStream serializeFile = new FileStream("Assets/ExternalResources/Resources/TextData/TextData.bytes", FileMode.Create))
            {
                serializeFile.Write(serializedData, 0, serializedData.Length);
            }
        }
    }

    public void ConvertData(ISheet Sheet, ref TextData OutData)
    {
        for (int i = ROW_HEADER_NUM; i < TEXT_NUM_PER_SHEET + ROW_HEADER_NUM; i++)
        {
            if (i <= Sheet.LastRowNum)
            {
                IRow row = Sheet.GetRow(i);
                ICell cell = row.GetCell(COLUMN_TEXT);
                if (cell == null)
                {
                    continue;
                }

                OutData.TextList.Add(row.GetCell(COLUMN_TEXT).StringCellValue);
            }
            else
            {
                // 空行にはダミーデータを追加.
                OutData.TextList.Add("");
            }
        }
    }
}

#endif // UNITY_EDITOR

コンバータを実行してファイルAssets/ExternalResources/Resources/TextData/TextData.bytesが出力されることを確認します。
次記事に続きます。

ゲームデータについて(4)可変データ

本記事では可変データの初期化・セーブロード処理について説明をします。

可変データクラスの実装

味方キャラクターのデータを作るケースを例とします。
Assets/GameData/にVariableDataというディレクトリを作成し、Assets/GameData/VariableData/にCharacterData.csというファイルを作成します。

using System;

[Serializable]
public class CharacterData
{
    private int _id = -1;
    public int id
    {
        get { return _id; }
        set { _id = value; }
    }
    private int _level = 0;
    public int level
    {
        get { return _level; }
        set { _level = value; }
    }
    private int _hp = 0;
    public int hp
    {
        get { return _hp; }
        set { _hp = value; }
    }

    private int _mp = 0;
    public int mp
    {
        get { return _mp; }
        set { _mp = value; }
    }

    private int _attack = 0;
    public int attack
    {
        get { return _attack; }
        set { _attack = value; }
    }

    private int _defense = 0;
    public int defense
    {
        get { return _defense; }
        set { _defense = value; }
    }

    private int _agility = 0;
    public int agility
    {
        get { return _agility; }
        set { _agility = value; }
    }

    public CharacterData()
    {
        Clear();
    }

    public void Init( int _id, int _level, int _hp, int _mp, int _attack, int _defense, int _agility )
    {
        id = _id;
        level = _level;
        hp = _hp;
        mp = _mp;
        attack = _attack;
        defense = _defense;
        agility = _agility;
    }
    
    public void Clear()
    {
        id = -1;
        level = 0;
        hp = 0;
        mp = 0;
        attack = 0;
        defense = 0;
        agility = 0;
    }    
    
    public void Copy( CharacterData data )
    {
        id = data.id;
        level = data.level;
        hp = data.hp;
        mp = data.mp;
        attack = data.attack;
        defense = data.defense;
        agility = data.agility;
    }
    
    public bool IsValid()
    {
        return id != -1;
    }
}
可変データ管理クラスの作成

可変データも固定データと同様に種類が増えていく想定なので一元管理するクラスを作ります。
Assets/GameData/VariableDataにVariableDataManager.csを作成します。

using System.Collections.Generic;
using UnityEngine.Assertions;

public static class VariableDataManager
{
    private static List<CharacterData> _characterList = null;
    private static List<CharacterData> characterList
    {
        get { return _characterList; }
        set { _characterList = value; }
    }

    static VariableDataManager()
    {
        Initialize();
    }
    private static void Initialize()
    {
        characterList = new List<CharacterData>();
    }

    public static CharacterData GetCharacterData(int index)
    {
        if (index < 0 || index >= characterList.Count)
        {
            Assert.IsTrue(false);
            return null;
        }

        return characterList[index];
    }
    public static CharacterData GetCharacterDataFormID(int id)
    {
        if (id < 0)
        {
            Assert.IsTrue(false);
            return null;
        }

        CharacterData characterData = characterList.Find(x => x.id == id);
        if (characterData == null)
        {
            Assert.IsTrue(false);
            return null;
        }

        return characterData;
    }
    public static int GetCharacterDataNum()
    {
        return characterList.Count;
    }
}
初期化処理

可変データは固定データのように外部ファイルをロードする必要はありませんが、ゲーム開始時に特定の数値で初期化をする必要があります。
先ほど作成したVariableDataManager.csに以下関数を追記します。
初期化時の値は固定データとして管理した方がよいかもしれません。

    public static void CreateData()
    {
        characterList.Clear();
        for (int index = 0; index < 4; ++index)
        {
            CharacterData characterData = new CharacterData();
            characterData.Init(index, 10, 10, 10, 10, 10, 10); // TODO:キャラクターの初期データは固定データとして管理する.
            characterList.Add(characterData);
        }
    }
初期化処理の呼び出し

先ほど作成した可変データの初期化処理をGameDataManagerから呼ぶようにします。
Assets/GameData/GameDataManager.csを開いて以下のように修正します。

    public static void Initialize()
    {
        // 固定データのロード.
        FixedDataManager.Serialize();

        // ここからを追加.
        // 可変データの新規作成.
        VariableDataManager.CreateData();
        // ここまでを追加.
    }

GameDataManager.Initialize();が呼ばれる時に可変データの初期化も行われることを確認します。

セーブロード処理

可変データはゲームの進行によって変化するデータですので、セーブロード処理を作る必要があります。
Assets/GameData/VariableData/VariableDataManager.csを以下のように修正。

using System.Collections.Generic;
using System.IO;    // これを追加.
using System.Runtime.Serialization.Formatters.Binary;    // これを追加.
using UnityEngine.Assertions;

(略)
    private static void Initialize()
    {
        characterList = new List<CharacterData>();
    }

    // ここからを追加.
    public static void SaveData(FileStream fileStream)
    {
        using (MemoryStream memoryStream = new MemoryStream())
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();

            binaryFormatter.Serialize(memoryStream, characterList);

            fileStream.Write(memoryStream.ToArray(), 0, (int)memoryStream.Length);
        }
    }

    public static void LoadData(FileStream fileStream)
    {
        BinaryFormatter binaryFormatter = new BinaryFormatter();
        characterList = binaryFormatter.Deserialize(fileStream) as List<CharacterData>;
    }
    // ここまでを追加.

追加したセーブロード関数をGameDataManagerから呼び出すようにします。
Assets/GameData/GameDataManager.csを開いて以下のように修正します。

// ここからを追加.
using System.IO;
// ここまでを追加.

public static class GameDataManager
{

(略)

    // ここからを追加.
    // セーブ処理.
    public static void SaveData(int fileNo)
    {
        using (FileStream serializeFile = new FileStream("SaveData" + fileNo + ".bin", FileMode.Create))
        using (MemoryStream memoryStream = new MemoryStream())
        {
            VariableDataManager.SaveData(serializeFile);
        }
    }

    // ロード処理.
    public static void LoadData(int fileNo)
    {
        using (FileStream serializeFile = new FileStream("SaveData" + fileNo + ".bin", FileMode.Open))
        {
            VariableDataManager.LoadData(serializeFile);
        }
    }
    // ここまでを追加.

}

適当な場所で以下関数を呼んでセーブ・ロードが正常に動作することを確認する。

GameDataManager.SaveData(int);
GameDataManager.LoadData(int);

これで可変データの初期化・セーブ・ロードができるようになりました。

セーブデータの生成場所について

上記のサンプルコードではセーブデータファイルはUnityのプロジェクトの直下に生成されます。
Unity Editor上ならこれでも問題ないと思いますが、各プラットフォームで動作させる場合は適切なパスを指定しましょう。

ゲームデータについて(3)固定データのつづき

前記事の続きです。 本記事では作りかけのコンバータの修正とゲームデータ管理クラスの作成をしてきます。

コンバータの修正

前記事で作成したAssets/Editor/FixedDataConverter.csを開いて以下のように修正します。

using UnityEngine;
using UnityEngine.Assertions;
using UnityEditor;
using System.Collections.Generic; // ここを追加.
using System.IO;
using System.Runtime.Serialization.Formatters.Binary; // ここを追加.
using NPOI.XSSF.UserModel;
using NPOI.SS.UserModel;

#if UNITY_EDITOR
public class FixedDataConverter : EditorWindow
{
    // ここからを追加.
    // Excelファイルのヘッダ行数.
    private const int ROW_HEADER_NUM = 1;

    // Excelファイルのカラム定義.
    private enum MONSTER_COLUMN
    {
        ID,
        COLUMN_B, // 不使用.
        HP,
        MP,
        ATK,
        DEF,
        AGI,
    }
    // ここまでを追加.

(略)

    public void ConvertMonster()
    {
        using (FileStream readFileStream = new FileStream("Assets/Editor/GameData/fixedData.xlsx", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            XSSFWorkbook book = new XSSFWorkbook(readFileStream);
            if (book == null)
            {
                Assert.IsTrue(false);
                return;
            }

            ISheet sheet = book.GetSheet("Monster");
            if (sheet == null)
            {
                Assert.IsTrue(false);
                return;
            }

            // ここからを追加.
            int rowFirst = sheet.FirstRowNum + ROW_HEADER_NUM;
            int rowNum = sheet.LastRowNum - rowFirst + 1;

            List<FixedMonsterData> monsterList = FixedDataManager.monsterList;
            monsterList.list.Clear();

            for (int rowCnt = 0; rowCnt < rowNum; rowCnt++)
            {
                IRow row = sheet.GetRow(rowFirst + rowCnt);
                if (row == null)
                {
                    continue;
                }

                FixedMonsterData data = new FixedMonsterData();

                for (int columnCnt = row.FirstCellNum; columnCnt <= row.LastCellNum; columnCnt++)
                {
                    ICell cell = row.GetCell(columnCnt);
                    if (cell == null)
                    {
                        continue;
                    }

                    switch ((MONSTER_COLUMN)columnCnt)
                    {
                        case MONSTER_COLUMN.ID:
                            {
                                Assert.AreEqual((int)cell.NumericCellValue, rowCnt);
                            }
                            break;
                        case MONSTER_COLUMN.HP:
                            {
                                data.hp = (int)cell.NumericCellValue;
                            }
                            break;
                        case MONSTER_COLUMN.MP:
                            {
                                data.mp = (int)cell.NumericCellValue;
                            }
                            break;
                        case MONSTER_COLUMN.ATK:
                            {
                                data.attack = (int)cell.NumericCellValue;
                            }
                            break;
                        case MONSTER_COLUMN.DEF:
                            {
                                data.defense = (int)cell.NumericCellValue;
                            }
                            break;
                        case MONSTER_COLUMN.AGI:
                            {
                                data.agilty = (int)cell.NumericCellValue;
                            }
                            break;
                        default:
                            {
                                break;
                            }
                    }
                }
                monsterList.Add(data);
            }

            byte[] serializedData = null;

            using (MemoryStream memoryStream = new MemoryStream())
            {
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(memoryStream, monsterList);
                serializedData = memoryStream.ToArray();
            }

            using (FileStream serializeFile = new FileStream("Assets/ExternalResources/Resources/FixedData/FixedMonsterData.bytes", FileMode.Create))
            {
                serializeFile.Write(serializedData, 0, serializedData.Length);
            }

            Debug.Log("Succeeded in converting : FixedMonsterData");
            // ここまでを追加.
        }
    }
}
#endif // UNITY_EDITOR

コンバートボタンを押してAssets/ExternalResources/Resources/FixedDataに以下にファイルFixedMonsterData.bytesが作成されることを確認します。
(ディレクトリがないと失敗するのであらかじめ作成しておく)

ゲームデータ管理クラスの作成

固定データを含むゲームデータ全般を管理するクラスを作成します。
Assets/GameDataにGameDataManager.csというファイルを作成します。

public static class GameDataManager
{
    static GameDataManager()
    {
    }

    public static void Initialize()
    {
        // 固定データのロード.
        FixedDataManager.Serialize();
    }
}

適当な場所で

GameDataManager.Initialize();

を呼んで固定データが正常に読まれることを確認します。
その後

FixedDataManager.GetMonsterData(int);

で固定データを取得できることを確認します。
これで固定データを編集・コンバート・ロードする処理ができました。

余談

今回はゲームが参照する固定データをバイナリファイルとして管理する方法を紹介しましたが、昨今のPCゲームではゲームデータをXMLファイルとして管理しているケースも見られます。
XMLファイルで管理することのメリットは今回説明したようなコンバータを作る手間がなく、Excelのような専用のアプリケーションも必要としないという点が挙げられます。
デメリットとしては可視性が高くユーザーに容易に編集されてしまう点でしょうか。
(バイナリファイルも暗号化しなければ大した差はありませんが)
ただ、そのPCゲームがModを許容(あるいは推奨)するスタンスであれば、ユーザーがゲームを改変できてしまうというのはデメリットとは言えないのかもしれません。