【Unity】群衆処理ならECSは必須!超簡単に導入できる手順まとめ

2024年7月10日

どうも、だらはです。
今回は、群衆処理に優位なECSの導入方法をまとめました。
コピペで導入できるようにしたため参考にしていただけると嬉しいです!

スポンサーリンク

ECSとは?

言葉で説明するよりUnity公式の動画を見たほうがイメージが掴めると思います。
私もこの動画をみてやってみたい!と思えるようになりました。
先ずは動画を見ていただき、詳細は解説にて後述します。

ECSに必要なパッケージ

以下がECSに必要なパッケージになります。
Unity自体のバージョンは6を使用します。

  1. Burst:高速なコンパイラ
  2. Collections:ECSで使うCollection機能
  3. Entities:ECSの本体
  4. Entities Graphics:ECS graphicsの本体
  5. Mathematics:ECSで使うVectorやMatrix機能
  6. Unity Physics:ECSで使うColliderやPhysics機能
  7. Universal RP (High Definition RP):URPかHDRPのどちらかを使用

※6:Unity Physicsのインストールについて、本ページの内容では使用しませんが、ColliderPhysicsを使うには必須です。
Package Managerよりインストール後、Package Manager内からSamplesをダウンロードしてください。

手順

早速ですが、手順をまとめます。
今回は、動的にPrefab(Cube)を生成して移動させる処理を実装していきます。

  1. 前項の「ECSに必要なパッケージ」をインストールする。
  2. Hierarchyにて現在開いているSceneを右クリックし[New Empty Sub Scene]からSubSceneを作成する。
  3. SubSceneを右クリックし、[GameObject -> 3D Object -> Plane]から台地となるPlaneを作成する。
  4. SubSceneを右クリックし、[GameObject -> 3D Object -> Cube]から動的生成するCubeを作成する。
  5. Assetsを右クリックし、[Create -> Entities -> IComponentData Script]からComponentを作成してEnemyと命名する。(中身は後述)
  6. Assetsを右クリックし、[Create -> Entities -> Baker Script]からEntityを作成してEnemyAuthoringと命名する。(中身は後述)
  7. Assetsを右クリックし、[Create -> Entities -> ISystem Script]からSystemを作成してEnemySystemと命名する。(中身は後述)
  8. Cube[EnemyAuthoring.cs]スクリプトをアタッチして、PrefabCubeを、Speedに2を設定する。(下図1)

以上です。
実行すると下図2のようにGameビューでのみCubeが動的生成されて移動するのがみられると思います。

◆下図1

◆下図2

◆Enemy.csスクリプト

using Unity.Entities;
using Unity.Mathematics;

public struct Enemy : IComponentData
{
    public Entity Prefab;
    public float3 spawnPos;
    public float speed;
    public float timeMax;
}

◆EnemyAuthoring.csスクリプト

using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Tilemaps;

class EnemyAuthoring : MonoBehaviour
{
    public GameObject Prefab;
    [SerializeField] public float speed = 2.0f;
}

class EnemyAuthoringBaker : Baker<EnemyAuthoring>
{
    public override void Bake(EnemyAuthoring authoring)
    {
        Entity entity = GetEntity(TransformUsageFlags.None);
        AddComponent(entity, new Enemy
        {
            Prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic),
            speed = authoring.speed,
            spawnPos = new float3(0, 0.5f, 0),
            timeMax = 1f,
        });
    }
}

◆EnemySystem.csスクリプト

using Unity.Burst;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Collections;

partial struct EnemySystem : ISystem
{
    float timeCnt;
    
    EntityManager entityManager;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        entityManager = state.EntityManager;
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        float deltaTime = SystemAPI.Time.DeltaTime;
        timeCnt += deltaTime;

        //Entityを直接生成せず、Bafferを使用して生成する。
        EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.Temp);

        //Entityの位置を更新
        foreach (var (enemy, localTransform) in SystemAPI.Query<Enemy, RefRW<LocalTransform>>())
        {
            //時間経過で敵を生成
            if (timeCnt > enemy.timeMax)
            {
                timeCnt = 0;
                Entity newEntity = ecb.Instantiate(enemy.Prefab);
                ecb.SetComponent(newEntity, LocalTransform.FromPosition(enemy.spawnPos));
            }

            float3 move = new float3(1f, 0, 0) * enemy.speed * deltaTime;
            localTransform.ValueRW.Position += move;
        }

        // EntityCommandBufferの命令を実行
        ecb.Playback(state.EntityManager);
        ecb.Dispose();
    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {
        
    }
}

解説

ECSでの開発は、従来までのUnityの開発方法と全く異なる手法になります。
従来はGameObjectごとに処理を定義していたため、GameObject数が増えるとその分だけ処理負荷が増大していました。
このオブジェクト指向の欠点を改善したのがECSであり、データ指向型の手法と言われています。
従来手法との関係性は以下のようなイメージとなります。

  • Entity:GameObjectそのもの
  • Component:データ(位置、速度、重さなど)
  • System:処理(位置や速度を計算するメソッド)

つまり、モノ(Entity)とデータ(Component)と処理(System)を分けることで、再利用性を高め、処理効率も上げようという手法です。

class EnemyAuthoringBaker : Baker<EnemyAuthoring>
{
    public override void Bake(EnemyAuthoring authoring)
    {
        Entity entity = GetEntity(TransformUsageFlags.None);
        AddComponent(entity, new Enemy
        {
            Prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic),
            speed = authoring.speed,
            spawnPos = new float3(0, 0.5f, 0),
            timeMax = 1f,
        });
    }
}

上記処理では、BakeによりEntityを生成します。
Component(Enemy.cs)で定義した構造体データに初期値を定義します。
具体的な処理は、System(EnemySystem.cs)で定義します。

public void OnUpdate(ref SystemState state)
{
    float deltaTime = SystemAPI.Time.DeltaTime;
    timeCnt += deltaTime;

    //Entityを直接生成せず、Bafferを使用して生成する。
    EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.Temp);

    //Entityの位置を更新
    foreach (var (enemy, localTransform) in SystemAPI.Query<Enemy, RefRW<LocalTransform>>())
    {
        //時間経過で敵を生成
        if (timeCnt > enemy.timeMax)
        {
            timeCnt = 0;
            Entity newEntity = ecb.Instantiate(enemy.Prefab);
            ecb.SetComponent(newEntity, LocalTransform.FromPosition(enemy.spawnPos));
        }

        float3 move = new float3(1f, 0, 0) * enemy.speed * deltaTime;
        localTransform.ValueRW.Position += move;
    }

    // EntityCommandBufferの命令を実行
    ecb.Playback(state.EntityManager);
    ecb.Dispose();
}

上記System(EnemySystem.cs)では、Entityを時間経過で生成しています。
foreach内でEntityを動的生成させる場合は、Bafferを使用して生成します。(使わないとエラーになる)
float3は、ECS版のVector3です。
初期値を持つデータは、Component(Enemy.cs)に定義して使用します。
一時的に使用する変数であればSystem(EnemySystem.cs)で定義しても良いです。

最後に

いかがでしたでしょうか。
ECSでは、ColliderRigidbodyも従来とはアタッチするものが異なります。
ECSでの物理演算については以下の記事を参照ください。

覚えることが多く大変ですが、処理負荷が強烈に軽減できるのは大きな魅力です。
今後もECSについて記事にまとめていきますので読んでいただけると嬉しいです!

以上、だらはでした。

スポンサーリンク

基礎

Posted by daraha_gm