コガネブログ

平日更新を目標に Unity や C#、Visual Studio、ReSharper などのゲーム開発アレコレを書いていきます

【Unity】MasterMemory の基本的な使い方

はじめに

この記事では MasterMemory の基本的な使い方を紹介していきます

目次

検証環境

  • Windows 10
  • Unity 2019.3.10f1
  • MasterMemory 2.2.2
  • MessagePack for C# 2.1.115

必要なファイルの入手

https://github.com/Cysharp/MasterMemory/releases

上記のページにアクセスして、下記の2つのファイルをダウンロードします

  • MasterMemory.Generator.zip
  • MasterMemory.Unity.unitypackage

https://github.com/neuecc/MessagePack-CSharp/releases

次に上記のページにアクセスして、下記の2つのファイルをダウンロードします

  • MessagePack.Unity.2.1.115.unitypackage
  • mpc.zip

.unitypackage のインポート

ダウンロードした下記の2つの .unitypackage を Unity プロジェクトにインポートします

  • MasterMemory.Unity.unitypackage
  • MessagePack.Unity.2.1.115.unitypackage

f:id:baba_s:20200502145954p:plain

マスタを管理するクラスの準備

f:id:baba_s:20200502150323p:plain

Unity プロジェクトに「Assets/Scripts/Master」フォルダを作成して
このフォルダ内に「Master.cs」を作成します

using MasterMemory;
using MessagePack;

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey] public int Id { get; set; }

    public string Name { get; set; }
}

作成した「Master.cs」を開いて上記のコードを記述します
ここでは ID と名前を持つキャラクターのマスタクラスを定義しました
これでマスタを管理するクラスの準備が完了しました

MasterMemory のジェネレータを実行

次は準備したマスタクラスを MasterMemory で使えるようにするために
MasterMemory のジェネレータを実行します

"【MasterMemory.Generator.zip 展開先のフォルダ】\win-x64\MasterMemory.Generator.exe" ^
    -i "【Unity プロジェクトのパス】\Assets\Scripts\Master" ^
    -o "【Unity プロジェクトのパス】\Assets\Scripts\Generated" ^
    -n "Master"

上記のように MasterMemory のジェネレータを実行するバッチファイルを書いて
実行することで

f:id:baba_s:20200502150803p:plain

マスタクラスを MasterMemory で使えるようにするためのクラスが自動生成されます

MessagePack のジェネレータを実行

準備したマスタクラスを MasterMemory で使えるようにするためには
MessagePack のジェネレータも実行する必要があります

"【mpc.zip 展開先のフォルダ】\win\mpc.exe" ^
    -i "【Unity プロジェクトのパス】\Assembly-CSharp.csproj" ^
    -o "【Unity プロジェクトのパス】\Assets\Scripts\Generated"

上記のように MessagePack のジェネレータを実行するバッチファイルを書いて
実行することで

f:id:baba_s:20200502152604p:plain

MessagePack で必要なクラスが自動生成されます
以上で準備したマスタクラスを MasterMemory で使用できるようになりました

ゲーム実行中にマスタをビルドして使用

マスタクラスが MasterMemory で使えるようになったため、
ゲーム実行中にマスタデータをビルドして使ってみます

f:id:baba_s:20200502154521p:plain

Unity プロジェクトに「Test.cs」を作成して

using Master;
using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

public sealed class Test : MonoBehaviour
{
    // MessagePack の初期化処理
    [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )]
    private static void Initialize()
    {
        StaticCompositeResolver.Instance.Register
        (
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );

        var options = MessagePackSerializerOptions.Standard.WithResolver( StaticCompositeResolver.Instance );
        MessagePackSerializer.DefaultOptions = options;
    }

    private void Start()
    {
        // キャラクターの配列の用意
        var characters = new[]
        {
            new Character { Id = 1, Name = "フシギダネ" },
            new Character { Id = 2, Name = "フシギソウ" },
            new Character { Id = 3, Name = "フシギバナ" },
            new Character { Id = 4, Name = "ヒトカゲ" },
            new Character { Id = 5, Name = "リザード" },
            new Character { Id = 6, Name = "リザードン" },
        };

        // データベースのビルド
        var builder = new DatabaseBuilder();
        builder.Append( characters );
        var databaseBinary = builder.Build();

        // データベースにアクセスするためのインスタンスの作成
        var database = new MemoryDatabase( databaseBinary );

        // キャラクターのデータベースを参照
        var characterTable = database.CharacterTable;

        // Id が 3 のキャラクターを取得
        Debug.Log( characterTable.FindById( 3 ).Name );

        // Id が 7 に一番近いキャラクターを取得
        Debug.Log( characterTable.FindClosestById( 7 ).Name );

        // Id が 1 から 3 のキャラクターをすべて取得
        foreach ( var n in characterTable.FindRangeById( 1, 3 ) )
        {
            Debug.Log( n.Name );
        }

        // Id が 3 のキャラクターが存在する場合は取得
        if ( characterTable.TryFindById( 3, out var result ) )
        {
            Debug.Log( result.Name );
        }

        // Id が 3 のキャラクターが存在するかどうか
        Debug.Log( characterTable.TryFindById( 3, out _ ) );

        // すべてのキャラクターを取得
        foreach ( var n in characterTable.All )
        {
            Debug.Log( n.Name );
        }

        // すべてのキャラクターを逆順で取得
        foreach ( var n in characterTable.AllReverse )
        {
            Debug.Log( n.Name );
        }

        // キャラクターの数を取得
        Debug.Log( characterTable.Count );
    }
}

