一個遊戲程序員的堅持 —— 論向量化編程

2020年12月7日,由騰訊遊戲學院舉辦的第四屆騰訊遊戲開發者大會(Tencent Game Developers Conference,簡稱TGDC)於線上舉行。來自重慶帕斯亞科技的CTO謝怡欣先生,分享了他對於向量化編程的一些看法。以下是分享視頻和文字實錄:

大家好,我是來自重慶帕斯亞科技的謝怡欣。

首先,我想向大家簡單介紹一下我的經歷。之前我在加拿大温哥華工作了幾年,是Offworld Industries的一個高級程序員,參與制作了一款射擊遊戲《Squad》。我還做過一個手遊,叫《Lionheart Tactics》。現在我在重慶帕斯亞科技擔任技術負責,帕斯亞科技是2011年成立的,我們專注於高創意度的沙盒獨立遊戲。我先後參與開發了《星球探險家》、《波西亞時光》和《超級巴基球》這些作品。

一個遊戲程序員的堅持 —— 論向量化編程

在演講開始之前,我想先給大家看一個視頻。大概在2016年的時候,我在Youtube上看到的這個視頻,來自於一個叫Mike Acton的程序員。他在視頻裏面就講述了一個他稱之為面向數據編程的概念。當時我看到那個視頻是非常的不自在,因為2016年的話,應該是屬於我剛熟悉面向對象編程的階段。當時他説的那些東西,基本上就是説你學的那些東西,基本都是“垃圾”,都沒什麼意義。這確實很難讓別人接受。但是我在潛意識當中,卻又覺得這個人説的話,某些觀點是挺有意義的。

我接下來要跟大家分享的視頻,就是他做技術分享之後的問答環節。我們可以看到一個比較年長的業界人士,對他分享觀點的一些質問。結果Mike Acton就直接把話給他懟回去了。請先看一下這個視頻:

確實態度不算特別的友好。一個很偶然的機會,我和一個做HR的小夥伴,就聊到了這個事情。我和她一起看了這個視頻,她就提出一個觀點,就是這個穿橘黃色衣服的Mike Acton,他的姿態是比較有攻擊性的。就是他很喜歡把手舉到肩膀以上,這樣的話實際上是給觀眾一個信息,就是我説的東西,或者是我的地位比你們要高。可能這也是我當時聽到他説的話,覺得難以接受的原因之一。當然,如果説有機會Mike Acton能看到我今天的演講的話,我想説我肯定不會有任何的不敬,我非常欣賞和佩服您的才華和知識。

這裏有另外一個鏈接,這個鏈接就是很早以前微軟一個寫Windows的程序員他的一個抱怨。他就是講Windows為什麼比其他的操作系統慢。他説因為Windows程序實際上是由於商業化開發,然後迭代了很多很多次,真正寫了很多代碼的那些程序員,可能早就被類似亞馬遜和谷歌這些公司挖走了。剩下的一些程序員,都是相對來説沒那麼多經驗,改代碼也不知道從何下手的。

一個遊戲程序員的堅持 —— 論向量化編程

從項目管理的角度説,實際上做代碼清理是不產生任何價值的。如果是增加新的功能,那就是增加它的價值。但如果只是清理代碼,可能就不會被你的領導所器重。

然後説一下面向對象編程。在我看來,根據程序員自身的水平,他對於這種編程的理解是相差比較大的。就算是很高級很有經驗的程序員,他對面向對象編程的一些設計模式,也會有一些細微的差別。在經過多次修改之後,肯定也都會產生剛才那個程序員抱怨的那種大片大片的死代碼。但是你又不是很敢刪,我相信很多在座的程序員都有過這樣的經歷。

此外,面向對象編程實際上對緩存是很不友好的,但是這一部分資料網上有很多,我就不再贅述了。還有一個點是,我不知道大家有沒有發現,現在主流的遊戲軟件、遊戲程序或者説應用程序,都只是用了一到兩個線程,很少有多線程能得到充分利用的。現在大多數的中高端硬件,都是支持十個以上的線程,而向量化編程的話,實際上是可以充分利用這些計算資源的。

