Unity ECS 系列(一)- Unity ECS簡(jiǎn)介

最近發(fā)覺剛看過的東西在腦中卻忘越來越快了,比如Unity的ECS,僅僅一個(gè)月,就忘的什么也不剩。有些記憶可能還是寫下來更持久些,從今天開始邊寫邊重新學(xué)吧。


了解過ECS的開發(fā)者都知道ECS與Unity原本的開發(fā)理念相差很大,需要所有Unity開發(fā)者重新去學(xué)習(xí)和適應(yīng)新的開發(fā)框架的代價(jià)還是很大的,Unity為何要做出這么大跨度的嘗試呢?

Unity正在嘗試解決什么問題?

以前我們基于Unity的GameObject/MonoBehaviour機(jī)制,可以非常簡(jiǎn)單為創(chuàng)作游戲編寫代碼,但最終往往讓代碼陷入難以閱讀,維護(hù)和優(yōu)化的境地。這是一系列因素聯(lián)合導(dǎo)致的:

  • 面向?qū)ο竽P?/p>

  • 由Mono編譯的非最優(yōu)機(jī)器碼

  • GC機(jī)制

  • 單線程開發(fā)


Entity-Component-System 登場(chǎng)

Entity-Component-System 是一種編寫代碼的方式,簡(jiǎn)稱ECS,近年因OW被廣泛熟知,ECS主要關(guān)注開發(fā)中一個(gè)很基本的問題:如何組建并處理游戲中的數(shù)據(jù)和行為。

后續(xù)文章我們會(huì)更具體的講解ECS的概念,本章我們簡(jiǎn)單介紹ECS在Unity中的使用。

采用ECS不但在設(shè)計(jì)上可以更好的進(jìn)行游戲編程,還可以利用Unity提供的JobSystem和Brush編譯器充分發(fā)揮多核處理器的性能。

Unity2017以后已經(jīng)發(fā)布了JobSystem,基于JobSystem可以在C#代碼中更好的實(shí)現(xiàn)多線程批處理技術(shù),JoySystem底層為多線程間的競(jìng)爭(zhēng)提供的安全保障。

對(duì)于開發(fā)者而言,更重要的是要使用一種新的思維方式和編碼方式來充分利用JobSystem。


ECS有什么不同?

MonoBehavior -我們的老戰(zhàn)友

MonoBehavior 既包含數(shù)據(jù)也包含行為。下面這段代碼演示了Rotator組件每幀都要對(duì)Transform組件進(jìn)行旋轉(zhuǎn)操作。


using UnityEngine;

class Rotator : MonoBehaviour
{
    // 數(shù)據(jù):可以在Inspector窗口中編輯的旋轉(zhuǎn)速度值
    public float speed;
    
    // 行為:從component中讀取速度值,然后修改Transform組件中的rotation
    void Update()
    {
        transform.rotation *= Quaternion.AngleAxis(Time.deltaTime * speed, Vector3.up);
    }
}

然而MonoBehaviour 是繼承于數(shù)個(gè)其它類的,且每個(gè)其它類包含了他們自己的數(shù)據(jù),除了Transform,上面代碼中沒有用到任何他們中的數(shù)據(jù)。這其實(shí)浪費(fèi)了很多不必要的內(nèi)存,因此我們?cè)谠O(shè)計(jì)一個(gè)系統(tǒng)時(shí),需要考慮哪些數(shù)據(jù)是我們真正需要的。

ComponentSystem -邁入新紀(jì)元的一步

using Unity.Entities;
using UnityEngine;

// 數(shù)據(jù):可以在Inspector窗口中編輯的旋轉(zhuǎn)速度值
class Rotator : MonoBehaviour
{
    public float Speed;
}

// 行為:繼承自ComponentSystem來處理旋轉(zhuǎn)操作
class RotatorSystem : ComponentSystem
{
    struct Group
    {
        // 定義該ComponentSystem需要獲取哪些components
        public Transform Transform;
        public Rotator   Rotator;
    }
    
    override protected void OnUpdate()
    {
        // 這里可以看第一個(gè)優(yōu)化點(diǎn):
        // 我們知道所有Rotator所經(jīng)過的deltaTime是一樣的,
        // 因此可以將deltaTime先保存至一個(gè)局部變量中供后續(xù)使用,
        // 這樣避免了每次調(diào)用Time.deltaTime的開銷。
        float deltaTime = Time.deltaTime;
        
        // ComponentSystem.GetEntities<Group>可以高效的遍歷所有符合匹配條件的GameObject
        // 匹配條件:即包含Transform又包含Rotator組件(在上面struct Group中定義)
        foreach (var e in GetEntities<Group>())
        {
            e.Transform.rotation *= Quaternion.AngleAxis(e.Rotator.Speed * deltaTime, Vector3.up);
        }
    }
}

在ECS模型中,Component(組件)只包含數(shù)據(jù)。

ComponentSystem 則包含行為,一個(gè) ComponentSystem 更新所有與之組件類型匹配的GameObject。


混合ECS:使用與 ComponentSystem 現(xiàn)有的 GameObject & components 一起工作

目前,現(xiàn)有的Unity工程基本都是基于MonoBehaviour&GameObject&components,如果想與現(xiàn)有GameObject&components一起使用ECS,混合ECS將是個(gè)不錯(cuò)的選擇。上面的例子演示了我們可以簡(jiǎn)單的遍歷訪問即包含Rotator又包含Transform組件的實(shí)體對(duì)象。