上記のコードを記述し、このコンポーネントをシーンのゲームオブジェクトにアタッチして
ゲームを実行してみると、正常にマスタデータを読み込んで使用できることが確認できます

ここではスクリプト上でマスタデータを作成する処理を直に記述していますが
例えば CSV や JSON からデータを読み込む処理に変更することで
外部アセットからマスタデータを読み込んで使用できるようになります

事前にマスタをビルドして使用

前項ではゲーム実行中にマスタをビルドして使用する方法を紹介しましたが、
MasterMemory にはマスタデータを事前に .bytes 形式にビルドする機能が備わっています
事前にマスタをビルドすることでより高速にマスタデータを読み込めるようになります

エディタ拡張で事前にマスタをビルドしてみます

f:id:baba_s:20200502155501p:plain

Unity プロジェクトに「Assets/Editor」フォルダを作成して
このフォルダ内に「Builder.cs」を作成します

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

public static class Builder
{
    [MenuItem( "Tools/Build" )]
    private static void Build()
    {
        // MessagePack の初期化
        // 初期化を複数回実行すると下記の例外が発生するが
        // データベースのビルドには影響がないため例外は無視する
        // InvalidOperationException: Register must call on startup(before use GetFormatter<T>).
        try
        {
            StaticCompositeResolver.Instance.Register
            (
                MasterMemoryResolver.Instance,
                GeneratedResolver.Instance,
                StandardResolver.Instance
            );

            var options = MessagePackSerializerOptions.Standard.WithResolver( StaticCompositeResolver.Instance );
            MessagePackSerializer.DefaultOptions = options;
        }
        catch
        {
        }

        // キャラクターの配列の用意
        var characters = new[]
        {
            new Character { Id = 1, Name = "フシギダネ" },
            new Character { Id = 2, Name = "フシギソウ" },
            new Character { Id = 3, Name = "フシギバナ" },
            new Character { Id = 4, Name = "ヒトカゲ" },
            new Character { Id = 5, Name = "リザード" },
            new Character { Id = 6, Name = "リザードン" },
        };

        // データベースのビルド
        var builder = new DatabaseBuilder();
        builder.Append( characters );
        var databaseBinary = builder.Build();

        // ビルドしたデータベースをファイルに保存
        var path = "Assets/Resources/master.bytes";
        var dir  = Path.GetDirectoryName( path );

        Directory.CreateDirectory( dir );

        using ( var stream = new FileStream( path, FileMode.Create ) )
        {
            stream.Write( databaseBinary, 0, databaseBinary.Length );
        }
        
        // Unity の Project ビューに反映
        AssetDatabase.Refresh();
    }
}

作成した「Builder.cs」を開いて上記のコードを記述します
これで事前にマスタデータをビルドできるようになりました

f:id:baba_s:20200502155627p:plain

Unity メニューの「Tools > Build」を選択すると

f:id:baba_s:20200502155650p:plain

Resources フォルダに「master.bytes」という名前のファイルが生成されます
これがマスタデータがビルドされたファイルになるため、
「Test.cs」でこのファイルからマスタデータを読み込むようにしていきます

using Master;
using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

public sealed class Test : MonoBehaviour
{
    // MessagePack の初期化処理
    [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )]
    private static void Initialize()
    {
        StaticCompositeResolver.Instance.Register
        (
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );

        var options = MessagePackSerializerOptions.Standard.WithResolver( StaticCompositeResolver.Instance );
        MessagePackSerializer.DefaultOptions = options;
    }

    private void Start()
    {
        // 事前にビルドしたマスタを読み込み
        var textAsset = Resources.Load<TextAsset>( "master" );
        var bytes     = textAsset.bytes;

        // データベースにアクセスするためのインスタンスの作成
        var database = new MemoryDatabase( bytes );

        // キャラクターのデータベースを参照
        var characterTable = database.CharacterTable;

        // すべてのキャラクターを取得
        foreach ( var n in characterTable.All )
        {
            Debug.Log( n.Name );
        }
    }
}

