YAMADA TAISHI’s diary

ゲームについてとか私の日記とか。このブログのあらゆるコードは好きにどうぞ。利用規約があるものは記事内のGitHubのRepositoryのリンクで貼られていると思うので、そちらを参照ください。

【後編】いいかげんエクセルでマスターデータ管理辞めません?UnityでのJson,SQLiteを使ったマスター管理

こんにちは、やまだたいし( https://twitter.com/OrotiYamatano )です。
後編です。 前編はこちら orotiyamatano.hatenablog.com

目次


1. DBeaverのダウンロード
2. SQLiteに必要なセットをインストールする
3. DBeaverでデータをつくる
4. SQLiteとUnityの接続設定
5. スクリプトからの一時データの書き出しとSQLiteデータの更新
6. MasterMemoryの設定
7. MasterMemory用に書き出し
8. MasterMemoryで読み込む

前編では5. スクリプトからの一時データの書き出しとSQLiteデータの更新までやりました。
後編は最後までやっていきましょう。

導入手順

6. MasterMemoryの設定


MasterMemoryの導入に移ります。

github.com

UnityのMasterMemoryはMessagePack依存ですので
MessagePackのunitypackageをダウンロードインポート(Getting Started(Unity)を読んでね)し、
次にMasterMemoryのパッケージをインポートします。

Unity上で動かすのはmpc.zipとMasterMemory.Generator.zipが必要になるのですが、
導入も面倒なので今回は.NET Coreの方で利用しようと思います。

(早くソースジェネレーター対応して欲しいですね。してくれたらこの辺考えなくて良いのですが……)

本記事で解説しても良いのですが、
もうインストール方法を書いている方がいるので参照させていただきます。
light11.hatenadiary.com

↑に倣ってインストール完了したらマスターメモリ用のクラスを作っていく。
とその前にSQLiteとMaster Dataとで同じような構成のクラスができるようになる。
同じように扱うためにInterfaceを用意する。

public interface ITestTable
{
    public int Id { get; set; }
    public string TextVal { get; set; }
}

前編で作ったTestTable.csにも適用しておく。
そして、Assets/Scripts/MasterフォルダにMasterMemory用のクラスを用意した。

using MasterMemory;
using MessagePack;

namespace Master  // 本記事では必須
{
    [MemoryTable("TestTable"), MessagePackObject( true )]
    public class MasterMemoryTestTable : ITestTable
    {
        [PrimaryKey] public int Id { get; set; }
        public string TextVal { get; set; }
    }
}

(Table名はAttributeから取得するのでクラス名は完全一致の必要はない)

そしてデータを格納するためのフォルダとしてAssets/Scripts/MasterにGeneratedというフォルダを作成。

ProjectRootで以下コマンドを打つ.
するとMasterMemory関連のスクリプトが生成される。
dotnet-mmgen -inputDirectory ./Assets/Scripts/Master -outputDirectory ./Assets/Scripts/Master/Generated -usingNamespace "MasterMemory"

-inputDirectory には↑で作ったクラスが入ったディレクトリを指定します。
-outputDirectory-usingNamespace で指定した名前空間に所属するクラスが生成されます。

次はMessagePackの関連のデータを生成する。
mpc -input ./Assets/Scripts/Master -output ./Assets/Scripts/Master/Generated

これでようやく関連クラスが生成された。

7. MasterMemory用に書き出し


さて、最低限の準備が出来たところで、
最終的にはbyte形式にした方が良いだろうと思うのでTabeDataEditor.csを以下のように改変した。

ClassLoaderに↓も追加

   public static void DynamicCopyPropertiesWithCommonInterface(object source, object target, Type interfaceType)
    {
        foreach (var prop in interfaceType.GetProperties())
        {
            var sourceProp = source.GetType().GetProperty(prop.Name);
            var targetProp = target.GetType().GetProperty(prop.Name);

            if (sourceProp == null || targetProp == null || !targetProp.CanWrite) continue;
            var valueToCopy = sourceProp.GetValue(source);
            targetProp.SetValue(target, valueToCopy);
        }
    }

SQLiteの中に入っているデータをすべてMasterMemory用に変換し書き出す処理として
SQLiteToMasterMemoryボタンを追加した。

SQLiteの値を取得してくる処理をメソッド化して共通化.
GetSQLiteValueメソッドに。

すべてのMasterのNameSpaceとTablesのNameSpaceのクラスを取得し、
同じInterfaceを所持していた場合にGetSQLiteValue で取得したデータをリフレクションを使ってIntaface経由で値を変換している。

最終的にはAssets/Master/Binary/Master.bytes へ保存という流れだ。
多分変換処理は大分重い処理だと予測されるため、やるのであれば頻繁にはおすすめしない。
開発環境ではSQLiteのデータを使いビルドデータではSQLiteをMasterMemoryへ変換されたデータを使うなどと利用分けすると良さそうだと思う。

このスクリプトにはMaster Memoryに新規追加されたクラスの処理は書かれていないので毎回生成コマンドを叩く必要があるが、まぁソースジェネレーター対応がされればその辺考えなくても済むんだろうなぁと思ってます。

(早くきて……)

(どうしても気になる方はmpc.zipとMasterMemory.Generator.zipをUnityに入れてエディタ拡張からそれぞれ生成処理を叩くのを作っても良いかもしれない,もしくはbatファイルを用意して叩くとか?)

6. MasterMemoryで読み込む


さて、書き込み処理が出来たところで、最後、MasterMoemoryのbyteデータの読み込みです。

DBLoad.csを書き換えてそこで読み込むようにしてみます。

using UnityEngine;

public class DBLoad : MonoBehaviour
{
    private async void Start()
    {
        var data = await TestTableRepository.GetDataAsync(1);
        Debug.Log(data.TextVal);
    }
}
using Cysharp.Threading.Tasks;
using Tables;

public static class TestTableRepository
{
    public static async UniTask<ITestTable> GetDataAsync(int index)
    {
        ITestTable data;
#if true // プリプロセッサディレクティブで使いわけしたりデータキャッシュできる仕組みにできるとさらに良さそう
        data = MasterMemoryData.DB.MasterMemoryTestTableTable.FindById(index);
#else
        var path = Application.dataPath + "/db/testdb";
        var db = new SQLiteAsyncConnection(path);
        data = await db.GetAsync<TestTable> (1);
#endif
        return data;
    }
}
using System.IO;
using MasterMemory;
using UnityEngine;

public static class MasterMemoryData
{
    private static MemoryDatabase _db;

    // とりあえず、シングルトンで処理(DIにするともっと良いよね)
    public static MemoryDatabase DB
    {
        get
        {
            if (_db == null)
            {
                DownloadMasterData();
            }
            return _db;
        }
    }

    private static void DownloadMasterData()
    {
        const string binaryPath = "Assets/Master/Binary/Master.bytes";

        var data = LoadBinaryData(binaryPath);
        if (data != null)
        {
            _db = new MemoryDatabase(data);
        }
    }
    
    // Note:実際はAddressablesとかで読んでくると良さそう.
    private static byte[] LoadBinaryData(string binaryPath)
    {
        if (File.Exists(binaryPath))
        {
            var binary = File.ReadAllBytes(binaryPath);
            return binary;
        }

        Debug.LogError("ファイルが見つかりません: " + binaryPath);

        return null;
    }
}

とりあえず、今回はシングルトンで処理してみました。
こちらもSQLite同様問題なく処理できました。

またInterfaceが共通化されているので場合に応じてSQLiteを読むか、
MasterMemortを使うかプリプロセッサディレクティブなどで差別化可能です。
工夫してみてください。

まとめ


とりあえず、今回はNameSpaceは決めで行い、ファイル格納箇所も直値を指定しましたが、ファイル格納箇所についてはProject Settingsなどに逃せばもう少し柔軟な設定が可能だと思います。

またSQLiteは許容する型が少ないですがMaster Memoryに格納する際にプロパティにて文字列から該当の型へ変換するロジックを書くことで、本来の型で処理することも可能です。

エクセル撲滅のため皆さん色々工夫してゆきましょう!

以上、やまだたいしでした。