接下來,我想説一下為什麼需要了解向量化編程。在我看來,向量化編程實際上是提高程序員的內力。內力是什麼東西?就比如説張無忌他的內功深厚,他學了九陽神功之後,感覺他學其他的武功都很快,基本上就是信手拈來。如果你作為一個程序員,有很強的內功的話,那你要學那些比如説遊戲客户端、服務器、全棧工程師,包括多線程編程什麼的,都會變得很容易。我覺得至少從我一段時間的學習經歷來看的話,我覺得真的是有這種效果的。

一個遊戲程序員的堅持 —— 論向量化編程

還有一點,簡單説一下。向量化編程實際上是簡化了多線程加鎖的邏輯,基本上是沒有什麼鎖的。或者説,鎖這個概念已經從框架層就給你模糊掉了,你是基本用不到這種東西的,所以説是一個很好的簡化。

向量化編程提高了代碼的可讀性。大家可以想一下,比如説你有一個函數,函數里面有很多很多行,實際上每一個行都是一個節點,然後每一個節點如果説是調用另外一個函數,它實際上在那個地方,就是一個分支。它就是可能分到另外一個深度的指數下面去了。那個東西又可能調用其他的函數,就分得更細一點。面向對象編程實際上是非常複雜的,基本上是比較鼓勵這種分支。

向量化編程實際上它也是一個樹形結構,但是相對來説要平坦很多,就是樹的複雜程度要簡化很多,也比較線性化。還有一點是,作為程序員,你學面向對象編程也就學幾年,我覺得應該差不多掌握以後,就可以考慮去學習一些新的技術和新的研究方向了。我覺得向量化編程就是一個不錯的選擇。

説到向量化編程,就不得不借助ECS框架。ECS在網上實際上是有非常多的很成熟的教程的。就是説為什麼它的速度快,這些緩存、數據對齊這一系列東西,在這裏我就不再贅述了。我就做個很簡單的介紹,然後再加上我自己的一些理解。

一個遊戲程序員的堅持 —— 論向量化編程

ECS就是Entity Component System的一個縮寫。Entity就是數字,是一個索引。Component就是組件,就是純數據的東西。當然這裏實際上在我們的實際開發當中,Component上面也可以帶一些簡單的方法,但是那個方法只是管自己的邏輯,就不會涉及到和其他數據類型的交互。肯定就是説Component也不會含有指針或者是任何複雜的那種數據類型。它可以是數組,可以是Entity,也可以是引用到另外的一個Entity,這個是沒問題的,因為Entity也是數字對吧。System就是系統,它是對指定Component結合的Entity進行數據變換。

在這裏我想説一下,它跟傳統的Object Oriented Programming差別沒有想象中的那麼大,從概念上幾乎是一樣的。Entity對應那邊就是Object,Component對應那邊可能就是Object上面的一個屬性。你像比如説一個英雄,在ECS的話,英雄可能就是Entity,他的那些屬性,就是那些Component。在面向對象編程的話,英雄就是Object,他的那些屬性,比如説他的Class裏面,可能有其他的一些字段。那麼從概念上面來講,這個基本上是一對一的。區別就是在於面向對象變成裏面的那些方法,實際上是和它的類是寫到一塊的。在那個方法裏面,基本上是想怎麼來就怎麼來。就是你想訪問什麼樣的數據,你就訪問什麼樣的數據,沒有什麼規定。ECS裏面的System的話,對數據的訪問是非常嚴格的。這也就是可能會勸退很多程序員的一個點。

一個遊戲程序員的堅持 —— 論向量化編程

我想再引入一個維度,就是從頻率這個維度來看。遊戲邏輯的編程頻率維度可能分低頻率和高頻率。低頻率時間基本上可以把它歸納成在一幀裏面,就能夠開始並且結束的。就是完成它所有的數據轉換的一個事件,就比如説開始播一個動畫,結束播一個動畫;怪物的產生,或者是説死亡;或者是説按了一個什麼鍵,這些都是低頻事件。高頻事件,就是一個持續的連續的行為。比如説一個角色,他在一直不停地動,説着是説不停地在播一個動畫,他需要每一幀都去維護。這個東西肯定也可以從函數的入口來判斷,比如説是Update,一般Update就是比較高頻,做按鍵的檢測或者鼠標的檢測,都是在Update裏面。那麼檢測實際上也是個高頻操作。但是至於檢測到按鍵之後做的那些事情,那個就是低頻事件。

