MasterMemory v3をunityに導入してマスターデータをロードして使用する

初めに

最近 MasterMemoryのv3が出たので触っていきます。今回はゲームのマスター(Google Sheet)を想定してダウンロードや使用についても行っていきます。

以下にて記事の内容のリポジトリを公開しています。

github.com

csvAssets/StreamingAssets/masterdata-dev1.csv に保存されています。

開発環境

  • Unity6000.0.38f1

導入

まずは NuGetForUnityを導入します。

Package Managerに URLから導入します。

次に NuGet For UnityのWindowを開いて MasterMemory を検索します。

また init を使用する場合は、以下のコードを定義しておく必要があります。

namespace System.Runtime.CompilerServices
{
    internal static class IsExternalInit { }
}

マスターデータの作成

以下の記事を参考に マスターデータ取得用の GET のエンドポイント作成します。

ayousanz.hatenadiary.jp

今回は マスターデータを作成して、CSVデータをローカルに保存しているものとします。

以下のようなマスターデータを定義します

id    name    nameJP  rotationCenterPlanetId  radius  gravity orbitalSpeed    lightIntensity  lightOuterRadius
int string  string  int float   float   float   float   float
id  惑星名   惑星名-日本語 公転の中心天体   惑星の半径 重力  公転速度(-の場合は反対回転) 天体のライトの強さ ライトの外側の半径
1   Earth   地球  1   1   1250    0.5 0.8 3
2   Mercury 水星  1   0.5 1450    0.5 0.8 3
3   Venus   金星  1   0.85    1200    0.5 0.8 3
4   Sun 太陽  1   0.5 1350    0.5 2   3
5   Moon    月 1   0.5 1600    0.5 0.5 3

このマスターデータのcsvを 以下のパスに保存しておきます。

Assets/StreamingAssets/masterdata-dev1.csv

実際のプロジェクトでは CSVの情報から MetaDatabase を使ってバイナリーに変換したり、マスターデータに一度入れた後にビルドしてバイナリーに保存したりなどの運用になりますが、今回はその辺の話はスキップします。

CSVをMasterMemoryのデータに変換

まずはマスターデータのデータベースに応じたクラスを作成します。

    [MemoryTable("planet"), MessagePackObject(true)]
    public record PlanetMaster
    {
        [PrimaryKey] public int Id { get; init; }
        public string Name { get; init; }
        public string NameJP { get; init; }
        public int RotationCenterPlanetId { get; init; }
        public float Radius { get; init; }
        public float Gravity { get; init; }
        public float OrbitalSpeed { get; init; }
        public float LightIntensity { get; init; }
        public float LightOuterRadius { get; init; }
    }

検索に使用するkeyには、[PrimaryKey] を付けておきます。

次のローカルにあるCSVからMasterMemoryにロードします。

            // CSV ファイルをテキストとして読み込み
            string csvText = await File.ReadAllTextAsync(FilePath, _cts.Token);

            // CSV から MasterMemory 用の MemoryDatabase を構築
            MemoryDatabase memoryDatabase = MasterMemoryLoader.BuildDatabaseFromCSV(csvText);

MasterMemoryからデータを検索

使用時には以下のように指定したkeyから検索をすることができます

           var allPlanets = memoryDatabase.PlanetMasterTable;
            Debug.Log($"データの数:{allPlanets.Count}");

            var firstPlanet = allPlanets.FindById(1);
            Debug.Log($"id:{firstPlanet.Id}, name:{firstPlanet.Name}, nameJP:{firstPlanet.NameJP}");

実行すると以下のようなログが表示されます。

自動生成ファイルについて

v3から MasterMemoryのデータを操作するためのフォーマッタは、Source Generatorによって自動生成されるようになりました。

実際には今回の場合は以下のようなコードが生成されています

// <auto-generated />
#pragma warning disable
#nullable enable

using MM;
using MasterMemory.Validation;
using MasterMemory;
using MessagePack;
using System.Collections.Generic;
using System;
using MM.Tables;

