🎮 Unityを愛する努力 〜Matrix対応編〜
前回、SupportChigadio のデータクラス生成機能(ClassDataID)についての記事を書きました。
記事の最後で「対応予定の機能」として挙げていた Matrix が形になってきたので、今回はその続編としてご紹介します。
この記事の対象読者
- 前回の記事でSupportChigadioのClassDataIDまでは触った人
- 「row×col」のような二次元の関連データをUnityで扱いたい人
📌 目次
🧩 Matrixとは何か
ClassDataIDは「1つのIDに対して1行のデータ」というシンプルな構造でした。
一方でゲームを作っていると、こういうデータが欲しくなることがよくあります。
- キャラA × キャラB のニックネーム(あだ名は相手によって変わる)
- キャラ × トークタイプ の組み合わせで変わる会話パターン
- 座席 × 時間帯 で変わる混雑度、みたいな二次元テーブル
つまり 「行(row)」と「列(col)」の組み合わせで1つのデータが決まる、二次元のテーブルです。これを表現するために追加したのがMatrix機能です。
イメージとしてはこんな感じの表です。
| row \ col | TestB_01 | TestB_02 |
|---|---|---|
| TestA_01 | "おいキャラC" 呼び | "よぉ" 呼び |
| TestA_02 | "先輩" 呼び | - |
rowとcolは、それぞれ既存のEnumやClassDataID(前回紹介した機能)を自由に組み合わせて指定できます。row側とcol側が同じ型(例えばキャラID同士)でも、違う型(キャラID×トークタイプID)でも対応しています。
🚀 セットアップ・操作の流れ
基本的な操作感は前回のClassDataIDと同じです。
🧩 SupportChigadio
| 項目 | 内容 |
|---|---|
| 必要ライブラリ | Addressable / UniTask |
| ステータス | 開発中(一部自動生成でエラーが出るファイルがあり修正中) |
| 目的 | 開発環境での煩わしさをできるだけフリーにすること |
- 左のスライドバーから ClassDataMatrixID を選択
- Matrix名(例:
TestMatrix)を決めて、row側の型・col側の型を選択(Enum / ClassDataIDどちらもOK) - フィールド(データの中身)を定義
- 表形式のUIで、row×colの各セルにデータを入力
- 保存 → C#生成 ボタン
- 最初のGridまで戻り、Matrix用の全バイナリ生成 / ヘッダー生成 ボタンを押す
C#生成を押すと、data/class_data_matrix_id フォルダの中に、Matrix名ごとのフォルダが作られ、その中に必要なスクリプトが一式自動生成されます。
🔧 自動生成される実際のコード
例として、row=キャラID、col=キャラID の NickNameChara というMatrixを作った場合を見てみます。
Row定義
using System.IO;
using System;
using System.Collections.Generic;
using UnityEngine;
using GameCore.Enums;
using GameCore.Tables.ID;
namespace GameCore.Tables {
public class TestMatrixMatrixRow : BaseClassDataMatrixRow {
[SerializeField]
protected TestATableID rowId_;
public TestATableID RowId { get => rowId_; }
[SerializeField]
protected TestBTableID colId_;
public TestBTableID ColId { get => colId_; }
[SerializeField]
protected int value;
public int Value { get => value; } // Default Value
public override void Read(int rowId, int colId, BinaryReader reader) {
rowId_ = (TestATableID)rowId;
colId_ = (TestBTableID)colId;
value = reader.ReadInt32();
}
}
}
自分がどのrow・colに属しているセルなのかを、RowId / ColId として自分自身が持っているので、セル単体を取り出したあとでも「誰から見た誰の呼び方か」が分かるようになっています。
Table定義(抜粋)
using System.IO;
using GameCore.Tables.ID;
using GameCore.Enums;
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
namespace GameCore.Tables {
public class TestMatrixMatrixTable : BaseClassDataMatrixID<TestATableID, TestBTableID, TestMatrixMatrixRow> {
static TestMatrixMatrixTable()
{
RowIndex = new TestMatrixMatrixRowIndex();
TableId = MatrixTableID.TestMatrix;
MatrixTableRegistry.Loaders[TableId] = (header, reader) => header.GetData<TestMatrixMatrixTable>(MatrixTableID.TestMatrix, reader);
MatrixTableRegistry.Unloaders[TableId] = () => { Table.Clear(); s_cellIndexCache.Clear(); };
}
public override void Read(BinaryReader reader) {
// rowKeys / colKeys の読み込み → 行インデックス → 各セル、の順に読み進める
...
}
// 以下、LoadSingleAsync / LoadRowAsync / LoadColumnAsync など
// セル・行・列単位でロードするためのメソッド群が自動で生えてくる
}
}
ClassDataIDのときは Table : Dictionary<TableID, Row> という一次元の辞書でしたが、Matrixは Dictionary<TRow, Dictionary<TCol, Row>> という二段構えの辞書になっています。
🕳 地味だけど一番作り込んだところ:部分ロード
Matrixは仕組み上、row×colの組み合わせが多いとデータ量が大きくなりがちです。「使う予定のないセルまで全部メモリに載せる」のはもったいないので、必要な行・列・セルだけをピンポイントで読み込めるようにしています。
やっていることはシンプルで、バイナリファイルを書き出す時点で
- テーブル全体の中の「この行はどこから始まるか」という行インデックス
- 各行ブロックの中に埋め込んだ「この列はどこから始まるか」というセルインデックス
をあらかじめ記録しておき、読み込み側はそのオフセット情報を頼りに、必要な場所だけSeekして読む、という形です。
Matrixのバイナリ構造(イメージ)
├─ rowKeys / colKeys 一覧
├─ 行インデックス(rowId, offset, size)× 行数
└─ 行ブロック × 行数
├─ セルインデックス(colId, offset, size)× 列数
└─ 各セルの実データ(列順に連結)
これのおかげで、「テーブル丸ごとロード」をしなくても、いきなり1セルだけ読みに行くAPIを呼んでも普通に動きます。初回アクセス時にそのテーブルの行インデックスだけその場で読んでキャッシュし、2回目以降はそのキャッシュを使い回す、という遅延ロードの仕組みになっています。
💻 コードでの呼び出し方
呼び出し方も、ClassDataID同様に自動生成されたユーティリティから呼ぶだけです。MatrixTableIdUtils.cs にMatrix名ごとの専用メソッドがまとめて生成されます。
// セル単体だけロード
LoadSingleNickNameChara(TestATableID.TestA_01, TestBTableID.TestB_01);
// 行全体をロード(colは指定しない)
LoadSingleRowNickNameChara(TestATableID.TestA_01);
// 列全体をロード(rowは指定しない)
LoadSingleColumnNickNameChara(TestBTableID.TestB_01);
// 複数セルをまとめてロード
LoadSingleNickNameChara(new[] {
(TestATableID.TestA_01, TestBTableID.TestB_01),
(TestATableID.TestA_02, TestBTableID.TestB_02),
});
引数の組み合わせ(単体/配列/行のみ/列のみ)によって、複数のオーバーロードが自動で生成されるので、状況に応じて一番シンプルな書き方を選べます。
データを取り出すときは、ClassDataIDの GetRow() と同じ感覚で、Matrix専用の拡張メソッドを使います。
var cell = TestATableID.TestA_01.GetCell(TestBTableID.TestB_01);
var nickName = cell.NickName;
// そのセルが存在するかだけ先に知りたい場合
if (TestATableID.TestA_01.HasCell(TestBTableID.TestB_01))
{
// ...
}
// 行全体、列全体をまとめて取りたい場合
var row = TestATableID.TestA_01.GetNickNameCharaMatrixRow(); // Dictionary<colType, Row>
var col = TestBTableID.TestB_01.GetNickNameCharaMatrixCol(); // Dictionary<rowType, Row>
ちなみに GetRow という名前は、ClassDataID側の拡張メソッドと名前がかぶってあいまい参照エラーになるため、Matrix側は Get{Matrix名}MatrixRow / Get{Matrix名}MatrixCol という専用の名前にしています。
🔮 さいごに
前回の記事で「対応予定」としていたMatrixが、ひとまず一通り形になりました。
- row×colの二次元データをUIから編集できる
- ClassDataIDと同じ感覚で自動生成コードから呼び出せる
- 必要なところだけピンポイントでロードできる部分ロード対応
という感じで、単純な一覧データだけでなく、キャラ同士の関係性のような「組み合わせもの」もSupportChigadioで扱えるようになってきました。
引き続き、State(状態機械)まわりの自動生成も進めているので、また形になったら記事にしようと思います。
⭐ 気になった方はぜひGitHubをチェックしてみてください → SupportChigadio


