はじめに
DOTSを初めて触る方で、前編をまだ読んでないならそちらを先に読むことをお勧めします。後編では、
- MonoBehaviourによる従来通りの実装
- Systemを利用した実装
- Systemの中でJobを利用した実装
以上の3つをただ1万個出したGameObject(Entity)を回転させたのみの簡単なプログラムでどうなるか?比較したいと思います。モバイル端末だとデフォルトのフレームレートが30なため60に設定しています。また実機で再現された問題も紹介したいと思います。
開発環境
ソフトウェア
- Unity 2022.3.10f1
- Xcode 15.0
- Entities v1.0.16
- Entities Graphics v1.0.16
ハードウェア
- Windows
- Intel Core i7-10870H
- GeForce RTX 3060 Laptop
- メモリ 32GB
- MacBook Pro
- Intel Core i5
- Intel Iris Plus Graphics 645
- メモリ 16GB
- Android
- Pixel6 (Android v14)
- iPhone
- iPhone12 Pro Max (iOS v16.5.1)
検証で利用するデータ
3つの比較実装の共通で利用している検証データです。
"prefabScale"は
、SystemでEntityをインスタンス化する時に、PrefabのルートのScaleが反映されず強制的に”1″になってしまうため使用しています。
`rotateSpeed`はZ軸を中心に回転させるのに使用している値です。GameObjectとEntityで同じ値を入れているのですが、GameObjectでは遅すぎて動いているのか分からなかったです。
- GameObject : rotateSpeed = 2
- Entity : rotateSpeed = 0.02
そのため今回の検証では動いていることが分かれば良いため、以上のように値を設定しています。
他の値は共通で以下の用に設定しています。
- lineMax : 100
- columnMax : 100
- interval : 0.5
- zPos : 50
- prefabScale : 0.1
/// <summary>
/// 比較デモ共通で利用するデータ
/// </summary>
[Serializable]
public struct DemoComparisonCommonData
{
/// <summary>
/// 1行の最大値
/// </summary>
public int lineMax;
/// <summary>
/// 1列の最大値
/// </summary>
public int columnMax;
/// <summary>
/// 間隔
/// </summary>
public float interval;
/// <summary>
/// 奥行
/// </summary>
public float zPos;
/// <summary>
/// Prefabの大きさ
/// </summary>
public float prefabScale;
/// <summary>
/// 回転速度
/// </summary>
public float rotateSpeed;
}
Prefab
Prefabはこんな感じのプロペラを作って回転させています。