我想再説明一點,就是從頻率這個維度來講,向量化編程可能是比較初級的。這是我在摸索過程當中,尋找出來的一條路徑。我不排除有其他更好更高效的維度,我就想引入相對來説比較簡單的編程實例。這個例子就是在遊戲當中,比如説你有NPC,他可能每一幀都要去檢測他的視野範圍裏面有沒有其他陣營裏的人。如果有,可能這個NPC就需要做一些反應。這段代碼是偽代碼,簡化了很多的一個版本。我只是想讓大家能夠看一看就好了。在Update裏面,就是做一個物理上面的查詢,Get OverlapSphere,把自己坦克的位置放進去,然後把自己的視野半徑放進去,最後看Collider,就是有沒有碰撞體。要是有碰撞體的話,在它上面去再去拿一個看它有沒有Tank的這個Component。如果説有,再生成特效,生成飛行道具,播一些音效這些之類的東西,這一段代碼大家可以看一下。

一個遊戲程序員的堅持 —— 論向量化編程

實際上,Physics.GetOverlapSphere,它實際上是一個很高頻的操作。就是我不管你這個坦克,只要是活着的,只要在那裏沒有做其他事情,它就會執行這個代碼。在Colliders Length大於0那一段代碼裏面,它實際上是一個低頻代碼,你真正遇到敵人了,你才會觸發的邏輯,就是這樣的一個高頻和低頻的分段。

然後就是向量化編程的一個實例,這個我引用了Unity dots最新出的Date-Oriented...TechStack的一些API。肯定也不是很完整,如果大家真的要去用的話,可能也要去參考一下他們官方網站上面的一些文檔,這裏我大概有那個意思就行了。

一個遊戲程序員的堅持 —— 論向量化編程

第一段就是EntityManager GetCompoentArray,就是把所有的坦克,比如説你有100個坦克、1000個坦克,把所有坦克的位置信息,放到一個數組裏面。第二句就是把它的視野半徑放到一個數組裏面。第三個就是説把它周圍有沒有東西這個狀態,放到一個數組裏面。然後那個Entities for Each就是dots,就是ECS很標準化的向量化的一個操作。就是把所有的坦克,它的位置還有它的視野做一個查詢。使用C++比較多的小夥伴就會發現,這個Physics.GetOverlapSphere實際上是一個const,是一個常稱之為常量函數的東西。它這個函數是不會改變任何狀態的,這種函數實際上是在向量化編程裏面是非常友好的。因為它不涉及到racing condition,就是不會產生那種比如説一個線程在讀一個內存,或者是説同時另一個線程又在往那個內存或者説往那個變量裏面寫東西的那種情況。這種對於多線程是非常友好的。

一個遊戲程序員的堅持 —— 論向量化編程

下面那個HITS,相當於是把它返回的結果就放到那個裏面。如果説你有十個盒子十個線程,然後這裏面有1000個坦克,每一個線程可能分到的就是100個坦克,那麼他們就分配1000個數組。第一個線程可能就是填充HITS數組從0到99的位置。以此類推,Schedule就是做這個事情的。最後那個Complete Dependency就是它主線程上面的一個阻塞。它的意思就是説等所有線程的工作全部都做完了以後,我們的主線程然後再開始往下面走。接下來就是和剛才的那種順序化寫法是一模一樣的。就是我現在有這個結果,我怎麼去做響應,比如説create effect,就是產生特效,創造飛行道具,或者是説播音效也好,這些東西就跟之前的實際上是一樣的。大家可以發現這個東西實際上就是在向量化一個高頻的操作,然後低頻的操作還是按照之前傳統的那種順序化的寫法寫出來。

一個遊戲程序員的堅持 —— 論向量化編程

這裏我想再做一個比喻。因為我平時有時候也玩一下樂高,有一次我在拼這個起重機的時候,我就發現一個比較有趣的事情。這是當時我拼的起重機的底盤,底盤有四個輪子,要支撐這四個輪子,就需要用到三個觸角的那種零件,左邊右邊各有一個,背後也有兩個,一共四個。拼裝這個零件的話,就是下面的這個步驟。大家看到從158-165一共八個步驟,172-179,又是八個步驟。你會發現,這個八步和那個八步實際上是一模一樣的,只是方向不一樣而已。

