Open18

EntityComponentSystemSamples内のHelloCubeサンプルを見てみる

たかせたかせ

共通

  • ベイクすることでサブジーンのゲームオブジェクトからエンティティを読み込む
    • Baker<T>:サブシーン内のT(GameObject)ごとに1回Bakeが実行

    • ここではRotationSpeedコンポーネントをアタッチしている

      class Baker : Baker<RotationSpeedAuthoring>
      {
          public override void Bake(RotationSpeedAuthoring authoring)
          {
              // The entity will be moved
              var entity = GetEntity(TransformUsageFlags.Dynamic);
              AddComponent(entity, new RotationSpeed
              {
                  RadiansPerSecond = math.radians(authoring.DegreesPerSecond)
              });
          }
      }
      
      public struct RotationSpeed : IComponentData
      {
          public float RadiansPerSecond;
      }
      
    • コンポーネントをアタッチするとPreviewに追加される(AddComponentのコードをコメントアウトなどすると消えるのでコンパイル時に追加されてることがわかる)

  • ランタイムにロードされるのはゲームオブジェクトではなくエンティティ
たかせたかせ
  • Systemが生成される(OnCreateが呼ばれる)のはメインシーンがロードされる前

    • シーンのロード前に問答無用でOnCreateやOnUpdateが呼ばれるのを防ぎたい
    • RequereForUpdateを使えばExecuteMainThreadコンポーネントを持つエンティティが生成されるまでOnUpdateが呼ばれないようにできる
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<ExecuteMainThread>();
    }
    
  • Systemはシーンに依らず動くので、RequireForUpdateで場合分けすることで実際に動くSystemを切り替える

    • ExecuteAuthoringオブジェクトのインスペクタにチェックをいれると、それに応じたコンポーネントがアタッチされる
    • RequireForUpdateが対応したコンポーネントを受け取るとOnUpdateが動いて処理が走る
    class Baker : Baker<ExecuteAuthoring>
    {
        public override void Bake(ExecuteAuthoring authoring)
        {
            var entity = GetEntity(TransformUsageFlags.None);
    
            if (authoring.MainThread) AddComponent<ExecuteMainThread>(entity);
            if (authoring.IJobEntity) AddComponent<ExecuteIJobEntity>(entity);
            if (authoring.Aspects) AddComponent<ExecuteAspects>(entity);
            if (authoring.Prefabs) AddComponent<ExecutePrefabs>(entity);
            if (authoring.IJobChunk) AddComponent<ExecuteIJobChunk>(entity);
            if (authoring.GameObjectSync) AddComponent<ExecuteGameObjectSync>(entity);
            if (authoring.Reparenting) AddComponent<ExecuteReparenting>(entity);
            if (authoring.EnableableComponents) AddComponent<ExecuteEnableableComponents>(entity);
            if (authoring.CrossQuery) AddComponent<ExecuteCrossQuery>(entity);
            if (authoring.RandomSpawn) AddComponent<ExecuteRandomSpawn>(entity);
            if (authoring.FirstPersonController) AddComponent<ExecuteFirstPersonController>(entity);
            if (authoring.FixedTimestep) AddComponent<ExecuteFixedTimestep>(entity);
            if (authoring.StateChange) AddComponent<ExecuteStateChange>(entity);
            if (authoring.ClosestTarget) AddComponent<ExecuteClosestTarget>(entity);
        }
    }
    
たかせたかせ

MainThread

  • Cubeが回転する
    • 子オブジェクトのCubeも回転
      • ECSが勝手にやってくれてる
  • サブシーンのCubeがRotationSpeedAuthoringを持つ

