Unityでローグライクなゲームを作ってみる(2) マスタデータ読み込み編

# はじめに
パート2では、パート1で作成したマスタデータをゲーム上で扱えるようにします。
ざっくりとした手順は下記のとおりです。

1. マスタデータを読み込む
2. データを受け取るクラスを用意
3. 値を格納して渡す


# 準備
データは、JSONのフォーマットになっているので「[MiniJSON](https://gist.github.com/darktable/1411710)」を用いて読み込みます。
※使い方は後述で説明いたします。
とりえあずUnityのAssetsフォルダにインポートしましょう。
（Pluginsなど適当なフォルダを作るといいかもです）


# マスタデータを読み込む
まずはJSON形式のデータを読み込みます。
さきほどインポートしたMiniJSONを試してみます。

```Example.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using MiniJSON;// 忘れずに.


// JSONの読み込みテストです.
public static void TestJSON()
{
    // UTF8形式でデータを読み込みます.
    StreamReader sr = new StreamReader("ここにマスタデータのパスを入れてください.", Encoding.UTF8);

    var source = sr.ReadToEnd();
    
    sr.Close();

    // リストにすべて格納.
    var table = Json.Deserialize(source) as IList;
}
```
単純に、データを読み込んだ後、MiniJSONのDeserializeを読んでみました。
（読み込めないときはパスや、文字コードを見直してみてください）

読み込んだデータはリスト型で格納されています。
パート1で登録した件数が正しくとれているかデバッグで確認しておきましょう。
また、table[0] にアクセスすることで、Dungeon.xlsmのカラムがはいっていると思います。
これも値が正しいか確認しておくと良いと思います。


# データを受け取るクラスを用意
データの値を扱えるようにしていきます。
クラスを用意して、マスタデータのカラムと同じ型の変数を準備しましょう。
ダンジョンのマスタデータ（Dungeon.xlsm）ならIDと名前が必要なので、下記のようになると思います。

```Dungeon.cs
using System;
using System.Collections;

public class Dungeon
{
    public long Id { get; private set; } // 外からの書き込み禁止.

    public string Name { get; private set; }
}
```

パート1でも見ましたが、JSONのデータはいくつものデータが配列に格納されています。
そして、1つあたりのデータはカラムの名前と値が対になっております。
"id" : "10001"
のような感じですね。
MiniJSONを用いることにより、これらは下記のように取得できます。

```Dungeon.cs
// データの値はカラムと同じ文字列で取得します.
// ex: { "id" : "10001", "name" : "foo" }
public void Deserialize(IDictionary param)
{
    this.Id   = long.Parse(param["id"] as string); // param["id"]で取得してパース.
    this.Name = param["name"] as string;
}
```
これで各変数にマスタデータの値を格納できるようになりました！

他の量産したマスタデータクラスでDeserialize関数は毎回用いるので、[インターフェース](https://msdn.microsoft.com/ja-jp/library/87d83y5b.aspx)を用意しておきましょう。
これでマスタデータクラスは必ずDeserializeを所持します。
（後ほどの初期化処理で便利です）

```IDeserialize.cs
using System;
using System.Collections;

public interface IDeserializer
{
    void Deserialize(IDictionary param);
}
```
上記のようなインターフェースを用意しました。

それを継承したマスタデータのインターフェースも用意します。
これを各マスタデータクラスに継承しておきましょう。

```IMsaterData.cs
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// マスタデータ共通インターフェース.
/// </summary>
public interface IMasterData : IDeserializer
{
    // 後々拡張します（予定）.
}
```

完成したDungeonクラスのサンプルです。

```Dungeon.cs
using System;
using System.Collections;

public class Dungeon : IMasterData
{
    public long Id { get; private set; } // 外からの書き込み禁止.

    public string Name { get; private set; }

    public void Deserialize(IDictionary param)
    {
        this.Id   = long.Parse(param["id"] as string);
        this.Name = param["name"] as string;
    }
}
```

必要なマスタデータは同じようにIMasterData.csを継承させて量産すれば大丈夫です。


# 値を格納して渡す(読み込んだマスタデータの取得)
ここまででファイルの読み込みと、クラスへの格納が可能になりました。
それらを組み合わせて、指定したマスタデータを取得する関数を作成します。

まずはざっくりとした関数のイメージです。

```Example.cs
/// <summary>
/// 指定したマスタデータを取得します.
/// </summary>
/// <typeparam name="T"><see cref="Model.Master.IMasterData"/>を継承したマスタデータの型.</typeparam>
/// <returns>初期化したマスタデータを返します.</returns>
public static List<T> GetMasterData<T>(List<long> id) where T : IMasterData, new() // new T() のため.
{
    // 1.マスタデータを読み込む.
    // 2.JSON形式のデータをデシリアライズ.
    // 3.ジェネリックなマスタデータクラスを生成. ここで new T() されます！.
    // 4.リストに格納してお渡しします.
}

// Dungeonマスタデータの取得例.
List<Dungeon> masterData = GetMasterData<Dungeon>();

```
ジェネリックなマスタデータの型を指定し、そのデータを取得する関数を用意します。


また、先程インターフェース（IMasterData.cs）を継承しましたので、[where](https://msdn.microsoft.com/ja-jp/library/bb384067.aspx)句で型の制約条件を指定しておきます。
後で説明がありますが、引数なしのコンストラクタをnewすると怒られるので[newの制約](https://msdn.microsoft.com/ja-jp/library/sd2w2ew5.aspx)も加えます。


では、1つ1つのデータに対して、先程のDeserializeインターフェースを呼び出し、カラムの値を初期化していきます。
（上記関数の3番目の箇所です。）
これでクラスを増やした分毎回読み込み処理を書かずに量産できます！

```Example.cs
// 最後に結果として渡すリストをこれに格納します.
List<T> result = new List<T>();

// インターフェースを通してデータを初期化します.
var table = Json.Deserialize(source) as IList;
foreach (var t in table)
{
    T master = new T();

    master.Deserialize(t as IDictionary);

    result.Add(master);
}

return result;
```

最後にいままでの処理を組み合わせたサンプルコードを記載しておきます。

```Example.cs
/// <summary>
/// 指定したマスタデータを取得します.
/// </summary>
/// <typeparam name="T"><see cref="Model.Master.IMasterData"/>を継承したマスタデータの型.</typeparam>
/// <returns>初期化したマスタデータを返します.</returns>
public static List<T> GetMasterData<T>() where T : IMasterData, new()
{
    // パスを結合します.
    var dataPath = "アセットのパス" + typeof(T).Name + ".json";

    // UTF8形式でデータを読み込みます.
    StreamReader sr = new StreamReader(dataPath, Encoding.UTF8);

    var source = sr.ReadToEnd();

    // ファイルは直ぐ閉じる.
    sr.Close();

    List<T> result = new List<T>();

    // インターフェースを通してデータを初期化します.
    var table = Json.Deserialize(source) as IList;
    foreach (var t in table)
    {
        T master = new T();

        master.Deserialize(t as IDictionary);

        result.Add(master);
    }

    return result;
}
```

以上でマスタデータ取得関数ができました！
余力があればUnityのコルーチンやasync/awaitで非同期な処理に拡張したり、
Promiseパターンなどで例外処理を入れたりしておくと幸せになれるかもしれません。
（ここで色々するとアレなのでいつか取り扱いたいです…！）

長くなってしまってすみません。
次回はいよいよダンジョンを生成する予定です。（予定です！）
お疲れ様でした！


# こんな感じで拡張できるよ！（余談）
いままでの処理をもうちょっと便利になるように拡張してみます。
1. パース処理を拡張する
2. 指定したIDのデータだけを取得する
3. マスタデータをキャッシュして処理を高速化してみる
上記について例を記載してみました。

 
### パース処理を拡張する
各マスタデータクラスにて、Deserialize関数内でParse処理を毎回書くのが億劫なので、簡単な[拡張メソッド](https://msdn.microsoft.com/ja-jp/library/bb383977.aspx)を用意してみました。

```ParseExtension.cs
namespace ModelExtension
{
	public static class ParseExtension
	{
	    /// <summary>
	    /// <see cref="long"/>型に変換します.
	    /// </summary>
	    public static long ToLong(this object target)
	    {
	        long result = 0;

	        if ( !long.TryParse(target as string, out result) )
	        {
	            // 必要ならエラー処理をここに書きます.
	        }
	        
	        return result;
	    }


	    // 必要なら他の型も用意.
	}
}
```
Dungeon.csも下記のように変更しました。

```Dungeon.cs
using ModelExtension;

public class Dungeon : IMasterData
{
    public long Id { get; private set; }

    public string Name { get; private set; }

    public void Deserialize(IDictionary param)
    {
        this.Id   = param["id"].ToLong();
        this.Name = param["name"].ToString();
    }
}
```
こちらのほうが**スッキリ**していると思います。


### 指定したIDのデータだけを取得する
先ほどの関数は全てのマスタデータを取得しておりましたので、IDが一致したものだけ取得するバージョンも用意してみました。
指定したIDでデータをフィルタリングして渡すイメージです。

IMasterData.csを拡張します。

```IMasterData.cs
/// <summary>
/// マスタデータ共通インターフェース.
/// </summary>
public interface IMasterData : IDeserializer
{
    long Id { get; } // setはprivateにするので省きます.
}
```
全マスタデータクラスにIDを持たせます。

続いて、フィルタリング処理を作成します。

```Example.cs
/// <summary>
/// マスタ取得時の指定IDが存在するかどうかフィルタリングします.
/// </summary>
/// <typeparam name="T">取得したいマスタデータのクラス</typeparam>
/// <param name="target">対象となる読み込んだ全マスタデータ</param>
/// <param name="id">取得したいマスタデータのID. nullで全件.</param>
/// <returns>一致したマスタデータ.</returns>
static List<T> Filter<T>(List<T> target, List<long> id) where T : IMasterData
{
    if (id == null) 
    {
        return target; // 指定されたIDはないので、全件取得になります.
    }
    else
    {
        return target.FindAll(x => id.Contains(x.Id)); // ジェネリックな型でもIdを持ってる.
    }
}

// IDが一致したデータのみ渡します.
return Filter(result, id);
```
単純な実装ですが、できました。
各マスタデータクラスは必ずIdを所持しますので、**ジェネリックな型での検索が可能**です。

上記関数を用いまして、GetMasterData関数を拡張します。

```Example.cs
/// <summary>
/// 指定したマスタデータを取得します.
/// </summary>
/// <typeparam name="T"><see cref="Model.Master.IMasterData"/>を継承したマスタデータの型.</typeparam>
/// <param name="id">取得したいデータのIDをリストに格納して渡してください.(NULL = 全データ)</param>
/// <returns>一致したマスタデータを返します.</returns>
public static List<T> GetMasterData<T>(List<long> id) where T : IMasterData, new() // new T() のため.
{
    var dataPath = "アセットのパス" + typeof(T).Name + ".json";

    // UTF8形式でデータを読み込みます.
    StreamReader sr = new StreamReader(dataPath, Encoding.UTF8);

    var source = sr.ReadToEnd();

    sr.Close();

    List<T> result = new List<T>();

    // インターフェースを通してデータを初期化します.
    var table = Json.Deserialize(source) as IList;
    foreach (var t in table)
    {
        T master = new T();

        master.Deserialize(t as IDictionary);

        result.Add(master);
    }

    // IDが一致したデータのみ渡します.
    return Filter(result, id);
}


// ex: id=1000 のDungeonマスタを取得.
List<Dungeon> masterData = null;

// ※例外処理を入れてないのでなんとなく…。
try
{
    masterData = GetMasterData<Dungeon>(new List<long> { 1000 });
}
catch(System.Exception e)
{
    // 何かしらのエラーが発生しました.
}

```

こうしてみました。
欲しいデータだけの取得や、他のマスタデータからの参照が容易になると思います。
（DungeonFloorからDungeonIdで該当のDungeonを検索など）

### キャッシュして処理を高速化してみる
毎回テキストデータをパースしているので、2回以上呼び出された場合にキャッシュしておいたデータを渡して、ある程度効率よくしておきます。

以下は単純な例です。

```Example.cs
// マスタデータをキャッシュする変数です.
static Dictionary<string, IList> cachedMasterDictionary = new Dictionary<string, IList>();

public static List<T> GetMasterData<T>(List<long> id) where T : IMasterData, new() // new T() のため.
{
    var dataPath = "アセットのパス" + typeof(T).Name + ".json";

    // ---拡張しました---

    // 同じキー(dataPath)が呼び出されたら.
    if (cachedMasterDictionary.ContainsKey(dataPath))
    {
        var cachedData = cachedMasterDictionary[dataPath] as List<T>;

        // キャッシュしていたデータを渡します.
        return Filter(cachedData, id);
    }

    // ----------------

    // UTF8形式でデータを読み込みます.
    StreamReader sr = new StreamReader(dataPath, Encoding.UTF8);

    var source = sr.ReadToEnd();

    sr.Close();

    List<T> result = new List<T>();

    // インターフェースを通してデータを初期化します.
    var table = Json.Deserialize(source) as IList;
    foreach (var t in table)
    {
        T master = new T();

        master.Deserialize(t as IDictionary);

        result.Add(master);
    }

    // ---拡張しました---

    // 初期化済みマスタデータをdataPathをキーとしてキャッシュしておきます.
    cachedMasterDictionary.Add(dataPath, result);

    // ----------------
    
    // IDが一致したデータのみ渡します.
    return Filter(result, id);
}
```
上記のようにキャッシュ処理を追加してみました。

以上、余談でした！