UnityECSで都市開発シミュレーションゲームを作る【その1】~ECSの基礎と住民を作る~
はじめに
どうも、土鍋です。
昨年正式リリースとなったUnityECSをやってみたいと思ったのですが、色々記事を探しても実際にゲームを作っているものは少なかったので、実際のゲームを作りながら記事を書いていこうと思います。
こちらの内容はIwakenLab Tech Conference 2024にて発表予定です。ぜひお越しください。
iwakenlab-tech-conference-24.studio.site
IwakenLab Tech Conference 2024 - connpass
ECSとは
ECSとは「Entity Component System」の略です。
UnityではDOTS(Data-Oriented Technology Stack)と言ったりする。
昨年正式リリース
データ指向アーキテクチャを取り入れて 大量のオブジェクトを扱うような場面でのパフォーマンスの高さが売り
従来のUnityとの違い

従来のUnityでは一つのコンポーネントにデータもロジックも含まれていました。
しかし、ECSではデータとメソッドを分割して考えるようなアーキテクチャになっています。
SubSceneとBake

ECSはオブジェクト指向ではない分通常のようなUnityの考え方がしづらいです。
しかし、SubSceneというもので従来のUnityのヒエラルキー操作のように扱うことが可能です。
この内容がBakeされることでECSのロジックに最適化されます。
これらを行うことによって従来のUnityのランダムなメモリアクセスが改善され、整頓されたメモリアクセスが可能になり、処理が高速化されます。
都市開発シミュレーションゲーム
都市開発シミュレーションゲームとはその名の通り、都市開発をシミュレーションするゲームで、ユーザーは資金を用いて建物や道路を建設し、経済を回し、街を発展させていくのが目標です。
「Cities: Skylines」「SimCity」「A列車で行こう9」などが有名ですね。
このジャンル、はまると時間を無限に溶かすほど楽しいのでおすすめです。(通常のゲームと違って終わりが明確にないのでw)
ECSに向いていそう
ECSは大量のエンティティの処理に向いています。つまり、大量の住民をシミュレーションする都市開発シミュレーションはECSに超最適なのではないのかと思い立って今回やってみました。
自分もまだまだ勉強中なので変な処理が多分に含まれる可能性が高いので注意してください。
ECSの導入
プロジェクトはURP・HDRPのいずれかで作成する必要があります。
PackageManagerから以下のパッケージをインポートします。
- com.unity.entities
- com.unity.entities.graphics
シーンを作成したら、SubSceneを作成します。

ECSの書き方で住民を作ってみる
住民を作ると言ってもただ目的地に移動するだけのキャラを作るだけです。
とはいえ、ECSでやるとなると今までのUnityの書き方で書けないので一苦労です。
Component
住民のデータのstructです。
using Unity.Entities; using UnityEngine; namespace Citizen { [System.Serializable] public struct CitizenBase : IComponentData { public int pocketMoney; // 所持金(今回は使ってない) public float moveSpeed; // 移動スピード public Vector3 destination; // 目的地 } }
Baker
MonoBehaviourを継承しているのでインスペクターで設定可能です。
このBakeを行ってあげることで、SubSceneのオブジェクト群がECS最適化されます。
using Unity.Entities; using UnityEngine; namespace Citizen { public class CitizenAuthoring : MonoBehaviour { [SerializeField] private int _pocketMoney; [SerializeField] private float _moveSpeed; [SerializeField] private Vector3 _destination; class Baker : Baker<CitizenAuthoring> { public override void Bake(CitizenAuthoring src) { var data = new CitizenBase() { pocketMoney = src._pocketMoney, moveSpeed = src._moveSpeed, destination = src._destination }; AddComponent(GetEntity(TransformUsageFlags.Dynamic),data); } } } }
System
ECSの利点である大量のエンティティの高速処理のためにBurstCompilerとC#JobSystemを活用しています。書き方が通常と異なりすぎてあんまり理解できていませんが、相当高速になります。
using Unity.Burst; using Unity.Entities; using Unity.Transforms; using UnityEngine; namespace Citizen { public partial struct CitizenSystem : ISystem { [BurstCompile] // BurstCompilerによる高速化 public void OnUpdate(ref SystemState state) { // Jobの発行処理 var job = new CitizenUpdateJob() { Elapsed = (float)SystemAPI.Time.ElapsedTime }; // 初期化 job.ScheduleParallel(); // Jobの予約 } } [BurstCompile] // BurstCompilerによる高速化 partial struct CitizenUpdateJob : IJobEntity // C#JobSystemによるUpdateの書き換え(partial→SourceGeneratorとの共存) { public float Elapsed; // 経過時間 void Execute(in CitizenBase citizen, ref LocalTransform transform) // クエリ条件から合致するEntityを探して実行 { Move(citizen, ref transform); } void Move(CitizenBase citizen, ref LocalTransform transform) { // 現在位置から目的地に向かって一定のスピードで移動 transform.Position = Vector3.MoveTowards(transform.Position, citizen.destination, citizen.moveSpeed * Elapsed); // 目的地に到達したかどうかのチェック if (Vector3.Distance(transform.Position, citizen.destination) < 0.01f) { Debug.Log("目的地に到達しました!"); } } } }
UnityEditorで確認

移動するだけですがいったん完成!

参考文献
Unity公式E-Book
↓今回のコードほとんど以下を参考にしています。