MonoBehaviourによる従来通りの実装
以下検証に使用したコードです。
/// <summary>
/// 従来の実装方法による比較デモのマネージャー
/// </summary>
public class ComDemoMonoManager : MonoBehaviour
{
/// <summary>
/// 回転させるGameObject
/// </summary>
[SerializeField]
private GameObject _prefab;
/// <summary>
/// 比較デモ共通で利用するデータ
/// </summary>
public DemoComparisonCommonData commonData;
/// <summary>
/// インスタンス化したゲームオブジェクト群
/// </summary>
private List<GameObject> _gameObjects = new List<GameObject>();
void Start()
{
//GameObject生成
var interval = commonData.interval;
var initXPos = ((float)commonData.lineMax / 2) * interval * -1f;
var initYPos = ((float)commonData.columnMax / 2) * interval;
for (int y=0; y < commonData.columnMax; y++)
{
var yPos = initYPos - (y * interval);
for (int x = 0; x < commonData.lineMax; x++)
{
var xPos = initXPos + (x * interval);
var position = new Vector3(xPos, yPos, commonData.zPos);
var go = Instantiate(_prefab,position,Quaternion.identity,transform);
_gameObjects.Add(go);
}
}
}
void Update()
{
foreach (var go in _gameObjects)
{
go.transform.Rotate(0f,0f,commonData.rotateSpeed);
}
}
}//DemoMonoManager End
Windows
MacBook Pro
Pixel6
iPhone12 Pro Max
Systemを利用した実装
以下検証に使用したコードです。オーサリングに関しては後述のJobと同じコードを使用しています。
/// <summary>
/// 比較実装共通で利用するオーサリング
/// </summary>
public class DemoComparisonAuthoring : MonoBehaviour
{
/// <summary>
/// 回転させるGameObject
/// </summary>
public GameObject prefab;
/// <summary>
/// 比較デモ共通で利用するデータ
/// </summary>
public DemoComparisonCommonData commonData;
class Baker : Baker<DemoComparisonAuthoring>
{
public override void Bake(DemoComparisonAuthoring authoring)
{
//自身のEntityの取得
var entity = GetEntity(TransformUsageFlags.None);
var prefabEntity = GetEntity(authoring.prefab, TransformUsageFlags.Dynamic);
authoring.commonData.prefabScale = authoring.prefab.transform.localScale.x;
AddComponent(entity, new ComparisonAuthoringData
{
entity = prefabEntity,
commonData = authoring.commonData
});
}
}
}//DemoComparisonAuthoring End
/// <summary>
/// 比較デモでオーサリングしたデータ
/// </summary>
public struct ComparisonAuthoringData : IComponentData
{
public Entity entity;
public DemoComparisonCommonData commonData;
}
/// <summary>
/// インスタンス化したEntityの判別用データ
/// </summary>
public struct PrefabObjectData : IComponentData {}
/// <summary>
/// Systemの実装による比較デモの初期化システム
/// </summary>
[BurstCompile]
public partial struct ComDemoInitSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExecuteDemoSystem>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
state.Enabled = false;
//ECB取得
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
//共通データ取得
var comparisonData = SystemAPI.GetSingleton<ComparisonAuthoringData>();
var commonData = comparisonData.commonData;
//Entityのインスタンス化
var instanceCount = commonData.columnMax * commonData.lineMax;
var xCounter = 0;
var yCounter = 0;
var interval = commonData.interval;
var initXPos = ((float)commonData.lineMax / 2) * interval * -1f;
var initYPos = ((float)commonData.columnMax / 2) * interval;
using var entities = state.EntityManager.Instantiate(comparisonData.entity,
instanceCount,
Allocator.Temp);
foreach (var entity in entities)
{
var yPos = initYPos - (yCounter * interval);
var xPos = initXPos + (xCounter * interval);
var transform = SystemAPI.GetComponentRW<LocalTransform>(entity);
transform.ValueRW.Position = new float3(xPos, yPos, commonData.zPos);
transform.ValueRW.Scale = commonData.prefabScale;
ecb.AddComponent<PrefabObjectData>(entity);
xCounter++;
if (xCounter == commonData.columnMax)
{
yCounter++;
xCounter = 0;
}
}
}
}//ComDemoInitSystem End
/// <summary>
/// Systemの実装による比較デモ
/// </summary>
[BurstCompile]
public partial struct ComDemoSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExecuteDemoSystem>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
//共通データ取得
var comparisonData = SystemAPI.GetSingleton<ComparisonAuthoringData>();
var commonData = comparisonData.commonData;
foreach (var (objectData,transform)
in SystemAPI.Query<RefRW<PrefabObjectData>,RefRW<LocalTransform>>())
{
transform.ValueRW = transform.ValueRW.RotateZ(commonData.rotateSpeed);
}
}
}//ComDemoSystem End
Windows
MacBook Pro
Pixel6
iPhone12 Pro Max
Systemの中でJobを利用した実装
以下検証に使用したコードです。
/// <summary>
/// JobSystemの実装による初期化システム
/// </summary>
[BurstCompile]
public partial struct ComDemoInitJobSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExecuteDemoJobSystem>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
state.Enabled = false;
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
new ProcessSpawnerJob()
{
ecb = ecb
}.Schedule();
}
}//ComDemoInitJobSystem End
/// <summary>
/// Entityを生成するJob
/// </summary>
[BurstCompile]
public partial struct ProcessSpawnerJob : IJobEntity
{
public EntityCommandBuffer ecb;
private void Execute(in ComparisonAuthoringData authoringData)
{
var commonData = authoringData.commonData;
var interval = commonData.interval;
var initXPos = ((float)commonData.lineMax / 2) * interval * -1f;
var initYPos = ((float)commonData.columnMax / 2) * interval;
for (int y=0; y < commonData.columnMax; y++)
{
var yPos = initYPos - (y * interval);
for (int x = 0; x < commonData.lineMax; x++)
{
var xPos = initXPos + (x * interval);
var position = new float3(xPos, yPos, commonData.zPos);
var newEntity = ecb.Instantiate(authoringData.entity);
var lt = LocalTransform.FromPositionRotationScale(position,
Quaternion.identity,
commonData.prefabScale);
ecb.SetComponent(newEntity,lt);
ecb.AddComponent<PrefabObjectData>(newEntity);
}
}
}
}//ProcessSpawnerJob End
/// <summary>
/// JobSystemの実装による比較デモ
/// </summary>
[BurstCompile]
public partial struct ComDemoJobSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExecuteDemoJobSystem>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
//共通データ取得
var comparisonData = SystemAPI.GetSingleton<ComparisonAuthoringData>();
var commonData = comparisonData.commonData;
new RotateJob()
{
rotateSpeed = commonData.rotateSpeed
}.ScheduleParallel();
}
}//DemoJobSystem End
/// <summary>
/// Entityを回転させるJob
/// </summary>
[BurstCompile]
public partial struct RotateJob : IJobEntity
{
public float rotateSpeed;
void Execute(ref LocalTransform transform, in PrefabObjectData objectData)
{
transform = transform.RotateZ(rotateSpeed);
}
}
Windows
MacBook Pro
Pixel6
iPhone12 Pro Max
Android実機でEntityが描画されない
動画を見てもらうと分かるように、何故かAndroidだと1万個のEntityが描画されないです。しかしズームしてカメラの描画範囲を狭めると描画されるので、存在はしているみたいです。何個なら描画できるか検証したところ、”3858個”までなら何故か描画できました。以下の参考にしたフォーラムではVulkanでないと描画できないみたいな事が書かれていましたが関係無かったです。
- https://forum.unity.com/threads/entities-not-rendering-on-android.1426641/
- https://issuetracker.unity3d.com/issues/opengl-android-entities-entities-are-not-displayed-in-a-build-when-using-opengl-graphics-api
iPhone実機でアプリ起動直後に落ちる
下記スクショのように、アプリ起動直後にメモリアクセスエラーでアプリが落ちることがありました。

