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も回転
- サブシーンの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
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
- 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
- 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
- 一定時間ごとに親子関係を変更する
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;
- EntityCommandBuffer
- エンティティの変更を記録できる
- コンポーネントの追加と削除、エンティティの作成と破棄、コンポーネント値の設定など
- クエリの中身の構造が変わるとき
- EntityにComponentを追加、削除するとクエリの中身が変わってしまう(アーキタイプが変わるため)
- Entityの作成や破棄でもクエリの中身が変わる
- Playbackで実際に命令を実行
- ここでは ECB を使用することが最良
- 代わりに EntityManager.RemoveComponent() を呼び出すとDynamicBuffer が無効になるため、EntityManager.RemoveComponent() を呼び出すたびに DynamicBuffer を再取得する必要がある
- WithNoneで「そのComponentを持っていない」でマッチできる
- WithEntityAccessでクエリ結果にEntityを持たせることができる
EnableableComponents
Componentの有効化・無効化
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;
}
- EnableableなComponentの場合はEnabledRefRWにする
- ここではEnableなComponentもDisableなComponentも取得したいのでIgnoreComponentEnabledStateオプションをつける
- EnableStateが無視されるよって意味
GameObjectSync
- GameObjectのCubeとUIをEntityでコントロールする
Directory
Prefabと管理対象オブジェクト(ここではUI)を参照するGameObject
DirectoryManaged
Directoryの値をEntityに渡すためのManagedクラス
- IComponentDataを実装
DirectorySystem
Directoryの値をEntityに渡す
- シーンからDirectoryクラスを探し、値をDirectoryManagedにコピー
- 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に渡す
- DirectoryManagedからPrefabを受け取り、GameObjectをInstantiate
- DirectoryManagedはGetSingletonで取得
- 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
{
...
}
CustomTransforms
2D向けのTrasnsformシステムの実装
- よくわからない
- 後で追記する
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を有効/無効化する
- VALUE
System
- CubeSpawnSystem
- Cubeの生成
- 最初にURPMaterialPropertyBaseColorを持つEntityを削除してるのは何のため?
- コメントアウトしても特に変わらなかった
- InputSytem
- マウスのクリック位置を取得してRayを飛ばす
- RayがあたったらHit Componentの値を1Fだけ変化させる
- SetStateSystem
- マウスのHit判定が出たときSpin Componentを操作するJobをスケジュールする
- VALUEモード以外はちゃんとECBを使う
- マウスのHit判定が出たときSpin Componentを操作するJobをスケジュールする
ClosestTarget
- Job TutorialのStep4と同じ
- アルゴリズムによる最適化をしている
基本的なパターンは掴めたので、次はゲームサンプルを見ていきたい。