游戲中的AOI(Area of Interest)算法
游戲的AOI算法應該算作游戲的基礎核心了,許多邏輯都是因為AOI進出事件驅動的,許多網絡同步數據也是因為AOI進出事件產生的。因此,良好的AOI算法和基於AOI算法的優化,是提高游戲性能的關鍵。
我在實踐中所熟知的游戲AOI算法大致有兩種,在此做一些總結,順便梳理一下,打算設計出一套統一的接口封裝不同的算法實現(網絡上還有些其他算法,因為不熟悉不作記錄了)。我所記錄的這兩種算法也算經典了,一個叫做網格法,一個叫做雙鏈表法。
統一接口設計:
AOI需求大概是這樣:
1.游戲地圖上有一些npc和玩家在移動,每一個這樣移動的對象我們叫做AOIEntity,每一個AOIEntity可以掛多個不同半徑的AOI,每一個這種半徑的AOI單元我們叫做AOINode,如此,AOIEntity擁有多個AOINode,然后每一個場景管理者AOIManager管理着多個這樣的AOIEntity對象。
2.AOI進出事件由三種行為產生:進入場景,離開場景,在場景移動,因為這是AOIEntity相互之間的作用,故因放在AOIManager中統一管理,接口類似這樣:
void AOIManager:Enter(AOIEntity *entity, cosnt Point& target_pos);
void AOIManager:Move(AOIEntity *entity, cosnt Point& target_pos);
void AOIManager:Leave(AOIEntity *entity);
3.添加一個AOINode的接口,主要參數是Id(用於標識這個AOI),半徑,進出事件的callback函數:
void AOIEntity:AddNode(int aoi_id, float radius, AOICB enter_cb, AOICB leave_cb);
4.獲取周圍對象和觀察者玩家對象集合的接口,這個可以在更上層,通過在響應進出事件的enter_cb, leave_cb中維護這樣的集合。
網格算法:
既是把整個場景用網格划分成一個一個小區域(划分粒度可調整),每一個區域是當前場景該區域內的AOIEntity集合,當有一個AOIEntity移動時,根據對象移動之前坐標和目的地坐標,算出移動前所在網格SrcGrid和目的地網格DstGrid,根據一個可調的偏移參數,算出受這次移動影響的各個網格所在的一個網格區域(通常是一個包含這些網格的一個大網格),遍歷每一個這樣的網格里的每一個AOIEntity,與這個移動AOIEntity互相作比較,主要是比較這些事情:
1.是不是對方曾經在我的一個AOINode的半徑內,移動后就不在了,是則產生離開回調;
2.是不是對方曾經不在我的一個AOINode的半徑內,移動后就出現了,是則產生進入回調;
注意雖然移動是一個AOIEntity在移動,但是這種比較卻要是互相的。
上面說的是網格算法的最簡單實現了,當然實踐上有許多地方可以優化和調整,包括使用更高效的數據結構,不細說。
雙鏈表算法:
* 此算法名字是自己取的,因為算法基本上就是圍繞兩個雙向鏈表在轉--代表X軸的鏈表(叫做LinkListX)和代表Y軸的鏈表(叫做LinkLIstY)。對於每一個AOI單元,以AOIEntity的坐標位置為中心,可以構造出一個AOI矩形(以四元組[xleft,xright,ytop,ybottom]表示)。LinkListX鏈接的是所有這樣的AOI矩形的xleft,xright,LinkListY鏈接的是所有這樣的AOI矩形的ytop,ybottom,並且兩者都是按照坐標值從小到大的順序鏈接起來的。這樣每一個AOI單元都在LinkListX,LinkListY上產生了總共4個節點,特殊的對於每一個可見的AOIEntity,以他們的坐標(XCenter,YCenter)在LinkListX,LinkListY上又產生了總共2個節點。現在當AOIEntity在場景中移動時,他所包含的在LinkList中的節點會相應的更改坐標值,而LinkList為了維護從小到大的順序,會遍歷鏈表,移動位置,直到重新有序。LinkList在這個過程,會產生AOI事件。
* 具體來說,當AOIEntity要移動到(targetX,targetY), 對應的AOI矩形變成[targetX-R, targetX+R, targetY-R, targetY+R],顯然這四個節點值的改變后LinkList不再有序,現在來調整LinkList,可以這樣來理解這個過程,對象先在X軸上移動到targetX,對應的是在LinkListX上移動,每次交換兩個節點的位置都應該判斷:1.兩者的擁有者是不是不同的Entity;2.是不是一個是代表Entity的節點,一個是代表AOI矩形邊界的節點;3.兩者的擁有者整體上能否確實產生AOI進出事件。然后在Y軸上移動到targetY,過程與X軸對稱。
* 可以總結一下,LinkList的節點的屬性:
struct LinkNode {
byte _type; // 代表類型,主要是區分AOI矩形的邊界和Entity本身
AOINode *_owner; // 屬於哪個AOI單元,這里把代表Entity本身的節點也當作一個R=0的AOI單元
int _pos_val; // 坐標值,
struct LinkNode *_next, *prev;
}
網格算法原理和實現都簡單,每次移動時遍歷的受影響的單元是以網格為單位,並不是直接以Entity為單位,會產生許多次無效的遍歷,對效率產生多少影響也是依賴網格的划分粒度和場景人數,不過總的來說對於不是海量的對象移動,加上一些上層邏輯相關的優化,一般的MMO已經是夠用了。
雙鏈表算法,巧妙的把一個AOI矩形拆成4個不同節點,每次移動遍歷的受影響的單元直接是以Entity為單位,省去了許多無效遍歷,但是在實現上要較網格算法復雜,另外其性能也是受場景中人數的影響。