【Unity】Source Generator で超便利になった!MasterMemory v3 の使い方まとめ

Source Generator で超便利になったMasterMemory v3 の使い方をまとめました。

Master Memory 3.0.3
Unity2022.3.28f1

MasterMemoryとは?

ゲーム開発では、キャラクターごとのパラメータやステージごとの情報など、全ユーザ共通の情報をマスタデータとして扱います。

マスタデータ

マスタデータは膨大な量になりがちで、この膨大な量のデータから必要なデータをできるだけ早く検索することが求められます。
クライアントサイドではマスタデータの検索時間、メモリ使用量、ロード時間がしばしば問題になります。
さらにこれらの問題は、技術選定はプロジェクト前半に行われるにもかかわらず、問題はプロジェクト後半になって表面化しがちという厄介な特性を持っています。

MasterMemoryはこのような問題を解決するためのマスタデータ管理システムで、以下の特徴を持っています。

  • 省メモリ
  • 高速なロード(初期化を含む)
  • 高速な検索

これを使うことで、上述のような問題が起こりがちなマスタデータを効率よく処理することができます。
なお、MasterMemory は OSS なので、どなたでもライセンス(MIT)の範囲内で使用することができます。

開発はCysharp社です。

github.com

バージョン3系が登場

さてこの Master Memory ですが、以前本ブログでも使い方を紹介する記事を書きました。

light11.hatenadiary.com

この記事を執筆した時の Master Memory はまだバージョン2系だったのですが、その後バージョン3系がリリースされました。
バージョン3系はいくつかのクラスが Source Generator によりコンパイル時に自動生成されるという素晴らしい変更が入っており、オペレーションがとても簡潔になっています。

そこで本記事では、バージョン3系の Master Memory のインストール〜ロードまでのやり方を簡単に紹介します。

インストール

バージョン3系の Master Memory は NuGetForUnity を使ってインストールします。
なのでまずは以下の手順で NuGetForUnity をインストールします。

  1. Package Manager を開く
  2. ウィンドウ左上の + ボタンを押下し、「Add package from git URL...」を選択
  3. 以下のURLを入力してAddボタンを押下
https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity

NuGetForUnity がインストールできたら、Unity の上部のメニューから NuGet > Manage NuGet Packages を選択します。
するとパッケージ検索ウィンドウが開くので、「MasterMemory」を検索してインストールします。

以上でインストールは完了です。簡単!(v2比)

テーブル構造定義クラスを作成する

次にテーブル構造を表すクラスを作成します。

using MasterMemory;
using MessagePack;

// クラスにMemoryTableアトリビュートをつける
// クラスにMessagePackObjectアトリビュートをつける
[MemoryTable("Stage"), MessagePackObject(true)]
public sealed class StageMaster
{
    // プライマリキーにはPrimaryKeyアトリビュートをつける
    [PrimaryKey]
    public string Id { get; set; }

    public string Name { get; set; }

    public int Difficulty { get; set; }
    
    public string EnemyGroupId { get; set; }
    
    public int Exp { get; set; }
    
    public string ResourceId { get; set; }
}

クラスとプライマリキーのプロパティに、それぞれ上記のコメントの通りにアトリビュートをつけておきます。

イミュータブルにしたければsetterをなくしてコンストラクタ引数に移してもいいですが、引数が増えてくるとテストダブルを作る時に構造変更に弱くなるし面倒だし見づらくなるので、最近は僕はsetter公開派です(マスタデータは)。

いずれにせよ上記のように書くだけで、関連するあらゆるクラスが Source Generator によるコンパイル時に自動生成されます。

バイナリをビルドする

次に実際のデータが入ったバイナリをビルドします。

バイナリは以下のようにビルドします。

using System.IO;
using MasterMemory;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;

