ゲームデータについて(2)固定データ

本記事(と次記事)ではゲームデータの一種である固定データについて、編集・コンバート・ロードする方法をUnity(C#)のサンプルコードを交えて簡単に説明します。

固定データの外部ファイル化

固定データを外部ファイル化し、必要な時(例えばタイトル画面からゲーム開始ボタンを押したタイミングなど)にロードさせるようにします。
固定データはExcelを使って編集し、バイナリにコンバートすることでゲームから読めるようにする方針とします。
これはゲームの開発環境でのみ行われる処理であり、ゲームの実行時には不要です。
UnityではAssets以下の任意の場所にEditorというディレクトリを作成すると、そこに配置されたものはゲームの実行時には参照されません。
参照:https://docs.unity3d.com/ja/2019.4/Manual/SpecialFolders.html
固定データを編集するExcelファイルやそのコンバートに必要なライブラリはAssets/Editor下に置くようにします。

NPOIのインストール

UnityのプロジェクトがExcelファイルを開けるようにするためにNPOIというライブラリを利用します。
Assets/EditorにNPOIというディレクトリを作成します。
https://www.nuget.org/packages/NPOI/
上記サイトから"Download Package"をクリック、
ダウンロードしたファイルの拡張子.nupkg.zipに変更して解凍、
解凍したファイルの/lib/net45/の中にある
NPOI.dll
NPOI.OOXML.dll
NPOI.OpenXml4Net.dll
NPOI.OpenXmlFormats.dll
をAssets/Editor/NPOIにコピーします。
また、以下のファイルも必要なので同じ要領でダウンロードして同くAssets/Editor/NPOIにコピーします。
https://www.nuget.org/packages/Portable.BouncyCastle/
からダウンロード・解凍したファイルの/lib/net40/の中にある
BouncyCastle.Crypto.dll
https://www.nuget.org/packages/SharpZipLib/
からダウンロード・解凍したファイルの/lib/net45/の中にある
ICSharpCode.SharpZipLib.dll

Excelデータの作成

Assets/EditorにGameDataというディレクトリを作成して固定データを編集するエクセルファイルを用意します。
ファイル名はfixedData.xlsxとして、下記のような感じでデータを作成します。
B列はコメントとして記述されている文字列でコンバート時には参照しません。
シート名は"Monster"とします。

コンバータの作成

Assets/Editor以下にFixedDataConverter.csというファイルを作成します。

using UnityEngine;
using UnityEngine.Assertions;
using UnityEditor;
using System.IO;
using NPOI.XSSF.UserModel;
using NPOI.SS.UserModel;

#if UNITY_EDITOR
public class FixedDataConverter : EditorWindow
{
    [MenuItem("Utility/Convert Fixed Data")]
    public static void ShowWindow()
    {
        EditorWindow.GetWindow(typeof(FixedDataConverter));
    }
    public void OnGUI()
    {
        if (GUI.Button(new Rect(8, 8, 120, 32), "Monster Data"))
        {
            ConvertMonster();
        }
    }

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

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

UnityのMenuのUtilityから"Convert Fixed Data"が選択できることと、選択したらMonsterというボタンを含むダイアログが表示されることを確認します。
また、ボタンを押すことでConvertMonster()が実行され、Excelファイルが開かれてその中のMonsterシートにアクセスできることを確認します。
(コンバート処理は後ほど実装します。)

固定データを保持するクラスを作成

次にゲームデータのロード先として読み込んだデータを保持するクラスを作成します。
Assets以下にGameData/FixedDataというディレクトリを作成し、Assets/GameData/FixedDataにFixedMonsterData.csというファイルを作成します。
固定データはゲーム実行時は不変ですがコンバータ実行時には代入する処理が必要になるので、setアクセサをUNITY_EDITORで括る形で定義します。

using System;

[Serializable]
public class FixedMonsterData
{
    private int _hp = 0;
    public int hp
    {
        get { return _hp; }
#if UNITY_EDITOR
        set { _hp = value; }
#endif // UNITY_EDITOR
    }

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

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

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

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

    public FixedMonsterData()
    {
    }

}
固定データ管理クラスの作成

固定データは種類が増えていくことを想定しているのでそれらを一元管理するクラスを作ります。
Assets/GameData/FixedDataにFixedDataManager.csを作成します。

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

public static class FixedDataManager
{
    private static List<FixedMonsterData> _monsterList = null;
    public static List<FixedMonsterData> monsterList
    {
        get { return _monsterList; }
    }

    static FixedDataManager()
    {
        Initialize();
    }

    private static void Initialize()
    {
        _monsterList = new List<FixedMonsterData>();
    }

    private static T ReadSerializedData<T>(string path) where T : class
    {
        T data = null;
        TextAsset serializeFile = Resources.Load(path) as TextAsset;
        using (MemoryStream memoryStream = new MemoryStream(serializeFile.bytes))
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            data = binaryFormatter.Deserialize(memoryStream) as T;
        }
        return data;
    }

    public static void Serialize()
    {
        _monsterList = ReadSerializedData<List<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];
    }
}

記事が長くなるので分割します。 次記事に続きます。

BinaryFormatterについて

上記のサンプルコードではBinaryFormatterというクラスを使用していますが、Microsoft .netによるとこのクラスは非推奨とされています。
参照:https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/binaryformatter-security-guide
要するに暗号化されないデータをサーバに送れるようになっていると攻撃に利用されるので危険、ということのようです。
サーバと通信しないスタンドアローンなゲームならもちろん気にする必要はありませんし、通信するゲームでもクライアント側のゲームデータをまるまるサーバに送るようなことをしない限り問題はないと思います。
ただ、ゲームデータが暗号化されていないとユーザーが自由に閲覧・編集できてしまうことには変わりないので、ユーザー間の公平性が重要になるオンライン仕様を実装するのであればゲームデータの暗号化は必須になります。

また、iOS環境ではBinaryFormatterが呼ばれる前に

Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes");

を呼ばないとエラーになるそうです。(iOS環境が手元に無いので試していません。)