スクリプトを変更して Unity でゲームを再生してみると
正常にマスタデータを読み込んで使用できていることが確認できます

このように、事前にマスタをビルドすることで
より高速にマスタデータを読み込めるようになります

基本的な使い方まとめ

  1. マスタを管理するクラスを定義する
  2. MasterMemory のジェネレータを実行する
  3. MessagePack のジェネレータを実行する
  4. マスタデータを準備する
    • スクリプト直書き
    • CSV・JSON などの外部アセットで準備
  5. マスタデータをビルドして使用する
    • ゲーム実行中にマスタデータをビルドして使用する
    • エディタ拡張などで事前にマスタデータをビルドして使用する

以上が MasterMemory の基本的な使い方になります

MasterMemory のジェネレータの補足

名前空間の指定

"【MasterMemory.Generator.zip 展開先のフォルダ】\win-x64\MasterMemory.Generator.exe" ^
    -i "【Unity プロジェクトのパス】\Assets\Scripts\Master" ^
    -o "【Unity プロジェクトのパス】\Assets\Scripts\Generated" ^
    -n "Master"

-n オプションで MasterMemory が出力するクラスが所属する名前空間を指定できます

キーが見つからなかった場合は null を返す

// KeyNotFoundException: DataType: Character, Key: -1
var character = characterTable.FindById( -1 );

デフォルトではキーが見つからなかった場合に KeyNotFoundException が投げられますが

"【MasterMemory.Generator.zip 展開先のフォルダ】\win-x64\MasterMemory.Generator.exe" ^
    -i "【Unity プロジェクトのパス】\Assets\Scripts\Master" ^
    -o "【Unity プロジェクトのパス】\Assets\Scripts\Generated" ^
    -n "Master" ^
    -t

-t オプションを指定すると null が返ってくるようになります

不変型

using MasterMemory;
using MessagePack;

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey] public int Id { get; }

    public string Name { get; }
}

上記のようにマスタのクラスを不変型にしたい場合は

"【MasterMemory.Generator.zip 展開先のフォルダ】\win-x64\MasterMemory.Generator.exe" ^
    -i "【Unity プロジェクトのパス】\Assets\Scripts\Master" ^
    -o "【Unity プロジェクトのパス】\Assets\Scripts\Generated" ^
    -n "Master" ^
    -c

-c オプションを指定します

using MasterMemory;
using MessagePack;

[MemoryTable("character"), MessagePackObject(true)]
public class Character
{
    [PrimaryKey] public int Id { get; }

    public string Name { get; }

    public Character(int Id, string Name)
    {
        this.Id = Id;
        this.Name = Name;
    }
}

すると、MasterMemory.Generator.exe 実行後にコンストラクタが自動で記述され、
不変型として扱われるようになります
(すでにコンストラクタを定義している場合は無視されます)

テーブルクラスの拡張

namespace Master.Tables
{
    // CharacterTable の部分クラス
    public sealed partial class CharacterTable
    {
        public int MaxId { get; private set; }

        // テーブル作成後に呼び出される関数
        partial void OnAfterConstruct()
        {
            MaxId = All.Max( x => x.Id );
        }
    }
}

テーブルクラスは部分クラスとして定義されているため
自作のプロパティや関数を別ファイルで定義することができます
テーブル作成後に OnAfterConstruct 関数が呼び出されるため
テーブル作成後に行いたい処理はこの関数に記述することができます

マスタを管理するクラスの追加

ゲーム開発中にマスタを増やす時は

using MasterMemory;
using MessagePack;

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey] public int Id { get; set; }

    public string Name { get; set; }
}

// ★
[MemoryTable( "item" ), MessagePackObject( true )]
public class Item
{
    [PrimaryKey] public int Id { get; set; }

    public string Name { get; set; }
}

「Assets/Scripts/Master」フォルダ内のスクリプトに新しいマスタのクラスを定義して
MasterMemory と MessagePack のジェネレータを実行して

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