下記のフォーラムを参考にすると、どうやらバージョンの組み合わせによって成功するみたいです。
- https://forum.unity.com/threads/xcode-15-conflict-with-unity-collections-package-jobs-package-exc_bad_access-code-257-address-0x.1450363/
- https://forum.unity.com/threads/launching-ecssamples-on-ios-crash-immediately.1135855/
筆者が試した時は、
- Xcode : beta 15.0 -> 15.0
- Unity : 2022.3.4f1 -> 2022.3.10f1
以上のようにアップデートしたら、アプリが落ちずに動くようになりました。
まとめ
各種FPSをまとめると以下のようになりました。ズームせずに全体表示している時の値です。
ハード\実装方法 | MonoBehaviour | System | System × Job |
Windows | 15~20 | 60 | 60 |
MacBook Pro | 6~9 | 12~16 | 12~16 |
Pixel6 | 10~12 | 18 | 18 |
iPhone12 Pro Max | 27 | 37 | 37 |
どのハードでもECSに置き換えるとパフォーマンスが向上されることが分かりました。ただ、Job化しても変化が見られなかったので、この実装程度だとあまり影響受けないかもしれないですね。副産物としてIntel製Macはやはりスペックが低くくて、iPhoneは高いことに驚きました。モバイル端末の実機動作ではまだ不安定な部分もあって、本格的にDOTSをプロジェクトに導入するのはまだ早いかもしれません。