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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2024/8/26 编程问答 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 游戏设计模式——面向数据编程思想 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言:隨著軟件需求的日益復雜發展,遠古時期面的向過程編程思想才漸漸萌生了面向對象編程思想。

當人們發現面向對象在應對高層軟件的種種好處時,越來越沉醉于面向對象,熱衷于研究如何更加優雅地抽象出對象。

然而現代開發中漸漸發現面向對象編程層層抽象造成臃腫,導致運行效率降低,而這是性能要求高的游戲編程領域不想看到的。

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

面向數據編程是什么?

先來一個簡單的比較:
?

  • 面向過程思想:考慮解決問題所需的各個步驟(函數)。
  • 面向對象思想:考慮解決問題所需的各個模型(類)。
  • 面向數據思想:考慮數據的存取及布局為核心思想(數據)。



那么所謂的考慮數據存儲/布局是什么意思呢?先引入一個有關CPU處理數據的概念:CPU多級緩存。

CPU多級緩存(CPU cache)

在組裝電腦購買CPU的時候,不知道大家是否留意過CPU的一個參數:N級緩存(N一般有1/2/3)

什么是CPU緩存:
?


更詳細來說,結構應該是:CPU<---->寄存器<---->CPU緩存<---->內存

可以看到CPU緩存是介于內存和寄存器之間的一個存儲區域,此外它存儲空間比內存小,比寄存器大。

為什么需要CPU多級緩存:

CPU的運行頻率太快了,而CPU訪問內存的速度很慢,這樣在處理器時鐘周期內,CPU常常需要等待寄存器讀取內存,浪費時間。

而CPU訪問CPU緩存則速度快很多。為了緩解CPU和內存之間速度的不匹配問題,CPU緩存則預先存儲好潛在可能會訪問的內存數據。

CPU多級緩存預先存的是什么:
?

  • 時間局部性:如果某個數據被訪問,那么在不久的將來它很可能再次被訪問。
  • 空間局部性:如果某個數據被訪問,那么與它相鄰的數據很快也能被訪問。
  • CPU多級緩存根據這兩個特點,一般存儲的是訪問過的數據+訪問數據的相鄰數據。



CPU緩存命中/未命中:
?

  • CPU把待處理的數據或已處理的數據存入緩存指定的地址中,如果即將要處理的數據已經存在此地址了,就叫作CPU緩存命中。
  • 如果CPU緩存未命中,就轉到內存地址訪問。



提高CPU緩存命中率

要盡可能提高CPU緩存命中率,就是要盡量讓使用的數據連續在一起。

由于面向數據編程技巧很多,本文篇幅有限,只介紹部分。

使用連續數組存儲要批處理的對象

1,傳統的組件模式,往往讓游戲對象持有一個或多個組件的引用數據(指針數據)。

