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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 综合教程 >内容正文

综合教程

游戏设计模式——面向数据编程

發(fā)布時(shí)間:2023/12/13 综合教程 27 生活家
生活随笔 收集整理的這篇文章主要介紹了 游戏设计模式——面向数据编程 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

注:面向數(shù)據(jù)編程文章已更新成markdown形式,并添加修改了一些內(nèi)容,而本文則作為舊文不再更新維護(hù)。

最新版博文如下:

【游戲設(shè)計(jì)模式——面向數(shù)據(jù)編程(新)】 https://www.cnblogs.com/KillerAery/p/11746639.html

前言:隨著軟件需求的日益復(fù)雜發(fā)展,遠(yuǎn)古時(shí)期面的向過程編程思想才漸漸萌生了面向?qū)ο缶幊趟枷搿?/p>

當(dāng)人們發(fā)現(xiàn)面向?qū)ο笤趹?yīng)對高層軟件的種種好處時(shí),越來越沉醉于面向?qū)ο螅瑹嶂杂谘芯咳绾胃觾?yōu)雅地抽象出對象。

然而現(xiàn)代開發(fā)中漸漸發(fā)現(xiàn)面向?qū)ο缶幊虒訉映橄笤斐捎纺[,導(dǎo)致運(yùn)行效率降低,而這是性能要求高的游戲編程領(lǐng)域不想看到的。

于是現(xiàn)代游戲編程中,面向數(shù)據(jù)編程的思想越來越被接受(例如Unity2018更新的ECS框架就是一種面向數(shù)據(jù)思想的框架)。


面向數(shù)據(jù)編程是什么?

先來一個(gè)簡單的比較:

面向過程思想:考慮解決問題所需的各個(gè)步驟(函數(shù))。
面向?qū)ο笏枷耄嚎紤]解決問題所需的各個(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ǔ)要批處理的對象

1,傳統(tǒng)的組件模式,往往讓游戲?qū)ο蟪钟幸粋€(gè)或多個(gè)組件的引用數(shù)據(jù)(指針數(shù)據(jù))。

(一個(gè)典型的游戲?qū)ο箢悾?種組件的指針)

class GameObject {
    //....GameObject的屬性
    Component1* m_component1;
    Component2* m_component2;
};

下面一幅圖顯示了這種傳統(tǒng)模式的結(jié)構(gòu):

游戲?qū)ο?組件往往是批處理操作較多(每幀更新/渲染/或其他操作)的對象。

這個(gè)傳統(tǒng)結(jié)構(gòu)相應(yīng)的每幀更新代碼:

GameObject g[MAX_GAMEOBJECT_NUM];

for(int i = 0; i < GameObjectsNum; ++i) {
      g[i].update();
      if(g[i].componet1 != nullptr)g[i].componet1->update();
      if(g[i].componet2 != nullptr)g[i].componet2->update();
}

而根據(jù)圖中可以看到,這種指來指去的結(jié)構(gòu)對CPU緩存極其不友好:為了訪問組件總是跳轉(zhuǎn)到不相鄰的內(nèi)存。

倘若游戲?qū)ο蠛徒M件的更新順序不影響游戲邏輯,則一個(gè)可行的辦法是將他們都以連續(xù)數(shù)組形式存在。

注意是對象數(shù)組,而不是指針數(shù)組。如果是指針數(shù)組的話,這對CPU緩存命中沒有意義(因?yàn)橐ㄟ^指針跳轉(zhuǎn)到不相鄰的內(nèi)存)。

GameObject g[MAX_GAMEOBJECT_NUM];
Component1 a[MAX_COMPONENT_NUM];
Component2 b[MAX_COMPONENT_NUM];

(連續(xù)數(shù)組存儲(chǔ)能讓下面的批處理中CPU緩存命中率較高)

for (int i = 0; i < GameObjectsNum; ++i) {
    g[i].update();
}
for (int i = 0; i < Componet1Num; ++i) {
    a[i].update();
}
for (int i = 0; i < Componet2Num; ++i) {
    b[i].update();
}

2,這是一個(gè)簡單的粒子系統(tǒng):

const int MAX_PARTICLE_NUM = 3000;
//粒子類
class Particle {
private:
    bool active;
    Vec3 position;
    Vec3 velocity;
    //....其它粒子所需方法
};

Particle particles[MAX_PARTICLE_NUM];
int particleNum;

它使用了典型的lazy策略,當(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è)被刪除掉的粒子,更改其屬性即可。

