【ゲームAI考察 #3】Unity + JobSystemでゲームAIを作る

# はじめに
今回の記事はゲームAIをUnityのJobSystemで作成してみました、という内容です。
実際やってみるといろいろメリットがありそうだったので記事を書きました。

[作成したAIを含むリポジトリのリンク](https://github.com/cushionA/NewCave/tree/main/Assets/Script/CharacterController/AI)

あと本記事は一応ゲームAIに関する連載なので過去記事を貼ります。
もし興味がおありでしたら読んでみてください。
（内容的にはそれぞれ独立しています）

[【ゲームAI考察 #1】汎用ゲームAIから条件分岐を徹底排除！！](https://qiita.com/cushionA/items/8a7c3d5fe614c8f0a88a)
[【ゲームAI考察 #2】ゲームAIで考える非同期/マルチスレッド処理](https://qiita.com/cushionA/items/d7eec2cb2609de4d94b1)

また、この記事はUnityに興味がある方向けであると最初に断っておきます。

:::note info

**当記事におけるコードの実行環境**
**Unity 6（ver 6000.0.35f1）**
**ユニットテストにはTestRunerとPerformance Testing Extensionを使用**
:::

# JobSystemとは？
本題に入る前にJobSystemについて軽く説明します。
JobSystemとはUnityの機能で、一言で言うと **①アンマネージ領域<sup>[注釈1](#注釈1)</sup>へのアクセス ＋ ②マルチスレッド ＋ ③データ指向設計<sup>[注釈2](#注釈2)</sup> で処理を高速化させる** というものになります。
（もし違ったらゴメンなさい） 

①アンマネージ領域に配置した連続データ（NativeContainer<sup>[注釈3](#注釈3)</sup>）を用意。

②書き込み対象の連続データを分割し、分割部分をワーカースレッドに割り当ててマルチスレッド処理を行う。（Jobの作成と分配）

③データ配列の要素の一つ一つに同じ処理を連続で施していく。

これに加え **BurstCompiler<sup>[注釈4](#注釈4)</sup>** という特別なコンパイラで最適化をかけることで、C#らしからぬ圧倒的な処理速度を実現します。

個人的な見解ですが、JobSystemは低レイヤーな最適化を施した処理を上手くラップした機能に見えます。
色々と制限をかけることで、C#の処理からガチガチに最適化したC++コードの生成ができるようなイメージです。
そして主な制限は以下です。

- **Job内で扱えるのは値型のみ。**
- **Job内でアクセスできるデータはアンマネージ領域に配置されたデータのみ。（= NativeContainer内のデータ）**
- **Job内ではUnityAPIにアクセスできない。**
- **Job内ではマルチスレッドで要素にアクセスするため、内部データは書きこみ専用、読み取り専用、のいずれかの状態にしなければならない。**

それではJobSystemの説明はこれくらいにしておきます。
念のため公式の資料も添えておくのでもし間違っているところあれば指摘お願いいたします。

[・日本語の公式電子書籍](https://unity3d.jp/game/game-ebooks/unity-dots/)
[・電子書籍最新版（英語）](https://unity.com/resources/dots-concepts-features-samples-resources-unity-6)
[・Unity 2022.2 におけるジョブシステムのパフォーマンススケーリングの改善 - パート 1：背景と API](https://unity.com/ja/blog/engine-platform/improving-job-system-performance-2022-2-part-1)

# JobSystemでゲームAIを作るメリットとは？
一通りの説明が済んだところで、私が実際に体感した範囲でJobSystemでAIを構築するメリットについて書いていきます。

## 1,GCを誘発しない。
これはJobSystemが持つ以下の制限が理由です。

> - **Job内で扱えるのは値型のみ。**
> - **Job内でアクセスできるデータはアンマネージ領域に配置されたデータのみ。
（= NativeContainer内のデータ）**

AIが使うキャラデータがアンマネージ領域に配置されているため、キャラの追加・削除によるGCを最小限にできます。

## 2,データ構造が洗練される。
自己定義のデータ型をJobSystemで使用する場合、以下の要件を満たしていることが望ましいです。

- **64バイト（一般的なCPUのキャッシュライン<sup>[注釈5](#注釈5)</sup>単位）以内の構造体であること。**
- **使用のタイミングが同じデータを集めていること。（キャッシュ効率向上のため）**
- **SOA<sup>[注釈6](#注釈6)</sup>（StructureOfArray）であること。**

この要件を満たしてデータを作ると、それだけで無駄や用途の重複がなく、人にもCPUにもやさしいデータ構造が出来上がります。
SOAは少し人間には優しくない感じありますが。

## 3,速い…！
速いです。
しかしJobSystemが速いというよりは、その前段階でデータ構造やメモリレイアウトを最適化した結果高速化している部分も大きいです。
それどころかJobSystemの実行自体にオーバーヘッドがあるため、JobSystemの効果を体感できるのはある程度データ数が増えた段階になる場合もあります。
（ = 実行負荷をJobSystemの恩恵が上回る段階）

これに関しては以下の結果をご覧ください。

|<font color=""> **JobAIの計測結果** </font>|
|:-:|
|![alt](https://github.com/cushionA/ImageStrage/blob/main/JobSystemAI/%E5%B9%B3%E5%9D%87%E8%A8%88%E6%B8%AC/JobAI%E8%A8%88%E6%B8%AC.png?raw=true)|

こちらは次の三つの処理速度を比較しています。

- **JobSystemを使用したAI。（JobAI）**
- **JobAIと同じ構造でJobSystemを使わなかったAI。（Job未使用AI）**
- **Dictionary、Listなどの標準コレクションのみで構成されたAI。（標準AI）**

ちなみに標準AIに関しても使用するデータ型は同じで、かつ要所でSpan<sup>[注釈7](#注釈7)</sup>を使用するなど可能な限りの最適化は行っています。

この結果を見る限りキャラクターが50体以下では標準コレクションのAIが優位傾向ですが、それ以降は完全にJobAIが優位、といった印象でしょうか。

では続いて次の結果もご覧ください。
こちらは一つ目の検証で使用したJobAIからさらに最適化を進めたSOAJobAIとSplitJobAIを使用した検証です。

|<font color=""> **SOA適用のJobAI計測結果** </font>|
|:-:|
|![alt](https://github.com/cushionA/ImageStrage/blob/main/JobSystemAI/%E5%B9%B3%E5%9D%87%E8%A8%88%E6%B8%AC/SoAJobAI%E8%A8%88%E6%B8%AC.png?raw=true)|

結果としては、見事に高速化しています。（SplitJobAIについては割愛しますが、失敗でした）
SOA適用のJobAI（SOAJobAI）は要素数50以前も以降も変わらず高パフォーマンスを維持しています。

最初の検証では要素数が多くないとJobの効果は実感しにくい、とも取れる結果でしたが、データ構造まで突き詰めると最初から恩恵が感じられます。

実際Jobは開始時にスケジューリング<sup>[注釈8](#注釈8)</sup>のコストがあるため、要素が少ないと本領を発揮しにくい部分はあります。
しかしデータ構造の最適化などで弱点はカバーできます。

この上で要素数が多くなった場合、つまり最悪のケースでも高速を維持できるのであれば、JobSystemを採用する価値は大いにアリです。

## 4,機能をハードに共通化できる
JobSystemとはデータ配列の要素ひとつひとつに対し同じ処理を連続で行います。
よってJobSystemを導入したAIでは、キャラクターは同じデータで構成され、同じ処理で判断を行います。

オブジェクト指向の特徴である多態性<sup>[注釈9](#注釈9)</sup>などを捨て去った画一的実装になっている訳ですが、キャラクターAIにおいてはこれが大きなメリットになります。

**つまり全てのキャラが同じデータを同じように処理しているという前提があれば、キャラ同士で連携がしやすいのです。**

たとえば敵味方でのデータのやり取りや、イベント（被攻撃時イベントや指揮官キャラの命令イベントなど）起動の際に考慮すべき事項が格段に減らせます。

個人的な考えですが、キャラAIなんて単体でどれだけ複雑に作ってもしょせん条件分岐でしかありません。
限界はありますし、分岐が増えれば保守が難しくなってしまいます。
キャラごとに作りこんでいくのも大変です。

だからこそ、キャラ同士の連携や相互影響の見せ方が本当の意味でAIの動きにバリエーションをもたらすと考えています。
自分がゲームを遊ぶ際も、外環境の影響を受け反応するようなAIにこそ魅力を感じますね。

あと処理が共通で一つだけなら処理を作りこみやすいです。
多態性やらで実装が散らかるとひとつひとつを作りこんで保守するのは不可能ですが、たった一つならどれだけ複雑に作りこんでも保守可能です。
全キャラのAIの平均レベルも上がります。

## 5,メリット総括
JobAIの様々なメリットを語りましたが、今回のゲームAIにおいて最も重要なメリットは高速化ではないと考えています。

むしろJobSystemを導入する過程でのデータ構造の最適化、そして処理の共通化こそが本旨だと捉えています。
一応最初は高速なAIを作るのが目的でしたが、いったん組み終えた今の認識はそうなりました。

もちろん高速化には意味があります。
処理が速ければその分新しい機能を追加する余裕ができるからです。
しかし私がJobSystemとAIの親和性が高いと思う理由は速さではありません。

多態性の名のもとデータや実装が散らかりがちなAIの分野において、整理された構造を求めるならJobSystemを導入するのが都合がよいと感じたからです。

JobSystemのルールに従いAIを組むだけで、私が考える理想のゲームAIにかなり近づきます。

# 自作AIについて紹介
これまで紹介したJobSystemの特性やメリットを踏まえて、私がどのようなAIを実装したのかを紹介したいと思います。
まずはAIの概略からお話します。

## 1,AIの概略
概略については以下の図をご覧ください。
説明はするので流し見でOKです。

|<font color=""> **実装したJobAIの構造図** </font>|
|:-:|
|![alt](https://github.com/cushionA/ImageStrage/blob/main/JobSystemAI/%E6%9C%80%E7%B5%82/JobAI%E3%81%AE%E6%A7%8B%E9%80%A0%E5%9B%B3_%E4%BF%AE%E6%AD%A3.png?raw=true)|

図が表すAIの処理の流れを端的に書くと次のようになります。

- **シングルトン<sup>[注釈10](#注釈10)</sup>に保持された、共通のキャラデータがNativeContainer（UnsafeList）としてJobSystemに渡される。（オレンジのハコの部分）**
- **JobSystem内で全キャラのデータに関して同一の判断処理が行われる。（赤のハコの部分）**
- **判断結果が判断結果データとして出力される。（青のハコの部分）**
- **各キャラは判断結果を参照し、その通りに動く。（水色のハコの部分）**

以上の通り判断と行動を完全に切り離した構造になっています。
これがJobSystemを利用したAIの概略です。

## 2,AIの処理について工夫した部分
次はこのAIの処理について、工夫した部分を紹介します。
それはずばり条件判断部分です。

とはいえ、このAIの基本動作はそこまで複雑ではありません。

①ある条件に当てはまる時、対応する行動を行う。（条件判断）
②この行動の対象はターゲット検索用の条件によって選択される。（ターゲット判断）

これだけです。

|<font color=""> **AIの基本動作** </font>|
|:-:|
|![alt](https://github.com/cushionA/ImageStrage/blob/main/JobSystemAI/%E6%9C%80%E7%B5%82/AI%E3%81%AE%E5%9F%BA%E6%9C%AC%E5%8B%95%E4%BD%9C.png?raw=true)|

そしてこの条件判断とターゲット判断において行われている処理も大変シンプルです。

たとえば条件判断なら「HPが50%以下のキャラがいる時」というような条件を使います。
ターゲット判断では「最も高度が高い / 低い位置にいるキャラをターゲットに」というような条件を設定します。

**しかし、ここにフィルタリング機能を導入することで、様々な状況に対応できるAIを構築しました。**

フィルタリング機能について簡単に説明すると、キャラの状態、ステータスに応じて条件を設定し、条件を満たすキャラだけを抽出して判断の対象にする……という感じです。

<!-- open属性なし -->
<details><summary><font color=""><strong>フィルター処理コード(展開)</strong></font></summary>

```C#:TargetFilter.cs

    /// <summary>
    /// 行動条件や対象設定条件で検査対象をフィルターするための構造体
    /// Flags属性付きの列挙型でキャラ情報をビット演算して、条件に当てはまらなければ除外。
    /// 32Byte
    /// </summary>
    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public struct TargetFilter
    {
        /// <summary>
        /// 各フィルター条件がAnd判断するかどうかを判断するかをビットフラグで持つための列挙型。
        /// And判断では複数指定した全条件が当てはまる必要がある。
        /// 4バイトでフラグ六個分の設定を持てる。
        /// </summary>
        [Flags]
        private enum FilterBitFlag
        {
            特徴フィルター_And判断 = 1 << 0,
            特殊効果フィルター_And判断 = 1 << 1,
            行動状態フィルター_And判断 = 1 << 2,
            イベントフィルター_And判断 = 1 << 3,
            弱点属性フィルター_And判断 = 1 << 4,
            使用属性フィルター_And判断 = 1 << 5
        }

        /// <summary>
        /// 各フィルター条件のAND/OR判定を管理するビットフラグ
        /// </summary>
        [Header("フィルター判定方法")]
        [SerializeField]
        private FilterBitFlag _filterFlags;

        /// <summary>
        /// 対象の陣営区分
        /// 複数指定あり
        /// </summary>
        [Header("対象の陣営")]
        [SerializeField]
        private CharacterSide _targetType;

        /// <summary>
        /// 対象の特徴
        /// 複数指定あり
        /// </summary>
        [Header("対象の特徴")]
        [SerializeField]
        private CharacterFeature _targetFeature;

        /// <summary>
        /// 対象の状態（バフ、デバフ）
        /// 複数指定あり
        /// </summary>
        [Header("対象が持つ特殊効果")]
        [SerializeField]
        private SpecialEffect _targetEffect;

        /// <summary>
        /// 対象の状態（逃走、攻撃など）
        /// 複数指定あり
        /// </summary>
        [Header("対象の状態")]
        [SerializeField]
        private ActState _targetState;

        /// <summary>
        /// 対象のイベント状況（大ダメージを与えた、とか）でフィルタリング
        /// 複数指定あり
        /// </summary>
        [Header("対象のイベント")]
        [SerializeField]
        private BrainEventFlagType _targetEvent;

        /// <summary>
        /// 対象の弱点属性でフィルタリング
        /// 複数指定あり
        /// </summary>
        [Header("対象の弱点")]
        [SerializeField]
        private Element _targetWeakPoint;

        /// <summary>
        /// 対象が使う属性でフィルタリング
        /// 複数指定あり
        /// </summary>
        [Header("対象の使用属性")]
        [SerializeField]
        private Element _targetUseElement;

        /// <summary>
        /// 検査対象キャラクターの条件に当てはまるかをチェックする。
        /// </summary>
        /// <param name="solidData"></param>
        /// <param name="stateInfo"></param>
        /// <returns></returns>
        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
        public byte IsPassFilter(in SolidData solidData, in CharacterStateInfo stateInfo)
        {
            // すべての条件を2つのuint4にパック
            uint4 masks1 = new(
            (uint)this._targetFeature,
            (uint)this._targetEffect,
            (uint)this._targetEvent,
            (uint)this._targetType
            );

            uint4 values1 = new(
            (uint)solidData.feature,
            (uint)stateInfo.nowEffect,
            (uint)stateInfo.brainEvent,
            (uint)stateInfo.belong
            );

            uint4 masks2 = new(
            (uint)this._targetState,
            (uint)this._targetWeakPoint,
            (uint)this._targetUseElement,
            0u
            );

            uint4 values2 = new(
            (uint)stateInfo.actState,
            (uint)solidData.weakPoint,
            (uint)solidData.attackElement,
            0u
            );

            // FilterBitFlagからAND/OR判定タイプを取得
            bool4 checkTypes1 = new(
            (_filterFlags & FilterBitFlag.特徴フィルター_And判断) != 0,
            (_filterFlags & FilterBitFlag.特殊効果フィルター_And判断) != 0,
            (_filterFlags & FilterBitFlag.イベントフィルター_And判断) != 0,
            false // targetTypeは常にOR判定
            );

            bool4 checkTypes2 = new(
            (_filterFlags & FilterBitFlag.行動状態フィルター_And判断) != 0,
            (_filterFlags & FilterBitFlag.弱点属性フィルター_And判断) != 0,
            (_filterFlags & FilterBitFlag.使用属性フィルター_And判断) != 0,
            false
            );

            // SIMD演算
            uint4 and1 = masks1 & values1;
            uint4 and2 = masks2 & values2;

            // 条件判定
            bool4 pass1 = EvaluateConditions(masks1, and1, checkTypes1);
            bool4 pass2 = EvaluateConditions(masks2, and2, checkTypes2);

            // すべての条件がtrueかチェック
            return (byte)(math.all(pass1 & pass2) ? 1 : 0);
        }

        /// <summary>
        /// 条件評価をSIMDで実行するヘルパーメソッド
        /// </summary>
        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
        private static bool4 EvaluateConditions(uint4 masks, uint4 andResults, bool4 isAndCheck)
        {
            bool4 zeroMasks = masks == 0u;
            bool4 andConditions = andResults == masks;
            bool4 orConditions = andResults > 0u;

            // ビット演算で条件選択を実現
            // isAndCheck が true の場合は andConditions、false の場合は orConditions
            bool4 selectedConditions = (isAndCheck & andConditions) | (!isAndCheck & orConditions);

            return zeroMasks | selectedConditions;
        }
    }

```
</details>

この機能によりシンプルな判断フローでも、キャラ情報をビット演算で照会し、処理対象を絞り込むことで結構細かく条件を作れたりします。

**条件例：**「毒の状態異常で、火、または光属性が弱点の、大ダメージを受けた直後でHP50％以下の味方がいる時行動Aを実行」

## 3,データの管理について工夫した部分

JobSystemで使用するデータの管理方法について工夫した部分を二つ紹介します。

|<font color=""> **データ管理 = 図で言う以下の部分** </font>|
|:-:|
|![alt](https://github.com/cushionA/ImageStrage/blob/main/JobSystemAI/%E5%9B%B3/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB_%E3%83%95%E3%82%A9%E3%83%BC%E3%82%AB%E3%82%B9%E7%94%BB%E5%83%8F.png?raw=true)|

### 工夫1・ハッシュテーブル<sup>[注釈11](#注釈11)</sup>でデータをラップした。
私はUnsafeListでキャラのデータを管理するにあたって、そのキャラ情報リストにハッシュテーブルをかぶせた自作データ型を作りました。

[自作データ型のソース（Git）](https://github.com/cushionA/NewCave/blob/main/Assets/Script/CharacterController/AI/Collection/SoACharaDataDic.cs)

このハッシュテーブルはキーをint型に限定していて、**キー重複が起こらないことが保証されています**。
何故ならハッシュ値として利用されているのがキャラのGameObjectのハッシュ値だからです。

UnityのGameObject型の ```GetHashCode()``` は内部的には ```GetInstanceID()``` （ゲームオブジェクトごとに識別用の一意のIDを返すメソッド）と同じ値を返す処理です。
よってキーをこのハッシュ値に限定する限りキーは一意で、重複を考慮しない実装にできます。

[GetHashCode関数の内部実装のリンク](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnityEngineObject.bindings.cs#L258)

そしてこのハッシュテーブルが必要になった理由は以下の二点です。

- **キャラクターのデータを随時更新するため。**
- **```GetComponent()``` の撲滅のため。**

一つずつ説明します。

#### 目的①キャラクターのデータを随時更新する
こちらはキャラ情報を更新する必要がある時にデータにアクセスしたいということです。
（例：被弾した際、移動した際、バフ/デバフをもらった際など）

そんな時のために、ゲームオブジェクトのハッシュ値を使用したハッシュテーブルでデータを参照することができるようにしています。

#### 目的②GetComponent() の撲滅
```GetComponent()```とはUnityエンジンの機能で、他のゲームオブジェクトに紐づいたインスタンスを（いわゆるコンポーネント）を取得するための処理です。

この処理はゲームを作る際、いたるところで用いられる処理で、たとえば攻撃をヒットさせた際なども相手のHP管理機能にアクセスするためにGetComponentしたりします。
しかしこれが重いので、可能な限り実行したくないです。

ですがなにをするにせよゲームオブジェクトのコンポーネントを取得するという作業は必要になります。

そこで ```GetComponent()``` ではなく ```GetHashCode()``` でコンポーネントやキャラ情報にアクセスできるようにしたのがこのハッシュテーブルです。

```GetHashCode()``` で取得したInt型のハッシュ値をキーにして、いつでもキャラ情報やキャラクターコントローラーにアクセスできるようにしています。

これによりキャラデータへのアクセスがかなり高速化します。
以下は効果の目安を出すために ```GetComponent()``` と ```GetHashCode()``` と ```GetInstanceID()``` を5000個のオブジェクトに対して実行した結果の比較です。

<table>
  <caption>実行速度比較</caption>
  <thead>
    <tr>
      <th>処理名</th> <th>速度(ms)</th>
    </tr>
  </thead>
  <tr>
    <td>GetComponent()</td> <td>0.7272
    </td>
  </tr>
    <tr>
    <td>GetHashCode()</td> <td>0.01755
    </td>
  </tr>
    <tr>
    <td>GetInstanceID()</td> <td>0.02303
    </td>
  </tr>
</table>

見ての通り70倍くらい違います！
つまり70倍高速化できています。
しかも ```GetComponent()``` はGCも誘発します。
これをなくせるというだけでもハッシュテーブルを用意した意味は大きいです。

### 工夫2・データを一か所でまとめて管理した
先述の通り、JobSystemはアンマネージ領域、つまりスタックにデータを積む仕組みになっています。
ですからGCで自動的に整理されません。

つまり自分で管理する必要があり、その管理に無駄があるといろいろと本末転倒なことになります。
最初にデータ構造を工夫した意味がなくなります。
これを達成するために、私は以下のような工夫をしています。

<dl>
  <dt><strong>・データはシングルトンで一元管理</strong></dt>
  <dd><strong><font color="">これにより、他のインスタンスでデータが作られないことを保証する。</font></strong></dd>
    <dt><strong>・全データ分のメモリを一括確保</strong></dt>
  <dd><strong><font color="">データを連続で詰めることでキャッシュ効率アップ + 解放時にまとめて解放可能。</font></strong></dd>
    <dt><strong>・キャラ数の上限をあらかじめ決める</strong></dt>
  <dd><strong><font color="">最初に上限分だけメモリを確保することで、その範囲内ではメモリの再確保が不要になる。</font></strong></dd>
</dl>


# まとめ
以上で私が組んだJobSystemのAIの紹介を終わります。
作ってみた感想ですが、思ったよりも自分の思う理想のAIとの親和性が大きくて驚きました。
しかしもっともっと最適化の余地があると思うので、なにかご意見やご指摘、提案などありましたらコメントで教えてください。

# 注釈
<a id="注釈1">注釈1: アンマネージ領域</a>
.NETのガベージコレクション（GC）によって自動的に管理されないメモリ領域。
明示的にメモリの確保と解放を行う必要があるが、GCによる一時停止が発生しません。
UnityのJobSystemでは、NativeContainerを通じてこの領域にアクセスする。

<a id="注釈2">注釈2: データ指向設計</a>
オブジェクト指向設計とは異なり、データの配置とアクセスパターンを重視した設計手法。
オブジェクト単位でデータが集まっているオブジェクト指向に対し、機能単位でデータを集めてキャッシュ効率を向上しています。

<a id="注釈3">注釈3: NativeContainer</a>
UnityのJobSystemで使用される、アンマネージ領域にデータを格納するコンテナ。
NativeArray、NativeList、UnsafeListなど。
Jobから安全にアクセスできて、かつ高速です。

[関連検証の記事リンク](https://qiita.com/cushionA/items/ff31e6266815dc9258c7)

<a id="注釈4">注釈4: BurstCompiler</a>
UnityのC#コードを高性能なネイティブコードに変換するコンパイラ。

<a id="注釈5">注釈5: キャッシュライン</a>
CPUキャッシュがメモリから一度に読み込むデータの単位。一般的に64バイト。
この単位に収まるデータ構造にすることで、一度のメモリアクセスで必要なデータがすべてキャッシュに載り、キャッシュの効率が良くなります。

<a id="注釈6">注釈6: SOA（Structure of Arrays）</a>
データ構造の設計思想の一つです。
通常のAOS（Array of Structures）では[{x,y,z}, {x,y,z}, {x,y,z}]のように構造体の配列として連続データを格納します。
対して、SOAでは{[x,x,x], [y,y,y], [z,z,z]}のように、各要素ごとに分けた配列として格納します。
この設計を採用すると連続アクセスによりキャッシュ効率が向上します。

<a id="注釈7">注釈7: Span</a>
.NET Core 2.1で導入された、メモリ領域への軽量なビューを提供する値型。
配列の一部分を効率的に扱ったり、スタック領域のデータを配列のように操作できます。
さらにSpanは値型なので、ガベージコレクションを発生させません。
C#のunsafeコードには欠かせない存在。

<a id="注釈8">注釈8: スケジューリング</a>
JobSystemにおいて、作成されたJobを利用可能なワーカースレッドに割り当てる処理。
この処理自体にもコストがあり、処理対象のデータが少ない場合、スケジューリングコストが処理の恩恵を上回ることも。

<a id="注釈9">注釈9: 多態性</a>
オブジェクト指向プログラミングの特徴の一つ。
同じインターフェースを持つ異なるクラスのオブジェクトを、統一的に扱える仕組みです。
実行時の型判定や仮想関数呼び出しによりパフォーマンスが低下する場合がアリ。

<a id="注釈10">注釈10: シングルトン</a>
デザインパターンの一つで、アプリケーション全体を通して特定のクラスのインスタンスが唯一つしか存在しないことを保証する設計パターンです。
グローバルなデータ管理や、重複して作成されては困るリソースの管理に使用される傾向。

<a id="注釈11">注釈11: ハッシュテーブル</a>
キーと値のペアを格納するデータ構造。
ハッシュ関数を使ってキーから格納位置を計算し、理論的にO(1)の時間でデータにアクセス可能。
いわゆるDictionaryやHashSetで使われています。