この記事は、KLab Engineer Advent Calendar 2024 の11日目の記事です。
こんにちは。KLabでエンジニアをしている @tsune2ne です。
去年Unity ECSが正式リリースされました。
これを機に学習していきます。
え?DOTSのリリースから何年経ってると思ってるんだ遅すぎるって?
いいんです。始めるのに遅すぎるということはないんです。
Unity ECSの経緯
課題
今まではGameObjectごとにMonoBehaviourクラスをAddComponentしてプログラミングすることが多いと思います。
これはオブジェクト指向プログラミングに近く、GameObjectのふるまいをC#クラスで表現できるので非常に理解しやすいです。
しかし、これをデータフローで見ると実は効率が悪いです。
データと処理をまとめて管理しいるため、横断的な操作をすると歯抜けになってしまいアクセス効率が落ちます。
解決策
オブジェクト指向ではデータとふるまいの紐づきが強すぎてメモリ配置を最適化できないと考え
データ(Entity)とふるまい(System)の設計を分離しようというのがECSの考えです。
これによりメモリアクセス速度の向上、マルチコアCPUの効率利用、シリアライズ・デシリアライズの高速化などのメリットがあります。
上図で緑のデータにアクセスするのにECSのほうが有利なことがわかるでしょう。
もちろん状況によりけりですが右のレイアウトにしやすいのがECSのメリットです。
はじめての Unity ECS - Entity Component System を使ってみよう!
ECSは以下の3つの要素に分割されます。
- Entity : データ、情報
- Component : 物体にデータを紐づける仕組み
- System : ふるまい、
UnityEditorでの見た目
UnityEditorのGUIでは現状のGameObjectにC#クラスをコンポーネントとして付属するのは直観的でわかりやすいです
ただデータ指向で効率化すると人間の感覚では直感的ではなくなります
Hierarchy上のGameObjectにC#クラスをAddComponentするというのは非常にわかりやすいです
そのため、ECSでもUnityEditorでは同じ感覚でAddComponentするようにできています
しかし実行するときにECS空間?変換する処理(Bake)を行います
サンプル実装
実行サンプルを作って比較してみましょう
雑に5000個のCubeを上下運動させます
実装したコードは下記です
https://github.com/tsune2ne/UnityECSSample
ECSのデータ定義
ECSコンポーネントごとに持つデータを定義します
データを持つだけで処理やふるまいは持ちません
public struct MoveRotate : IComponentData
{
public float Angle;
public float Speed;
public float3 BasePosition;
public bool IsInitialized;
}
GameObjectコンポーネントをEntityに変換
ECS空間?で持つためのEntityおよびEntityへの変換処理を記述します
起動時に自動で変換されます
public class MoveRotateAuthoring : MonoBehaviour
{
public float Angle;
public float Speed;
class RotateBaker : Baker<MoveRotateAuthoring>
{
public override void Bake(MoveRotateAuthoring authoring)
{
// Entityを生成
var entity = GetEntity(TransformUsageFlags.Dynamic); // LocalTransformアリ
AddComponent(entity, new MoveRotate
{
Angle = authoring.Angle,
Speed = authoring.Speed,
});
}
}
}
Baker処理とは?
UnityEditorで設定した値をECSコンポーネントに変換する処理のこと
ふるまいの定義
データのふるまいを記述します
これはHierarchy上にAddComponentしなくても実行されます
partialなのはC#のSource Generatorで補助的なコードを自動生成しているためです。
[BurstCompile]
public partial struct DancerSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
// MoveRotateとLocalTransformが付与されてるEntityを検索して処理
foreach (var (rotate, xform) in SystemAPI.Query<
RefRW<MoveRotate>, // MoveRotateを書き込み権限で取得
RefRW<LocalTransform>>()) // LocalTransformを書き込み権限で取得
{
// 初期化処理
if (!rotate.ValueRW.IsInitialized)
{
rotate.ValueRW.Speed = Random.Range(0.1f, 10f);
rotate.ValueRW.BasePosition = xform.ValueRO.Position;
rotate.ValueRW.IsInitialized = true;
}
// 移動処理
rotate.ValueRW.Angle += Time.deltaTime * rotate.ValueRW.Speed;
xform.ValueRW.Position.y = Mathf.Cos(rotate.ValueRW.Angle) + rotate.ValueRW.BasePosition.y;
}
}
}
上記はJobSystem対応されておらずECSの効果が発揮されないので対応します
[BurstCompile]
public partial struct MoveRotateSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// 初期化処理
foreach (var (rotate, xform) in SystemAPI.Query<
RefRW<MoveRotate>,
RefRW<LocalTransform>>())
{
if (!rotate.ValueRW.IsInitialized)
{
// UnityEngine.Randomはシングルスレッド限定なので先に決めてしまう
rotate.ValueRW.Speed = Random.Range(0.1f, 10f);
rotate.ValueRW.BasePosition = xform.ValueRO.Position;
rotate.ValueRW.IsInitialized = true;
}
}
// ジョブインスタンスを生成して並列実行予約
var ecb = GetEntityCommandBuffer(ref state);
new ProcessRotateMoveJob
{
DeltaTime = Time.deltaTime,
Ecb = ecb
}.ScheduleParallel();
}
EntityCommandBuffer.ParallelWriter GetEntityCommandBuffer(ref SystemState state)
{
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
return ecb.AsParallelWriter();
}
[BurstCompile]
public partial struct ProcessRotateMoveJob : IJobEntity
{
public float DeltaTime;
public EntityCommandBuffer.ParallelWriter Ecb;
// 引数のComponentを持つEntityを検索して実行される
void Execute([ChunkIndexInQuery] int chunkIndex, ref MoveRotate rotate, ref LocalTransform xform)
{
// 移動処理
rotate.Angle += DeltaTime * rotate.Speed;
xform.Position.y = Mathf.Cos(rotate.Angle) + rotate.BasePosition.y;
}
}
}
性能評価
Profilerを眺めて比較してみましょう
↓ECS+BurstCompiler+JobSystemで動かした場合のPlayerLoop
かなり減らせてますね!
※ただサンプル数1000個だと有意差は見られませんでした(なんで?)
※Windows + Unity2022.3.51f1 + Entities1.3.5で検証
緑色が今回作った移動処理です。それぞれのジョブで実行されてますね。
データ指向設計(DOD)の話
このオブジェクト指向ではなくデータ中心の考え方をデータ指向設計(DOD)と呼ばれています。
既存とは全く違う設計が必要になるため、公式で学習コースが用意されています。
気になる人はぜひ受講してみてください。
まとめ
雑なサンプルを作ってもプロファイラーに変化があったので、武器として知っておく価値は十分あります。
処理がボトルになっていて並列化で改善できる個所だけECS化するのが既存PJへの最適解です。
PJ全体でECS化すると性能面で大きなアドバンテージを得られますが
既存のオブジェクト指向からデータ指向へパラダイムシフトが必要になります。
開発メンバー全員への教育コストが必要です。
またオブジェクト指向設計は人数のスケールアップがしやすいのが利点であるため
データ指向設計で多人数開発をすると可読性やパッケージ化に新しいノウハウが必要になります。
そのコストを払ってまでECS化するかは難しい判断になりそうです。