// ISystemを実装
public partial struct RotationSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<ExecuteMainThread>();
    }

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

        // LocalTransform コンポーネントと RotationSpeed コンポーネントを持つすべてのエンティティをループします。
        foreach (var (transform, speed) in
                 SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
        {
            // ValueRW と ValueRO は両方とも、実際のコンポーネント値への参照を返します。
            // 違いは、ValueRW が読み取り/書き込みアクセスの安全性チェックを行う一方で、
            // ValueRO は読み取り専用アクセスの安全性チェックを行います。
            transform.ValueRW = transform.ValueRO.RotateY(
                speed.ValueRO.RadiansPerSecond * deltaTime);
        }
    }
}
  • ROは読み取り専用、RWは読み/書き
  • RefRWやRefROはコンポーネントのWrapperタイプ
  • ValueRWはref T型、ValueROはreadonly ref T型
    • ここで実際の参照を取得できる
  • 読み取りだけすればいいときはValueROでアクセスしたほうがいい
    • ValueRWで取得しても問題はないが…安全性のため?
  • SystemAPI.Time.DeltaTimeを使っているのはNetcodeに対応するため
たかせたかせ

IJobEntity

  • 回転に加えてY軸方向にスケールが変化
  • MainThreadサンプルに加えてY軸方向にスケールが変化する
  • Jobを使う

RotationSystem

[BurstCompile]
public void OnUpdate(ref SystemState state)
{
    var job = new RotateAndScaleJob
    {
        deltaTime = SystemAPI.Time.DeltaTime,
        elapsedTime = (float)SystemAPI.Time.ElapsedTime
    };
    job.Schedule();
}
  • job.Schedule()では実際には state.Dependency = job.Schedule(state.Dependency) が実行されている
    • このjobが他のjobに適切に依存するために必要

RotateAndScaleJob : IJobEnetity

public float deltaTime;
public float elapsedTime;

// Execute()のパラメータからクエリを作成
// ここで、クエリは LocalTransform、PostTransformMatrix、
// RotationSpeedコンポーネントを持つすべてのエンティティと一致
void Execute(ref LocalTransform transform, ref PostTransformMatrix postTransform, in RotationSpeed speed)
{
    transform = transform.RotateY(speed.RadiansPerSecond * deltaTime);
    postTransform.Value = float4x4.Scale(1, math.sin(elapsedTime), 1);
}
  • 3つのコンポーネントを持つエンティティと一致 = 同じアーキタイプのチャンクを指定する
  • PostTransformMatrixはスケールに関するコンポーネント
  • RotationSpeedはReadOnlyでいいのでrefではなくin
たかせたかせ

Aspects

https://www.youtube.com/watch?v=32TLgtA9yUM&t=703s

Aspectとは

  • 複数のComponentをまとめたもの
  • Queryを取得するときAspectが持つすべてのComponentを持っているEntityにマッチする
  • Entityが持つComponentの数が多くなってきたときに本領発揮する
// アスペクトを使用して立方体を回転
// クエリには、VerticalMovementAspect のすべてのコンポーネントが含まれます。
// コンポーネントとは異なり、SystemAPI.Query のアスペクト タイプ パラメータは
// RefRW または RefRO でラップされていないことに注意してください。
foreach (var movement in
         SystemAPI.Query<VerticalMovementAspect>())
{
    movement.Move(elapsedTime);
}
// このアスペクトではLocalTransform コンポーネントと RotationSpeed コンポーネントをラップ
readonly partial struct VerticalMovementAspect : IAspect
{
    readonly RefRW<LocalTransform> m_Transform;
    readonly RefRO<RotationSpeed> m_Speed;

    public void Move(double elapsedTime)
    {
        m_Transform.ValueRW.Position.y = (float)math.sin(elapsedTime * m_Speed.ValueRO.RadiansPerSecond);
    }
}
たかせたかせ

Prefabs

https://youtu.be/32TLgtA9yUM?t=847

  • Cubeが複数生成されて-y方向に移動する
  • y座標が0以下になったら削除
  • SubSceneにはSpawnerAuthoringを持つSpawnerオブジェクトが配置
    • これにプレハブを指定する

SpawnerAuthoring

Spawner ComponentをBake

class Baker : Baker<SpawnerAuthoring>
{
    public override void Bake(SpawnerAuthoring authoring)
    {
        var entity = GetEntity(TransformUsageFlags.None);
        AddComponent(entity, new Spawner
        {
            Prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic)
        });
    }
}

