[Unity]Unity ECS v1.3.2を使ってゲームを作ってみました (3)―System編

[Unity]Unity ECS v1.3.2を使ってゲームを作ってみました (3)―System編

第二事業部 エンジニアの孫(ソン)と申します。
Unity ECS を実戦的に活用したゲーム開発の記事が少ないと感じたため、私の経験を共有します。
今回はSystemの書き方から全体管理までをまとめて紹介して完結させようと思います。
[Unity]Unity ECS v1.3.2を使ってゲームを作ってみました (2)―Component編の続きです。

実戦での書き方

Systemの書き方

Systemの組み方は、用途、参照型の有無、スレッドの制限によってさまざまですが、性能を重視する場合は
ISystem + IJobEntity + ScheduleParallel() + [BurstCompiler] を組み合わせて使用します。
ISystemから派生した struct System は、参照型を持たない処理に適しています。
IJobEntityはforeachを使って記述でき、Entity 間の依存関係がない処理を簡潔に書けるため、これを選びました。
ScheduleParallel()を使うことで、IJobEntity をワーカースレッドで並列に実行できます。
[BurstCompiler]を付けることで、最適化されたアセンブリコードが生成されます。

参照型を持つ場合は SystemBaseから派生した Systemを使用することになります。
また、値型と参照型が深く絡んでない場合は、値型の処理をメソッドなどで分割し、SystemBase内にIJobEntity 、ScheduleParallel()、 [BurstCompiler]を併用することも可能です。
以下の3つがSystemの書き方の例です。

敵の攻撃System

・敵が攻撃状態でプレイヤーと向き合っている場合、徐々にダメージを与える処理です。

・Jobの構成イメージは以下の通りです。

foreach (var プレイヤー in プレイヤーComponentArray)
{
   foreach (var 敵 in 敵ComponentArray)
   {
       // 敵パラメータの計算
       // プレイヤーにダメージ
   }
}

力まかせ法を使っていましたが、区間分割アルゴリズムと組み合わせると、処理速度がさらに向上します。

・System内でEntityQueryを使って敵のトランスフォーム配列を取得します。検索オプションにEnemyComponentを入れて敵を識別するようになっています。
以下が最小限のEntityQueryの作成方法と使用パターンのサンプルです。

// EntityQueryの作成
var queryBuilder = new EntityQueryBuilder(Allocator.Temp).WithAll<EnemyComponent>();
var enemyQuery = state.GetEntityQuery(queryBuilder);

// Job(IJobEntity)のEntityを絞り込む使用パータン
_job.ScheduleParallel(enemyQuery);

// データ配列に変換する使用パータン
var componentArray = enemyQuery.ToComponentDataArray<EnemyComponent>(Allocator.Temp);
foreach (var component in componentArray)
{
   // componentデータ変更
}

 

敵の攻撃で実際に使用した全体コードが以下になります。

プレイヤーの攻撃System

・プレイヤーが攻撃フラグを立てた際、向き合っている敵にダメージを与える処理です。

・System内に使っているEntityCommandBufferは、マルチスレッドのコンポーネント構造変更コマンドがメインスレッドに戻る際に実行される仕組みです。これは、Job内ではEntityやComonentの追加 / 削除 / 無効化ができないため、EntityCommandBufferを使ってメインスレッドで遅延実行しています。
以下が最小限のEntityCommandBufferが単一Systemで使用する際のサンプルです。

// EntityCommandBufferの作成
var ecb = new EntityCommandBuffer(Allocator.TempJob);
var ecbParallel = ecb.AsParallelWriter();


// Jobに渡す
_job.EcbParallel = ecbParallel;
// Job内でデータ構造変更
EcbParallel.SetComponentEnabled<HpComponent>(index, entity, false);


// コマンドの適用
ecb.Playback(state.EntityManager);
ecb.Dispose();

 

プレイヤーの攻撃で実際に使用した全体コードが以下になります。

リザルト判定System

・クリア:敵を全滅させる
・ゲームオーバー:プレイヤーのHPが0になる

・シーン上のGameObject(参照型)に対して処理を実行するため、SystemBaseを選びました。
また、コンポーネントの値変更を伴う計算処理がないため、IJobEntity 、ScheduleParallel()、 [BurstCompiler]は使用していません。

ゲーム全体の管理

AwakeにSystemとEntityを生成し、Updateで更新を実行しています。
Systemに[DisableAutoCreation]を使用することで、Unity ECSの自動生成とは別に、より読みやすく実行順番を管理しています。

このように、Unity ECSを使ってシンプルなゲームが書けました。

その他

インストール

Unity 6では、Package ManagerでEntitiesパッケージ名を検索するだけで簡単にインストールできるようになりました。
また、Assets→Create→Entitiesには空のスクリプトテンプレート(IComponentData Script、ISystem Script、IJobEntity Script、Baker Script)が用意されています。

ビルド

Unity ECSのビルドはEntities 0.5までは専用のビルドパッケージが必要でしたが、Entities 1.0以降は通常のビルド方法でビルドできるようになりました。

最後に

この記事を読んで少しでもUnity ECSに興味を持っていただけたら幸いです。
ぜひ、Unity ECSを使ってゲームを作成し、体験してみてください。

参考資料

EntityQuery
EntityCommandBuffer
Entities[1.3.2]Changelog
Unity ECSのKDTree区間分割アルゴリズム、Entity2万体
© Unity Technologies Japan/UCL