天天看點

Unity的Dots技術入門

前言

看過Dots技術宣傳片,當時确實被震驚到了,腦袋裡閃過一句話:時代變了!大人。DOTS是Data-Oriented Technology Stack(資料導向的技術棧):借助Unity全新高性能多線程式資料導向型技術堆棧(DOTS),充分利用當今的多核處理器。遊戲的運作速度會變得更快。

1.Unity的Dots由來

這種技術在遊戲領域最先成功的案例來自守望先鋒,各位可以去看看《守望先鋒》架構設計與網絡同步,ECS全稱是Entity Component System,即實體、元件和系統。

  • 實體一般用途的對象,可以用唯一ID來辨別。
  • 元件是一組資料,可以是實體一部分,也可以是實體和世界互動的中間資料(注意:沒有功能)。實體由若幹元件組成的,否則它是空盒子。
  • 系統基于實體中的特定元件來實作特定功能,但是系統是集中批量處理所有的實體的。

ECS這種架構有什麼好處呢?相比OOP方式實作Component,ECS中相同元件的資料是連續的,System集中處理這些元件,可以友善利用CPU Cache提升效率。由于System處理的可能是幾個元件,ECS将元件盡量的最小化(否則會有很多資料備援),這樣就實作資料的扁平化。同時System間禁止互相調用,最大程度的實作代碼解耦。而OOP最被诟病的調用層次深,結構複雜等問題都被解決了。

Dots就是由實體元件系統(ECS)、任務系統(Job System)、Burst Compiler編譯器三部分組成。DOTS保證相同類型元件在記憶體中都是順序排列,極大程度增加緩存的命中率,此外配合任務系統(Job System)讓開發者無需頭疼多線程同時通路資料需要手動加解鎖的麻煩,最終加持Burst Compiler讓性能飛起來。

2.與OOP程式設計對比

在對比之前,需要了解CPU相關概念:

  • 1.CPU處理資料速度非常快,記憶體條和硬碟跟不上它的速度。
  • 2.CPU自身有三級緩存,用來提高CPU擷取資料速度。n級緩存最快(n是1->3),但容量最小。
  • 3.記憶體是指CPU拿取資料的最開始的地方,CPU通路記憶體速率遠小于三級緩存速率。
  • 4.CPU操作資料會從三級緩存中取得資料,速度非常快,但有些情況下資料不在三級緩存中(缺頁中斷),就需要尋址到記憶體中,并且把目标資料重新放到三級緩存中,提高下一次通路速度。

很多傳統遊戲引擎是基于面向對象來設計的,遊戲世界中的東西都是對象(GameObject),每個對象都有叫做Update方法,遊戲引擎周遊所有對象,依次調用其Update方法。有些引擎甚至定義多種Update方法,在同一幀的不同時機去調用(Unity通過反射機制調用這些Update函數)。 OOP程式設計模式其實是有極大的缺陷的。因為遊戲對象由很多部分聚合而成,引擎的功能子產品很多,不同子產品關注的部分往往互不相關。比如音效子產品資料并不需要關心實體碰撞資料、渲染子產品資料不關心遊戲邏輯資料,對于某種子產品沒有必要的資料儲存到CPU中,我們可以稱為垃圾資料。從自然意義上說,把遊戲對象屬性聚合是很正常的事情,對象生命期管理也是最合理的方式。但這種對象大量存在時就會嚴重性能問題,接下來展示Unity的MonoBehaviour所繼承的Component子類。

Unity的Dots技術入門

其實MonoBehaviour繼承關系是這樣的,Behaviour->Component->Object,這些子類全部加起來的資料、屬性可不是以上這麼點,有興趣可以進去看看這些子類都定義了什麼資料、屬性、方法。MonoBehaviour對象過多會導緻大量無用資料占據CPU的緩存(比如控制transform旋轉,裡面卻有音效、實體、相機等等屬性),結論是傳統引擎的設計思路對CPU緩存不太友好導緻使用率不高,出現CPU缺頁中斷情況會比較頻繁,下面給出圖解:

Unity的Dots技術入門