struct Spawner : IComponentData
{
    public Entity Prefab;
}
  • PrefabもGetEntityでEntityに変換する

SpawnSystem

Cube Entityを複数生成

[BurstCompile]
public void OnUpdate(ref SystemState state)
{
    // RotationSpeed Componentを持つすべてのEntityに一致するQueryを作成します。
    // (Queryはソース生成時にキャッシュされるため、更新のたびにQueryを再作成するコストは発生しません。)
    var spinningCubesQuery = SystemAPI.QueryBuilder().WithAll<RotationSpeed>().Build();

    // 現在Cubeが存在しない場合にのみCubeを生成します。
    if (spinningCubesQuery.IsEmpty)
    {
        // Spawnerを持つEntityは一つしか生成されないのでこれで取得
        var prefab = SystemAPI.GetSingleton<Spawner>().Prefab;

        // Entityをインスタンス化すると、同じComponentタイプと値を持つコピー Entityが作成されます。
        // instances配列の使用はOnUpdate内で完結するのでAllocator.Temp
        var instances = state.EntityManager.Instantiate(prefab, 500, Allocator.Temp);

        // new Random() とは異なり、CreateFromIndex() はランダム シードをハッシュします
        // 同様のシードが同様の結果を生成しないようにします。
        var random = Random.CreateFromIndex(updateCounter++);

        foreach (var entity in instances)
        {
            // Entityの LocalTransform Componentを新しい位置で更新します。
            var transform = SystemAPI.GetComponentRW<LocalTransform>(entity);
            transform.ValueRW.Position = (random.NextFloat3() - new float3(0.5f, 0, 0.5f)) * 20;
        }
    }
}

※GetSingletonで取得できるEntityが一つ以外だと例外を投げる

たかせたかせ

IJobChunk

https://www.youtube.com/watch?v=32TLgtA9yUM&t=1117s

  • IJobEntityではクエリでマッチしたEntityごとに一回Executeが呼ばれてた
  • IJobChunkではクエリでマッチしたChunkごとにExecuteが呼ばれる

RotationSystem

[BurstCompile]
public void OnUpdate(ref SystemState state)
{
    var spinningCubesQuery = SystemAPI.QueryBuilder().WithAll<RotationSpeed, LocalTransform>().Build();

    var job = new RotationJob
    {
        // Chunk内に格納されたComponent配列を扱うためにComponentTypeHandleが必要
        TransformTypeHandle = SystemAPI.GetComponentTypeHandle<LocalTransform>(),
        RotationSpeedTypeHandle = SystemAPI.GetComponentTypeHandle<RotationSpeed>(true), // ReadOnly = true
        DeltaTime = SystemAPI.Time.DeltaTime
    };

    state.Dependency = job.Schedule(spinningCubesQuery, state.Dependency);
}
  • IJobEntity とは異なり、IJobChunk には手動でクエリを渡す必要がある
  • job.Schedule()だけじゃだめ
    • IJobChunk は、state.Dependency JobHandle を暗黙的に渡したり割り当てたりしない
  • ComponentTypeHandleがすでにスケジュールされている別のジョブで使用されている場合、例外をスローする
    • 読み取り専用なら重複してもOKなので、できるだけReadOnlyをつける

RotationJob : IJobChunk

[BurstCompile]
struct RotationJob : IJobChunk
{
    public ComponentTypeHandle<LocalTransform> TransformTypeHandle;
    [ReadOnly] public ComponentTypeHandle<RotationSpeed> RotationSpeedTypeHandle;
    public float DeltaTime;
    
    public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
        in v128 chunkEnabledMask)
    {
        // 後で誰かがクエリまたはコンポーネントのタイプを変更した場合に備えて、このガード チェックを追加することをお勧めします。
        Assert.IsFalse(useEnabledMask);

        // Chunk内のComponentのNativeArrayを取得して扱う
        var transforms = chunk.GetNativeArray(ref TransformTypeHandle);
        var rotationSpeeds = chunk.GetNativeArray(ref RotationSpeedTypeHandle);
        for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++)
        {
            transforms[i] = transforms[i].RotateY(rotationSpeeds[i].RadiansPerSecond * DeltaTime);
        }
    }
}
  • useEnabledMaskはComponentが無効になる可能性があることを意味する
    • Chunk内の 1 つ以上のEntityに無効なクエリのComponentがある場合に true になる