namespace MM
{
   public sealed class MemoryDatabase : MemoryDatabaseBase
   {
        public PlanetMasterTable PlanetMasterTable { get; private set; } = default!;

        public MemoryDatabase(
            PlanetMasterTable PlanetMasterTable
        )
        {
            this.PlanetMasterTable = PlanetMasterTable;
        }

        public MemoryDatabase(byte[] databaseBinary, bool internString = true, MessagePack.IFormatterResolver? formatterResolver = null, int maxDegreeOfParallelism = 1)
            : base(databaseBinary, internString, formatterResolver, maxDegreeOfParallelism)
        {
        }

        protected override void Init(Dictionary<string, (int offset, int count)> header, System.ReadOnlyMemory<byte> databaseBinary, MessagePack.MessagePackSerializerOptions options, int maxDegreeOfParallelism)
        {
            if (maxDegreeOfParallelism == 1)
            {
                InitSequential(header, databaseBinary, options, maxDegreeOfParallelism);
            }
            else
            {
                InitParallel(header, databaseBinary, options, maxDegreeOfParallelism);
            }
        }

        void InitSequential(Dictionary<string, (int offset, int count)> header, System.ReadOnlyMemory<byte> databaseBinary, MessagePack.MessagePackSerializerOptions options, int maxDegreeOfParallelism)
        {
            this.PlanetMasterTable = ExtractTableData<PlanetMaster, PlanetMasterTable>(header, databaseBinary, options, xs => new PlanetMasterTable(xs));
        }

        void InitParallel(Dictionary<string, (int offset, int count)> header, System.ReadOnlyMemory<byte> databaseBinary, MessagePack.MessagePackSerializerOptions options, int maxDegreeOfParallelism)
        {
            var extracts = new Action[]
            {
                () => this.PlanetMasterTable = ExtractTableData<PlanetMaster, PlanetMasterTable>(header, databaseBinary, options, xs => new PlanetMasterTable(xs)),
            };
            
            System.Threading.Tasks.Parallel.Invoke(new System.Threading.Tasks.ParallelOptions
            {
                MaxDegreeOfParallelism = maxDegreeOfParallelism
            }, extracts);
        }

        public ImmutableBuilder ToImmutableBuilder()
        {
            return new ImmutableBuilder(this);
        }

        public DatabaseBuilder ToDatabaseBuilder()
        {
            var builder = new DatabaseBuilder();
            builder.Append(this.PlanetMasterTable.GetRawDataUnsafe());
            return builder;
        }

        public DatabaseBuilder ToDatabaseBuilder(MessagePack.IFormatterResolver resolver)
        {
            var builder = new DatabaseBuilder(resolver);
            builder.Append(this.PlanetMasterTable.GetRawDataUnsafe());
            return builder;
        }

#if !DISABLE_MASTERMEMORY_VALIDATOR

        public ValidateResult Validate()
        {
            var result = new ValidateResult();
            var database = new ValidationDatabase(new object[]
            {
                PlanetMasterTable,
            });

            ((ITableUniqueValidate)PlanetMasterTable).ValidateUnique(result);
            ValidateTable(PlanetMasterTable.All, database, "Id", PlanetMasterTable.PrimaryKeySelector, result);

            return result;
        }

#endif

        static MasterMemory.Meta.MetaDatabase? metaTable;

        public static object? GetTable(MemoryDatabase db, string tableName)
        {
            switch (tableName)
            {
                case "planet":
                    return db.PlanetMasterTable;
                
                default:
                    return null;
            }
        }

#if !DISABLE_MASTERMEMORY_METADATABASE

        public static MasterMemory.Meta.MetaDatabase GetMetaDatabase()
        {
            if (metaTable != null) return metaTable;

            var dict = new Dictionary<string, MasterMemory.Meta.MetaTable>();
            dict.Add("planet", MM.Tables.PlanetMasterTable.CreateMetaTable());

            metaTable = new MasterMemory.Meta.MetaDatabase(dict);
            return metaTable;
        }

#endif
    }
}

参考記事

light11.hatenadiary.com

light11.hatenadiary.com