Unity推出的DOTS技術,通過ECS架構來提高CPU的緩沖命中率,Job System提供方便的多線程代碼編寫,Burst Compiler編譯生成高性能代碼。
下面我們分別用普通的方式和DOTS的方式來實現10000個運動的Cube同屏渲染的例子來看下其性能區別。
普通方式
1. 先創建OPPMoveScript.cs來實現Cube的隨機旋轉和移動:

1 using UnityEngine; 2 3 public class OPPMoveScript : MonoBehaviour 4 { 5 private Vector3 _rotateSpeed; 6 private Vector3 _moveSpeed; 7 private float _moveDistance; 8 9 private Vector3 _origin; 10 private float _distanceSqr; 11 12 void Start() 13 { 14 _rotateSpeed.x = Random.value * 360; 15 _rotateSpeed.y = Random.value * 360; 16 _rotateSpeed.z = Random.value * 360; 17 18 _moveSpeed.x = Random.Range(0.5f, 1f); 19 _moveSpeed.z = Random.Range(0.5f, 1f); 20 _moveSpeed.y = Random.Range(0.5f, 1f); 21 22 _moveDistance = Random.Range(0.5f, 1f) * 5; 23 24 _origin = transform.position; 25 _distanceSqr = _moveDistance * _moveDistance; 26 } 27 28 void Update() 29 { 30 transform.Rotate(_rotateSpeed * Time.deltaTime); 31 transform.Translate(_moveSpeed * Time.deltaTime); 32 33 if (Vector3.SqrMagnitude(transform.position - _origin) > _distanceSqr) 34 { 35 _moveSpeed = -_moveSpeed; 36 } 37 } 38 }
2. 再創建OPPCreateScript.cs來生成10000個Cube:

1 using UnityEngine; 2 3 public class OPPCreateScript : MonoBehaviour 4 { 5 public int MaxCount = 10000; 6 7 public float RandomNum = 10f; 8 9 void Start() 10 { 11 GameObject go = Resources.Load<GameObject>("OPP/Prefabs/OPPCube"); 12 13 for (int i = 0; i < MaxCount; i++) 14 { 15 Vector3 pos = new Vector3(); 16 pos.x = Random.value * RandomNum * 2 - RandomNum; 17 pos.y = Random.value * RandomNum * 2 - RandomNum; 18 pos.z = Random.value * RandomNum * 2 - RandomNum; 19 GameObject.Instantiate(go, pos, Quaternion.LookRotation(Vector3.forward)); 20 } 21 } 22 }
最終效果如下:
整體的實現非常簡單,需要注意的是Material里開啟GPU Instancing,這樣可以對Cube進行合批。
可以看到在我的機器上CPU的開銷大概在100ms左右。
DOTS方式
在開始實現效果之前,我們需要先知道該如何使用DOTS在場景中創建一個Cube。
盡管在ECS中,提供了ConvertToEntity的腳本,只要簡單的掛到普通的GameObject之上,就可以將其轉換為對應的Entity對象,但是由於其內部使用了大量的反射導致存在性能問題所以在實際項目中基本上不會采用該方式來開發。
所以下面我們會使用ECS的方式來創建該Cube。
創建一個Cube
ECS中,Component是存放數據的地方,所以我們需要將Cube的信息添加到Component里,而System是處理所有數據的地方。由於我們只是要顯示一個Cube,Unity已經提供了我們所需要的Component和System,所以我們只要創建一個Entity並且設置好對應的數據即可。
注:DOTS中顯示一個3D對象至少需要LocalToWorld、RenderMesh和RenderBounds這3個Component。
LocalToWorld存放的是旋轉和位置數據,RenderMesh存放渲染所需的mesh和material等數據,RenderBounds則存放AABB盒的信息。
其中mesh和material需要從資源中獲取,所以我們提前編寫一個類AssetHolder,並且創建對應的Prefab將mesh和material設置好。

1 using UnityEngine; 2 3 public class AssetHolder : MonoBehaviour 4 { 5 public Mesh mesh; 6 7 public Material material; 8 }
接下來我們創建一個腳本用來添加Cube:

