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 }View Code
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 }View Code
最终效果如下:
整体的实现非常简单,需要注意的是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 }View Code
接下来我们创建一个脚本用来添加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 }View Code
运行起来后,可以在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 }View Code
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 }View Code
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 }View Code
最终效果如下:
可以看到,通过DOTS的实现,同样的效果CPU开销只有12ms左右,性能提升是上面普通方式实现的10倍。