たかせたかせ

Reparenting

Componentの追加、削除とEntityCommandBuffer

https://www.youtube.com/watch?v=32TLgtA9yUM&t=1253s

  • 一定時間ごとに親子関係を変更する

ReparentingSystem

// ※1
var ecb = new EntityCommandBuffer(Allocator.Temp);

if (attached)
{
    // 子から Parent コンポーネントを削除して、すべての子をRotatorから切り離します。
    DynamicBuffer<Child> children = SystemAPI.GetBuffer<Child>(rotatorEntity);
    for (int i = 0; i < children.Length; i++)
    {
        // ※2
        ecb.RemoveComponent<Parent>(children[i].Value);
    }
}
else
{
    // 親コンポーネントを立方体に追加して、すべての小さな立方体をローテーターにアタッチします。
    foreach (var (transform, entity) in
             SystemAPI.Query<RefRO<LocalTransform>>()
                 .WithNone<RotationSpeed>() // ※3
                 .WithEntityAccess())   // ※4
    {
        ecb.AddComponent(entity, new Parent { Value = rotatorEntity });
    }
}

// 実際に実行
ecb.Playback(state.EntityManager);

attached = !attached;
  1. EntityCommandBuffer
    • エンティティの変更を記録できる
    • コンポーネントの追加と削除、エンティティの作成と破棄、コンポーネント値の設定など
    • クエリの中身の構造が変わるとき
      • EntityにComponentを追加、削除するとクエリの中身が変わってしまう(アーキタイプが変わるため)
      • Entityの作成や破棄でもクエリの中身が変わる
    • Playbackで実際に命令を実行
  2. ここでは ECB を使用することが最良
    • 代わりに EntityManager.RemoveComponent() を呼び出すとDynamicBuffer が無効になるため、EntityManager.RemoveComponent() を呼び出すたびに DynamicBuffer を再取得する必要がある
  3. WithNoneで「そのComponentを持っていない」でマッチできる
  4. WithEntityAccessでクエリ結果にEntityを持たせることができる
たかせたかせ

EnableableComponents

Componentの有効化・無効化

https://youtu.be/32TLgtA9yUM?t=1488

RotationSpeed

// Bake内
SetComponentEnabled<RotationSpeed>(entity, authoring.StartEnabled);

// IEnableableComponentを追加
struct RotationSpeed : IComponentData, IEnableableComponent
{
    public float RadiansPerSecond;
}
  • クエリで弾かれる可能性がある

RotationSystem

foreach (var rotationSpeedEnabled in
         SystemAPI.Query<EnabledRefRW<RotationSpeed>>() //※1
         .WithOptions(EntityQueryOptions.IgnoreComponentEnabledState) // ※2
)
{
    rotationSpeedEnabled.ValueRW = !rotationSpeedEnabled.ValueRO;
}
  1. EnableableなComponentの場合はEnabledRefRWにする
  2. ここではEnableなComponentもDisableなComponentも取得したいのでIgnoreComponentEnabledStateオプションをつける
    • EnableStateが無視されるよって意味
たかせたかせ

GameObjectSync

https://www.youtube.com/watch?v=32TLgtA9yUM&t=1601s

  • GameObjectのCubeとUIをEntityでコントロールする

Directory

Prefabと管理対象オブジェクト(ここではUI)を参照するGameObject

DirectoryManaged

Directoryの値をEntityに渡すためのManagedクラス

  • IComponentDataを実装

DirectorySystem

Directoryの値をEntityに渡す

  1. シーンからDirectoryクラスを探し、値をDirectoryManagedにコピー
  2. Entityを生成してAddComponentDataの引数にDirectoryManagedを渡し、Entityに値を追加する

※ManagedなオブジェクトにアクセスするのでBurstコンパイルはできない