1 using Unity.Entities; 2 using Unity.Mathematics; 3 using Unity.Rendering; 4 using Unity.Transforms; 5 using UnityEngine; 6 7 public class SimpleECSCreateScript : MonoBehaviour 8 { 9 void Start() 10 { 11 AssetHolder assetHolder = Resources.Load<GameObject>("AssetHolder").GetComponent<AssetHolder>(); 12 Mesh mesh = assetHolder.mesh; 13 Material material = assetHolder.material; 14 15 //獲取默認的World和EntityManager 16 World world = World.DefaultGameObjectInjectionWorld; 17 EntityManager entityManager = world.EntityManager; 18 //創建Cube需要的所有Component對應的類型集合 19 EntityArchetype archetype = entityManager.CreateArchetype( 20 ComponentType.ReadOnly<LocalToWorld>(), 21 ComponentType.ReadOnly<RenderMesh>(), 22 ComponentType.ReadWrite<RenderBounds>() 23 ); 24 //創建Entity 25 Entity entity = entityManager.CreateEntity(archetype); 26 //為Entity設置數據 27 entityManager.SetComponentData(entity, new LocalToWorld() 28 { 29 Value = new float4x4(rotation: quaternion.identity, translation: new float3(0, 0, 0)) 30 }); 31 entityManager.SetSharedComponentData(entity, new RenderMesh() 32 { 33 mesh = mesh, 34 material = material 35 }); 36 entityManager.SetComponentData(entity, new RenderBounds() 37 { 38 Value = new AABB() 39 { 40 Center = new float3(0, 0, 0), 41 Extents = new float3(0.5f, 0.5f, 0.5f) 42 } 43 }); 44 } 45 }
運行起來后,可以在Game和Scene中看到一個紅色的Cube,但是在Hierarchy中是看不到的,因為Entity不是GameObject。
實現10000個Cube同屏運動
知道怎么創建一個Cube並顯示后,就可以實現10000個Cube同屏運動的效果了,在此之前,有幾個需要注意的地方先說一下:
- 由於是我們自己的效果,就得編寫自己的Component和System來實現邏輯;
- System類只要存在,就默認會自己創建並添加到World中執行,也就是說我們只需要創建System類並編寫好代碼即可,游戲一運行該System類就會自己創建並執行;但是一般來說,我們還是希望自己控制System的生命周期,所以我們一般會添加不自動創建的標簽[DisableAutoCreation]來取消其自動創建的特性;
- World中會存在默認的幾個System,我們自己實現的System如果要執行OnUpdate方法,就必須添加到這幾個默認的System中的某一個里,這里我們會添加到SimulationSystemGroup中;
好了,下面直接上代碼:
1. ECSMoveData.cs,這里記錄我們會用到的數據信息:

1 using Unity.Entities; 2 using Unity.Mathematics; 3 4 public struct ECSMoveData : IComponentData 5 { 6 public float3 rotateSpeed; 7 public float3 moveSpeed; 8 public float moveDistance; 9 10 public float3 origin; 11 public float distanceSqr; 12 }
2. ECSMoveSystem.cs,這里包含了所有Entity的創建和運動的邏輯:

1 using Unity.Entities; 2 using Unity.Mathematics; 3 using Unity.Rendering; 4 using Unity.Transforms; 5 using UnityEngine; 6 using Random = UnityEngine.Random; 7 8 [DisableAutoCreation] 9 public class ECSMoveSystem : SystemBase 10 { 11 private Mesh _mesh; 12 private Material _material; 13 14 private EntityManager _entityManager; 15 private EntityArchetype _archetype; 16 17 private Entity[] _entities; 18 19 protected override void OnCreate() 20 { 21 base.OnCreate(); 22 23 AssetHolder assetHolder = Resources.Load<GameObject>("AssetHolder").GetComponent<AssetHolder>(); 24 _mesh = assetHolder.mesh; 25 _material = assetHolder.material; 26 27 World world = World.DefaultGameObjectInjectionWorld; 28 _entityManager = world.EntityManager; 29 30 _archetype = _entityManager.CreateArchetype( 31 ComponentType.ReadOnly<LocalToWorld>(), 32 ComponentType.ReadOnly<RenderMesh>(), 33 ComponentType.ReadWrite<RenderBounds>(), 34 ComponentType.ReadOnly<Translation>(), 35 ComponentType.ReadOnly<Rotation>(), 36 ComponentType.ReadOnly<ECSMoveData>() 37 ); 38 } 39 40 public void CreateAllEntity(int maxCount, float randomNum) 41 { 42 _entities = new Entity[maxCount]; 43 for (int i = 0; i < maxCount; i++) 44 { 45 float x = Random.value * randomNum * 2 - randomNum; 46 float y = Random.value * randomNum * 2 - randomNum; 47 float z = Random.value * randomNum * 2 - randomNum; 48 _entities[i] = CreateEntity(x, y, z); 49 } 50 } 51 52 private Entity CreateEntity(float x, float y, float z) 53 { 54 Entity entity = _entityManager.CreateEntity(_archetype); 55 _entityManager.SetComponentData(entity, new LocalToWorld() 56 { 57 Value = new float4x4(rotation: quaternion.identity, translation: new float3(0, 0, 0)) 58 }); 59 _entityManager.SetSharedComponentData(entity, new RenderMesh() 60 { 61 mesh = _mesh, 62 material = _material 63 }); 64 _entityManager.SetComponentData(entity, new RenderBounds() 65 { 66 Value = new AABB() 67 { 68 Center = new float3(0, 0, 0), 69 Extents = new float3(0.5f, 0.5f, 0.5f) 70 } 71 }); 72 _entityManager.SetComponentData(entity, new Translation() 73 { 74 Value = new float3(x, y, z) 75 }); 76 _entityManager.SetComponentData(entity, new Rotation() 77 { 78 Value = quaternion.identity 79 }); 80 float moveDistance = Random.Range(0.5f, 1f) * 5; 81 _entityManager.SetComponentData(entity, new ECSMoveData() 82 { 83 rotateSpeed = new float3(Random.value * math.PI * 2, Random.value * math.PI * 2, Random.value * math.PI * 2), 84 moveSpeed = new float3(Random.Range(0.5f, 1f), Random.Range(0.5f, 1f), Random.Range(0.5f, 1f)), 85 moveDistance = moveDistance, 86 origin = new float3(x, y, z), 87 distanceSqr = moveDistance * moveDistance 88 }); 89 return entity; 90 } 91 92 protected override void OnUpdate() 93 { 94 float deltaTime = Time.DeltaTime; 95 Entities.ForEach((ref ECSMoveData moveData, ref Translation translation, ref Rotation rotation) => 96 { 97 rotation.Value = math.mul(math.normalize(rotation.Value), 98 quaternion.AxisAngle(moveData.rotateSpeed, deltaTime)); 99 100 translation.Value += new float3(moveData.moveSpeed.x * deltaTime, moveData.moveSpeed.y * deltaTime, 101 moveData.moveSpeed.z * deltaTime); 102 103 if (math.distancesq(translation.Value, moveData.origin) > moveData.distanceSqr) 104 { 105 moveData.moveSpeed = -moveData.moveSpeed; 106 } 107 }).Schedule(); 108 } 109 110 protected override void OnDestroy() 111 { 112 base.OnDestroy(); 113 114 for (int i = 0; i < _entities.Length; i++) 115 { 116 _entityManager.DestroyEntity(_entities[i]); 117 } 118 } 119 }
3. ECSCreateScript.cs,這里創建System並添加到World中執行:

1 using Unity.Entities; 2 using UnityEngine; 3 4 public class ECSCreateScript : MonoBehaviour 5 { 6 public int MaxCount = 10000; 7 8 public float RandomNum = 10f; 9 10 void Start() 11 { 12 World world = World.DefaultGameObjectInjectionWorld; 13 ECSMoveSystem moveSystem = world.GetOrCreateSystem<ECSMoveSystem>(); 14 moveSystem.CreateAllEntity(MaxCount, RandomNum); 15 SimulationSystemGroup systemGroup = world.GetOrCreateSystem<SimulationSystemGroup>(); 16 systemGroup.AddSystemToUpdateList(moveSystem); 17 } 18 }
最終效果如下:
可以看到,通過DOTS的實現,同樣的效果CPU開銷只有12ms左右,性能提升是上面普通方式實現的10倍。