public static class Builder
{
    [MenuItem( "Tools/Build" )]
    private static void Build()
    {
        // MessagePack の初期化
        // 初期化を複数回実行すると下記の例外が発生するが
        // データベースのビルドには影響がないため例外は無視する
        // InvalidOperationException: Register must call on startup(before use GetFormatter<T>).
        try
        {
            StaticCompositeResolver.Instance.Register
            (
                MasterMemoryResolver.Instance,
                GeneratedResolver.Instance,
                StandardResolver.Instance
            );

            var options = MessagePackSerializerOptions.Standard.WithResolver( StaticCompositeResolver.Instance );
            MessagePackSerializer.DefaultOptions = options;
        }
        catch
        {
        }

        // キャラクターの配列の用意
        var characters = new[]
        {
            new Character { Id = 1, Name = "フシギダネ" },
            new Character { Id = 2, Name = "フシギソウ" },
            new Character { Id = 3, Name = "フシギバナ" },
            new Character { Id = 4, Name = "ヒトカゲ" },
            new Character { Id = 5, Name = "リザード" },
            new Character { Id = 6, Name = "リザードン" },
        };

        // ★アイテムの配列を用意
        var items = new[]
        {
            new Item { Id = 1, Name = "キズぐすり" },
            new Item { Id = 2, Name = "いいキズぐすり" },
            new Item { Id = 3, Name = "すごいキズぐすり" },
        };

        // データベースのビルド
        var builder = new DatabaseBuilder();
        builder.Append( characters );
        builder.Append( items ); // ★アイテムの配列を登録
        var databaseBinary = builder.Build();

        // ビルドしたデータベースをファイルに保存
        var path = "Assets/Resources/master.bytes";
        var dir  = Path.GetDirectoryName( path );

        Directory.CreateDirectory( dir );

        using ( var stream = new FileStream( path, FileMode.Create ) )
        {
            stream.Write( databaseBinary, 0, databaseBinary.Length );
        }
        
        // Unity の Project ビューに反映
        AssetDatabase.Refresh();
    }
}

マスタをビルドする処理を記述します
事前にマスタをビルドする場合は Unity メニューからビルドしておきます

using Master;
using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

public sealed class Test : MonoBehaviour
{
    // MessagePack の初期化処理
    [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )]
    private static void Initialize()
    {
        StaticCompositeResolver.Instance.Register
        (
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );

        var options = MessagePackSerializerOptions.Standard.WithResolver( StaticCompositeResolver.Instance );
        MessagePackSerializer.DefaultOptions = options;
    }

    private void Start()
    {
        // 事前にビルドしたマスタを読み込み
        var textAsset = Resources.Load<TextAsset>( "master" );
        var bytes     = textAsset.bytes;

        // データベースにアクセスするためのインスタンスの作成
        var database = new MemoryDatabase( bytes );

        // すべてのキャラクターを取得
        foreach ( var n in database.CharacterTable.All )
        {
            Debug.Log( n.Name );
        }

        // すべてのアイテムを取得
        foreach ( var n in database.ItemTable.All )
        {
            Debug.Log( n.Name );
        }
    }
}

これで、新しいマスタを使用できるようになります

マスタのクラスのプロパティに適用できる属性

PrimaryKey

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey]
    public int Id { get; set; }

    [PrimaryKey]
    public string Name { get; set; }
}

PrimaryKey 属性は必ず1つ付与する必要があります

var character = characterTable.FindByIdAndName( ( 1, "フシギダネ" ) );

PrimaryKey 属性が付いていると Find 関数で使用できるようになります
PrimaryKey 属性が複数付いていると Find 関数で一緒に使用できるようになります

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey( keyOrder: 1 )]
    public int Id { get; set; }

    [PrimaryKey( keyOrder: 0 )]
    public string Name { get; set; }
}
var character = characterTable.FindByNameAndId( ( "フシギダネ", 1 ) );

keyOrder で順番を指定することができます

NonUnique

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey, NonUnique]
    public int Id { get; set; }
    
    public string Name { get; set; }
}
var characters = characterTable.FindById( 1 );
foreach ( var character in characters )
{
}

NonUnique 属性が付いていると Find 関数で複数の結果を受け取れるようになります

SecondaryKey

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey]
    public int Id { get; set; }
    
    [SecondaryKey( 0 )]
    public string Type1 { get; set; }
    
    [SecondaryKey( 0 )]
    public string Type2 { get; set; }
}
var characters = characterTable.FindByType1AndType2( ( "くさ", "どく" ) );

PrimaryKey が適用されたプロパティ以外にも Find 関数で使用したい場合は
SecondaryKey 属性を使用します
SecondaryKey 属性の引数の数値を同じにしておくと Find 関数で一緒に使用できます

StringComparisonOption

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey]
    public int Id { get; set; }
    
    [StringComparisonOption( StringComparison.InvariantCultureIgnoreCase )]
    public string Name { get; set; }
}

StringComparisonOption 属性で文字列の比較方法を指定できます

IgnoreMember

[MemoryTable( "character" ), MessagePackObject( true )]
public class Character
{
    [PrimaryKey]
    public int Id { get; set; }
    
    [IgnoreMember]
    public string Name { get; set; }
}

IgnoreMember 属性が指定されたプロパティは MessagePack で無視されます