最近發(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帮匾。