UnityのマスタデータにMasterMemoryを使ってみる
はじめに
この記事は、マスタデータ用に作られた型安全なデータベース、「MasterMemory」をUnityで使ってみた記事です。
Cysharp/MasterMemory: Embedded Typed Readonly In-Memory Document Database for .NET Core and Unity.
- Unity: 2019.2.1f1
- MasterMemory: 1.1.7
MasterMemoryを使う
ReadMeがとても丁寧なのでそれに従うだけです。
まず最初にMasterMemoryのReleaseページからMasterMemory.Generator.zipとMasterMemory.Unity.unitypackageをダウンロードしてきます。
また、MasterMemoryが依存しているMessagePack-CSharpのReleaseページからMessagePack.Unity.1.7.3.5.unitypackage と MessagePackUniversalCodeGenerator.zip をダウンロードしてきます。
unitypackageを両方とも読み込みます。
テスト用にAssets/Scrpts/Tables/Person.cs
を作成します。
using MasterMemory;
using MessagePack;
public enum Gender {
Male,
Female,
Unknown
}
// table definition marked by MemoryTableAttribute.
// database-table must be serializable by MessagePack-CSsharp
[MemoryTable ("person"), MessagePackObject (true)]
public class Person {
// index definition by attributes.
[PrimaryKey]
public int PersonId { get; set; }
// secondary index can add multiple(discriminated by index-number).
[SecondaryKey (0), NonUnique]
[SecondaryKey (1, keyOrder : 1), NonUnique]
public int Age { get; set; }
[SecondaryKey (2), NonUnique]
[SecondaryKey (1, keyOrder : 0), NonUnique]
public Gender Gender { get; set; }
public string Name { get; set; }
}
Unityのプロジェクトの直下にGeneratorTools/
というディレクトリを作り、そこにzipを解凍します。
Assets/Editor/CodeGenerator.cs
を作成します。
using System.Diagnostics;
using UnityEditor;
using UnityEngine;
public class MenuItems : MonoBehaviour {
[MenuItem ("MasterMemory/CodeGenerate")]
private static void Generate () {
ExecuteMasterMemoryCodeGenerator ();
ExecuteMessagePackCodeGenerator ();
}
private static void ExecuteMasterMemoryCodeGenerator () {
UnityEngine.Debug.Log ($"{nameof(ExecuteMasterMemoryCodeGenerator)} : start");
var exProcess = new Process ();
var rootPath = Application.dataPath + "/..";
var filePath = rootPath + "/GeneratorTools/MasterMemory.Generator";
var exeFileName = "";
#if UNITY_EDITOR_WIN
exeFileName = "/win-x64/MasterMemory.Generator.exe";
#elif UNITY_EDITOR_OSX
exeFileName = "/osx-x64/MasterMemory.Generator";
#elif UNITY_EDITOR_LINUX
exeFileName = "/linux-x64/MasterMemory.Generator";
#else
return;
#endif
var psi = new ProcessStartInfo () {
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
FileName = filePath + exeFileName,
Arguments = $@"-i ""{Application.dataPath}/Scripts/Tables"" -o ""{Application.dataPath}/Scripts/Generated"" -n ""MasterData""",
};
var p = Process.Start (psi);
p.EnableRaisingEvents = true;
p.Exited += (object sender, System.EventArgs e) => {
var data = p.StandardOutput.ReadToEnd ();
UnityEngine.Debug.Log ($"{data}");
UnityEngine.Debug.Log ($"{nameof(ExecuteMasterMemoryCodeGenerator)} : end");
p.Dispose ();
p = null;
};
}
private static void ExecuteMessagePackCodeGenerator () {
UnityEngine.Debug.Log ($"{nameof(ExecuteMessagePackCodeGenerator)} : start");
var exProcess = new Process ();
var rootPath = Application.dataPath + "/..";
var filePath = rootPath + "/GeneratorTools/MessagePackUniversalCodeGenerator";
var exeFileName = "";
#if UNITY_EDITOR_WIN
exeFileName = "/win-x64/mpc.exe";
#elif UNITY_EDITOR_OSX
exeFileName = "/osx-x64/mpc";
#elif UNITY_EDITOR_LINUX
exeFileName = "/linux-x64/mpc";
#else
return;
#endif
var psi = new ProcessStartInfo () {
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
FileName = filePath + exeFileName,
Arguments = $@"-i ""{Application.dataPath}/../Assembly-CSharp.csproj"" -o ""{Application.dataPath}/Scripts/Generated/MessagePack.Generated.cs""",
};
var p = Process.Start (psi);
p.EnableRaisingEvents = true;
p.Exited += (object sender, System.EventArgs e) => {
var data = p.StandardOutput.ReadToEnd ();
UnityEngine.Debug.Log ($"{data}");
UnityEngine.Debug.Log ($"{nameof(ExecuteMessagePackCodeGenerator)} : end");
p.Dispose ();
p = null;
};
}
}
「MasterMemory>CodeGenerate」を実行します。
Assets/Scripts/Generated/
にコードが生成されます。
名前空間MasterData
でコードを生成するようにしていますが、必要に応じて適宜変更してください。
Assets/Scripts/Initializer.cs
を作成します。
using MasterData;
using MessagePack.Resolvers;
using UnityEngine;
public static class Initializer {
[RuntimeInitializeOnLoadMethod (RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void SetupMessagePackResolver () {
CompositeResolver.RegisterAndSetAsDefault (new [] {
MasterMemoryResolver.Instance, // set MasterMemory generated resolver
GeneratedResolver.Instance, // set MessagePack generated resolver
StandardResolver.Instance // set default MessagePack resolver
});
}
}
適当なスクリプトを作成して試してみます。
using MasterData;
using MasterMemory;
using UnityEngine;
public class Test : MonoBehaviour {
void Start () {
// to create database, use DatabaseBuilder and Append method.
var builder = new DatabaseBuilder ();
builder.Append (new Person[] {
new Person { PersonId = 0, Age = 13, Gender = Gender.Male, Name = "Dana Terry" },
new Person { PersonId = 1, Age = 17, Gender = Gender.Male, Name = "Kirk Obrien" },
new Person { PersonId = 2, Age = 31, Gender = Gender.Male, Name = "Wm Banks" },
new Person { PersonId = 3, Age = 44, Gender = Gender.Male, Name = "Karl Benson" },
new Person { PersonId = 4, Age = 23, Gender = Gender.Male, Name = "Jared Holland" },
new Person { PersonId = 5, Age = 27, Gender = Gender.Female, Name = "Jeanne Phelps" },
new Person { PersonId = 6, Age = 25, Gender = Gender.Female, Name = "Willie Rose" },
new Person { PersonId = 7, Age = 11, Gender = Gender.Female, Name = "Shari Gutierrez" },
new Person { PersonId = 8, Age = 63, Gender = Gender.Female, Name = "Lori Wilson" },
new Person { PersonId = 9, Age = 34, Gender = Gender.Female, Name = "Lena Ramsey" },
});
// build database binary(you can also use `WriteToStream` for save to file).
byte[] data = builder.Build ();
// -----------------------
// for query phase, create MemoryDatabase.
// (MemoryDatabase is recommended to store in singleton container(static field/DI)).
var db = new MemoryDatabase (data);
// .PersonTable.FindByPersonId is fully typed by code-generation.
Person person = db.PersonTable.FindByPersonId (10);
// Multiple key is also typed(***And * **), Return value is multiple if key is marked with `NonUnique`.
RangeView<Person> result = db.PersonTable.FindByGenderAndAge ((Gender.Female, 23));
Debug.Log ("result");
foreach (var p in result) {
Debug.Log ($"{p.Name}");
}
// Get nearest value(choose lower(default) or higher).
RangeView<Person> age1 = db.PersonTable.FindClosestByAge (31);
Debug.Log ("age1");
foreach (var p in age1) {
Debug.Log ($"{p.Name}");
}
// Get range(min-max inclusive).
RangeView<Person> age2 = db.PersonTable.FindRangeByAge (20, 29);
Debug.Log ("age2");
foreach (var p in age2) {
Debug.Log ($"{p.Name}");
}
}
}
正しく動作していることが確認できます。
マスタデータのBuildを事前にエディタ上で行うようにします。
Assets/Editor/MasterDataBuilder.cs
を作成します。
using System.IO;
using MasterData;
using UnityEditor;
using UnityEngine;
public static class MasterDataBuilder {
[MenuItem ("MasterMemory/Build")]
static void BuildMasterData () {
var builder = new DatabaseBuilder ();
builder.Append (new Person[] {
new Person { PersonId = 0, Age = 13, Gender = Gender.Male, Name = "Dana Terry" },
new Person { PersonId = 1, Age = 17, Gender = Gender.Male, Name = "Kirk Obrien" },
new Person { PersonId = 2, Age = 31, Gender = Gender.Male, Name = "Wm Banks" },
new Person { PersonId = 3, Age = 44, Gender = Gender.Male, Name = "Karl Benson" },
new Person { PersonId = 4, Age = 23, Gender = Gender.Male, Name = "Jared Holland" },
new Person { PersonId = 5, Age = 27, Gender = Gender.Female, Name = "Jeanne Phelps" },
new Person { PersonId = 6, Age = 25, Gender = Gender.Female, Name = "Willie Rose" },
new Person { PersonId = 7, Age = 11, Gender = Gender.Female, Name = "Shari Gutierrez" },
new Person { PersonId = 8, Age = 63, Gender = Gender.Female, Name = "Lori Wilson" },
new Person { PersonId = 9, Age = 34, Gender = Gender.Female, Name = "Lena Ramsey" },
});
byte[] data = builder.Build ();
var resourcesDir = $"{Application.dataPath}/Resources";
Directory.CreateDirectory (resourcesDir);
var filename = "/master-data.bytes";
using (var fs = new FileStream (resourcesDir + filename, FileMode.Create)) {
fs.Write (data, 0, data.Length);
}
Debug.Log ($"Write byte[] to: {resourcesDir + filename}");
AssetDatabase.Refresh ();
}
}
「MasterMemory > Build」を実行します。
Registerされていないと言って怒られます。
次のようにコードを変えます。
using System.IO;
using MasterData;
using MessagePack.Resolvers;
using UnityEditor;
using UnityEngine;
public static class MasterDataBuilder {
[MenuItem ("MasterMemory/Build")]
static void BuildMasterData () {
CompositeResolver.RegisterAndSetAsDefault (new [] {
MasterMemoryResolver.Instance,
GeneratedResolver.Instance,
StandardResolver.Instance
});
var builder = new DatabaseBuilder ();
builder.Append (new Person[] {
new Person { PersonId = 0, Age = 13, Gender = Gender.Male, Name = "Dana Terry" },
new Person { PersonId = 1, Age = 17, Gender = Gender.Male, Name = "Kirk Obrien" },
new Person { PersonId = 2, Age = 31, Gender = Gender.Male, Name = "Wm Banks" },
new Person { PersonId = 3, Age = 44, Gender = Gender.Male, Name = "Karl Benson" },
new Person { PersonId = 4, Age = 23, Gender = Gender.Male, Name = "Jared Holland" },
new Person { PersonId = 5, Age = 27, Gender = Gender.Female, Name = "Jeanne Phelps" },
new Person { PersonId = 6, Age = 25, Gender = Gender.Female, Name = "Willie Rose" },
new Person { PersonId = 7, Age = 11, Gender = Gender.Female, Name = "Shari Gutierrez" },
new Person { PersonId = 8, Age = 63, Gender = Gender.Female, Name = "Lori Wilson" },
new Person { PersonId = 9, Age = 34, Gender = Gender.Female, Name = "Lena Ramsey" },
});
byte[] data = builder.Build ();
var resourcesDir = $"{Application.dataPath}/Resources";
Directory.CreateDirectory (resourcesDir);
var filename = "/master-data.bytes";
using (var fs = new FileStream (resourcesDir + filename, FileMode.Create)) {
fs.Write (data, 0, data.Length);
}
Debug.Log ($"Write byte[] to: {resourcesDir + filename}");
AssetDatabase.Refresh ();
}
}
1回目はうまく生成できますが2回目以降に怒られます。
try-catchで握りつぶすことにします。
using System.IO;
using MasterData;
using MessagePack.Resolvers;
using UnityEditor;
using UnityEngine;
public static class MasterDataBuilder {
[MenuItem ("MasterMemory/Build")]
static void BuildMasterData () {
try {
CompositeResolver.RegisterAndSetAsDefault (new [] {
MasterMemoryResolver.Instance,
GeneratedResolver.Instance,
StandardResolver.Instance
});
} catch { }
var builder = new DatabaseBuilder ();
builder.Append (new Person[] {
new Person { PersonId = 0, Age = 13, Gender = Gender.Male, Name = "Dana Terry" },
new Person { PersonId = 1, Age = 17, Gender = Gender.Male, Name = "Kirk Obrien" },
new Person { PersonId = 2, Age = 31, Gender = Gender.Male, Name = "Wm Banks" },
new Person { PersonId = 3, Age = 44, Gender = Gender.Male, Name = "Karl Benson" },
new Person { PersonId = 4, Age = 23, Gender = Gender.Male, Name = "Jared Holland" },
new Person { PersonId = 5, Age = 27, Gender = Gender.Female, Name = "Jeanne Phelps" },
new Person { PersonId = 6, Age = 25, Gender = Gender.Female, Name = "Willie Rose" },
new Person { PersonId = 7, Age = 11, Gender = Gender.Female, Name = "Shari Gutierrez" },
new Person { PersonId = 8, Age = 63, Gender = Gender.Female, Name = "Lori Wilson" },
new Person { PersonId = 9, Age = 34, Gender = Gender.Female, Name = "Lena Ramsey" },
});
byte[] data = builder.Build ();
var resourcesDir = $"{Application.dataPath}/Resources";
Directory.CreateDirectory (resourcesDir);
var filename = "/master-data.bytes";
using (var fs = new FileStream (resourcesDir + filename, FileMode.Create)) {
fs.Write (data, 0, data.Length);
}
Debug.Log ($"Write byte[] to: {resourcesDir + filename}");
AssetDatabase.Refresh ();
}
}
これでEditor上からデータを生成できるようになりました。
今回はひとまず生成したデータをAssets/Resources/
に配置しています。
次にこの生成したmaster-data.bytes
を利用するコードを書きます。
MasterDataDB.csに次のように。
using MasterData;
using UnityEngine;
public class MasterDataDB {
private static MemoryDatabase _db = new MemoryDatabase ((Resources.Load ("master-data") as TextAsset).bytes);
public static MemoryDatabase DB => _db;
}
Test.csに次のように記述します。
using MasterData;
using MasterMemory;
using UnityEngine;
public class Test : MonoBehaviour {
void Start () {
var db = MasterDataDB.DB;
Person person = db.PersonTable.FindByPersonId (10);
Debug.Log ($"{person}");
RangeView<Person> result = db.PersonTable.FindByGenderAndAge ((Gender.Female, 23));
Debug.Log ("result");
foreach (var p in result) {
Debug.Log ($"{p.Name}");
}
RangeView<Person> age1 = db.PersonTable.FindClosestByAge (31);
Debug.Log ("age1");
foreach (var p in age1) {
Debug.Log ($"{p.Name}");
}
RangeView<Person> age2 = db.PersonTable.FindRangeByAge (20, 29);
Debug.Log ("age2");
foreach (var p in age2) {
Debug.Log ($"{p.Name}");
}
}
}
Resources/から読み込んだデータをMemoryDatabaseのコンストラクタに渡しています。
MemoryDatabaseのインスタンスはシングルトンなりDIなりで使い回すことが推奨されているようです。 今回は適当なシングルトンに放り込んでいます。
いくつかテーブルを追加してみます。 RPGのスキルをイメージしたテーブルを追加します。 スキルにはスキルレベルが存在する、といった想定です。
Assets/Scripts/Tables/Skill.cs
を作成します。
using MasterMemory;
using MessagePack;
[MemoryTable ("skill"), MessagePackObject (true)]
public class Skill {
[PrimaryKey]
public int SkillID { get; set; }
public string SkillName { get; set; }
}
Assets/Scripts/Tables/SkillParameter.cs
を作成します。
using MasterMemory;
using MessagePack;
[MemoryTable ("skillParameter"), MessagePackObject (true)]
public class SkillParameter {
[PrimaryKey, NonUnique]
[SecondaryKey (0)]
public int SkillID { get; set; }
[SecondaryKey (0)]
public int SkillLv { get; set; }
public int Damage { get; set; }
}
Skill.csのスキルIDはユニークなものです。 同じスキルのレベル違いのパラメータを表現するためにSkillParameter.csを用意しています。 SkillParameter.csのスキルIDにはNonUniqueをつけています。 スキルに対応するパラメータはレベル違いで存在します。 SkillParameter.csのスキルIDとスキルレベルを組み合わせた場合にはUniqueとしています。
「MasterMemory > CodeGenerate」を実行します。
Generatedにコードが増えます。
Assets/Editor/MasterDataBuilder.cs
のマスタデータの生成部分を変更します。
using System.Collections.Generic;
using System.IO;
using MasterData;
using MessagePack.Resolvers;
using UnityEditor;
using UnityEngine;
public static class MasterDataBuilder {
[MenuItem ("MasterMemory/Build")]
private static void BuildMasterData () {
try {
CompositeResolver.RegisterAndSetAsDefault (new [] {
MasterMemoryResolver.Instance,
GeneratedResolver.Instance,
StandardResolver.Instance
});
} catch { }
var builder = new DatabaseBuilder ();
builder = BuildParson (builder);
builder = BuildSkill (builder);
builder = BuildSkillParameter (builder);
byte[] data = builder.Build ();
var resourcesDir = $"{Application.dataPath}/Resources";
Directory.CreateDirectory (resourcesDir);
var filename = "/master-data.bytes";
using (var fs = new FileStream (resourcesDir + filename, FileMode.Create)) {
fs.Write (data, 0, data.Length);
}
Debug.Log ($"Write byte[] to: {resourcesDir + filename}");
AssetDatabase.Refresh ();
}
private static DatabaseBuilder BuildParson (DatabaseBuilder builder) {
builder.Append (new Person[] {
new Person { PersonId = 0, Age = 13, Gender = Gender.Male, Name = "Dana Terry" },
new Person { PersonId = 1, Age = 17, Gender = Gender.Male, Name = "Kirk Obrien" },
new Person { PersonId = 2, Age = 31, Gender = Gender.Male, Name = "Wm Banks" },
new Person { PersonId = 3, Age = 44, Gender = Gender.Male, Name = "Karl Benson" },
new Person { PersonId = 4, Age = 23, Gender = Gender.Male, Name = "Jared Holland" },
new Person { PersonId = 5, Age = 27, Gender = Gender.Female, Name = "Jeanne Phelps" },
new Person { PersonId = 6, Age = 25, Gender = Gender.Female, Name = "Willie Rose" },
new Person { PersonId = 7, Age = 11, Gender = Gender.Female, Name = "Shari Gutierrez" },
new Person { PersonId = 8, Age = 63, Gender = Gender.Female, Name = "Lori Wilson" },
new Person { PersonId = 9, Age = 34, Gender = Gender.Female, Name = "Lena Ramsey" },
});
return builder;
}
private static DatabaseBuilder BuildSkill (DatabaseBuilder builder) {
builder.Append (new Skill[] {
new Skill { SkillID = 0, SkillName = "スキル0" },
new Skill { SkillID = 1, SkillName = "スキル1" },
new Skill { SkillID = 2, SkillName = "スキル2" },
new Skill { SkillID = 3, SkillName = "スキル3" },
});
return builder;
}
private static DatabaseBuilder BuildSkillParameter (DatabaseBuilder builder) {
var skillParameters = new List<SkillParameter> ();
for (int i = 0; i < 4; i++) {
for (int lv = 1; lv < 10; lv++) {
skillParameters.Add (new SkillParameter {
SkillID = i,
SkillLv = lv,
Damage = lv * 100
});
}
}
builder.Append (skillParameters);
return builder;
}
}
「MasterMemory > Build」を実行しマスタデータを生成します。
場合によってはGeneratedフォルダを削除してもう一度生成する必要があるかもです。
適当にスキルとレベル、ダメージを表示するスクリプトを作ってみます。
using MasterData;
using MasterMemory;
using UnityEngine;
public class Test : MonoBehaviour {
void Start () {
var db = MasterDataDB.DB;
foreach (var skill in db.SkillTable.All) {
Debug.Log ($"Skill Name: {skill.SkillName}");
var lv = (int) Random.Range (1, 10);
Debug.Log ($"Skill Lv: {lv}");
var parameter = db.SkillParameterTable
.FindBySkillIDAndSkillLv ((skill.SkillID, lv));
Debug.Log ($"Skill Damage: {parameter.Damage}");
}
}
}
正しく実行できました。
おわりに
MasterMemoryを使ってみました。 型安全にアクセスできるのは便利です。 適当にコードを書けばIntelisenceで候補が出てきて楽にかけます。
現状ではマスタデータがソースコードにベタ書きの状況ですが、 そこらへんでなにか工夫をしてみてもよいかもですね。