日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

使用DOTS制作一款第三人称僵尸射击游戏

發(fā)布時(shí)間:2024/8/26 编程问答 82 豆豆
生活随笔 收集整理的這篇文章主要介紹了 使用DOTS制作一款第三人称僵尸射击游戏 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

我們正在使用面向數(shù)據(jù)技術(shù)棧DOTS重構(gòu)Unity的核心基礎(chǔ)。許多游戲工作室在使用C# Job System、實(shí)體組件系統(tǒng)ECS和Burst Compiler后,都無(wú)一例外地感受到明顯的性能提升,其中就包含了瑞典游戲工作室Far North Entertainment。

在Unite Copenhagen大會(huì)上,我們與Far North Entertainment工作室的成員進(jìn)行深入交流,了解他們?nèi)绾卧趥鹘y(tǒng)的Unity項(xiàng)目中應(yīng)用DOTS功能。
?


Far North Entertainment

瑞典的游戲工作室Far North Entertainment是由5位來(lái)自工程研究專(zhuān)業(yè)的好友共同創(chuàng)建。自2018年初在Gear VR平臺(tái)發(fā)布《Down to Dungeon》游戲之后,該公司一直致力開(kāi)發(fā)一款末日僵尸生存游戲。

這款末日僵尸生存游戲的獨(dú)特之處在于僵尸的數(shù)量,開(kāi)發(fā)團(tuán)隊(duì)希望實(shí)現(xiàn)成千上萬(wàn)個(gè)饑渴的僵尸追逐玩家的效果。然而在構(gòu)建原型時(shí),他們遇到了許多性能方面的問(wèn)題。

開(kāi)發(fā)中主要的瓶頸在于對(duì)龐大數(shù)量僵尸的進(jìn)行生成、銷(xiāo)毀、更新和添加動(dòng)畫(huà),雖然開(kāi)發(fā)團(tuán)隊(duì)嘗試了對(duì)象池和動(dòng)畫(huà)實(shí)例化等方法,但效果仍不顯著。因此,技術(shù)總監(jiān)Anders Eriksson將目光投向DOTS,從面向?qū)ο?#xff08;Object-oriented)設(shè)計(jì)轉(zhuǎn)為面向數(shù)據(jù)(Data-oriented)設(shè)計(jì)。

Anders Eriksson表示:促成我們思維模式發(fā)生改變的關(guān)鍵是停止考慮對(duì)象和對(duì)象層級(jí),轉(zhuǎn)為思考數(shù)據(jù)是如何變換和訪問(wèn)的。這意味著代碼不必圍繞具體事物來(lái)編寫(xiě),不用處理過(guò)去最常見(jiàn)的情況。

對(duì)于同樣在試著轉(zhuǎn)換思維模式的開(kāi)發(fā)者,Anders Eriksson的建議是:先弄清楚要解決的問(wèn)題和解決方案的相關(guān)數(shù)據(jù)。是否會(huì)對(duì)相同數(shù)據(jù)集執(zhí)行相同的處理過(guò)程?可以把多少關(guān)聯(lián)數(shù)據(jù)打包到CPU緩存行中?如果想轉(zhuǎn)換現(xiàn)有代碼的話,那么要確定會(huì)給緩存行加入的垃圾數(shù)據(jù)量。能否將運(yùn)算過(guò)程分配到多個(gè)線程上,能否利用SIMD指令?

在進(jìn)一步學(xué)習(xí)后,開(kāi)發(fā)團(tuán)隊(duì)了解到Unity組件系統(tǒng)的實(shí)體只是組件流中的查找ID。組件只是數(shù)據(jù),而系統(tǒng)包含了所有邏輯,系統(tǒng)會(huì)使用稱(chēng)為“Archetypes(原型)”的特別組件標(biāo)識(shí)來(lái)過(guò)濾實(shí)體。

Anders Eriksson表示:我們將ECS看作SQL數(shù)據(jù)庫(kù)可以幫助我們更好地理解它。每個(gè)Archetype原型是一張表格,每行代表一個(gè)組件,每列代表一個(gè)獨(dú)特的實(shí)體。我們可以使用系統(tǒng)查詢(xún)這些Archetype原型表,在實(shí)體上執(zhí)行操作。

開(kāi)始使用DOTS

為了更好地理解,Anders Eriksson研究了實(shí)體組件系統(tǒng)的文檔和ECS示例項(xiàng)目,以及Unity與Nordeus合作制作的示例項(xiàng)目。

