威狐小編:11 月 16 –20 日,中國(guó) Unity 線上技術(shù)大會(huì)以直播形式召開,為廣大開發(fā)者帶來(lái)了一場(chǎng)有關(guān)前沿技術(shù)和優(yōu)秀案例的線上盛會(huì)。在11月20日晚的游戲?qū)?chǎng)中,來(lái)自西山居的資深引擎開發(fā)工程師蘇泰梁,為廣大開發(fā)者詳細(xì)講解了《劍網(wǎng)3:指尖江湖》游戲客戶端開發(fā)中所運(yùn)用到的動(dòng)態(tài)骨骼技術(shù),以及性能優(yōu)化方案。
以下是演講實(shí)錄:
蘇泰梁:大家好,我先做一下自我介紹,我叫蘇泰梁,來(lái)自西山居,現(xiàn)在主要負(fù)責(zé)《劍網(wǎng)3:指尖江湖》優(yōu)化方面的工作,非常榮幸今天有機(jī)會(huì)來(lái)到Unity的線上技術(shù)大會(huì)分享。
今天主要跟大家分享的是在指尖江湖項(xiàng)目上的優(yōu)化案例,動(dòng)態(tài)骨骼DynamicBone的優(yōu)化。
我們先來(lái)看看什么是動(dòng)態(tài)骨骼?這里我們可以看兩個(gè)視頻。這是我錄制的指尖江湖創(chuàng)建角色的視頻,這是藏劍山莊門派的莊主葉英,大家可以關(guān)注一下他的衣服、頭發(fā)和擺動(dòng)效果和袖子,這個(gè)階段我拖拽角色左右晃動(dòng)一下,可以看到在整個(gè)過(guò)程中葉英的頭發(fā)、衣服的擺動(dòng)都是比較真實(shí)、自然的,這些地方都用了動(dòng)態(tài)骨骼的效果,而且基本都是動(dòng)態(tài)骨骼實(shí)時(shí)模擬的效果而不是美術(shù)K出來(lái)的動(dòng)畫。
當(dāng)然,動(dòng)態(tài)骨骼和動(dòng)作的融合也非常好,比如這段的動(dòng)作把葉英的衣服吹起來(lái),效果也還可以。接下來(lái)我們?cè)倏戳硗庖欢我曨l,對(duì)比一下葉英的衣服、頭發(fā)是怎樣的。在這個(gè)視頻中,我禁用了所有動(dòng)態(tài)骨骼的效果,相信大家應(yīng)該能看出差異,在整個(gè)轉(zhuǎn)動(dòng)的過(guò)程中,他的頭發(fā)、衣服都是硬梆梆的,沒有動(dòng)態(tài)骨骼樂(lè)觀模擬,這個(gè)效果就非常差,非常僵硬。
好,什么是動(dòng)態(tài)骨骼,動(dòng)態(tài)骨骼其實(shí)是一款名叫DynamicBone的插件,它一般用來(lái)模擬飄帶、衣袖、裙擺、頭發(fā)等的擺動(dòng)效果,效果還是比較逼真的,可以大大節(jié)省動(dòng)作K幀的工作量。在指尖江湖用的比較普遍,比如說(shuō)玩家、NBC、坐騎、各種趣物、掛件都會(huì)用上,是一個(gè)用得非常廣的功能。
右圖是指尖的一個(gè)角色姬別情,我把它身上用到動(dòng)態(tài)骨骼的地方都用數(shù)字標(biāo)識(shí)了出來(lái),1、2、3、4,有的長(zhǎng),有的短,都有動(dòng)態(tài)的效果。
我們先感受一下動(dòng)態(tài)骨骼的用法,假設(shè)我們要給姬別情最長(zhǎng)的飄帶加上動(dòng)態(tài)骨骼效果,這時(shí)候我們先找到這根飄帶的根結(jié)點(diǎn)的位置,找到它在對(duì)象樹上的根結(jié)點(diǎn),然后把它拖到動(dòng)態(tài)骨骼的組件的節(jié)點(diǎn)上,再配置一下參數(shù)就可以了,非常簡(jiǎn)單。
下面有很多參數(shù),比如說(shuō)阻尼系數(shù),有彈性系數(shù),干性系數(shù),慣性系數(shù)等等很多。阻尼的話可以理解類似一種阻力或者摩擦力,它會(huì)減少速度,彈性的話它會(huì)把你拉扯到一個(gè)目標(biāo)位置,各種參數(shù)很多,感興趣的同學(xué)可以自己研究一下。
好,我們了解了動(dòng)態(tài)骨骼的用法,再來(lái)看看簡(jiǎn)化版的模擬過(guò)程,動(dòng)態(tài)骨骼在整個(gè)過(guò)程中它的簡(jiǎn)化的模擬流程是怎樣的。
我們還是以飄帶為例假設(shè)它有5個(gè)節(jié)點(diǎn),在上一幀它是處于垂直的狀態(tài),然后在當(dāng)前幀它稍微往右移了一點(diǎn),當(dāng)然它還有一點(diǎn)點(diǎn)的偏移、旋轉(zhuǎn),一般都是由于模型的位移或者動(dòng)作帶來(lái)的。除了根結(jié)點(diǎn),其他的節(jié)點(diǎn)都會(huì)從上一幀的位置模擬,然后根據(jù)每個(gè)節(jié)點(diǎn)的參數(shù),比如前面提到的各種慣性系數(shù)、彈性系數(shù)、各種參數(shù),會(huì)對(duì)每個(gè)節(jié)點(diǎn)進(jìn)行相關(guān)的模擬運(yùn)算,然后得出一個(gè)最佳的位置。
最后,會(huì)把這個(gè)位置更新到每個(gè)骨骼節(jié)點(diǎn)上,同時(shí)根據(jù)父子節(jié)點(diǎn)的關(guān)系、位置,最新的位置,然后來(lái)修正這個(gè)旋轉(zhuǎn),這樣的話整體上看起來(lái)非常自然。
我們?cè)賮?lái)看看每根骨骼在這個(gè)過(guò)程中要做的事情。核心的代碼主要集中在組件的Update和LateUpdate中。在Update中,它需要對(duì)每根骨骼重置一下它的位置,在LateUpdate中要做大量的模擬運(yùn)算,并且最終會(huì)設(shè)置到骨骼,前面也提到。這里面列出了完整的一個(gè)模擬運(yùn)算。除了前面提到的阻尼、彈性之類的,還有風(fēng)力、重力各種模擬運(yùn)算,印刷量是比較大的。
我們假設(shè)場(chǎng)景里面有30個(gè)角色模型,每個(gè)模型有10條骨骼鏈,每條骨骼鏈有10根骨骼,那一共有3000根骨骼,在指尖江湖里20個(gè)玩家再加上坐騎、NBC之類的,3000根骨骼,這個(gè)數(shù)量比較正常。
這么多根骨骼,每根骨骼還要做這么多事情,性能怎么樣?我們可以先看看優(yōu)化前的數(shù)據(jù)。
這是小米Max2在組成動(dòng)態(tài)骨骼中的CPU消耗,它占了CPU消耗大概10%的占比,這是一個(gè)非常大的開銷。這個(gè)模塊占了總CPU消耗的十分之一,這確實(shí)是非常大的開銷。
動(dòng)態(tài)骨骼為什么會(huì)這么耗?前面提到數(shù)量非常多,一共可能有3000根的骨骼。然后它的運(yùn)算很復(fù)雜,需要做大量的模擬運(yùn)算。值得一提的是,在整個(gè)模擬運(yùn)算的過(guò)程中每根骨骼都要進(jìn)行世界坐標(biāo)、世界旋轉(zhuǎn)、世界矩陣等世界變化的獲取和設(shè)置等操作。
說(shuō)到世界變化相關(guān)的操作,在Unity里面要特別注意,因?yàn)檫@是一個(gè)非常耗時(shí)的操作。比如說(shuō)獲取和設(shè)置世界變化在Unity的頂層并沒有世界坐標(biāo)的屬性,每次只有局部坐標(biāo)的屬性。所以,每次獲取或者設(shè)置都是根據(jù)父子節(jié)點(diǎn),一層一層往上變,所以說(shuō)整個(gè)過(guò)程非常耗時(shí)。
在《指尖江湖》里面,很多骨骼的層數(shù)都是非常深的,比如這張圖。
我看了一下最深大概有20層,這里面確實(shí)掛了動(dòng)態(tài)骨骼的效果,所以它的層級(jí)很深,在計(jì)算世界坐標(biāo)、旋轉(zhuǎn)的過(guò)程中開銷是非常大的,層級(jí)越深,消耗就會(huì)越大。
最后一點(diǎn)就是它的模擬是在Update和LateUpdate中完成的,每幀都需要做,也就是說(shuō)這是一個(gè)固定的常態(tài)性能開銷。
我們了解了它為什么這么耗,現(xiàn)在介紹一下我們做的動(dòng)態(tài)骨骼第一版的優(yōu)化??梢韵瓤纯聪旅孢@三點(diǎn),這是我們做優(yōu)化的過(guò)程中經(jīng)常提起的一個(gè)三原則,第一個(gè)是優(yōu)先考慮,能否不做了,吃力不討好,白白浪費(fèi)工作量的事情最是不應(yīng)該的,尤其是在性能上。如果不做也能達(dá)到效果,那肯定是最好的優(yōu)化,都不做了,那基本上是什么優(yōu)化都沒有,什么開銷都沒有。還是得做的話,再考慮能否少做一些。最后不得不做的時(shí)候再考慮能否做得更好,這個(gè)可能有點(diǎn)抽象,我們還是具體分析一下。
下面看看《指尖江湖》的這張姬別情的圖,我在1、2、3、4上面再用紅色標(biāo)出這個(gè)骨骼的長(zhǎng)度,這個(gè)大小下我覺得四根骨骼鏈大家都會(huì)看得比較清晰。
好,我們?cè)倏催@張圖,這張圖很小。
4的話這根飄帶非常長(zhǎng),看得還是比較清晰的,但是1和2是不是已經(jīng)看不大清楚了,尤其是骨骼2我用線連起來(lái),標(biāo)出來(lái)了,其實(shí)這個(gè)時(shí)候它已經(jīng)很短,在這種情況下,即使生硬一點(diǎn),我估計(jì)看不大出來(lái)了,尤其是在手機(jī)上就更小。所以就有了我們第一版的優(yōu)化思路,根據(jù)骨骼鏈屏幕的投影程度,過(guò)短的骨骼鏈就直接關(guān)閉動(dòng)態(tài)骨骼的效果。
投影長(zhǎng)度可以使用骨骼鏈靜態(tài)的長(zhǎng)度,再加上游戲的FOV,一般動(dòng)態(tài)變化比較少。所以計(jì)算它的屏幕投影長(zhǎng)度可以做到幾乎沒有消耗。
一般手機(jī)的寬度是70毫米,大概在2毫米以下都看不大清楚。長(zhǎng)度還可以根據(jù)機(jī)型、機(jī)器情況和性能情況分不同的畫質(zhì)進(jìn)行定制,當(dāng)然還可以實(shí)時(shí)地跟進(jìn)不同的壓力情況,實(shí)時(shí)調(diào)整。
來(lái)能否少做呢?我們還是利用投影長(zhǎng)度,可以把適中的骨骼,比如說(shuō)1或者3的骨骼在某些情況下只保持最基礎(chǔ)剛性的運(yùn)算,保留最基本的效果。
最后,再考慮能否做得更好。到了這步就只能是死磕算法了,在算法層面進(jìn)行優(yōu)化,盡可能地減少消耗。這里我們使用的局部坐標(biāo),減少世界坐標(biāo)的操作,因?yàn)榍懊嫣岬搅耸澜缱鴺?biāo)的操作是非常耗時(shí)的操作,使用一些Catche來(lái)減少重復(fù)的運(yùn)算。最后就是減少Component的數(shù)量,一個(gè)角色、一個(gè)組件就可以支持多條骨骼鏈的配置,因?yàn)镃omponent的數(shù)量多存在一些常態(tài)開銷,這就是我們第一版的優(yōu)化。
我們來(lái)看看優(yōu)化后的數(shù)據(jù),效果還是比較明顯的,開銷從10%直接降到6.5%,優(yōu)化了大概35%的CPU開銷。這里在總占比中有大概3.5%的開銷,我覺得是比較可觀的。
不過(guò),6.5%的CPU開銷感覺還是挺多,細(xì)心的同學(xué)我感覺已經(jīng)注意到了,這是第一版優(yōu)化。既然有第一版,那我們就有第二版,接下來(lái)再繼續(xù)介紹一下我們第二版的優(yōu)化。
第二版的優(yōu)化,能否再進(jìn)一步優(yōu)化?最好是不做,或者是少做。這里就需要提到一個(gè)概念,就是Unity Job System,這是Unity提供的一套多線程編程框架。我們第二版優(yōu)化的核心思想就是使用多線程,盡可能地減少主線程做的事情,讓別人來(lái)干活。
那Job System是Unity提供的一套多線程編程框架,跟一般的多線程有什么不同?為什么Unity需要額外提供一套多線程框架?這個(gè)問(wèn)題在Unity中寫過(guò)多線程的人可能都遇到過(guò)一個(gè)坑,一般的線程沒辦法操作Unity對(duì)象,這是Unity的強(qiáng)制限制了。Unity會(huì)報(bào)錯(cuò),并且告訴你說(shuō)這個(gè)只能在主線程訪問(wèn),直到Job System出現(xiàn),才使得這成為可能。雖然說(shuō)現(xiàn)在局限挺大,但是起碼有可能。這就是第一個(gè)不同,它使得多線程中操作Unity對(duì)象成為可能。
第二個(gè),它有強(qiáng)大的線程安全檢測(cè)機(jī)制,這個(gè)非常重要。比如主線程和Job線程之間一些數(shù)據(jù)的讀寫安全問(wèn)題,有強(qiáng)大的檢測(cè)機(jī)制,保證的數(shù)據(jù)不會(huì)寫壞。
學(xué)過(guò)多線程的人我估計(jì)都清楚,線程數(shù)據(jù)安全是一個(gè)非常頭疼的事情,比如C++里面如果一塊數(shù)據(jù)在多線程里被寫壞了,它可能不會(huì)立即出現(xiàn)問(wèn)題,不知道跑到什么時(shí)候突然就宕機(jī)了,這個(gè)時(shí)候你再查就非常困難,因?yàn)樗呀?jīng)不是第一現(xiàn)場(chǎng),很早之前就已經(jīng)被寫壞了,這是一個(gè)非常頭疼的事情。但是在Job System上可能就不存在這個(gè)問(wèn)題,它的安全檢測(cè)機(jī)制非常簡(jiǎn)單。
第三點(diǎn),它還有非常高的性能,可以充分地利用多核的CPU。Job System跟Unity引擎頂層的C++共享work線程池,work線程池會(huì)通過(guò)一個(gè)Job隊(duì)列來(lái)減少上下文的切換和競(jìng)爭(zhēng)問(wèn)題,這樣就可以充分地利用多核CPU的資源,從而提高性能。當(dāng)然,它還有別的優(yōu)勢(shì),我們后面再繼續(xù)介紹。
接下來(lái)感受一下Job System簡(jiǎn)單的用法。在這里,并行計(jì)算兩個(gè)數(shù)據(jù)原始物中的一個(gè)值相加,然后復(fù)制到另外一個(gè)數(shù)據(jù)。首先我們要生成一個(gè)Job,然后集成Unity I Job相關(guān)的接口,只用特定的數(shù)據(jù)結(jié)構(gòu)證明自己的數(shù)據(jù),就可以在Execute函數(shù)中寫需要在Job中運(yùn)行的邏輯。這個(gè)例子也非常簡(jiǎn)單,Execute里面就是把A和B的數(shù)據(jù)元素直接相加,然后復(fù)制到C的數(shù)據(jù)中。
最后,寫的Job是可以通過(guò)Schedule這個(gè)函數(shù)把自己的Job推到work線程進(jìn)行執(zhí)行,使用上非常簡(jiǎn)單和直觀。
我們?cè)賮?lái)看看動(dòng)態(tài)骨骼Job化的示意。這個(gè)是簡(jiǎn)化后偽代碼,是按原算法直接轉(zhuǎn)化后的一個(gè)示意。我這里會(huì)將耗時(shí)的所有操作都Job化。比如Update中的InitTransform操作,它會(huì)需要從這幾步坐標(biāo)。還有就是lateupdate中的各種模擬運(yùn)算,它對(duì)應(yīng)的就是update particle1函數(shù),update particle2、ApplyParticles ToTransforms,它都會(huì)提取到對(duì)應(yīng)的Job中。
但實(shí)際上直接轉(zhuǎn)化成Job是存在不少問(wèn)題的。我們?cè)谥苯愚D(zhuǎn)換的基礎(chǔ)上做了很多的加速優(yōu)化,這個(gè)后續(xù)會(huì)有介紹。
先來(lái)看看Job優(yōu)化后的數(shù)據(jù),這是經(jīng)過(guò)很多版優(yōu)化后的數(shù)據(jù),相對(duì)于上一版的話優(yōu)化了84%的CPU時(shí)間,效果是非常非常明顯的。
左邊是優(yōu)化前,右邊是優(yōu)化后的Profiler數(shù)據(jù),我現(xiàn)在已經(jīng)用紅線把work線程的執(zhí)行情況標(biāo)識(shí)出來(lái)。我們可以看到左側(cè)優(yōu)化前的work線程一直處于idol狀態(tài),什么事都沒做。但是,主線程其實(shí)壓力是非常大的,因?yàn)樗械臇|西都放在了主線程來(lái)做。
右邊的話是我們Job化后的情況,可以看到所有的Job都在緊密地連接,緊密地執(zhí)行,而且很好地分布到了所有work線程,充分利用了多核的并行加速的效果。
當(dāng)然,這兩幅圖的時(shí)間軸單位是不一樣的,右側(cè)是我們不斷地放大后的數(shù)據(jù),主要是為了讓大家看清楚Job的執(zhí)行情況。實(shí)際上相對(duì)左圖的話,如果按單位來(lái)算,大概只有1毫米那么寬,大概是0.15毫秒左右。
這個(gè)是我們?cè)谠嬷苯愚D(zhuǎn)化到Job的基礎(chǔ)上經(jīng)過(guò)多版優(yōu)化后的數(shù)據(jù)。我們用到了一些比較關(guān)鍵的加速手段。接下來(lái)給大家介紹一下最關(guān)鍵的幾個(gè)加速手段。
第一個(gè),讓Job真正并行起來(lái)。這個(gè)不知道大家看到后有什么感覺,為什么說(shuō)讓Job真正并行起來(lái)呢?難道Job不是并行嗎?我們先看一段我們?cè)?jīng)一版的數(shù)據(jù)。我們這一版數(shù)據(jù)說(shuō)實(shí)話優(yōu)化完后發(fā)現(xiàn)優(yōu)化后的數(shù)據(jù)比優(yōu)化前的數(shù)據(jù)還差。大家可以看到上面是主線程,下面是Job線程,主線程一直在等待Job線程執(zhí)行。為什么?這是因?yàn)閁nity有一個(gè)問(wèn)題,Transform相關(guān)的Job只要Transform都在同一個(gè)根結(jié)點(diǎn)下,它都是沒辦法進(jìn)行的。這是所有Transform Job繞不開的一個(gè)話題。
可能這里有點(diǎn)抽象,我們?cè)倏匆幌挛覀兊氖褂们闆r。我們所有的player都放在了一個(gè)PlayerSet的分節(jié)點(diǎn)上,在同一個(gè)根結(jié)點(diǎn)下的對(duì)象Transform,它們之間的操作都是不能并行的,所以即使我們每個(gè)player對(duì)應(yīng)一個(gè)Job,但是實(shí)際上跑起來(lái)是沒辦法并行的,因?yàn)樗麄兌荚谕粋€(gè)根結(jié)點(diǎn)上。
這會(huì)帶來(lái)什么問(wèn)題呢?主要有兩個(gè)問(wèn)題。一個(gè)是主線程跟Transform Job是沒辦法并行的,會(huì)出現(xiàn)Wait的情況。對(duì)應(yīng)到左下圖,可以看到紅框1里的函數(shù)就是WaitForJob GroupID。這個(gè)是因?yàn)橹骶€程中有一個(gè)跟Transform相關(guān)的操作,大家應(yīng)該可以看到這上面Transform.Get_hasChange。它的核心就是因?yàn)樗麄冞@個(gè)操作的Transform和Job中的Transform是屬于一個(gè)根結(jié)點(diǎn),這個(gè)時(shí)候主線程就需要等待相關(guān)的Job線程,相當(dāng)于沒有在并行,因?yàn)橹骶€程一直在等待。
第二個(gè)問(wèn)題就是Transform Job之間也是沒辦法并行的。即使你把Transform相關(guān)的幾個(gè)Job推到Job線程來(lái)執(zhí)行,但是Job線程之間的Transform如果還是屬于一個(gè)根結(jié)點(diǎn),它們自己也沒有辦法并行。
對(duì)應(yīng)到左下圖紅框2的Profiler數(shù)據(jù),這一段數(shù)據(jù)大家可以看到其實(shí)它只在一個(gè)Job線程中執(zhí)行,并沒有像我們前面看到的Profiler數(shù)據(jù)一樣會(huì)分散到所有的work線程進(jìn)行執(zhí)行,還是只在一個(gè)Job線程上執(zhí)行,就是它沒有充分利用多核的并行,相當(dāng)于是一個(gè)單線程的Job,這樣效率是非常低的。
為什么會(huì)有這個(gè)限制呢?實(shí)際上父子節(jié)點(diǎn)Transform操作存在關(guān)聯(lián)性,前面提到了Transform的世界坐標(biāo)、操作指令,所以說(shuō)它可能存在一些安全問(wèn)題,這樣的話這個(gè)線程還是比較能理解。但是非父子關(guān)系的節(jié)點(diǎn),我猜也許為了邏輯統(tǒng)一和簡(jiǎn)單。
那怎么樣讓Transform Job并行起來(lái)?其實(shí)方法也很簡(jiǎn)單,我們可以將所有的模型都評(píng)估到頂層,比如我們可以把PlayerSet去掉,直接把player全部平鋪到頂層,這樣所有的player之間都可以并行。但是,一個(gè)player可能會(huì)有十多根骨骼鏈,每根骨骼鏈上面還有十幾個(gè)節(jié)點(diǎn),這些都沒辦法并行。當(dāng)然,我們還可以再進(jìn)一步,將每條骨骼鏈平鋪到頂層,比如說(shuō)player有個(gè)尾巴,我直接把這條尾巴拉到頂層。這樣的話,如果它有多條骨骼鏈,所有的骨骼鏈之間每個(gè)角色都是可以運(yùn)行的,當(dāng)然極限情況是我們還可以把這個(gè)骨骼鏈中的每個(gè)節(jié)點(diǎn)都平鋪到頂層,這樣的話所有的骨骼節(jié)點(diǎn)在整個(gè)過(guò)程中都是可以并行。
這很完美,但骨骼平鋪可能會(huì)導(dǎo)致關(guān)聯(lián)的骨骼動(dòng)作失效。這是什么問(wèn)題,什么意思?在某些情況下,動(dòng)作就會(huì)失效,沒動(dòng)作。為什么會(huì)出現(xiàn)關(guān)聯(lián)的動(dòng)作失效的問(wèn)題?我們先來(lái)看看這幅圖。
左邊是角色的對(duì)象數(shù),右邊是一個(gè)動(dòng)作文件。這個(gè)動(dòng)作文件它綁定的tail4、tail5這兩根骨骼。Unity在Animator中會(huì)根據(jù)動(dòng)作文件中對(duì)應(yīng)的每根骨骼的名字在左邊的對(duì)象數(shù)中進(jìn)行查找,然后一層一層往上找,找到對(duì)應(yīng)的節(jié)點(diǎn)就可以綁定成功了,如果沒有找到,它就綁定失敗。
當(dāng)然,實(shí)際上是用的名字的哈修,效率肯定會(huì)高不少。然后Animator在每次Enable都會(huì)做一次綁定,還有一些別的情況下也會(huì)觸發(fā)綁定。所以說(shuō)如果在觸發(fā)綁定的時(shí)候把左邊的節(jié)點(diǎn)移走,這個(gè)時(shí)候動(dòng)作的綁定可能就會(huì)找不到而失敗,動(dòng)作就沒有效果。
那怎么解決?我們通過(guò)源碼發(fā)現(xiàn)可以在引擎頂層加用Catche或者是指定映射關(guān)系來(lái)解決。什么意思,它還是通過(guò)左側(cè)對(duì)象數(shù)查找的時(shí)候可以優(yōu)先使用映射的關(guān)系或者是Cache中的數(shù)據(jù),這樣的話即使我們把它的骨骼鏈或者節(jié)點(diǎn)平鋪到頂層,這個(gè)時(shí)候它重新綁定不會(huì)有任何的影響。
當(dāng)然最后哪一種平鋪策略更合適?具體還是根據(jù)實(shí)際情況定的。在《指尖江湖》模型級(jí)別的平鋪并行效果已經(jīng)非常好。當(dāng)然,這跟后續(xù)的各種加速手段的優(yōu)化也是分不開的。
接下來(lái)我們繼續(xù)介紹下一個(gè)加速手段。
這里再介紹讓Job真正并行起來(lái)的另外兩個(gè)非常重要的加速手段。我們可以先看看左側(cè)兩個(gè)Job代碼。上面的Job代碼是從Transform中獲取局部坐標(biāo)和局部旋轉(zhuǎn)的代碼。下面那個(gè)Job代碼是局部坐標(biāo)和局部旋轉(zhuǎn)映射回Transform中,這兩個(gè)Transform就是我們跟Transform相關(guān)的Job的代碼,已經(jīng)優(yōu)化到了一個(gè)極其簡(jiǎn)單的程度。
我們?yōu)槭裁匆堰@兩個(gè)Transform相關(guān)的代碼優(yōu)化到這么簡(jiǎn)單?前面提到Transform Job在相同的root點(diǎn)下是沒辦法并行的,很容易跟主線程出現(xiàn)一些wait,所以我們減少Transform相關(guān)Job的邏輯,這個(gè)時(shí)候它跟主線程出現(xiàn)wait的可能性就會(huì)降低。當(dāng)然,即使出現(xiàn)wait,可能wait的時(shí)間也不會(huì)太長(zhǎng),因?yàn)樗浅:?jiǎn)單,執(zhí)行起來(lái)非常快。
還有就是非Transform相關(guān)的Job是沒有Transform并行的限制,這樣我們就可以把大量的邏輯,幾乎所有的邏輯全部移出來(lái),然后放到非Transform一般的Job中執(zhí)行,一般的Job沒有這個(gè)限制,它可以充分地利用work多核的性能,然后充分地并行,這樣它就會(huì)大大提速,因?yàn)樗鼪]有跟Transform或者是root節(jié)點(diǎn)的限制,這就是我們?yōu)槭裁匆堰@個(gè)東西單獨(dú)拎出來(lái)并且把它簡(jiǎn)化到極致。
當(dāng)然,這里面可能細(xì)心的同學(xué)還注意到設(shè)置的時(shí)候最后把它設(shè)置到Transform也是用了局部坐標(biāo),其實(shí)在前面也介紹過(guò),官方在整個(gè)代碼中用了大量的世界坐標(biāo)、世界旋轉(zhuǎn)、世界矩陣的獲取和設(shè)置的操作。其實(shí)這些都是非常耗時(shí)的操作,我們只獲取了設(shè)置局部坐標(biāo)和旋轉(zhuǎn)。我們也對(duì)比過(guò)相應(yīng)的性能,世界坐標(biāo)跟局部坐標(biāo)設(shè)置的對(duì)比,我們對(duì)比了兩者的性能,局部坐標(biāo)大概有80%以上的性能的提升,提升效果非常明顯。
我們?cè)倏纯从覀?cè)的加速手段。這里主要是利用腳本的執(zhí)行順序可以做到一些真正并行加速的效果。比如上面的1就是讓Job在所有腳本中開始執(zhí)行,這樣的話,這個(gè)Job線程就可以跟主線程有最大的并行的時(shí)間,橫寬的話就是update相關(guān)的Job,把它放到了所有腳本的最前面。
另外一個(gè)我們可以通過(guò)腳本執(zhí)行順序決定Job具體和哪些腳本或者哪些模塊并行。這樣有什么好處?我們可以巧妙地避開這個(gè)wait。比如這里下面的紅框我把動(dòng)態(tài)骨骼放在了UI Pannel的前面,用過(guò)NGY的人都知道UI Pannel是NGY中管理所有UI組件的一個(gè)模塊,在它的lateupdate中會(huì)做大量的核批和填充的操作,這些操作非常耗時(shí)。但在這個(gè)過(guò)程中它肯定沒有用到動(dòng)態(tài)骨骼的效果,基本上不會(huì)在界面上畫動(dòng)態(tài)骨骼的效果。
它也是一個(gè)非常耗時(shí)的操作,這樣動(dòng)態(tài)骨骼跟它并行的話就可以完美地錯(cuò)開。因?yàn)樗挥玫絼?dòng)態(tài)骨骼的組件,并且它在我們游戲中又是獨(dú)立的根結(jié)點(diǎn),不會(huì)跟別的玩家或者是NPC組一個(gè)同樣的根結(jié)點(diǎn),這樣它就可以完美地跟DynamicBone進(jìn)行并行,不出現(xiàn)wait。這些加速就可以大大地緩解Transform Job可能出現(xiàn)的wait的情況。
接下來(lái)我們?cè)俳榻B一下加速手段2,它可以大大提速Job的代碼效率。這里的核心是使用Burst Compiler和Mathematics數(shù)學(xué)庫(kù)的加速。Burst是Unity的一個(gè)代碼編譯優(yōu)化工具,它可以針對(duì)目標(biāo)機(jī)器進(jìn)行專門的優(yōu)化。數(shù)學(xué)庫(kù)的話它是支持SMD的加速,SMD就是一條指令可以同時(shí)操作多個(gè)數(shù)據(jù)。一般一條指令只操作一個(gè)數(shù)據(jù),它因?yàn)榭梢酝瑫r(shí)操作多個(gè)數(shù)據(jù),那在3D運(yùn)算中,比如向量或者矩陣運(yùn)算的加速效果非常明顯。
我們看一下兩者數(shù)據(jù)的差別,上圖是沒有開Burst的效果,下面是開了Burst的效果。這里的提升是不是非常的夸張,我第一次也被震驚了,這里起碼50倍以上的性能差異,提升效果非常明顯。
前面我們看數(shù)據(jù)實(shí)際上給過(guò)一張圖,其實(shí)是下面這張圖放大后的效果,它確實(shí)只有這么一點(diǎn)點(diǎn),當(dāng)然這完全得益于這兩者的加速,如果沒有這兩者的加速,前面即使搞定了并行,估計(jì)Job線程還是算不出來(lái),可能主線程還需要一些等待。
使用上非常簡(jiǎn)單,Burst和Mathematics都是屬于Unity Pacage中的一個(gè)包,就需要在Package Manager中把這兩個(gè)導(dǎo)入,在Burst Compiler是需要優(yōu)選相應(yīng)的包,然后在自己的Job上聲明一個(gè)Burst Compiler就可以了,非常簡(jiǎn)單。
下面的數(shù)學(xué)庫(kù)也只需要把它的庫(kù)引用進(jìn)來(lái),然后再使用它的類型就好。比如說(shuō)使用Float3來(lái)替換一般的我們平時(shí)用的Vector3,這樣的話這兩個(gè)加速就可以用起來(lái),當(dāng)時(shí)的效果極其明顯。
最后給大家再介紹一些加速手段,一個(gè)是利用面向數(shù)據(jù)的設(shè)計(jì)可以減少Cache Missing,核心的話還是利用Unity ECS的思想,盡可能讓Job操作數(shù)據(jù)是連續(xù)的。優(yōu)化前其實(shí)每個(gè)動(dòng)態(tài)骨骼都是個(gè)組件,數(shù)據(jù)在內(nèi)存里面都是分散的。當(dāng)然,每根骨骼更新的時(shí)候都需要從不同的內(nèi)存位置來(lái)拿數(shù)據(jù),所以它的Cache Missing非常嚴(yán)重。
用Job的話,我們就可以把游戲中所有動(dòng)態(tài)骨骼數(shù)據(jù)都存在一個(gè)連續(xù)的數(shù)據(jù)中,這樣Job就會(huì)逐個(gè)處理數(shù)字中的數(shù)據(jù),因?yàn)閿?shù)字的內(nèi)存是連續(xù)的,所以可以大大地降低Cache Missing,提高數(shù)據(jù)訪問(wèn)的性能。
第二點(diǎn)就是盡量地減少Job的數(shù)量,Schedule也需要時(shí)間開銷。這里可以看一張圖,這是我們?cè)?jīng)的一版優(yōu)化,我們拆分了很多的Job,最后發(fā)現(xiàn)Schedule的時(shí)間需要很多,有點(diǎn)得不償失。
第三點(diǎn)是盡可能減少數(shù)據(jù)拷貝,可能需要拆分動(dòng)態(tài)數(shù)據(jù)和靜態(tài)數(shù)據(jù)。前面也看到了Transform相關(guān)的Job拆分到Job之后所有的數(shù)據(jù)在主線程做的事情其實(shí)是非常少的,經(jīng)驗(yàn)、核心的邏輯和計(jì)算都放到了Job上進(jìn)行。主線程只剩下更新數(shù)據(jù),并且把更新數(shù)據(jù)到Job,把Job推向work線程,這么簡(jiǎn)單的事情。但是可能在動(dòng)態(tài)骨骼上的一些數(shù)據(jù)更新頻度還是有點(diǎn)高的,一些位置相關(guān)的東西,可能更新比較頻繁。
Job用過(guò)的人可能都知道,它只支持Structs數(shù)據(jù)類型,而沒辦法使用Calsses,就是它只支持子類型,不能支持引用類型,大家都知道Structs類型的數(shù)組是沒辦法單獨(dú)修改元素中某一項(xiàng)屬性,就類似于修改Transform.Position一樣,只能整體布置,沒辦法單獨(dú)直接修改,比如直接修改Transform.Position.x,這是沒有效果的,因?yàn)閜osition它返回的是一個(gè)Structs。
所以我們這里只能用Structs存儲(chǔ)數(shù)據(jù),并且它還需要存在數(shù)據(jù)中,Structs數(shù)據(jù)結(jié)構(gòu)如果全部塞在一起,它的數(shù)據(jù)結(jié)構(gòu)是很大的,因?yàn)榍懊婵吹剿母鞣N參數(shù)數(shù)據(jù)量是非常大的,這樣需要頻繁復(fù)制、拷貝是需要很多時(shí)間,而且這個(gè)是實(shí)打?qū)嵉脑谥骶€程需要做的事情。所以這就需要做到數(shù)據(jù)的動(dòng)態(tài)分離,拆動(dòng)態(tài)數(shù)據(jù)和靜態(tài)數(shù)據(jù),現(xiàn)在動(dòng)態(tài)數(shù)據(jù)更新的數(shù)據(jù)盡可能地少一些,靜態(tài)數(shù)據(jù)基本上都不需要更新,就可以大大地提升性能。
這就是我們最核心的一些加速手段。
最后我們看一下整個(gè)Job優(yōu)化后數(shù)據(jù)的總結(jié)。假設(shè)原版的數(shù)據(jù)是100%,我們?cè)谧龅谝话鎯?yōu)化之后能減35%,最后通過(guò)Job優(yōu)化相對(duì)原版就可以減90%的優(yōu)化。這個(gè)效果還是非常明顯的,它意味著優(yōu)化前和優(yōu)化后有10倍的性能差異,效果很好。但實(shí)際上這一版數(shù)據(jù)是直接跳過(guò)了第一版優(yōu)化的數(shù)據(jù)。
實(shí)際上我們這版數(shù)據(jù)還沒有考慮到屏幕的裁減等優(yōu)化的數(shù)據(jù),當(dāng)然結(jié)合第一版優(yōu)化的數(shù)據(jù),我們?cè)谟螒蛑幸呀?jīng)有了,但是由于時(shí)間關(guān)系,這個(gè)數(shù)據(jù)沒有準(zhǔn)備上。
最后,我們?cè)賮?lái)回顧一下今天講的內(nèi)容。這里講了DynamicBone兩版的優(yōu)化,第一版優(yōu)化主要是常規(guī)的優(yōu)化手段,根據(jù)骨骼鏈的屏幕投影長(zhǎng)度來(lái)決定它是關(guān)閉還是減少運(yùn)算。第二是在算法上優(yōu)化,局部坐標(biāo)替代世界坐標(biāo)或者是加入Cache等等。
第二版優(yōu)化主要是使用多線程,使用Job System,然后可以通過(guò)平鋪?lái)攲觼?lái)解決Transform Job無(wú)法并行的問(wèn)題。另外就是可以簡(jiǎn)化和拆分Job和腳本的執(zhí)行順序,解決Job的并行問(wèn)題。另外,我們可以通過(guò)Burst Compiler和Mathematics數(shù)學(xué)庫(kù)進(jìn)行加速。最后是介紹了面向數(shù)據(jù)的設(shè)計(jì)和節(jié)省Job輸入量,還有動(dòng)靜分離的數(shù)據(jù)。
今天的分享就到這,謝謝大家,大家有任何問(wèn)題可以在評(píng)論區(qū)提問(wèn)和討論。
404726