下面我想來談談關于服務器上NPC的設計以及NPC智能等一些方面涉及到的問題。首先,我們需要知道什么是NPC,NPC需要做什么。NPC的全稱是(Non-Player Character),很顯然,他是一個character,但不是玩家,那么從這點上可以知道,NPC的某些行為是和玩家類似的,他可以行走,可以戰斗,可以呼吸(這點將在后面的NPC智能里面提到),另外一點和玩家物件不同的是,NPC可以復生(即NPC被打死以后在一定時間內可以重新出來)。其實還有最重要的一點,就是玩家物件的所有決策都是玩家做出來的,而NPC的決策則是由計算機做出來的,所以在對NPC做何種決策的時候,需要所謂的NPC智能來進行決策。
下面我將分兩個部分來談談NPC,首先是NPC智能,其次是服務器如何對NPC進行組織。之所以要先談NPC智能是因為只有當我們了解清楚我們需要NPC做什么之后,才好開始設計服務器來對NPC進行組織。
NPC智能
NPC智能分為兩種,一種是被動觸發的事件,一種是主動觸發的事件。對于被動觸發的事件,處理起來相對來說簡單一些,可以由事件本身來呼叫NPC身上的函數,比如說NPC的死亡,實際上是在NPC的HP小于一定值的時候,來主動呼叫NPC身上的OnDie() 函數,這種由事件來觸發NPC行為的NPC智能,我稱為被動觸發。這種類型的觸發往往分為兩種:
一種是由別的物件導致的NPC的屬性變化,然后屬性變化的同時會導致NPC產生一些行為。由此一來,NPC物件里面至少包含以下幾種函數:
class NPC {
public:
??? // 是誰在什么地方導致了我哪項屬性改變了多少。
??? OnChangeAttribute(object_t *who, int which, int how, int where);
Private:
??? OnDie();
??? OnEscape();
??? OnFollow();
??? OnSleep();
??? // 一系列的事件。
}
這是一個基本的NPC的結構,這種被動的觸發NPC的事件,我稱它為NPC的反射。但是,這樣的結構只能讓NPC被動的接收一些信息來做出決策,這樣的NPC是愚蠢的。那么,怎么樣讓一個NPC能夠主動的做出一些決策呢?這里有一種方法:呼吸。那么怎么樣讓NPC有呼吸呢?
一種很簡單的方法,用一個計時器,定時的觸發所有NPC的呼吸,這樣就可以讓一個NPC有呼吸起來。這樣的話會有一個問題,當NPC太多的時候,上一次NPC的呼吸還沒有呼吸完,下一次呼吸又來了,那么怎么解決這個問題呢。這里有一種方法,讓NPC異步的進行呼吸,即每個NPC的呼吸周期是根據NPC出生的時間來定的,這個時候計時器需要做的就是隔一段時間檢查一下,哪些NPC到時間該呼吸了,就來觸發這些NPC的呼吸。
上面提到的是系統如何來觸發NPC的呼吸,那么NPC本身的呼吸頻率該如何設定呢?這個就好象現實中的人一樣,睡覺的時候和進行激烈運動的時候,呼吸頻率是不一樣的。同樣,NPC在戰斗的時候,和平常的時候,呼吸頻率也不一樣。那么就需要一個Breath_Ticker來設置NPC當前的呼吸頻率。
那么在NPC的呼吸事件里面,我們怎么樣來設置NPC的智能呢?大體可以概括為檢查環境和做出決策兩個部分。首先,需要對當前環境進行數字上的統計,比如說是否在戰斗中,戰斗有幾個敵人,自己的HP還剩多少,以及附近有沒有敵人等等之類的統計。統計出來的數據傳入本身的決策模塊,決策模塊則根據NPC自身的性格取向來做出一些決策,比如說野蠻型的NPC會在HP比較少的時候仍然猛撲猛打,又比如說智慧型的NPC則會在HP比較少的時候選擇逃跑。等等之類的。
至此,一個可以呼吸,反射的NPC的結構已經基本構成了,那么接下來我們就來談談系統如何組織讓一個NPC出現在世界里面。
NPC的組織
這里有兩種方案可供選擇,其一:NPC的位置信息保存在場景里面,載入場景的時候載入NPC。其二,NPC的位置信息保存在NPC身上,有專門的事件讓所有的NPC登陸場景。這兩種方法有什么區別呢?又各有什么好壞呢?
前一種方法好處在于場景載入的時候同時載入了NPC,場景就可以對NPC進行管理,不需要多余的處理,而弊端則在于在刷新的時候是同步刷新的,也就是說一個場景里面的NPC可能會在同一時間內長出來。而對于第二種方法呢,設計起來會稍微麻煩一些,需要一個統一的機制讓NPC登陸到場景,還需要一些比較麻煩的設計,但是這種方案可以實現NPC異步的刷新,是目前網絡游戲普遍采用的方法,下面我們就來著重談談這種方法的實現:
首先我們要引入一個“靈魂”的概念,即一個NPC在死后,消失的只是他的肉體,他的靈魂仍然在世界中存在著,沒有呼吸,在死亡的附近漂浮,等著到時間投胎,投胎的時候把之前的所有屬性清零,重新在場景上構建其肉體。那么,我們怎么來設計這樣一個結構呢?首先把一個場景里面要出現的NPC制作成圖量表,給每個NPC一個獨一無二的標識符,在載入場景之后,根據圖量表來載入屬于該場景的NPC。在NPC的OnDie() 事件里面不直接把該物件destroy 掉,而是關閉NPC的呼吸,然后打開一個重生的計時器,最后把該物件設置為invisable。這樣的設計,可以實現NPC的異步刷新,在節省服務器資源的同時也讓玩家覺得更加的真實。
(這一章節已經牽扯到一些服務器腳本相關的東西,所以下一章節將談談服務器腳本相關的一些設計)
補充的談談啟發式搜索(heuristic searching)在NPC智能中的應用。
其主要思路是在廣度優先搜索的同時,將下一層的所有節點經過一個啟發函數進行過濾,一定范圍內縮小搜索范圍。眾所周知的尋路A*算法就是典型的啟發式搜索的應用,其原理是一開始設計一個Judge(point_t* point)函數,來獲得point這個一點的代價,然后每次搜索的時候把下一步可能到達的所有點都經過Judge()函數評價一下,獲取兩到三個代價比較小的點,繼續搜索,那些沒被選上的點就不會在繼續搜索下去了,這樣帶來的后果的是可能求出來的不是最優路徑,這也是為什么A*算法在尋路的時候會走到障礙物前面再繞過去,而不是預先就走斜線來繞過該障礙物。如果要尋出最優化的路徑的話,是不能用A*算法的,而是要用動態規劃的方法,其消耗是遠大于A*的。
那么,除了在尋路之外,還有哪些地方可以應用到啟發式搜索呢?其實說得大一點,NPC的任何決策都可以用啟發式搜索來做,比如說逃跑吧,如果是一個2D的網絡游戲,有八個方向,NPC選擇哪個方向逃跑呢?就可以設置一個Judge(int direction)來給定每個點的代價,在Judge里面算上該點的敵人的強弱,或者該敵人的敏捷如何等等,最后選擇代價最小的地方逃跑。下面,我們就來談談對于幾種NPC常見的智能的啟發式搜索法的設計:
Target select (選擇目標):
首先獲得地圖上離該NPC附近的敵人列表。設計Judge() 函數,根據敵人的強弱,敵人的遠近,算出代價。然后選擇代價最小的敵人進行主動攻擊。
Escape(逃跑):
在呼吸事件里面檢查自己的HP,如果HP低于某個值的時候,或者如果你是遠程兵種,而敵人近身的話,則觸發逃跑函數,在逃跑函數里面也是對周圍的所有的敵人組織成列表,然后設計Judge() 函數,先選擇出對你構成威脅最大的敵人,該Judge() 函數需要判斷敵人的速度,戰斗力強弱,最后得出一個主要敵人,然后針對該主要敵人進行路徑的Judge() 的函數的設計,搜索的范圍只可能是和主要敵人相反的方向,然后再根據該幾個方向的敵人的強弱來計算代價,做出最后的選擇。
Random walk(隨機走路):
這個我并不推薦用A*算法,因為NPC一旦多起來,那么這個對CPU的消耗是很恐怖的,而且NPC大多不需要長距離的尋路,只需要在附近走走即可,那么,就在附近隨機的給幾個點,然后讓NPC走過去,如果碰到障礙物就停下來,這樣幾乎無任何負擔。
Follow Target(追隨目標):
這里有兩種方法,一種方法NPC看上去比較愚蠢,一種方法看上去NPC比較聰明,第一種方法就是讓NPC跟著目標的路點走即可,幾乎沒有資源消耗。而后一種則是讓NPC在跟隨的時候,在呼吸事件里面判斷對方的當前位置,然后走直線,碰上障礙物了用A*繞過去,該種設計會消耗一定量的系統資源,所以不推薦NPC大量的追隨目標,如果需要大量的NPC追隨目標的話,還有一個比較簡單的方法:讓NPC和目標同步移動,即讓他們的速度統一,移動的時候走同樣的路點,當然,這種設計只適合NPC所跟隨的目標不是追殺的關系,只是跟隨著玩家走而已了。
下面我將分兩個部分來談談NPC,首先是NPC智能,其次是服務器如何對NPC進行組織。之所以要先談NPC智能是因為只有當我們了解清楚我們需要NPC做什么之后,才好開始設計服務器來對NPC進行組織。
NPC智能
NPC智能分為兩種,一種是被動觸發的事件,一種是主動觸發的事件。對于被動觸發的事件,處理起來相對來說簡單一些,可以由事件本身來呼叫NPC身上的函數,比如說NPC的死亡,實際上是在NPC的HP小于一定值的時候,來主動呼叫NPC身上的OnDie() 函數,這種由事件來觸發NPC行為的NPC智能,我稱為被動觸發。這種類型的觸發往往分為兩種:
一種是由別的物件導致的NPC的屬性變化,然后屬性變化的同時會導致NPC產生一些行為。由此一來,NPC物件里面至少包含以下幾種函數:
class NPC {
public:
??? // 是誰在什么地方導致了我哪項屬性改變了多少。
??? OnChangeAttribute(object_t *who, int which, int how, int where);
Private:
??? OnDie();
??? OnEscape();
??? OnFollow();
??? OnSleep();
??? // 一系列的事件。
}
這是一個基本的NPC的結構,這種被動的觸發NPC的事件,我稱它為NPC的反射。但是,這樣的結構只能讓NPC被動的接收一些信息來做出決策,這樣的NPC是愚蠢的。那么,怎么樣讓一個NPC能夠主動的做出一些決策呢?這里有一種方法:呼吸。那么怎么樣讓NPC有呼吸呢?
一種很簡單的方法,用一個計時器,定時的觸發所有NPC的呼吸,這樣就可以讓一個NPC有呼吸起來。這樣的話會有一個問題,當NPC太多的時候,上一次NPC的呼吸還沒有呼吸完,下一次呼吸又來了,那么怎么解決這個問題呢。這里有一種方法,讓NPC異步的進行呼吸,即每個NPC的呼吸周期是根據NPC出生的時間來定的,這個時候計時器需要做的就是隔一段時間檢查一下,哪些NPC到時間該呼吸了,就來觸發這些NPC的呼吸。
上面提到的是系統如何來觸發NPC的呼吸,那么NPC本身的呼吸頻率該如何設定呢?這個就好象現實中的人一樣,睡覺的時候和進行激烈運動的時候,呼吸頻率是不一樣的。同樣,NPC在戰斗的時候,和平常的時候,呼吸頻率也不一樣。那么就需要一個Breath_Ticker來設置NPC當前的呼吸頻率。
那么在NPC的呼吸事件里面,我們怎么樣來設置NPC的智能呢?大體可以概括為檢查環境和做出決策兩個部分。首先,需要對當前環境進行數字上的統計,比如說是否在戰斗中,戰斗有幾個敵人,自己的HP還剩多少,以及附近有沒有敵人等等之類的統計。統計出來的數據傳入本身的決策模塊,決策模塊則根據NPC自身的性格取向來做出一些決策,比如說野蠻型的NPC會在HP比較少的時候仍然猛撲猛打,又比如說智慧型的NPC則會在HP比較少的時候選擇逃跑。等等之類的。
至此,一個可以呼吸,反射的NPC的結構已經基本構成了,那么接下來我們就來談談系統如何組織讓一個NPC出現在世界里面。
NPC的組織
這里有兩種方案可供選擇,其一:NPC的位置信息保存在場景里面,載入場景的時候載入NPC。其二,NPC的位置信息保存在NPC身上,有專門的事件讓所有的NPC登陸場景。這兩種方法有什么區別呢?又各有什么好壞呢?
前一種方法好處在于場景載入的時候同時載入了NPC,場景就可以對NPC進行管理,不需要多余的處理,而弊端則在于在刷新的時候是同步刷新的,也就是說一個場景里面的NPC可能會在同一時間內長出來。而對于第二種方法呢,設計起來會稍微麻煩一些,需要一個統一的機制讓NPC登陸到場景,還需要一些比較麻煩的設計,但是這種方案可以實現NPC異步的刷新,是目前網絡游戲普遍采用的方法,下面我們就來著重談談這種方法的實現:
首先我們要引入一個“靈魂”的概念,即一個NPC在死后,消失的只是他的肉體,他的靈魂仍然在世界中存在著,沒有呼吸,在死亡的附近漂浮,等著到時間投胎,投胎的時候把之前的所有屬性清零,重新在場景上構建其肉體。那么,我們怎么來設計這樣一個結構呢?首先把一個場景里面要出現的NPC制作成圖量表,給每個NPC一個獨一無二的標識符,在載入場景之后,根據圖量表來載入屬于該場景的NPC。在NPC的OnDie() 事件里面不直接把該物件destroy 掉,而是關閉NPC的呼吸,然后打開一個重生的計時器,最后把該物件設置為invisable。這樣的設計,可以實現NPC的異步刷新,在節省服務器資源的同時也讓玩家覺得更加的真實。
(這一章節已經牽扯到一些服務器腳本相關的東西,所以下一章節將談談服務器腳本相關的一些設計)
補充的談談啟發式搜索(heuristic searching)在NPC智能中的應用。
其主要思路是在廣度優先搜索的同時,將下一層的所有節點經過一個啟發函數進行過濾,一定范圍內縮小搜索范圍。眾所周知的尋路A*算法就是典型的啟發式搜索的應用,其原理是一開始設計一個Judge(point_t* point)函數,來獲得point這個一點的代價,然后每次搜索的時候把下一步可能到達的所有點都經過Judge()函數評價一下,獲取兩到三個代價比較小的點,繼續搜索,那些沒被選上的點就不會在繼續搜索下去了,這樣帶來的后果的是可能求出來的不是最優路徑,這也是為什么A*算法在尋路的時候會走到障礙物前面再繞過去,而不是預先就走斜線來繞過該障礙物。如果要尋出最優化的路徑的話,是不能用A*算法的,而是要用動態規劃的方法,其消耗是遠大于A*的。
那么,除了在尋路之外,還有哪些地方可以應用到啟發式搜索呢?其實說得大一點,NPC的任何決策都可以用啟發式搜索來做,比如說逃跑吧,如果是一個2D的網絡游戲,有八個方向,NPC選擇哪個方向逃跑呢?就可以設置一個Judge(int direction)來給定每個點的代價,在Judge里面算上該點的敵人的強弱,或者該敵人的敏捷如何等等,最后選擇代價最小的地方逃跑。下面,我們就來談談對于幾種NPC常見的智能的啟發式搜索法的設計:
Target select (選擇目標):
首先獲得地圖上離該NPC附近的敵人列表。設計Judge() 函數,根據敵人的強弱,敵人的遠近,算出代價。然后選擇代價最小的敵人進行主動攻擊。
Escape(逃跑):
在呼吸事件里面檢查自己的HP,如果HP低于某個值的時候,或者如果你是遠程兵種,而敵人近身的話,則觸發逃跑函數,在逃跑函數里面也是對周圍的所有的敵人組織成列表,然后設計Judge() 函數,先選擇出對你構成威脅最大的敵人,該Judge() 函數需要判斷敵人的速度,戰斗力強弱,最后得出一個主要敵人,然后針對該主要敵人進行路徑的Judge() 的函數的設計,搜索的范圍只可能是和主要敵人相反的方向,然后再根據該幾個方向的敵人的強弱來計算代價,做出最后的選擇。
Random walk(隨機走路):
這個我并不推薦用A*算法,因為NPC一旦多起來,那么這個對CPU的消耗是很恐怖的,而且NPC大多不需要長距離的尋路,只需要在附近走走即可,那么,就在附近隨機的給幾個點,然后讓NPC走過去,如果碰到障礙物就停下來,這樣幾乎無任何負擔。
Follow Target(追隨目標):
這里有兩種方法,一種方法NPC看上去比較愚蠢,一種方法看上去NPC比較聰明,第一種方法就是讓NPC跟著目標的路點走即可,幾乎沒有資源消耗。而后一種則是讓NPC在跟隨的時候,在呼吸事件里面判斷對方的當前位置,然后走直線,碰上障礙物了用A*繞過去,該種設計會消耗一定量的系統資源,所以不推薦NPC大量的追隨目標,如果需要大量的NPC追隨目標的話,還有一個比較簡單的方法:讓NPC和目標同步移動,即讓他們的速度統一,移動的時候走同樣的路點,當然,這種設計只適合NPC所跟隨的目標不是追殺的關系,只是跟隨著玩家走而已了。