此外,關(guān)于面向數(shù)據(jù)設(shè)計(jì)的學(xué)習(xí)材料也對(duì)團(tuán)隊(duì)有很大的幫助。CppCon 2014大會(huì)上Mike Acton關(guān)于面向數(shù)據(jù)設(shè)計(jì)的演講開(kāi)闊了他們的眼界,讓開(kāi)發(fā)團(tuán)隊(duì)了解了這種編程方式。
?


Far North Entertainment的開(kāi)發(fā)團(tuán)隊(duì)在博客上發(fā)表了許多學(xué)習(xí)心得,今年9月,他們?cè)赨nite Copenhagen大會(huì)上進(jìn)行演講,介紹了轉(zhuǎn)換到面向數(shù)據(jù)思維的經(jīng)驗(yàn)。

本文的內(nèi)容將以這次演講作為基礎(chǔ),并且詳細(xì)地講解該團(tuán)隊(duì)?wèi)?yīng)用ECS、C# Job System和Burst Compiler的具體方法。

排列僵尸數(shù)據(jù)

Anders Eriksson表示:我們面臨的主要問(wèn)題是客戶(hù)端的轉(zhuǎn)換信息插入,以及對(duì)上千個(gè)實(shí)體的轉(zhuǎn)向信息。

開(kāi)發(fā)團(tuán)隊(duì)最初使用面向?qū)ο蟮姆椒?#xff0c;編寫(xiě)了ZombieView腳本的抽象,它繼承了更為常用的EntityView父類(lèi)。EntityView是附加在游戲?qū)ο蟮腗onoBehaviour,它會(huì)用作游戲模型的可視化展示。每個(gè)ZombieView腳本會(huì)在Update函數(shù)中處理相應(yīng)的信息轉(zhuǎn)換和朝向信息插入。

這種方法似乎挺不錯(cuò),但問(wèn)題是每個(gè)實(shí)體會(huì)在內(nèi)存中占用隨機(jī)的位置。這意味著,在訪問(wèn)數(shù)千個(gè)實(shí)體時(shí),CPU需要從內(nèi)存中逐個(gè)獲取實(shí)體數(shù)據(jù),這個(gè)過(guò)程非常耗時(shí)。

如果將數(shù)據(jù)存在整齊的連續(xù)內(nèi)存塊中,CPU則可以同時(shí)緩存所有實(shí)體數(shù)據(jù)。現(xiàn)今大多數(shù)CPU在每個(gè)運(yùn)行周期中可以從緩存獲取128比特或256比特的數(shù)據(jù)量。

開(kāi)發(fā)團(tuán)隊(duì)決定改用DOTS系統(tǒng)生成敵人,希望借此解決客戶(hù)端的性能瓶頸問(wèn)題。首先,要轉(zhuǎn)換的是ZombieView腳本中的Update函數(shù),團(tuán)隊(duì)確定了哪些代碼要?jiǎng)澐值讲煌南到y(tǒng)中,以及哪些是必要的數(shù)據(jù)。