一個遊戲程序員的堅持 —— 論向量化編程

當時我就在想,它只需要重複四遍做四個輪子而已,那如果説現在要重複一百遍的話,你會用什麼樣的一個流程去做呢?你是會按部就班地從零件包裏找那兩個零件,然後按照説明書的步驟一步一步拼起來嗎?那你光是找零件的動作,就需要重複8x100=800次,相當的耗費時間。但是也有另外一個辦法,比如你先做158,再做172,然後再回過頭做158,再做172,你一次性找100個158的那兩個零件,找100對就行了。同理,159也是找100對那兩個零件。用這個流程的話,找零件這個步驟你只會重複8次,其他時間你都在做很高效的拼接。

一個遊戲程序員的堅持 —— 論向量化編程

這個例子實際上是可以體現向量化編程的一個很核心的思想,就是順序執行和向量化執行的差別。遇到這種數量級比較大的,100次或者説更多次的這種操作,你可以想辦法把它向量化,然後再對於那種低頻的操作,還是以順序化的方式去書寫。

一個遊戲程序員的堅持 —— 論向量化編程

最後我再講一下向量化變成在實際應用當中的優劣勢。優勢還是挺明顯的,剛剛也大概提了一下,它的代碼調用數的深度,低於面向對象編程,能夠產生爭議的點比較少,代碼管理的成本也會低一些。System代碼基本上不需要時間去讀,你只要把數據結構定義好,你這個數據是用來幹什麼的就行。System代碼實際上就是把數據A變成數據B,就是一個數組,數組A變成數組B,就是一個非常簡單非常透明的操作。

劣勢,確實這個技術的起點會比較高,在寫System代碼的時候,需要把所有的數據的讀寫關係,是隻讀還是隻寫,還是又讀又寫,這些東西要把它摸索得很清楚,你才可以寫出比較好的System的代碼。在這方面確實門檻是比面向對象編程是要高一些的。然後算法從單線程改成多線程,難度確實是比較高。在這個點上我想給大家一個建議,一開始不要想把所有的算法,所有的在順序化,或者説面向對象編程的那種思維,想出來的那種算法,都把它改成多線程。我覺得這是一個難度比較大的問題。可能從項目管理上面來説,可以先就用單線程寫一下就好了。如果説這個東西真的在最後產品測試的時候發現花的時間太多,我們需要優化,然後在那個時候,再考慮怎麼把它的高頻的那些操作向量化。

還有最後一點,如果説用ECS這套框架,寫順序化執行的代碼,它的boilerplate會比較多。如果説是面向對象編程,你有一個實例,你點一下,自動就把它的屬性這些成員變量就給你點出來了。在ECS就用Get Component data,如果説對數據有改動,還會再用Set Component data,把它賦值賦回去,大概就是這個樣子。

然後下面是我在自己學習向量化編程的時候,自己摸索做的一個展示。這是一個比較典型的塔防的一個DEMO。你們可能會發現有的時候這些小蟲子會消失,實際上它們是被炮塔攻擊了,只是沒有添加特效。大家可以看到比較多的蟲子,它們是沿着這個地形去走的。我特別花時間做了一個爬牆的邏輯,就是每個蟲子實際上都做了兩個射線查詢,來判斷自己是不是在牆上。當然有些岩石是沒有做碰撞體的,所以説它可能就是從岩石上面就穿過去了。當時也是時間比較趕,所以説也沒有做太多的那種細節的打磨。

在這個場景裏面差不多有七八千到一萬個蟲子,每一幀都做了射線查詢,以及它的周圍有哪些蟲子,避免蟲子與蟲子之間有穿插的現象,當時也是在開發環境裏面維持了有三十幀的樣子。

今天我的演講就到此結束,謝謝大家!

版權聲明:本文源自 網絡, 於,由 楠木軒 整理發佈,共 6193 字。

轉載請註明: 一個遊戲程序員的堅持 —— 論向量化編程 - 楠木軒