1. 程式人生 > >Unity2018新功能之Entity Component System(ECS)二

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>

,這是Unity新的集合而非C#的,需要引用Unity.Collections,因為我要建立很多個實體,超過了它的範圍就沒用了。

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的元件。

所有的元件都需要繼承SharedComponentDataWrapperComponentDataWrapper,資料(struct)需要繼承ISharedComponentDataIComponentData。使用SharedComponentDataWrapperISharedComponentData可顯著降低記憶體,建立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引擎做出來的遊戲效能差的標籤。加油!