Unity 6 + ECS 1.3 で Boids シミュレーションを書き直してみた
はじめに
だいぶ前、2018 年の終わりごろに Boids(群体)シミュレーションを ECS の勉強のために書きました。
上記記事では当時の API を使っており、Unity 側では ECS の実装もまだ色々と検証中だったように思われます。最近は全く追っていませんでしたがあれから 6 年ほど経過し、API 体系も円熟したと思われるため、あらためて現状の実装の調査も兼ねて書き直してみることにしました。以前の実装と比較しながら色々と考察もしていければと思います。
Boids 自体の実装解説については以前の記事をご参照ください。本記事では「ECS のセットアップ」の項以降の内容について、新しい ECS の仕組みの概念獲得のためにあれこれ試行錯誤していく形式で展開していきます。前回はチュートリアル形式でしたが、今回は一つ一つ意味を確認しながら見ていきますので、結構冗長な感じの記事になっています。
目次
- はじめに
- 目次
- 環境
- デモ
- コード
- ECS の正式版リリースについて
- ECS パッケージ
- エンティティ・コンポーネント・システムの実装と調査
- Boids の実装をしていく
- 近傍検索
- Boids のアルゴリズムの実装
- パラメタのリアルタイム更新
- エディタのデバッグ描画
- 中心座標の移動
- その他
- 近傍探索のパフォーマンス改善
- Burst 化
- Job 化
- 近傍探索とバッファへの格納
- 一時バッファの削除
- パフォーマンス
- おわりに
環境
デモ
最終的には Job + Burst で 20000 個体ほど出してもフレームレートに影響しない形になりました。
コード
ECS の正式版リリースについて
ECS は Unity 2022.2 のタイミングより ECS の正式版(バージョン 1.0)をリリースしました。
公式で専用のページも作られています。
なんか日本語訳が怪しい(製品名であるはずの Unity まで自動翻訳してしまっている...)ので、英語版を別途翻訳して見るほうが良いかもしれません。。。
公式のサンプルは GitHub に上がっています。
日本語としては Keijiro さんが UTJ 日本語オフィシャル YouTube チャンネルにていくつか解説を上げてくださっています。
こちらの動画で紹介されている Keijiro さんのサンプルもあります。
その他公式で様々なリソースが用意されています。
ECS パッケージ
ECS 関連のパッケージを Package Manager から入れる必要があります。
詳しくは後述しますが、以下の 2 つのパッケージを入れる必要があります。
- Entities
- ECS のコア部分を提供
- Entities Graphics
- ECS でオブジェクト描画する部分を提供
エンティティ・コンポーネント・システムの実装と調査
以前のセットアップ
さて、以前の記事では以下のようにマネージャ経由でエンティティを生成し、コンポーネントを付与する形でセットアップしていました。
// Entity マネージャの取得
var manager = World.Active.GetOrCreateManager<EntityManager>();
// アーキタイプの作成
var archetype = manager.CreateArchetype(typeof(Hoge), ...);
// エンティティの生成とコンポーネントの初期化
for (int i = 0; i < n; ++i)
{
var entity = manager.CreateEntity(archetype);
manager.SetComponentData(entity, new Hoge { Value = random.NextFloat3(1f) });
...
}
ただ、シンプルなセットアップであればこれで良いですが、位置や回転がバラバラだったり、各オブジェクトが異なるパラメタセットを持っていたり、といったケースにおいてはセットアップが大変です。
SubScene
そこで Unity 2020 あたりのタイミングから SubScene という仕組みが導入されました。
SubScene では通常のシーンのように GameObject と MonoBehaviour のコンポーネントのセットアップによりシーン構築を行います。するとこれらが自動的に ECS の世界へと変換される、という仕組みになっています。具体的に見ていきましょう。
適当な SubScene を作成、配置し、その中に適当なマテリアルを適用したオブジェクトを配置してみます。メインシーンにて SubScene の右端にチェックボックスがあるのでそれにチェックを入れてみると、次のように Inspector の下でどのようなコンポーネントが自動セットアップされるかが出てきます。
ECS の構造は通常の Unity のそれとは異なるのでシーンは特殊な構造で扱われます。この SubScene はメインシーン上では GameObject と SubScene という MonoBehaviour 継承コンポーネントとして配置されているようです。
SubScene コンポーネントはシーンファイルである .unity を保持し、ロード・アンロードの役割を行うもののようですね。 このあたりの詳しい流れは先の UTJ の動画に詳しくまとめられているのでそちらをご参照ください。
ちなみに、仕組みとしては以前も Pure ECS / Hybrid ECS という形の区分けがされており、後者は GameObject ベースで ECS をセットアップするものでした。SubScene の導入により、これらが洗練されたようです。
エンティティとコンポーネントと Baker
さて、ではこの SubScene でセットアップしたオブジェクトにコンポーネントを付与してみます。コンポーネントの付与は Baker という仕組みを通じて行います。
Baker は GameObject をエンティティへと変換するものです。次のようなコードを書いてみます。
public struct Velocity : IComponentData
{
public float3 Value;
}
public struct Acceleration : IComponentData
{
public float3 Value;
}
public class FishAuthoring : MonoBehaviour
{
public float Speed = 1f;
}
public class FishBaker : Baker<FishAuthoring>
{
public override void Bake(FishAuthoring src)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new Velocity()
{
Value = UnityEngine.Random.insideUnitSphere * src.Speed
});
AddComponent(entity, new Acceleration()
{
Value = 0f
});
}
}
IComponentData
継承の Velocity
および Acceleration
が ECS コンポーネントです。
ここに値を受け渡すための FishAuthoring
はオーサリングコンポーネントと呼ばれる役割のもので、シリアライズされたエディタ上で使用できる変数を有し、ECS コンポーネントへ値を受け渡す役割をします。SubScene のゲームオブジェクトにはこちらを付与する形になります。
そして FishBaker
は Baker<T>
継承したクラスであり、この中の Bake
関数が自動実行されます。FishAuthoring
オーサリングコンポーネントを付与した GameObject に対してはこの Baker が処理され、AddComponent
した ECS コンポーネントが自動付与されることになります。Bake
関数の中を覗いてみると、GetEntity()
でまずエンティティを取得しています。引数としては TransformUpdateFlag
を与えており、これは GameObject の Transform コンポーネントがどのような ECS コンポーネントへと変換されるべきかを指定するものとなっています。
固定オブジェクトのような場合は不要な ECS コンポーネントの付与を避けることができる訳ですね。Dynamic
を指定した場合は LocalTransform
と LocalToWorld
が付与され、移動可能になります。
ではこの FishAuthoring
を GameObject に付与してインスペクタを見てみます:
このようにセットアップした ECS コンポーネント及び GetEntity
で指定した TransformUpdateFlag
に応じたコンポーネントが付与されている事がわかりました。またセットアップした値が各 ECS コンポーネントに設定されていますね。なお、この Bake 処理は様々なタイミング(サブシーンの開閉や保存など)で行われるようで、そのため予期しないバグを避けるため Baker はステートレスにしないといけないようです。
ちなみに MeshRenderer
のチェックを外すといくつかの Unity.Rendering.*
系の ECS コンポーネントが外れるのがわかります。また、作成した FishAuthoring
も外すと何もなくなりますね。
これらから、GetEntity()
したり、対応標準 MonoBehavior コンポーネント(MeshRenderer
など)をつけると自動で色々とセットアップされることがわかります。こうしたエンティティについた複数のコンポーネントの組み合わせ(アーキタイプ)が暗黙的に行われる Bake 処理によって決定されています。
ちなみに、Inspector の右上にある◯から Runtime を選択すると、Inspector 上でどのようなコンポーネントへと変換されたかがわかります。Mixed にすると実行時のみ見えるようになります。
システム
さて、ではこの ECS コンポーネントを見て動かしてみましょう。ECS ではデータとしてのコンポーネントをシステムによって処理します。システムは以前はコンポーネントのデータを取ってくるのに専用の構造体を用意したりと色々と手間が要りました。以前のコードを以下に示してみます。
public class MoveSystem : ComponentSystem
{
struct Data
{
public readonly int Length;
public ComponentDataArray<Position> positions;
[
Source: View source