ComponentSystem 是怎么訪問Rotator和Transform的?

為了能像上面例子中那樣可以遍歷所有匹配組件類型的實(shí)體,這些實(shí)體必須由 EntityManager 創(chuàng)建。

ECS 框架提供了一個(gè)叫 GameObjectEntity 的組件,在OnEnable時(shí),GameObjectEntity會(huì)在GameObject上創(chuàng)建一個(gè)含有所有組件的實(shí)體(Entity)。所以ComponentSystems 可以獲取完整的GameObject及其所有組件。

因此在目前的情況下,如果你需要在ComponentSystems訪問一個(gè)GameObject,則必須在該GameObject上添加一個(gè)GameObjectEntity組件。

如何將現(xiàn)有代碼轉(zhuǎn)為混合ECS?

我們要把MonoBehaviour.Update轉(zhuǎn)換為ComponentSystems.OnUpdate的方式,可以繼續(xù)將所有的數(shù)據(jù)保存在MonoBehaviour中,這是一種很簡(jiǎn)單向ECS的過渡方式。

因此場(chǎng)景數(shù)據(jù)仍然存在于GameObjects & components中,可以繼續(xù)使用GameObject.Instantiate以創(chuàng)建實(shí)例等。

混合ECS的優(yōu)點(diǎn):

  • 數(shù)據(jù)與行為的分離的方式,會(huì)讓代碼整體看起來更清晰

  • 系統(tǒng)對(duì)許多對(duì)象是可以進(jìn)行批量操作的,避免了一些無意義的調(diào)用。(見上面deltaTime優(yōu)化)

  • 我們可以繼續(xù)使用現(xiàn)有的Inspectors, Editor tools等工具

混合ECS的缺點(diǎn):

  • 實(shí)例化時(shí)間并沒有得到優(yōu)化

  • 加載時(shí)間并沒有得到優(yōu)化

  • 數(shù)據(jù)是隨機(jī)訪問的,沒有線性內(nèi)存訪問的高效性

  • 沒有發(fā)揮多核功能

  • 沒有SIMD

因此,使用ComponentSystem, GameObject 和 MonoBehaviour 結(jié)合是編寫ECS代碼的一個(gè)簡(jiǎn)易的改變?;旌螮CS提供了一些簡(jiǎn)單的性能改進(jìn),但是它并沒有充分發(fā)揮ECS的所有性能優(yōu)勢(shì)。


純ECS: 使用IComponentData & Jobs全面提升性能

通常讓游戲具有更好的性能是選擇ECS的一個(gè)重要原因,但如果我們利用CPU的SIMD特性來編寫所有代碼,其實(shí)最終的性能和基于ECS編寫的是差不多的。

結(jié)合ECS與C# JobSystem將提供SIMD的可能性,以發(fā)揮CPU最大性能。

C# JobSystem 只支持structs和NativeContainers,并不支持托管數(shù)據(jù)類型。所以,在C# JobSystem中,只有IComponentData數(shù)據(jù)可以被安全的訪問。

另外,EntityManager內(nèi)部保證了ComponentData(組件)數(shù)據(jù)的線性內(nèi)存布局,這是C# JobSystem中可以高效的使用IComponentData最重要的依據(jù)。

using System;
using Unity.Entities;

// 定義一個(gè)ComponentData用于存儲(chǔ)旋轉(zhuǎn)速度
[Serializable]
public struct RotationSpeed : IComponentData
{
    public float Value;
}

// ComponentDataWrapper用于將ComponentData添加到GameObject,
// 這一步需要手動(dòng)添加,將來Unity會(huì)自動(dòng)化這步操作。
public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { } 
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

// IJobProcessComponentData 是遍歷匹配組件類型Entity的一種很簡(jiǎn)易的方式,
// 這比使用 IJobParallelFor 更方便、有效。
// Entity的處理(Execute)是并行的,主線程只負(fù)責(zé)調(diào)度Job
public class RotationSpeedSystem : JobComponentSystem
{
    [BurstCompile]
    struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
    {
        public float dt;

        // IJobProcessComponentData 聲明了需要讀取 RotationSpeed 和寫入 Rotation.
        public void Execute(ref Rotation rotation, [ReadOnly]ref RotationSpeed speed)
        {
            rotation.Value = math.mul(math.normalize(rotation.Value), math.axisAngle(math.up(), speed.Value * dt));
        }
    }

    // 繼承自JobComponentSystem會(huì)讓系統(tǒng)為Job提供必要的依賴關(guān)系,
    // 其它之前任何寫入Rotation或RotationSpeed的JobComponentSystem都將參與依賴計(jì)算.
    // 這里必須返回調(diào)度后的JobHandle,以便系統(tǒng)處理依賴執(zhí)行順序。
    // 這樣處理的優(yōu)點(diǎn):
    //  * 主線程是非阻塞的,只需考慮依賴關(guān)系調(diào)度Job,當(dāng)依賴項(xiàng)全部執(zhí)行完成,Job才會(huì)執(zhí)行。
    //  * 依賴項(xiàng)的構(gòu)成是自動(dòng)計(jì)算的,因此我們可以模塊化的編寫多線程代碼。
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new RotationSpeedRotation() { dt = Time.deltaTime };
        return job.Schedule(this, 64, inputDeps);
    } 
}

下一次我們將更詳細(xì)的介紹ECS。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容