游戏设计模式——面向数据编程思想
前言:隨著軟件需求的日益復(fù)雜發(fā)展,遠(yuǎn)古時(shí)期面的向過程編程思想才漸漸萌生了面向?qū)ο缶幊趟枷搿?/span>
當(dāng)人們發(fā)現(xiàn)面向?qū)ο笤趹?yīng)對(duì)高層軟件的種種好處時(shí),越來越沉醉于面向?qū)ο?#xff0c;熱衷于研究如何更加優(yōu)雅地抽象出對(duì)象。
然而現(xiàn)代開發(fā)中漸漸發(fā)現(xiàn)面向?qū)ο缶幊虒訉映橄笤斐捎纺[,導(dǎo)致運(yùn)行效率降低,而這是性能要求高的游戲編程領(lǐng)域不想看到的。
于是現(xiàn)代游戲編程中,面向數(shù)據(jù)編程的思想越來越被接受(例如Unity2018更新的ECS框架就是一種面向數(shù)據(jù)思想的框架)。
面向數(shù)據(jù)編程是什么?
先來一個(gè)簡(jiǎn)單的比較:
?
- 面向過程思想:考慮解決問題所需的各個(gè)步驟(函數(shù))。
- 面向?qū)ο笏枷?#xff1a;考慮解決問題所需的各個(gè)模型(類)。
- 面向數(shù)據(jù)思想:考慮數(shù)據(jù)的存取及布局為核心思想(數(shù)據(jù))。
那么所謂的考慮數(shù)據(jù)存儲(chǔ)/布局是什么意思呢?先引入一個(gè)有關(guān)CPU處理數(shù)據(jù)的概念:CPU多級(jí)緩存。
CPU多級(jí)緩存(CPU cache)
在組裝電腦購買CPU的時(shí)候,不知道大家是否留意過CPU的一個(gè)參數(shù):N級(jí)緩存(N一般有1/2/3)
什么是CPU緩存:
?
更詳細(xì)來說,結(jié)構(gòu)應(yīng)該是:CPU<---->寄存器<---->CPU緩存<---->內(nèi)存
可以看到CPU緩存是介于內(nèi)存和寄存器之間的一個(gè)存儲(chǔ)區(qū)域,此外它存儲(chǔ)空間比內(nèi)存小,比寄存器大。
為什么需要CPU多級(jí)緩存:
CPU的運(yùn)行頻率太快了,而CPU訪問內(nèi)存的速度很慢,這樣在處理器時(shí)鐘周期內(nèi),CPU常常需要等待寄存器讀取內(nèi)存,浪費(fèi)時(shí)間。
而CPU訪問CPU緩存則速度快很多。為了緩解CPU和內(nèi)存之間速度的不匹配問題,CPU緩存則預(yù)先存儲(chǔ)好潛在可能會(huì)訪問的內(nèi)存數(shù)據(jù)。
CPU多級(jí)緩存預(yù)先存的是什么:
?
- 時(shí)間局部性:如果某個(gè)數(shù)據(jù)被訪問,那么在不久的將來它很可能再次被訪問。
- 空間局部性:如果某個(gè)數(shù)據(jù)被訪問,那么與它相鄰的數(shù)據(jù)很快也能被訪問。
- CPU多級(jí)緩存根據(jù)這兩個(gè)特點(diǎn),一般存儲(chǔ)的是訪問過的數(shù)據(jù)+訪問數(shù)據(jù)的相鄰數(shù)據(jù)。
CPU緩存命中/未命中:
?
- CPU把待處理的數(shù)據(jù)或已處理的數(shù)據(jù)存入緩存指定的地址中,如果即將要處理的數(shù)據(jù)已經(jīng)存在此地址了,就叫作CPU緩存命中。
- 如果CPU緩存未命中,就轉(zhuǎn)到內(nèi)存地址訪問。
提高CPU緩存命中率
要盡可能提高CPU緩存命中率,就是要盡量讓使用的數(shù)據(jù)連續(xù)在一起。
由于面向數(shù)據(jù)編程技巧很多,本文篇幅有限,只介紹部分。
使用連續(xù)數(shù)組存儲(chǔ)要批處理的對(duì)象
1,傳統(tǒng)的組件模式,往往讓游戲?qū)ο蟪钟幸粋€(gè)或多個(gè)組件的引用數(shù)據(jù)(指針數(shù)據(jù))。
(一個(gè)典型的游戲?qū)ο箢?#xff0c;包含了2種組件的指針)
?
下面一幅圖顯示了這種傳統(tǒng)模式的結(jié)構(gòu):
?
游戲?qū)ο?組件往往是批處理操作較多(每幀更新/渲染/其它操作)的對(duì)象。
這個(gè)傳統(tǒng)結(jié)構(gòu)相應(yīng)的每幀更新渲染代碼:
?
而根據(jù)圖中可以看到,這種指來指去的結(jié)構(gòu)對(duì)CPU緩存極其不友好:為了訪問組件總是跳轉(zhuǎn)到不相鄰的內(nèi)存。
倘若游戲?qū)ο蠛徒M件的更新順序不影響游戲邏輯,則一個(gè)可行的辦法是將他們都以連續(xù)數(shù)組形式存在。
注意是對(duì)象數(shù)組,而不是指針數(shù)組。如果是指針數(shù)組的話,這對(duì)CPU緩存命中沒有意義(因?yàn)橐ㄟ^指針跳轉(zhuǎn)到不相鄰的內(nèi)存)。
?
(連續(xù)數(shù)組存儲(chǔ)能讓下面的批處理中CPU緩存命中率較高)
?
2,這是一個(gè)簡(jiǎn)單的粒子系統(tǒng):
?
它使用了典型的lazy策略,二手QQ拍賣當(dāng)要?jiǎng)h除一個(gè)粒子時(shí),只需改變active標(biāo)記,無需移動(dòng)內(nèi)存。
然后利用標(biāo)記判斷,每幀更新的時(shí)候可以略過刪除掉的粒子。
當(dāng)需要?jiǎng)?chuàng)建新粒子時(shí),只需要找到第一個(gè)被刪除掉的粒子,更改其屬性即可。
表面上看這很科學(xué),實(shí)際上這樣做CPU緩存命中率不高:每次批處理CPU緩存都加載過很多不會(huì)用到的粒子數(shù)據(jù)(標(biāo)記被刪除的粒子)。
一個(gè)可行的方法是:當(dāng)要?jiǎng)h除粒子時(shí),將隊(duì)列尾的粒子內(nèi)存復(fù)制到該粒子的位置,并記錄減少后的粒子數(shù)量。
(移動(dòng)內(nèi)存(復(fù)制內(nèi)存)操作是程序員最不想看到的,但是實(shí)際運(yùn)行批處理帶來的速度提升相比刪除的開銷多的非常多,這也是面向數(shù)據(jù)編程的奇妙之處。)
?
這樣我們就可以保證在這個(gè)粒子批量更新操作中,CPU緩存總是能以高命中率擊中。
?
冷數(shù)據(jù)/熱數(shù)據(jù)分割
有人可能認(rèn)為這樣能最大程度利用CPU緩存:把一個(gè)對(duì)象所有要用的數(shù)據(jù)(包括組件數(shù)據(jù))都塞進(jìn)一個(gè)類里,而沒有任何用指針或引用的形式間接存儲(chǔ)數(shù)據(jù)。
實(shí)際上這個(gè)想法是錯(cuò)誤的,我們不能忽視一個(gè)問題:CPU緩存的存儲(chǔ)空間是有限的
于是我們希望CPU緩存存儲(chǔ)的是經(jīng)常使用的數(shù)據(jù),而不是那些少用的數(shù)據(jù)。這就引入了冷數(shù)據(jù)/熱數(shù)據(jù)分割的概念了。
?
- 熱數(shù)據(jù):經(jīng)常要操作使用的數(shù)據(jù),我們一般可以直接作為可直接訪問的成員變量。
- 冷數(shù)據(jù):比較少用的數(shù)據(jù),我們一般以引用/指針來間接訪問(即存儲(chǔ)的是指針或者引用)。
一個(gè)栗子:對(duì)于人類來說,生命值位置速度都是經(jīng)常需要操作的變量,是熱數(shù)據(jù);
而掉落物對(duì)象只有人類死亡的時(shí)候才需要用到,所以是冷數(shù)據(jù);
?
更多小細(xì)節(jié)(不常用)
面向數(shù)據(jù)編程還有更多小細(xì)節(jié),但是這些都不常用,就只作為一種思考面向數(shù)據(jù)編程的另類角度。
對(duì)多維數(shù)組的遍歷:int a[100][100];
?
內(nèi)循環(huán)應(yīng)該是對(duì)x遞增還是對(duì)y遞增比較快?答案是:對(duì)y遞增比較快。
因?yàn)閷?duì)y的遞增,結(jié)果是一個(gè)int大小的跳轉(zhuǎn),也就是說容易訪問到相鄰的內(nèi)存,即容易擊中CPU緩存。
而對(duì)x的遞增,結(jié)果是100個(gè)int大小的跳轉(zhuǎn),不容易擊中CPU。
而內(nèi)循環(huán)如果是y的話,那么就能內(nèi)外循環(huán)總共遞增100*100次y。
但內(nèi)循環(huán)如果是x的話,那么就內(nèi)外循環(huán)總共只能遞增100次y,相比上者,CPU擊中比較少。
額外
面向數(shù)據(jù)編程可以說是對(duì)CPU優(yōu)化的一個(gè)重要思想。
但是在實(shí)際開發(fā)中,一定要注意不能忘記這個(gè)原則:
不要過早優(yōu)化!
面向數(shù)據(jù)編程說到底不是針對(duì)軟件需求的,而是針對(duì)CPU優(yōu)化的。
在游戲的迭代開發(fā)的后期,要是CPU性能出現(xiàn)瓶頸,才應(yīng)去考慮使用面向數(shù)據(jù)編程技巧。
總結(jié)
以上是生活随笔為你收集整理的游戏设计模式——面向数据编程思想的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 游戏中的AI及实用算法逻辑
- 下一篇: 从《王者荣耀》来聊聊游戏的帧同步