Unity2018新功能之Entity Component System(ECS)二
,我們簡單的介紹了ECS的概念和Entities的使用方法,我將在這篇文章中繼續深入講解ECS的框架。
首先我們先來看下面這一張圖。這是我通過ECS框架創建出來的5萬個Cube,FPS基本穩定在30左右,因為預設是實時光照,所以渲染壓力有點大,這不是我們這篇文章的討論範圍。
為什麼ECS那麼牛逼呢?
我們來看看我們傳統的開發方式,打個比方GameObject元件,GameObject裡面既有Transform,renderer,rigidbody,collider,又有GetComponent等屬性和方法,當我們在例項化GameObject的時候,是把裡面所有的東西全部都加到記憶體裡面的,而且是離散式的!即物體和它的元件(Component)並非在同一個記憶體區段,每次存取都非常耗時。而ECS會確保所有的元件資料(Component Data)都緊密的連線再一起,這樣就能確保存取記憶體資料時以最快的速度存取。真正做到需要什麼就用什麼,不帶一點浪費的。
我們一起來建立ECS程式碼
1.建立實體管理器和實體。
由於上一篇文章沒有講如何建立實體管理器(EntityManager)和建立實體(Entity),那麼我們接下來就講一下如何用程式碼建立實體管理器(EntityManager)和建立實體(Entity)。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Mathematics; using Unity.Entities; using Unity.Transforms; using Unity.Collections; using Unity.Rendering; public class Main : MonoBehaviour { public EntityManager entityManager; public EntityArchetype entityArchetype; public int createPrefabCout; public GameObject prefab; public Mesh mesh; public Material material; // Use this for initialization void Start () { //建立實體管理器 entityManager = World.Active.GetOrCreateManager<EntityManager>(); //建立基礎元件的原型 entityArchetype = entityManager.CreateArchetype(typeof(Position), typeof(Rotation), typeof(RotationSpeed)); if (prefab) { for (int i = 0; i < createPrefabCout; i++) { //建立實體 Entity entities = entityManager.CreateEntity(entityArchetype); //設定元件 entityManager.SetComponentData(entities, new Position { Value = UnityEngine.Random.insideUnitSphere * 100 }); entityManager.SetComponentData(entities, new Rotation { Value = quaternion.identity }); entityManager.SetComponentData(entities, new RotationSpeed { Value = 100 }); //新增並設定元件 entityManager.AddSharedComponentData(entities, new MeshInstanceRenderer { mesh = this.mesh, material = this.material, }); } //NativeArray<Entity> entityArray = new NativeArray<Entity>(createPrefabCout, Allocator.Temp); //for (int i = 0; i < createPrefabCout; i++) //{ // entityManager.Instantiate(prefab, entityArray); // entityManager.SetComponentData(entityArray[i], new Position { Value = UnityEngine.Random.insideUnitSphere*10 }); // entityManager.AddSharedComponentData(entityArray[i], new MeshInstanceRenderer // { // mesh = this.mesh, // material = this.material, // }); //} //entityArray.Dispose(); } } }
我先建立了一個EntityManager管理器,然後在它下面生成了一個Archetype,這種EntityArchetype能確保存放在裡面的Component Data都緊密的相連,當一次性產生大量的Entities並給予各種邏輯運算時,這種結構在記憶體與快取之間的移動效能直逼memcpy。
雖然Prefab是傳統Game Object,但在例項化成Entity時,Unity會自動把跟ECS無關的Component拆離,只留下Component Data生成Entity物體。這種跟Unity整合的流程,可以稱為Hybrid ECS,也是Unity在整合ECS的主要路線。
註釋的地方本來是用NativeArray<T>
2.建立旋轉速度元件
using System.Collections;
using System.Collections.Generic;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
[System.Serializable]
public struct RotationSpeed : IComponentData
{
public float Value;
}
public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { };
在上一篇文章中的元件我是直接用傳統的方式掛載的,這次我改成用ECS的元件。
所有的元件都需要繼承SharedComponentDataWrapper或ComponentDataWrapper,資料(struct)需要繼承ISharedComponentData或IComponentData。使用SharedComponentDataWrapper與ISharedComponentData可顯著降低記憶體,建立100個cube和一個cube的消耗記憶體的差異幾乎為零。
3.建立旋轉速度系統
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public class RotationSpeedSystem : JobComponentSystem
{
[BurstCompile]
struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
{
public float dt;
public void Execute(ref Rotation rotation, ref RotationSpeed speed)
{
rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), speed.Value * dt));
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new RotationSpeedRotation() { dt = Time.deltaTime };
return job.Schedule(this, inputDeps);
}
}
RotationSpeedRotation裡面的Execute,可以理解為傳統模式中的Update,不過是並行執行的。相關邏輯就是計算執行時間、運算位置並賦值。
想要把RotationSpeedRotation中的變數和我們找到的物體聯絡起來,且在Job系統中並行執行就需要JobHandle OnUpdate。他的作用是把我們包裝起來的業務邏輯【就是Execute】放到Job系統執行【多核心平行計算】,並且把找到的物體和RotationSpeedRotation中的變數關聯起來。
我們一起來執行ECS框架
程式碼寫完之後我們建立一個Cube的預設,掛載上GameObjectEntity實體,PositionComponent,RotationComponent,RotationSpeedComponent元件,再建立一個材質,最後在場景掛上Main指令碼給變數賦值,執行Unity3d,我們將看到文章開頭的那張圖片,Good!
其中PositionComponent,RotationComponent和MeshInstanceRenderer是Entities自帶的元件,前兩個比較好了解,MeshInstanceRenderer是為了建立網格模型和材質,使我們建立的實體能夠被視覺化,不然我們的實體將是透明的,我們也可以自己寫,這裡就不再做解釋了。
由於我們的建立的物件已經脫離了GameObject,所以我們在Hierarchy面板看不到創建出來的物件。
我們開啟 Window>Analysis>Entity Debugger,我們就可以看到我們創建出來的物件的資訊了,左邊是系統的資訊,顯示我們整個專案裡面所有的系統,沒有被使用的系統會顯示為not run。右邊是實體的資訊,顯示我們建立的所有的實體,我們可以看見我們總的建立了5萬個實體。
我們隨便點選一個實體,便可以看到該實體所包含的所有元件資訊。
我們展開所有元件,發現元件的資料是不能修改的,聽Unity中國的技術總監楊棟老師說,Entities的開發團隊正在完善這個框架,使它能夠像傳統的開發模式一樣編輯。
注:由於這個ECS框架是預覽版(目前使用的是0.0.12-preview.14)的還不是很完善,在使用上並不是那麼友好,而且沒有做異常處理經常導致Unity奔潰。比如CreateArchetype加了ComponentType之後,如果沒有SetComponentData該ComponentType,Unity3d就會奔潰。
如果用ECS框架開發一個大型遊戲專案,那這個專案到底需要多少個ComponentData和ComponentSystem(此處隱藏著一個破涕為笑的表情),我真的為我們Entities開發團隊感到驕傲(此處隱藏著一個捂臉的表情)。
希望Unity3d的Entities開發團隊能夠更好的完善ECS這個框架,我對這個框架抱有很大的希望,因為它的出現改變了Unity3d引擎做出來的遊戲效能差的標籤。加油!