public void OnUpdate(ref SystemState state)
{
    // 一回だけ実行されるようにする
    state.Enabled = false;

    // Directoryクラスを探す
    var go = GameObject.Find("Directory");
    if (go == null)
    {
        throw new Exception("GameObject 'Directory' not found.");
    }

    var directory = go.GetComponent<Directory>();

    // DirectoryのすべてのフィールドをDirectoryManagedに格納
    var directoryManaged = new DirectoryManaged();
    directoryManaged.RotatorPrefab = directory.RotatorPrefab;
    directoryManaged.RotationToggle = directory.RotationToggle;

    // Entityを生成して値を追加
    var entity = state.EntityManager.CreateEntity();
    state.EntityManager.AddComponentData(entity, directoryManaged);
}

RotationInitSystem

GameObjectをInstantiateしてRotationSpeed Componentを持つEntityに渡す

  1. DirectoryManagedからPrefabを受け取り、GameObjectをInstantiate
    • DirectoryManagedはGetSingletonで取得
  2. RotatorGOを生成してGameObjectを格納し、RotatorGOをEntityにAddComponentする

※ManagedなオブジェクトにアクセスするのでBurstコンパイルはできない

foreach (var (goPrefab, entity) in
         SystemAPI.Query<RotationSpeed>()
             .WithNone<RotatorGO>()
             .WithEntityAccess())
{
    var go = GameObject.Instantiate(directory.RotatorPrefab);
		
		// イテレータの中でComponentを追加するのでECBを使う
    ecb.AddComponent(entity, new RotatorGO(go));
}

RotatorGO

生成したGameObjectを格納する

  • IComponentDataを実装

RotationSystem

DirectoryManagedのUIのToggleがOnのときGameObjectを回転させる

  • 正確に言うとEntityを回転させて、その値をGameObjectのTransformに代入する

※ManagedなオブジェクトにアクセスするのでBurstコンパイルはできない

public void OnUpdate(ref SystemState state)
{
		// Toggleの情報を取得
    var directory = SystemAPI.ManagedAPI.GetSingleton<DirectoryManaged>();
    if (!directory.RotationToggle.isOn)
    {
        return;
    }

    float deltaTime = SystemAPI.Time.DeltaTime;

		// LocalTransform、RotationSpeed、RotatorGOを持つEntityがマッチ
    foreach (var (transform, speed, go) in
             SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>, RotatorGO>())
    {
        transform.ValueRW = transform.ValueRO.RotateY(
            speed.ValueRO.RadiansPerSecond * deltaTime);

        // GameObjectとEntityのRotationを同期
        go.Value.transform.rotation = transform.ValueRO.Rotation;
    }
}
たかせたかせ

CrossQuery

Entity同士の距離による当たり判定の実装

2つのEntityを比較する

CollisionSystem

衝突判定をする

  • QueryからToComponentDataArrayでComponent、ToEntityArrayでEntityの配列がコピーされる
    • これを取得できちゃえば他Entityとの比較ができる
var boxQuery = SystemAPI.QueryBuilder()
                .WithAll<LocalTransform, DefaultColor, URPMaterialPropertyBaseColor>().Build();
      
// すべてのbox translationsとEntity ID の一時コピーを作成する必要がある
var boxTransforms = boxQuery.ToComponentDataArray<LocalTransform>(Allocator.Temp);
var boxEntities = boxQuery.ToEntityArray(Allocator.Temp);

foreach (var (transform, defaultColor, color, entity) in
         SystemAPI.Query<RefRO<LocalTransform>, RefRO<DefaultColor>,
                 RefRW<URPMaterialPropertyBaseColor>>()
             .WithEntityAccess())
{
    // ボックスの色をデフォルトにリセットします
    color.ValueRW.Value = defaultColor.ValueRO.Value;

    // このボックスが別のボックスと交差する場合は色を変更します
    for (int i = 0; i < boxTransforms.Length; i++)
    {
        var otherEnt = boxEntities[i];
        var otherTrans = boxTransforms[i];

        // ボックスはそれ自体と交差してはいけないため、他のエンティティの ID が現在のエンティティの ID と一致するかどうかを確認します。
        if (entity != otherEnt && math.distancesq(transform.ValueRO.Position, otherTrans.Position) < 1)
        {
            color.ValueRW.Value.y = 0.5f; // set green channel
            break;
        }
    }
}

