背景
玩家視野設計背景
AOI(Area of Interest)
,即感興趣區域。可以看成在遊戲中玩家在所在場景中實時看到的區域,即AOI會隨着玩家的移動而改變。在遊戲中,會有很多個場景(地圖),每個地圖中都有很多玩家或者npc,視野數據是相互的, 玩家A 可以看到 玩家B , 玩家B 同時也會看到 玩家A 。當 玩家 在地圖中做一些行爲操作的時候,如果想要玩家的行爲被別人看到,就需要把玩家的行爲廣播給地圖中的所有玩家。場景中玩家的行爲只要有修改就廣播給場景中其他所有玩家,這種方式是最優的方式呢?
- 對於一些玩家數量較少的場景,比如組隊副本等。玩家會看到地圖中所有的玩家和npc,可以稱之爲
全視野場景
。這種場景中,單個玩家有操作採取廣播給場景中的其他玩家的方式是可行的。 - 對於一些
大型的PVP玩法
,同一個場景中有幾百甚至上千人參加的時候,假設場景中共有1000人蔘加。任意一個玩家只要移動就要廣播給剩下的999人,當1000個人同時移動的話,服務器需要處理999 * 999(百萬級別)條消息,相當於 n*n 。場景中所有人移動一下就產生百萬級別的數據
,還沒有考慮玩家的技能釋放等其他的行爲操作的同步,這麼大數據量容易造成服務器消息的堆積,無法做到及時響應。同時,客戶端每一個玩家都會處理其他1000個玩家的實時信息,容易造成客戶端的卡頓,如果消息處理不過來也會出現包數據的丟失。同時發往客戶端的包的數量太大,還會消耗大量的流量。這種方式肯定不是我們想要的,遊戲體驗會非常差。
我們可以發現,在遊戲中,玩家始終會出現在屏幕的中央,隨着玩家的移動,視野也會隨着玩家的移動同時移動,但是始終都是以玩家爲中心。玩家所能看到的就是遊戲屏幕內以玩家爲中心向四周360度擴展的視野信息。那麼如果修改玩家的 AOI ,控制在以玩家爲中心 屏幕內所有所見對象記爲玩家的視野數據
;當玩家有行爲操作的時候,只需要通知在玩家視野中的其他用戶,相對於通知場景中的全部玩家來說可以減少大量沒有必要的消息的推送,同時也能節省很多流量。因爲距離太遠的其他用戶是不在玩家當前屏幕顯示範圍內的,把玩家的行爲操作通知給這些玩家是沒有什麼意義的。
但是如果該場景中所有玩家都在打一個boss,場景中玩家全部都集中在一起,那麼玩家當前屏幕中的視野數據又變成場景中的全部玩家了。同時受客戶端性能的限制,客戶端有最大的同屏人數上限,因此我們需要限制玩家的視野上限
。當場景中玩家全部集中的一起的時候按照優先級(社交關係,敵對關係,距離等)進行篩選,篩選出指定數量的對象算入玩家的視野對象。
因此,針對大型PVP玩法的活動,當場景中人數非常多的時候,玩家的視野採用限制視野上限,並且以玩家爲中心360度指定範圍設定爲玩家視野
的方式。以玩家爲中心,向四周360度擴展,可以延伸出以玩家爲中心的正方形區域,也可以是以玩家爲中心的圓形區域。但是由於地圖時正方形的,圓形並不適合進行座標範圍臨界點的計算,因此最好的就是按照以玩家爲中心向外360度擴展的正方形區域。我們可以發現圍出來的剛好是一個以玩家爲中心的正方形區域。因此可以想到按照各自劃分地圖區域,玩家在格子的中心,以玩家爲中心的正方形區域剛好可以分成九宮格 3 * 3。並且九宮格的區域需要大於等於遊戲屏幕的顯示區域
,根據具體情況合理設計每個地圖格子的長度即可。因此玩家的視野數據可以採用九宮格存儲
的形式。
地圖
地圖劃分格子
在遊戲中,地圖大小都是正方形,並且是以 X 軸, Z 軸爲水平面的(遊戲中都以cm爲單位)。把地圖按照指定的大小分成若干個格子,如以 7m(需要根據實際的地圖大小設置單個格子的長度大小) 爲單位劃分塊。當地圖被劃分爲多個地圖塊之後,通過玩家的座標點可以計算出玩家哪個地圖塊中。
在 X 軸方向進行劃分,X軸地圖總長度爲256m,假設每個塊的大小爲7m,那麼在X 軸,共有多少個格子呢? 256 / 7 = 36.57142857142857,不滿足一個格子的直接按照 1 個格子來計算,即X 軸上共有36 + 1 = 37塊格子,Z軸同理。假設每個塊的大小爲8m,256 / 8 = 32,剛好可以劃分爲32個塊。當地圖大小固定的時候,不同大小的塊,會得到不用的格子數,有的可以整除,有的不能整除(不能整除的不滿足 1 塊大小算作 1 塊),即向上取整
。爲了避免進行是否能夠整除的條件判斷,統一採用加 1,即採用 地圖長度 / 每個格子長度 + 1 來計算劃分的格子數。因此我們劃分的總格子數總是會比場景中的地圖要大的
。假設地圖大小是256m * 256m,每個格子的大小是7m * 7m,下面都是按照這個大小進行討論。
X 軸的總格子數 X_AREA_NUM_PER_MAP = X軸地圖長度 / X軸每個格子長度 + 1
Z 軸的總格子數 Z_AREA_NUM_PER_MAP = Z軸地圖長度 / Z軸每個格子長度 + 1
整個地圖共有的格子數 MAX_DYN_AREA_NUM = X_AREA_NUM_PER_MAP * Z_AREA_NUM_PER_MAP + 1(此處加 1 個格子是爲了安全起見)
遊戲中可以採用一維數組
的方式進行存儲地圖中所有的格子
。格子數 - 1 = 數組下標,通過下標進而得到數組中存儲的對應共享內存ID,從而可以得到對應的視野塊對象(1個格子可以視作一個視野塊對象)。
地圖的視野塊(格子)和數組下標的關係如下:
後面可能會用到的變量:
X_AREA_NUM_PER_MAP
----------- X 軸方向劃分的總格子數
Z_AREA_NUM_PER_MAP
----------- Z 軸方向劃分的總格子數
MAX_DYN_AREA_NUM
----------- 整個地圖所有的格子數
Actor對象
------------ 玩家 或者 NPC 等統稱Actor對象
地圖格子對象
------------ 視野塊對象
座標點定位玩家所在地圖塊
如何根據具體的座標點找到玩家所處於哪個格子中呢?
X軸列方向:第一個格子的範圍 [0,7),第二個格子範圍 [7,14),第三個格子範圍[14,21)…
Z軸行方向:第一個格子的範圍 [0,7),第二個格子範圍 [7,14),第三個格子範圍[14,21)…
已知玩家的座標pos(8, 100, 15),假設地圖大小爲256m * 256m,格子的大小爲7m * 7m。
可以先按照二維數組下標分析:
在 X 軸,X 所在格子數爲 X = 8 / 7 = 1,即在X軸上第2列格子中(如圖),X軸數組下標爲1
在 Z 軸,Z 所在格子數爲 Z = 15 / 7 = 2,即在Z軸上第3層格子中(如圖),Z軸數組下標爲2
按照二維數組計算下標的方式,玩家所在數組的下標爲: Z * X_AREA_NUM_PER_MAP + X = 2 * 37 + 1 = 75
視野塊
地圖上每個格子可以看做一個視野塊
,通過這個格子可以獲取座標位於該格子裏面的所有 Actor 對象。那麼每個視野塊中的所有 Actor 對象是如何存儲起來的?
- 可以使用HashSet進行存儲,存儲在當前視野塊中的所有玩家的ActorID。HashSet中元素不會重複,同時可以滿足快速查找刪除,可以滿足我們的需求。
- 如果以單向鏈表的形式存儲在視野塊中的話,查找刪除的複雜度都是O(n),顯然不合適。可以發現視野塊中存儲該視野塊中所有 Actor 對象的數據結構
需要支持快速查找、刪除、插入
操作。可以想到,通過採用雙向鏈表的形式實現。
但是雙向鏈表也會弊端,它是通過獲取Actor對象上存儲的雙向鏈表節點來知道下一個節點的對象的,如果某個對象不存在了,那麼就獲取不了下一個節點的值,視野塊的雙向鏈表就無法遍歷了,只能重置。
雙向鏈表存儲視野塊中所有Actor對象的具體實現如下:
在視野塊對象中只需要存儲鏈表的頭結點 + 鏈表的總數
,頭結點即Actor對象的ActorID(可唯一標識Actor 對象),然後在Actor對象中存儲雙向鏈表節點(鏈表上一個Actor對象的ActorID + 鏈表下一個對選哪個的ActorID)。這樣通過每個視野塊中的頭結點即可遍歷屬於該視野塊中的所有Actor對象。
雙向鏈表刪除
操作。這樣的話當Actor對象移動到下一個視野塊的時候,可以很快的進行鏈表刪除
操作,BeforeA <----->ActorA<----->AfterA,若要從視野塊中刪除ActorA,直接BeforeA<------>After A,即可實現。
雙向鏈表插入
操作。當有新的玩家移動到當前視野塊,可以採用尾插法的形式實現鏈表插入
。通過視野塊對象即可獲取到頭結點的HeadActorID,頭結點的Actor對象中存儲了雙向鏈表節點,可以獲取到上一個節點的ActorID,即尾節點的TailActorID。即TailActorID<------>HeadActorID,假設要插入的的爲ActorA,,則TailActorID<------>ActorA<------>HeadActorID即可實現尾部插入。
假設雙向鏈表數據結構如下:
struct STGIDListNode
{
// 同一鏈表前一個gid
ActorID m_iPrevGID;
// 同一鏈表後一個gid
ActorID m_iNextGID;
};
雙向鏈表插入節點
stPushNode
:要插入的Actor對象的雙向鏈表節點對象
iPushGID:要插入的Actor對象的ActorID
stTailNode
:尾結點
stHeadNode
:頭結點
雙向鏈表遍歷
地圖、視野塊、玩家的關係
一個遊戲會有很多個場景;
每個場景對應一個地圖資源;
每個地圖上會分爲多個格子,每個格子都是一個視野塊對象;
每個視野塊上會有很多個玩家或者npc。
玩家視野
玩家九宮格視野
玩家視野變動會有以下情況:
主動
。玩家操作引起的視野刷新:
玩家進入地圖,會搜索玩家的視野信息。
玩家移動,從一個視野塊移動到另一個視野塊(如果在同一個格子中移動,不會刷新視野)。被動
。其他原因造成玩家視野的變動:
其他玩家進入/離開視野,會引起自身視野的變動。
如何判定玩家是否進入進的格子?
通過新舊座標來計算,座標 / 每個視野格子大小
計算出所在格子數,通過判斷格子數是否相同來判斷是否在同一個視野格子來判斷玩家是否進入新的視野塊。
當玩家在同一個格子中移動的時候不需要重新搜索玩家視野
。爲什麼呢?
因爲我們的視野信息是規則是按照一定優先級規則隨機篩選九宮格中的玩家。如果玩家在同一個格子中移動的時候也要刷新視野,因爲視野信息是隨機篩選的,所以會造成玩家在同一個格子中移動的時候,每次篩選的視野信息是會不一樣的,即周圍的玩家一直在變換,玩家突然出現突然消失,這樣並不是我們想要的效果,並且這樣對體驗是非常不好的,並且如果只要移動一下就重新搜索視野,視野搜索的調用也太頻繁。因此,在同一個格子中移動的時候不需要重新搜索玩家的視野信息。
玩家的視野信息會直接存儲在玩家身上
,當玩家有其他操作的行爲的時候比如釋放技能等,直接獲取玩家的視野數據,廣播給視野內的其他玩家即可。
玩家移動引起視野變動,玩家視野處理規則:
玩家每次移動到一個新格子,以新位置向外擴展的九宮格爲標準,新增的格子即爲重新搜索的格子(加入到玩家視野
) + 重疊部分的格子保留在視野中(玩家視野中繼續保留
) + 不在新的九宮格中的是需要刪除的格子(從玩家視野中刪除
)。
視野信息一般都是相互的
,當把A對象從自己的視野中刪除的時候,同時也要把自己從A對象的視野中刪除。
玩家移動引起視野刷新的可能情況
玩家移動,新增搜索視野塊的可能情況有以下幾種:
玩家Before
:指的是玩家之前的位置
綠色:
新增的需要搜索的格子
黃色:
繼續留在玩家視野中的格子
灰色:
需要從玩家視野中刪除的格子
移動方向 | 視野改變相關的格子。 |
---|---|
向上 移動一格 |
|
向下 移動一格 |
|
向左 移動一格 |
|
向右 移動一格 |
|
向左上 移動一格 |
|
向右上 移動一格 |
|
向左下 移動一格 |
|
向右下 移動一格 |
|
其他情況 (移動超過一個格子) 因爲新增視野塊數量太大,因此直接需要重新搜索視野 |
舉個例子,從A視野塊進入到B視野塊
,視野信息會如何進行處理呢?
玩家從A移動到B,A視野塊對象的雙向鏈表中刪去玩家,B視野塊對象中的雙向鏈表插入玩家
優先級篩選視野規則
我們知道玩家的視野是有上限的,當九宮格視野塊中的對象大於玩家的視野上限的時候,我們就需要有一些篩選規則,根據指定的優先級規則
選擇選取相應數量的對象。
優先級枚舉
:
0
:社交關係。
1
:交互npc
2-9
:8個不同等級的距離
社交關係、npc關係,一般用於可以直接通過玩家可以獲取對應對象
這一類。比如隊伍信息,鏢車信息,這些可以直接通過玩家身上存儲的信息獲得對應的對象。
玩家視野篩選流程:
- 獲取玩家在移動後的新位置需要
新增搜索的格子列表
。 - 獲取玩家身上存儲的視野信息,把
玩家當前視野中的所有玩家
都標記爲 CURR_VIEW(玩家當前視野中)。 收集社交關係的視野
,比如玩家的隊伍信息,遍歷玩家當前隊伍中的所有的成員(在線並且在同一張地圖):
如果之前就在玩家的視野中(CURR_VIEW標記,2中已標記),修改視野標記爲KEEP_IN_VIEW(繼續留在玩家視野中)
如果之前不再玩家的視野中,加入到社交優先級隊列中,等待後續篩選。收集npc視野
,比如鏢車等,可以通過玩家身上存儲的信息獲取npc對象的這一類。
如果之前就在玩家的視野中(CURR_VIEW標記,2中已標記),修改視野標記爲KEEP_IN_VIEW(繼續留在玩家視野中)
如果之前不再玩家的視野中,加入到npc優先級隊列中,等待後續篩選。處理距離相關視野
,遍歷新增搜索的格子(1 中得到的)。針對每一個格子,遍歷該格子(視野塊中雙向鏈表)中的雙向鏈表,獲取每一個鏈表對象:
若標記爲KEEP_IN_VIEW的對象總數 < 玩家視野上限
並且鏈表對象之前就在玩家的視野中
(標記CURR_VIEW),修改視野標記爲KEEP_IN_VIEW(繼續留在玩家視野中);
否則
如果鏈表對象是npc,放入npc的優先隊列中;如果是玩家,根據距離等級放入對應距離的優先級隊列。新舊視野合並
。- 找出並把所有
新加入視野中的對象存儲到EnterList
-------即篩選優先級列表中的玩家放入進入玩家視野列表計算剩餘未分配名額LeftAllotNum
= 玩家視野上限 - 標記KEEP_IN_VIEW的玩家總數(前面肯定在標記爲KEEP_IN_VIEW的時候設置了計數)。- 如果
剩餘未分配名額 LeftAllotNum < 社交關係隊列總數 + npc隊列總數
,即可分配給社交關係和npc關係的名額不夠,則隨機刪除保留在玩家視野中的玩家(標記爲KEEP_IN_VIEW的,肯定不能刪除和玩家是社交關係/npc),刪除後即爲社交關係+npc優先級隊列留餘額。 - 給最高優先級的隊列(社交關係+ npc)分配進入視野的名額。(最高優先級隊列的對象全部進入視野,選取粒度設置爲1)
- 給各距離優先級隊列分配進入視野的名額。根據權重對剩餘名額LeftAllotNum進行分配,分配到不同距離優先級隊列中,同時分配選取粒度(選取粒度 = (隊列中元素總數 - 1) / 可給該隊列分配的名額數 + 1)。如果各個距離優先級分配完之後還有剩餘名額,直接分配給第一距離優先級隊列。
- 各個優先級隊列根據已經分配的名額隨機選取進入視野的對象EnterList列表,篩選出來的對象存入。首先
按照指定的粒度
進行選取(達到隨機的效果),如果還有剩下的名額沒用完,按照1粒度
選取,如果還有剩下的,直接分配給下一個優先級隊列
。
- 找出
離開視野的對象存儲到LeaveList,並從視野中刪除
-------即刪除需要離開玩家之前視野中的對象- 遍歷玩家身上存儲的當前視野列表數據,標記爲CURR_VIEW的即是需要刪除的對象(因爲如果要繼續保留在玩家視野中,那部分對象已經標記爲KEEP_IN_VIEW了)。把要刪除的對象存儲到LeaveList,並且從視野中刪除,同時從對方的視野中把自己刪除(因爲視野是相互的)。
把EnterList列表中的對象加入到玩家視野
------------------------把優先級列表中篩選出來的玩家加入玩家視野- 遍歷EnterList中要加入到玩家視野的對象叫做A。如果A是玩家,並且A的視野已滿,則找一個距離足夠遠的,從A的視野中刪除,並且也把A從對方視野中刪除。這樣A的視野就有一個位置來加入當前玩家了。A和玩家互相加入視野。
- 下發視野變化數據,離開視野的notify + 進入視野的notify
向2中LeaveList列表中的玩家下發離開視野的通知
向1中EnterList列表中的玩家下發進入視野的通知
- 找出並把所有