(一個典型的游戲對象類,包含了2種組件的指針)
?

  • class GameObject {
  • ? ? //....GameObject的屬性
  • ? ? Component1* m_component1;
  • ? ? Component2* m_component2;
  • };
  • 復制代碼


    下面一幅圖顯示了這種傳統模式的結構:
    ?


    游戲對象/組件往往是批處理操作較多(每幀更新/渲染/其它操作)的對象。

    這個傳統結構相應的每幀更新渲染代碼:
    ?

  • GameObject g[MAX_GAMEOBJECT_NUM];
  • for(int i = 0; i < GameObjectsNum; ++i) {
  • ? ?? ?g[i].update();
  • ? ?? ?g[i].draw();
  • ? ?? ?if(g[i].componet1 != nullptr)g[i].componet1->update();
  • ? ?? ?if(g[i].componet2 != nullptr)g[i].componet2->update();
  • }
  • 復制代碼


    而根據圖中可以看到,這種指來指去的結構對CPU緩存極其不友好:為了訪問組件總是跳轉到不相鄰的內存。

    倘若游戲對象和組件的更新順序不影響游戲邏輯,則一個可行的辦法是將他們都以連續數組形式存在。

    注意是對象數組,而不是指針數組。如果是指針數組的話,這對CPU緩存命中沒有意義(因為要通過指針跳轉到不相鄰的內存)。
    ?

  • GameObject g[MAX_GAMEOBJECT_NUM];
  • Component1 a[MAX_COMPONENT_NUM];
  • Component2 b[MAX_COMPONENT_NUM];
  • 復制代碼


    (連續數組存儲能讓下面的批處理中CPU緩存命中率較高)
    ?

  • for (int i = 0; i < GameObjectsNum; ++i) {
  • ? ? g[i].update();
  • ? ? g[i].draw();
  • }
  • for (int i = 0; i < Componet1Num; ++i) {
  • ? ? a[i].update();
  • }
  • for (int i = 0; i < Componet2Num; ++i) {
  • ? ? b[i].update();
  • }
  • 復制代碼


    2,這是一個簡單的粒子系統:
    ?

  • const int MAX_PARTICLE_NUM = 3000;
  • //粒子類
  • class Particle {
  • private:
  • ? ? bool active;
  • ? ? Vec3 position;
  • ? ? Vec3 velocity;
  • ? ? //....其它粒子所需方法
  • };
  • Particle particles[MAX_PARTICLE_NUM];
  • int particleNum;
  • 復制代碼


    它使用了典型的lazy策略,二手QQ拍賣當要刪除一個粒子時,只需改變active標記,無需移動內存。

    然后利用標記判斷,每幀更新的時候可以略過刪除掉的粒子。

    當需要創建新粒子時,只需要找到第一個被刪除掉的粒子,更改其屬性即可。

  • for (int i = 0; i < particleNum; ++i) {
  • ? ? if (particles[i].isActive()) {
  • ? ?? ???particles[i].update();
  • ? ? }
  • }
  • 復制代碼


    表面上看這很科學,實際上這樣做CPU緩存命中率不高:每次批處理CPU緩存都加載過很多不會用到的粒子數據(標記被刪除的粒子)。

    一個可行的方法是:當要刪除粒子時,將隊列尾的粒子內存復制到該粒子的位置,并記錄減少后的粒子數量。

    (移動內存(復制內存)操作是程序員最不想看到的,但是實際運行批處理帶來的速度提升相比刪除的開銷多的非常多,這也是面向數據編程的奇妙之處。)
    ?

  • particles[i] = particles[particleNum];
  • particleNum--;
  • 復制代碼


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

  • for (int i = 0; i < particleNum; ++i) {
  • ? ? particles[i].update();
  • }
  • 復制代碼


    冷數據/熱數據分割

    有人可能認為這樣能最大程度利用CPU緩存:把一個對象所有要用的數據(包括組件數據)都塞進一個類里,而沒有任何用指針或引用的形式間接存儲數據。

    實際上這個想法是錯誤的,我們不能忽視一個問題:CPU緩存的存儲空間是有限的

    于是我們希望CPU緩存存儲的是經常使用的數據,而不是那些少用的數據。這就引入了冷數據/熱數據分割的概念了。
    ?

    • 熱數據:經常要操作使用的數據,我們一般可以直接作為可直接訪問的成員變量。
    • 冷數據:比較少用的數據,我們一般以引用/指針來間接訪問(即存儲的是指針或者引用)。



    一個栗子:對于人類來說,生命值位置速度都是經常需要操作的變量,是熱數據;

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

  • class Human {
  • private:
  • ? ? float health;
  • ? ? float power;
  • ? ? Vec3 position;
  • ? ? Vec3 velocity;
  • ? ? LootDrop* drop;
  • ? ? //....
  • };
  • class LootDrop{
  • ? ? Item[2] itemsToDrop;
  • ? ? float chance;
  • ? ? //....
  • };
  • 復制代碼


    更多小細節(不常用)

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

    對多維數組的遍歷: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];
  • 復制代碼


    內循環應該是對x遞增還是對y遞增比較快?答案是:對y遞增比較快。

    因為對y的遞增,結果是一個int大小的跳轉,也就是說容易訪問到相鄰的內存,即容易擊中CPU緩存。

    而對x的遞增,結果是100個int大小的跳轉,不容易擊中CPU。

    而內循環如果是y的話,那么就能內外循環總共遞增100*100次y。

    但內循環如果是x的話,那么就內外循環總共只能遞增100次y,相比上者,CPU擊中比較少。

    額外

    面向數據編程可以說是對CPU優化的一個重要思想。



    但是在實際開發中,一定要注意不能忘記這個原則:

    不要過早優化!

    面向數據編程說到底不是針對軟件需求的,而是針對CPU優化的。

    在游戲的迭代開發的后期,要是CPU性能出現瓶頸,才應去考慮使用面向數據編程技巧。

    與50位技術專家面對面20年技術見證,附贈技術全景圖

    總結

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

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。