public static class BinaryGenerator
{
    [MenuItem("Example/Generate Binary")]
    private static void Run()
    {
        // MessagePackの初期化(ボイラープレート)
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance, // 自動生成されたResolver
            StandardResolver.Instance // MessagePackの標準Resolver
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        // Csvとかからデータを入れる(今回はテストのためコードで入れる)
        var stageMasters = new[]
        {
            new StageMaster
            {
                Id = "stage-01-001",
                Name = "初心者の森",
                Difficulty = 100,
                EnemyGroupId = "enemy-01-001",
                Exp = 100,
                ResourceId = "resource-01-001"
            },
            new StageMaster
            {
                Id = "stage-01-002",
                Name = "迷いの湿地帯",
                Difficulty = 200,
                EnemyGroupId = "enemy-01-002",
                Exp = 200,
                ResourceId = "resource-01-002"
            },
            new StageMaster
            {
                Id = "stage-01-003",
                Name = "炎の山脈",
                Difficulty = 400,
                EnemyGroupId = "enemy-01-003",
                Exp = 400,
                ResourceId = "resource-01-003"
            },
            new StageMaster
            {
                Id = "stage-02-001",
                Name = "氷結の洞窟",
                Difficulty = 500,
                EnemyGroupId = "enemy-02-001",
                Exp = 500,
                ResourceId = "resource-02-001"
            },
            new StageMaster
            {
                Id = "stage-02-002",
                Name = "幽霊の墓地",
                Difficulty = 600,
                EnemyGroupId = "enemy-02-002",
                Exp = 600,
                ResourceId = "resource-02-002"
            },
            new StageMaster
            {
                Id = "stage-02-003",
                Name = "竜の城塞",
                Difficulty = 1000,
                EnemyGroupId = "enemy-02-003",
                Exp = 1000,
                ResourceId = "resource-02-003"
            }
        };

        // DatabaseBuilderを使ってバイナリデータを生成する
        var databaseBuilder = new DatabaseBuilder();
        databaseBuilder.Append(stageMasters);
        var binary = databaseBuilder.Build();

        // できたバイナリは永続化しておく
        var path = "Assets/Example/Binary/StageMaster.bytes";
        var directory = Path.GetDirectoryName(path);
        if (!Directory.Exists(directory))
            Directory.CreateDirectory(directory);
        File.WriteAllBytes(path, binary);
        AssetDatabase.Refresh();
    }
}

説明はコメントを参照してください。

出力されたバイナリファイルはAddressableやらで配布したりアプリに組み込んだりするとして、次節でこれを読み込みます。

バイナリを読み込む

バイナリからマスタデータを読み込むには、以下のようにMemoryDatabaseを使います。

MemoryDatabase内のXxxMasterTableから、マスタデータを検索することができます。

using MasterMemory;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;
using UnityEngine;

public static class BinaryLoader
{
    [MenuItem("Example/Load Binary")]
    private static void Run()
    {
        // MessagePackの初期化(ボイラープレート)
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance, // 自動生成されたResolver
            StandardResolver.Instance // MessagePackの標準Resolver
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        // ロード(テスト用にAssetDatabaseを使っているが実際にはAddressableなどで)
        var path = "Assets/Example/Binary/StageMaster.bytes";
        var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
        var binary = asset.bytes;

        // MemoryDatabaseをバイナリから作成
        var memoryDatabase = new MemoryDatabase(binary);
        // テーブルからデータを検索
        var stage = memoryDatabase.StageMasterTable.FindById("stage-01-002");
        Debug.Log(stage.Name); // 迷いの湿地帯
    }
}

なお上記ではmessagePackResolversMessagePackSerializer.DefaultOptionsに登録していますが、MemoryDatabaseを複数使用する際にはこのmessagePackResolversの設定が全てのMemoryDatabaseに使われます。

これでも大抵の場合は問題ないのですが、もしMemoryDatabaseごとにIFormatterResolverを分けたい場合には、MemoryDatabaseのコンストラクタ引数に渡すこともできます(引数に渡さなかった場合にはDefaultOptionsが使われます)。

基本的な使い方は以上です。

その他覚えておくべきこと

基本的な使い方は以上ですが、Master Memory を使う上でその他にいくつか覚えておくべき事項があります。
これらについては本記事のスコープ外としますが、以下の v2 の記事の「その他覚えておくべきこと」が参考になりますのでご一読ください。

light11.hatenadiary.com

また以下の記事も MasterMemory(v2)に関するものなので、必要に応じて参照してください。

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

参考

github.com