IJobChunkを使う方法

  • チャンクごとに、チャンク上のComponentを配列で取得してIndexで指定してあげればEntityとComponentの一時コピーの作成を回避できる
    • chunk.GetNativeArrayで配列を取得
たかせたかせ

RandomSpawn

円形状にランダムにCubeをスポーン

すべてのCubeに対してランダムシード値を設定する

SpawnSystem

// 前のフレームで生成されたエンティティから NewSpawn タグ コンポーネントを削除します。
var newSpawnQuery = SystemAPI.QueryBuilder().WithAll<NewSpawn>().Build();
state.EntityManager.RemoveComponent<NewSpawn>(newSpawnQuery);

// Spawn the boxes
var prefab = SystemAPI.GetSingleton<Config>().Prefab;
state.EntityManager.Instantiate(prefab, count, Allocator.Temp);

// スポーンされたすべてのボックスには一意のシードが必要なので、
// seedOffset はフレームごとにボックスの数だけ増加する必要があります。
seedOffset += count;

new RandomPositionJob
{
    SeedOffset = seedOffset
}.ScheduleParallel();
[WithAll(typeof(NewSpawn))]
[BurstCompile]
partial struct RandomPositionJob : IJobEntity
{
    ...
}
  • WithAllでtypeofの後に続くComponentを持つEntity全てに対してExecuteする
たかせたかせ

FirstPersonController

FPSカメラの実装

CameraSystem

  • structの宣言に[UpdateAfter(typeof(System))]がついていた

    • 実行順序を変更できる
    • InputSystem→ControllerSystem→CameraSystemの順に指定されていた
      • 実行順を入力→移動量計算→カメラ移動にしたいから
  • GetComponentLookup

    • 指定したEntityが持つComponentを取得できる
    // EntityをキーとするLocalTransform Componentのコンテナ
    var transformLookup = SystemAPI.GetComponentLookup<LocalTransform>(true);
    
    // Entityを指定すると、そのEntityのComponentを取得できる
    var controllerTransform = transformLookup[controllerEntity];
    
  • Cameraに関してはUnityEngineのTransformを使う必要があるのでBurstCompileは無し

たかせたかせ

FixedRateSpawnerAuthoring

FixedUpdate的に動かす

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
public partial struct FixedRateSpawnerSystem : ISystem
{
	...
}
たかせたかせ

StateChange

Componentの状態を3つの方法で管理する

  • Componentの中の値
  • Component自体の有効/無効化
  • Componentの追加/削除

クリックした位置から円形状に色が変わる&色が変わったCubeは回転する

Components

  • Config : IComponentData
    • シーンの設定を保持
  • Hit : IComponentData
    • クリック時のヒットデータ
  • Spin : IComponentData, IEnableableComponent
    • 回転
  • enum Mode
    • VALUE
      • SpinのIsSpinningが変わる
    • STRUCTURAL_CHANGE
      • Spin Component自体を付けたり外したりする
    • Enableable_component
      • Spin Componentを有効/無効化する

System

  • CubeSpawnSystem
    • Cubeの生成
    • 最初にURPMaterialPropertyBaseColorを持つEntityを削除してるのは何のため?
      • コメントアウトしても特に変わらなかった
  • InputSytem
    • マウスのクリック位置を取得してRayを飛ばす
    • RayがあたったらHit Componentの値を1Fだけ変化させる
  • SetStateSystem
    • マウスのHit判定が出たときSpin Componentを操作するJobをスケジュールする
      • VALUEモード以外はちゃんとECBを使う
たかせたかせ

ClosestTarget

  • Job TutorialのStep4と同じ
  • アルゴリズムによる最適化をしている

たかせたかせ

基本的なパターンは掴めたので、次はゲームサンプルを見ていきたい。