for (int i = 0; i < particleNum; ++i) {
    if (particles[i].isActive()) {
        particles[i].update();
    }
}

表面上看這很科學(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í)際執(zhí)行批處理帶來的速度提升相比刪除的開銷多的非常多,除非你移動(dòng)的內(nèi)存對象大小實(shí)在大到令人發(fā)指)

particles[i] = particles[particleNum];
particleNum--;

這樣我們就可以保證在這個(gè)粒子批量更新操作中,CPU緩存總是能以高命中率擊中。

for (int i = 0; i < particleNum; ++i) {
    particles[i].update();
}

冷數(shù)據(jù)/熱數(shù)據(jù)分割

有人可能認(rèn)為這樣能最大程度利用CPU緩存:把一個(gè)對象所有要用的數(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è)栗子:對于人類來說,生命值位置速度都是經(jīng)常需要操作的變量,是熱數(shù)據(jù);

而掉落物對象只有人類死亡的時(shí)候才需要用到,所以是冷數(shù)據(jù);

class Human {
private:
    float health;
    float power;
    Vec3 position;
    Vec3 velocity;
    LootDrop* drop;
    //....
};

class LootDrop{
    Item[2] itemsToDrop;
    float chance;
    //....
};

頻繁調(diào)用的函數(shù)盡可能不要做成虛函數(shù)

C++的虛函數(shù)機(jī)制,簡單來說是兩次地址跳轉(zhuǎn)的函數(shù)調(diào)用,這對CPU緩存十分不友好,往往命中失敗。

實(shí)際上虛函數(shù)可以優(yōu)雅解決很多面向?qū)ο蟮膯栴},然而在游戲程序如果有很多虛函數(shù)都要頻繁調(diào)用(例如每幀調(diào)用),很容易引發(fā)性能問題。

解決方法是,把這些頻繁調(diào)用的虛函數(shù)盡可能去除virtual特性(即做成普通成員函數(shù)),并避免調(diào)用基類對象的成員函數(shù),代價(jià)是這樣一改得改很多與之牽連代碼。

所以最好一開始設(shè)計(jì)程序時(shí),需要先想好哪些最好不要寫成virtual函數(shù)。

這實(shí)際上就是在優(yōu)雅與性能之間尋求一個(gè)平衡。

更多小細(xì)節(jié)(不常用)

面向數(shù)據(jù)編程還有更多小細(xì)節(jié),但是這些都不常用,就只作為一種思考面向數(shù)據(jù)編程的另類角度。

對多維數(shù)組的遍歷:int a[100][100];

for(int x=0;x<100;++x)
for(int y=0;y<100;++y)
a[x][y];

for(int y=0;y<100;++y)
for(int x=0;x<100;++x)
a[x][y];

內(nèi)循環(huán)應(yīng)該是對x遞增還是對y遞增比較快?答案是:對y遞增比較快。

因?yàn)閷?y 的遞增,結(jié)果是一個(gè)int大小的跳轉(zhuǎn),也就是說容易訪問到相鄰的內(nèi)存,即容易擊中CPU緩存。

而對 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擊中比較少。


額外

該更新一下我對面向?qū)ο蠛兔嫦驍?shù)據(jù)的看法:

先說結(jié)論:應(yīng)該兼有。因?yàn)橛螒虺绦蚴且粋€(gè)既需要高性能又復(fù)雜的工程。

使用面向?qū)ο蟮挠螒虺绦蛐率郑3>陀幸粋€(gè)問題:過度設(shè)計(jì)/過度抽象,什么都想用設(shè)計(jì)模式封裝一下抽象一下。

這就很容易導(dǎo)致一些過度設(shè)計(jì)/過度抽象導(dǎo)致游戲性能太差。

博主現(xiàn)在的項(xiàng)目風(fēng)格都比較偏向面向數(shù)據(jù)思想,盡量減少虛函數(shù)的使用,多利用數(shù)據(jù)組合成對象,而不是重寫各種基類虛函數(shù)。

對于一些數(shù)據(jù)結(jié)構(gòu)的考量,也盡量偏多使用連續(xù)存儲(chǔ)的結(jié)構(gòu)(例如數(shù)組)。

如何兼有兩種思想,這種玄學(xué)的問題可能得靠自己去感悟,多嘗試和測試性能差別。


游戲設(shè)計(jì)模式系列-其他文章:

https://www.cnblogs.com/KillerAery/category/1307176.html

總結(jié)

以上是生活随笔為你收集整理的游戏设计模式——面向数据编程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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