使用Ecs技術對CPU緩存使用率有質的提高,因為儲存到CPU緩存的資料都比較純淨(ECS中的實體、元件),這樣可以有效降低CPU缺頁中斷,是以Dots相對于OOP程式設計有以下優點:

  • 1.ECS對CPU緩存友好,減少CPU缺頁中斷,防止CPU擷取資料慢,進而CPU出現等待情況。
  • 2.JobSystem編寫多線程代碼可以提供高性能,顯著提高幀速率和延長移動裝置的電池壽命。
  • 3.Burst Compiler,用于生成高度優化的本地代碼。

3.Dots官方案例

拉取Unity官方案例AngryBots_ECS,部落客使用Unity2020.2.0a打開案例工程,發現一大堆的問題和報錯(剩下渲染管線方面問題無法解決,但不影響運作),接下來分析如何一步步解決這些問題。

Unity的Dots技術入門

首先是Hybrid Renderer版本問題,看報錯好像依賴了High Definition RP包(但是下載下傳RP後還是不行),是以這裡嘗試把Hybrid Renderer更新到最新的版本,于是第一個報錯就消失了。出現了以下報錯就和接龍一樣…

Unity的Dots技術入門

Job System不存在OnCreateManager接口,可以進入到基類ComponentSystemBase裡,發現了OnCreate生命周期函數,可以重寫它來代替報錯函數。

Unity的Dots技術入門

World.Active去代替World.DefaultGameObjectInjectionWorld,可能Active是老版本的命名方式,GameObjectConversionUtility.ConvertGameObjectHierarchy接口所需參數也不一緻,可以改成一下代碼段:

var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
	manager = World.DefaultGameObjectInjectionWorld.EntityManager;
	bulletEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(bulletPrefab, settings);
           
Unity的Dots技術入門

最後TimeData.deltaTime改成大寫開頭即可,全部報錯改好後會發現運作時還有問題,可能是Burst Compiler的問題,重新開機Unity後所有問題都得到解決了。

如果你終于走到這步,可能會很高興。但啟動遊戲後會發現看不到子彈和敵人…不使用Ecs是可以看到子彈的,如下圖所示:

Unity的Dots技術入門

ps:莫名其妙死亡的話,隻是看不到的敵人将主角殺死,上面說過可能是渲染管線的問題,經過在下長時間努力,大概知道問題所在了。嘗試Edit/Project Settings/Graphics下将Scriptable Render Pipeline Settings設定成none,可以看到一下畫面(可以看到子彈、敵人)。

Unity的Dots技術入門

具體解決方案不太清楚,如果有人知道麻煩下面留言,萬分感謝~

感覺GameObjectConversionSettings或Hybrid Rendering問題,但不知道如何修改。

案例示範視訊

4.實戰DOTS技術

使用Unity2020.2.0a建立新工程後,需要導入hybrid、entities(entities依賴jobs、burst),如下圖所示:

Unity的Dots技術入門

低版本Unity可能不存在此檔案,并且manifest.json也不存在jobs、burst這些依賴包,這時需要手動拉取。接下來講述下3種拉取方式:

  • 使用Package Manager拉取需要包(可能搜尋不出來)。
  • 打開Package Manager後,視窗左上角有個加号,點開有三個選項Add package from xxx(有時低版本不出現Package Manager)。
  • 在manifest.json裡添加兩個依賴包(最穩定的方法):

    “com.unity.entities”: “0.14.0-preview.18”,

    “com.unity.rendering.hybrid”: “0.8.0-preview.18”,

依賴包全部導入後,接下來先嘗試用DOTS技術寫多個Cube旋轉吧。

  • 1.建立旋轉方塊元件:
using Unity.Entities;

public struct RotationCubeComponent : IComponentData
{
    public float speed;
}
           

注意必須是結構體,以後文章會分析為什麼是結構體,如何才可以使用類,結構體比類的優點等等。

  • 2.建立旋轉方塊事件
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Jobs;

public class RotationCubeSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;
        Entities
           .WithName("RotationCubeSystem")
           .ForEach((ref RotationCubeComponent rotationSpeed, ref Rotation rotation) =>
           {
               rotation.Value = math.mul(
                   math.normalize(rotation.Value),
                   quaternion.AxisAngle(math.up(), rotationSpeed.speed * deltaTime));
           })
           .ScheduleParallel();
    }
}
           

ScheduleParallel已經使用Job System多線程技術了,不使用多線程處理可以自行百度。

  • 3.建立實體管理器
using Unity.Entities;
using UnityEngine;

