Unity ECS 系列(一)- Unity ECS簡介

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


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

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

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

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

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

  • GC機制

  • 單線程開發(fā)


Entity-Component-System 登場

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

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

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

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

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


ECS有什么不同?

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

MonoBehavior 既包含數(shù)據(jù)也包含行為将宪。下面這段代碼演示了Rotator組件每幀都要對Transform組件進行旋轉(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ù)個其它類的较坛,且每個其它類包含了他們自己的數(shù)據(jù)印蔗,除了Transform,上面代碼中沒有用到任何他們中的數(shù)據(jù)丑勤。這其實浪費了很多不必要的內(nèi)存华嘹,因此我們在設(shè)計一個系統(tǒng)時,需要考慮哪些數(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()
    {
        // 這里可以看第一個優(yōu)化點:
        // 我們知道所有Rotator所經(jīng)過的deltaTime是一樣的耙厚,
        // 因此可以將deltaTime先保存至一個局部變量中供后續(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 則包含行為秉剑,一個 ComponentSystem 更新所有與之組件類型匹配的GameObject泛豪。


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

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

ComponentSystem 是怎么訪問Rotator和Transform的价卤?

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

ECS 框架提供了一個叫 GameObjectEntity 的組件慎璧,在OnEnable時床嫌,GameObjectEntity會在GameObject上創(chuàng)建一個含有所有組件的實體(Entity)。所以ComponentSystems 可以獲取完整的GameObject及其所有組件胸私。

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

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

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

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

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

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

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

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

混合ECS的缺點:

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

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

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

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

  • 沒有SIMD

因此,使用ComponentSystem, GameObject 和 MonoBehaviour 結(jié)合是編寫ECS代碼的一個簡易的改變扳肛∩倒遥混合ECS提供了一些簡單的性能改進,但是它并沒有充分發(fā)揮ECS的所有性能優(yōu)勢挖息。


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

通常讓游戲具有更好的性能是選擇ECS的一個重要原因,但如果我們利用CPU的SIMD特性來編寫所有代碼兽肤,其實最終的性能和基于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;

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

// ComponentDataWrapper用于將ComponentData添加到GameObject闹啦,
// 這一步需要手動添加沮明,將來Unity會自動化這步操作。
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的一種很簡易的方式窍奋,
// 這比使用 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會讓系統(tǒng)為Job提供必要的依賴關(guān)系江场,
    // 其它之前任何寫入Rotation或RotationSpeed的JobComponentSystem都將參與依賴計算.
    // 這里必須返回調(diào)度后的JobHandle纺酸,以便系統(tǒng)處理依賴執(zhí)行順序。
    // 這樣處理的優(yōu)點:
    //  * 主線程是非阻塞的址否,只需考慮依賴關(guān)系調(diào)度Job吁峻,當(dāng)依賴項全部執(zhí)行完成,Job才會執(zhí)行在张。
    //  * 依賴項的構(gòu)成是自動計算的用含,因此我們可以模塊化的編寫多線程代碼。
    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)容合作請聯(lián)系作者
  • 序言:七十年代末啄骇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瘟斜,更是在濱河造成了極大的恐慌缸夹,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件螺句,死亡現(xiàn)場離奇詭異虽惭,居然都是意外死亡,警方通過查閱死者的電腦和手機蛇尚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門芽唇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人取劫,你說我怎么就攤上這事匆笤。” “怎么了谱邪?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵炮捧,是天一觀的道長。 經(jīng)常有香客問我惦银,道長咆课,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任扯俱,我火速辦了婚禮书蚪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蘸吓。我一直安慰自己善炫,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布库继。 她就那樣靜靜地躺著箩艺,像睡著了一般窜醉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艺谆,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天榨惰,我揣著相機與錄音,去河邊找鬼静汤。 笑死琅催,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的虫给。 我是一名探鬼主播藤抡,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抹估!你這毒婦竟也來了缠黍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤药蜻,失蹤者是張志新(化名)和其女友劉穎瓷式,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體语泽,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡贸典,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了踱卵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片廊驼。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖颊埃,靈堂內(nèi)的尸體忽然破棺而出蔬充,到底是詐尸還是另有隱情,我是刑警寧澤班利,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站榨呆,受9級特大地震影響罗标,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜积蜻,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一闯割、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竿拆,春花似錦宙拉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煌贴。三九已至,卻和暖如春锥忿,著一層夾襖步出監(jiān)牢的瞬間牛郑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工敬鬓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留淹朋,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓钉答,卻偏偏與公主長得像础芍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子数尿,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容