游戲世界是一個(gè)2D網(wǎng)格,最首要的是對(duì)位置和朝向進(jìn)行插值處理。僵尸的前進(jìn)方向由兩個(gè)浮點(diǎn)值表示,最后的組件是一個(gè)目標(biāo)位置組件,它會(huì)跟蹤敵人的服務(wù)器位置。
?

  • [Serializable]
  • public? ?struct PositionData2D : IComponentData
  • {
  • ? ? public float2 Position;
  • }
  • [Serializable]
  • public struct HeadingData2D? ?: IComponentData
  • {
  • ? ? public float2 Heading;
  • }
  • [Serializable]
  • public struct TargetPositionData? ?: IComponentData
  • {
  • ? ? public float2 TargetPosition;
  • }
  • 復(fù)制代碼


    然后是為敵人創(chuàng)建Archetype原型。Archetype原型是一組屬于特定實(shí)體的組件集,也可以說(shuō)是一個(gè)組件標(biāo)識(shí)。在項(xiàng)目中,由于敵人需要使用更多的組件,而且部分組件需要游戲?qū)ο蟮囊?#xff0c;因此開(kāi)發(fā)團(tuán)隊(duì)使用了預(yù)制件來(lái)定義Archetype原型。

    他們的方法是:在ComponentDataProxy中包裝組件數(shù)據(jù),ComponentDataProxy會(huì)把數(shù)據(jù)轉(zhuǎn)化為可附加到預(yù)制件的MonoBehaviour。當(dāng)調(diào)用EntityManager執(zhí)行實(shí)例化操作,并傳入預(yù)制件時(shí),系統(tǒng)會(huì)創(chuàng)建帶有預(yù)制件上所有組件數(shù)據(jù)的實(shí)體。所有組件數(shù)據(jù)都存儲(chǔ)在稱(chēng)為“ArchetypeChunks(原型數(shù)據(jù)塊)”的16kb大小的數(shù)據(jù)塊中。

    下圖展示了原型數(shù)據(jù)塊中的組件數(shù)據(jù)流的組織方式。
    ?


    Anders Eriksson解釋說(shuō):原型數(shù)據(jù)塊的一個(gè)主要優(yōu)點(diǎn)是,系統(tǒng)不必在創(chuàng)建新實(shí)體時(shí)處理新的堆分配,因?yàn)閮?nèi)存已預(yù)先分配。因此在創(chuàng)建實(shí)體時(shí),系統(tǒng)會(huì)直接在原型數(shù)據(jù)塊的組件流末尾處寫(xiě)入數(shù)據(jù)。

    只有當(dāng)創(chuàng)建的實(shí)體數(shù)據(jù)不符合數(shù)據(jù)塊類(lèi)型時(shí),系統(tǒng)才會(huì)需要執(zhí)行額外的堆分配。在這種情況下,系統(tǒng)會(huì)創(chuàng)建新的16kb原型數(shù)據(jù)塊來(lái)進(jìn)行分配,如果有相同類(lèi)型的空原型數(shù)據(jù)塊,則會(huì)將其重新利用。隨后,系統(tǒng)會(huì)將新實(shí)體的數(shù)據(jù)寫(xiě)入到新數(shù)據(jù)塊的組件流中。

    對(duì)僵尸進(jìn)行多線程處理

    現(xiàn)在數(shù)據(jù)被緊湊地打包,并在內(nèi)存中以對(duì)緩存友好的方式布局好,開(kāi)發(fā)團(tuán)隊(duì)可以輕易利用C# Job System在多個(gè)CPU內(nèi)核上并行運(yùn)行代碼。

    下一步是創(chuàng)建可以在所有包含PositionData2D、HeadingData2D和TargetPositonData組件的原型數(shù)據(jù)塊中過(guò)濾掉所有實(shí)體的系統(tǒng)。

    為此,Anders Eriksson及其團(tuán)隊(duì)編寫(xiě)了JobComponentSystem腳本,在OnCreate函數(shù)上構(gòu)建查詢(xún)功能。

    代碼如下所示:
    ?

  • private EntityQuery m_Group;
  • protected override void? ?OnCreate()
  • {
  • ? ?? ? base.OnCreate();
  • ? ?? ? var query = new EntityQueryDesc
  • ? ?? ? {
  • ? ?? ?? ?? ???All = new []
  • ? ?? ?? ?? ???{
  • ? ?? ?? ?? ?? ?? ?? ?ComponentType.ReadWrite<PositionData2D>(),
  • ? ?? ?? ?? ?? ?? ?? ?ComponentType.ReadWrite<HeadingData2D>(),
  • ? ?? ?? ?? ?? ?? ?? ?ComponentType.ReadOnly<TargetPositionData>()
  • ? ?? ?? ?? ???},
  • ? ?? ? };
  • ? ?? ? m_Group = GetEntityQuery(query);
  • }
  • 復(fù)制代碼


    這些代碼會(huì)執(zhí)行一次查詢(xún),過(guò)濾掉所有包含位置、朝向和目標(biāo)的實(shí)體。然后,開(kāi)發(fā)團(tuán)隊(duì)通過(guò)C# Job System在每幀上調(diào)度任務(wù),將運(yùn)算過(guò)程分配到多個(gè)工作線程上。

    Andres Eriksson表示:C# Job System的優(yōu)點(diǎn)在于,C# Job System也在Unity的源碼中使用,因此我們不必?fù)?dān)心出現(xiàn)多個(gè)線程在執(zhí)行過(guò)程中同時(shí)占用相同CPU內(nèi)核,游戲賬號(hào)交易產(chǎn)生互相阻礙各自執(zhí)行的性能問(wèn)題。

    由于成千上萬(wàn)的敵人意味著在運(yùn)行時(shí)會(huì)有大量的原型數(shù)據(jù)塊要匹配查詢(xún)過(guò)程,所以開(kāi)發(fā)團(tuán)隊(duì)選擇使用IJobChunk,它可以在不同的工作線程上正確地分配各個(gè)數(shù)據(jù)塊。

    在每幀上,名稱(chēng)為“UpdatePositionAndHeadingJob”的新作業(yè)會(huì)處理游戲中敵人的位置和轉(zhuǎn)向插值。

    調(diào)度作業(yè)的代碼如下所示:

  • protected override JobHandle OnUpdate(JobHandle inputDeps)
  • {
  • ? ? ? ? var positionDataType? ?? ? = GetArchetypeChunkComponentType<PositionData2D>();
  • ? ? ? ? var headingDataType? ?? ???= GetArchetypeChunkComponentType<HeadingData2D>();
  • ? ? ? ? var targetPositionDataType = GetArchetypeChunkComponentType<TargetPositionData>(true);
  • ? ? ? ? var updatePosAndHeadingJob = new UpdatePositionAndHeadingJob
  • ? ? ? ? {
  • ? ? ? ? ? ? ? ? PositionDataType = positionDataType,
  • ? ? ? ? ? ? ? ? HeadingDataType = headingDataType,
  • ? ? ? ? ? ? ? ? TargetPositionDataType = targetPositionDataType,
  • ? ? ? ? ? ? ? ? DeltaTime = Time.deltaTime,
  • ? ? ? ? ? ? ? ? RotationLerpSpeed = 2.0f,
  • ? ? ? ? ? ? ? ? MovementLerpSpeed = 4.0f,
  • ? ? ? ? };
  • ? ? ? ? return updatePosAndHeadingJob.Schedule(m_Group, inputDeps);
  • 復(fù)制代碼


    作業(yè)的聲明如下:
    ?

  • public struct UpdatePositionAndHeadingJob : IJobChunk
  • {
  • ? ? public ArchetypeChunkComponentType<PositionData2D> PositionDataType;
  • ? ? public ArchetypeChunkComponentType<HeadingData2D> HeadingDataType;
  • ? ? [ReadOnly]
  • ? ? public ArchetypeChunkComponentType<TargetPositionData> TargetPositionDataType;
  • ? ? [ReadOnly] public float DeltaTime;
  • ? ? [ReadOnly] public float RotationLerpSpeed;
  • ? ? [ReadOnly] public float MovementLerpSpeed;
  • 復(fù)制代碼


    當(dāng)一個(gè)工作線程從隊(duì)列中抽調(diào)一個(gè)作業(yè)時(shí),它會(huì)調(diào)用該作業(yè)的執(zhí)行核心。

    下面是執(zhí)行核心的代碼:
    ?

  • public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
  • {
  • var chunkPositionData? ?? ? = chunk.GetNativeArray(PositionDataType);
  • var chunkHeadingData? ?? ???= chunk.GetNativeArray(HeadingDataType);
  • var chunkTargetPositionData = chunk.GetNativeArray(TargetPositionDataType);
  • for (int i = 0; i < chunk.Count; i++)
  • {
  • var target? ?? ? = chunkTargetPositionData[i];
  • var positionData = chunkPositionData[i];
  • var headingData??= chunkHeadingData[i];
  • float2 toTarget = target.TargetPosition - positionData.Position;
  • float distance??= math.length(toTarget);
  • headingData.Heading = math.select(
  • headingData.Heading,
  • math.lerp(headingData.Heading,
  • math.normalize(toTarget),
  • math.mul(DeltaTime, RotationLerpSpeed)),
  • distance > 0.008
  • );
  • positionData.Position = math.select(
  • target.TargetPosition,
  • math.lerp(
  • positionData.Position,
  • target.TargetPosition,
  • math.mul(DeltaTime, MovementLerpSpeed)),
  • distance <= 1
  • );
  • chunkPositionData[i] = positionData;
  • chunkHeadingData[i]??= headingData;
  • }
  • }
  • 復(fù)制代碼


    Anders Eriksson指出:你可能注意到我們使用了Select函數(shù)而不是Branch函數(shù),這樣做的原因是避免所謂的分支誤預(yù)測(cè)。

    Select函數(shù)會(huì)在兩種表達(dá)式中選擇匹配當(dāng)前條件的一種,如果表達(dá)式并不需要很多的運(yùn)算量,我建議使用Select,因?yàn)樗p便,不必等待CPU從分支誤預(yù)測(cè)問(wèn)題中恢復(fù)過(guò)來(lái)。

    使用Burst Compiler提升性能

    對(duì)于敵人位置和朝向的插值,完成DOTS轉(zhuǎn)換的最后一步是啟用Burst Compiler。

    由于已經(jīng)在連續(xù)數(shù)組中排列好數(shù)據(jù),又使用了Unity的全新Mathematics庫(kù),因此只需給作業(yè)添加上BurstCompile屬性便可啟用該功能。
    ?

  • [BurstCompile]
  • public struct UpdatePositionAndHeadingJob : IJobChunk
  • {
  • ? ? public ArchetypeChunkComponentType<PositionData2D> PositionDataType;
  • ? ? public ArchetypeChunkComponentType<HeadingData2D> HeadingDataType;
  • ? ? [ReadOnly]
  • ? ? public ArchetypeChunkComponentType<TargetPositionData> TargetPositionDataType;
  • ? ? [ReadOnly] public float DeltaTime;
  • ? ? [ReadOnly] public float RotationLerpSpeed;
  • ? ? [ReadOnly] public float MovementLerpSpeed;
  • 復(fù)制代碼


    Burst Compiler可以提供單指令多數(shù)據(jù)流(SIMD),機(jī)器指令可以對(duì)多個(gè)輸入數(shù)據(jù)集進(jìn)行操作,通過(guò)一個(gè)指令產(chǎn)生多個(gè)輸出數(shù)據(jù)集。這樣就可以在128比特大小的緩存中加入更多正確的數(shù)據(jù)。

    通過(guò)結(jié)合Burst Compiler、易于緩存的數(shù)據(jù)布局和C# Job System,開(kāi)發(fā)團(tuán)隊(duì)取得了很大的速度提升效果。

    下面是性能對(duì)比圖表展示了在每個(gè)轉(zhuǎn)換步驟后速度的變化。
    ?


    結(jié)果顯示:對(duì)于客戶(hù)端上僵尸位置和朝向的插值過(guò)程上,開(kāi)發(fā)團(tuán)隊(duì)完全擺脫了此前遇到的瓶頸。數(shù)據(jù)的排布方式會(huì)更便于緩存,而且緩存行上只有相關(guān)的數(shù)據(jù)。所有的CPU內(nèi)核都能夠投入工作,而B(niǎo)urst Compiler的輸出數(shù)據(jù)都是帶有SIMD指令的高度優(yōu)化機(jī)器代碼。

    DOTS使用技巧

    下面分享Far North Entertainment開(kāi)發(fā)團(tuán)隊(duì)對(duì)DOTS的一些使用技巧:

    使用數(shù)據(jù)流的模式進(jìn)行思考,因?yàn)樵贓CS中,實(shí)體只是用于并行組件數(shù)據(jù)流的查詢(xún)索引。

    將ECS看作關(guān)系型數(shù)據(jù)庫(kù),Archetype原型是表格,組件是行,而實(shí)體是表格內(nèi)的索引(列)。

    將數(shù)據(jù)組織到連續(xù)的數(shù)組中,從而利用好CPU緩存和硬件預(yù)取器。

    不再以創(chuàng)建對(duì)象層級(jí)作為第一件事,在弄清楚真正要解決的問(wèn)題前,制定通用的解決方案。

    要考慮垃圾回收過(guò)程。對(duì)于性能資源緊張的位置,要避免進(jìn)行過(guò)多的堆分配,并利用好Unity的Native容器。但要注意的是,此時(shí)需要手動(dòng)進(jìn)行清理過(guò)程。



    了解抽象部分的開(kāi)銷(xiāo),注意虛擬函數(shù)的調(diào)用開(kāi)銷(xiāo)。

    通過(guò)使用C# Job System,利用好所有的CPU內(nèi)核。

    熟悉面向的硬件。Burst Compiler是否生成了SIMD指令?此時(shí)要使用Burst Inspector進(jìn)行分析。

    避免浪費(fèi)緩存行。在將數(shù)據(jù)打包為UDP數(shù)據(jù)包時(shí),要考慮如何將數(shù)據(jù)打包存到緩存行上。

    針對(duì)已經(jīng)在制作階段的項(xiàng)目,Anders Eriksson的建議是:找出游戲中出現(xiàn)性能問(wèn)題的具體位置,看看能否在這些位置使用DOTS。開(kāi)發(fā)者沒(méi)必要轉(zhuǎn)換整個(gè)代碼庫(kù)。

    結(jié)語(yǔ)

    Anders Eriksson總結(jié)說(shuō):Unite大會(huì)上發(fā)布的DOTS動(dòng)畫(huà)功能、Unity Physics和Live Link讓我們感到非常興奮,我們會(huì)在游戲中更多地利用DOTS功能,希望可以將更多的游戲?qū)ο筠D(zhuǎn)換為ECS實(shí)體,而且Unity看起來(lái)在這個(gè)目標(biāo)上取得了很好的進(jìn)展。

    總結(jié)

    以上是生活随笔為你收集整理的使用DOTS制作一款第三人称僵尸射击游戏的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。