public class EntitiesManger : MonoBehaviour, IConvertGameObjectToEntity
{
    public float FloCubeSpeed = 10f;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var data = new RotationCubeComponent { speed = FloCubeSpeed };
        dstManager.AddComponentData(entity,data);
    }
}
           

GameObject轉Entity,繼承IConvertGameObjectToEntity重寫Convert函數。

  • 4.批量生成實體
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public class EntitiesBuilder : MonoBehaviour
{
    public GameObject cube;

    void Start()
    {
        var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
        var prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(cube, settings);
        var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;

        for (var x = 0; x < 100; x++)
        {
            for (var y = 0; y < 100; y++)
            {
                var instance = entityManager.Instantiate(prefab);
                var position = transform.TransformPoint(new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F));
                entityManager.SetComponentData(instance, new Translation { Value = position });
            }
        }
    }
}
           

将EntitiesManger和Convert To Entity挂載到Cube之後,制作成Prefab即可。然後将EntitiesBuilder随便挂載到場景的物體。

具體原理分析和DOTS深度的東西後面文章會說明,至于官方案例子彈為什麼不顯示,部落客知道具體原因也會更新的。

5.總結

既然DOTS這麼牛逼,為啥還沒普及?很簡單,DOTS現階段在完成度上就是接近廢品的殘次品,雖然經過Unity長達近三年的宣傳,DOTS到今天完成度依然極低,基本上處于不可用的狀态,一款遊戲引擎必不可少的部分可簡單列為下面八個,要把Dots技術推動起來,遊戲引擎要進行大量重構,或者保留傳統的MonoBehaviour将Dots技術插接進去,接下來看看Unity對這八大子產品完成情況。

  • Physics-3分 實體自然是遊戲引擎不可或缺的部分,DOTS配套的Unity Physics處于非常早期的狀态,Authoring工具幾乎為零,甚至Joint或者CharacterConroller都要從Sample裡找代碼,由于它提供了一套新的API,開發過程隻能用極難使用來形容,來看看Trigger事件代碼就知道了,而使用過程中也是Bug層出不窮,經常會遇到類似Mesh Collider Bake錯誤,而與之同期宣傳的Havok Physics for Unity更是奇妙,在長達半年的時間裡,隻是單純引入Havok便會導緻HDRP的渲染出問題.而更進階的布料、粒子、地形等也處于幾乎為零的狀态。
  • Graphics-3分 DOTS與之配套的是圖形系統是Hybrid Renderer,不過目前也隻支援Mesh Renderer,什麼Particle、Trail、VFX都還沒影兒,更可惜的是官方現在優先關注于HDRP相容性(雖然也不怎麼樣),URP或者Built-In Pipeline就基本别想了,遇到了問題也隻能雙手一攤。
  • Audio-0分 Unity目前隻開發了底層的 DSP 系統,上層的DOTS Audio說了一兩年了連影子都沒有,完成度接近于零。
  • Animation-0分 Unity Animation依然處于極早期的開發中,DOTS Sample展示了些最基本的使用,類似 Animator 這樣的高層方案現在還沒影兒。
  • Network-2分 也是Unity吹了兩年的東西,DOTS Sample中用的NetCode隻不過把FPSSample中的部分代碼挪過去了,目前版本号 0.0.4,意思就是太早期了别用。
  • UI-0分 IMGUI,UI Element和UGUI三個Unity的産品均不支援DOTS。
  • AI-0分 Navmesh,Ml-Agent,Ai Planner三個Unity的産品均不支援DOTS。
  • Input-2分 老的Input System相對簡單與DOTS沒啥關系,可新的Input System居然也不支援DOTS,幸運的是InputSystem相對獨立,嫁接進DOTS并不困難。

對于遊戲引擎子產品全部改成DOTS是有難度的,不然Unity公司對于DOTS支援程度就不是如此了(畢竟對遊戲行業算是革命),在下大膽預言未來有某個天才或Unity公司完全實作了DOTS并且達到可以商用的地方,将會是巨大的利潤和商機。大家可能比較關心的是目前DOTS能不能接入遊戲,隻能說部分子產品可以使用(某些經典批處理場景),個人建議不要把還沒成型的技術接入到項目中,現在DOTS還有很多限制,知道有這個東西即可。參考資料和案例:DOTS開源案例、DOTS參考資料、DOTS實用案例