數據結構在遊戲中的應用
在遊戲的編寫中,不可避免的出現很多應用數據結構的地方,有些簡單的遊戲,只是由幾個數據結構的組合,所以說,數據結構在遊戲編程中扮演著很重要的角色。
本文主要講述數據結構在遊戲中的應用,其中包括對鏈表、順序表、棧、隊列、二叉樹及圖的介紹。讀者在閱讀本文以前,應對數據結構有所了解,並且熟悉C/C++語言的各種功用。好了,現在我們由鏈表開始吧!
1、鏈表
在這一節中,我們將通過一個類似雷電的飛機射擊遊戲來講解鏈表在遊戲中的應用。在飛機遊戲中,鏈表主要應用在發彈模塊上。首先,飛機的×××是要頻繁的出現,消除,其個數也是難以預料的。鏈表主要的優點就是可以方便的進行插入,刪除操作。我們便將鏈表這一數據結構引入其中。首先,分析下面的源代碼,在其中我們定義了坐標結構和×××鏈表。
struct CPOINT
{
int x; // X軸坐標
int y; // Y軸坐標
};
struct BULLET
{
struct BULLE* next; // 指向下一個×××
CPOINT bulletpos; // ×××的坐標
int m_ispeed; // ×××的速度
};
接下來的代碼清單是飛機類中關於×××的定義:
class CMYPLANE
{
public:
void AddBullet(struct BULLET); // 加入×××的函數,每隔一定時間加彈
void RefreshBullet(); // 刷新×××
struct BULLET st_llMyBullet; // 聲明飛機的×××鏈表
};
在void AddBullet(struct BULLET*)中,我們要做的操作只是將一個結點插入鏈表中,並且每隔一段時間加入,就會產生連續發彈的效果。
這是加彈函數主要的源代碼:
void AddBullet(struct BULLET)
{
struct BULLET st_llNew,st_llTemp; // 定義臨時鏈表
st_llNew=_StrucHead; // 鏈表頭(已初始化)
st_llNew->(BULLET st_llMyBullet
st_llTemp= =_NewBullet; // 臨時存值
st_llNew->next=st_llTemp->next; st_llTemp->next=st_llNew;
}
函數Void RefreshBullet()中,我們只要將鏈表歷遍一次就行,將×××的各種數據更新,其中主要的源代碼如下:
while(st_llMyBullet->next!=NULL)
{
// 查找
st_llMyBullet->bulletpos.x-=m_ispeed; // 更新×××數據
………
st_llMyBullet=st_llMyBullet->next; // 查找運算
}
經過上面的分析,在遊戲中,鏈表主要應用在有大規模刪除,添加的應用上。不過,它也有相應的缺點,就是查詢是順序查找,比較耗費時間,並且存儲密度較小,對空間的需求較大。
如果通過對遊戲數據的一些控制,限定大規模的添加,也就是確定了內存需求的上限,可以應用順序表來代替鏈表,在某些情況下,順序表可以彌補鏈表時間性能上的損失。當然,應用鏈表,順序表還是主要依靠當時的具體情況。那麽,現在,進入我們的下一節,遊戲中應用最廣的數據結構 — 順序表。
2、順序表
本節中,我們主要投入到RPG地圖的建設中,聽起來很嚇人,但是在RPG地圖系統中(特指磚塊地圖系統),卻主要使用數據結構中最簡單的成員 — 順序表。
我們規定一個最簡單的磚塊地圖系統,視角為俯視90度,並由許多個順序連接的圖塊拼成,早期RPG的地圖系統大概就是這樣。我們這樣定義每個圖塊:
struct TILE // 定義圖塊結構
{
int m_iAcesse; // 紀錄此圖塊是否可以通過
…… // 其中有每個圖塊的圖片指針等紀錄
};
當m_iAcesse=0,表示此圖塊不可通過,為1表示能通過。
我們生成如下地圖:
TILE TheMapTile[10][5];
並且我們在其中添入此圖塊是否可以通過,可用循環將數值加入其中,進行地圖初始化。
如圖表示:
0 1 2 3 4 5 6 7 8 9
0 1 1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 1 1 1 1 0
2 0 0 0 0 0 1 1 1 1 0
3 0 0 0 0 0 1 1 1 1 0
4 1 1 1 1 1 1 1 1 1 1
圖1
從上圖看到這個地圖用順序表表示非常直接,當我們控制人物在其中走動時,把人物將要走到的下一個圖塊進行判斷,看其是否能通過。比如,當人物要走到(1,0)這個圖塊,我們用如下代碼判斷這個圖塊是否能通過:
int IsAcesse(x,y)
{
return TheMapTile[x,y].m_iAcesse; // 返回圖塊是否通過的值
}
上述只是簡單的地圖例子,通過順序表,我們可以表示更復雜的磚塊地圖,並且,現在流行的整幅地圖中也要用到大量的順序表,在整幅中進行分塊。
好了,現在我們進入下一節:
3、棧和隊列
棧和隊列是兩種特殊的線性結構,在遊戲當中,一般應用在腳本引擎,操作界面,數據判定當中。在這一節中,主要通過一個簡單的腳本引擎函數來介紹棧,隊列和棧的用法很相似,便不再舉例。
我們在設置腳本文件的時候,通常會規定一些基本語法,這就需要一個解讀語法的編譯程序。這裏列出的是一個語法檢查函數,主要功能是檢查“()”是否配對。實現思想:我們規定在腳本語句中可以使用“()”嵌套,那麽,便有如下的規律,左括號和右括號配對一定是先有左括號,後有右括號,並且,在嵌套使用中,左括號允許單個或連續出現,並與將要出現的有括號配對銷解,左括號在等待右括號出現的過程中可以暫時保存起來。當右括號出現後,找不到左括號,則發生不配對現象。從程序實現角度講,左括號連續出現,則後出現的左括號應與最先到來的右括號配對銷解。左括號的這種保存和與右括號的配對銷解的過程和棧中後進先出原則是一致的。我們可以將讀到的左括號壓入設定的棧中,當讀到右括號時就和棧中的左括號銷解,如果在棧頂彈不出左括號,則表示配對出錯,或者,當括號串讀完,棧中仍有左括號存在,也表示配對出錯。
大致思想便是這樣,請看代碼片斷:
struct // 定義棧結構
{
int m_iData[100]; // 數據段
int m_iTop; // 通常規定棧底位置在向量低端
}SeqStack;
int Check(SeqStack *stack) // 語法檢查函數
{
char sz_ch;
int boolean; Push(stack,‘# ‘); // 壓棧,#為判斷數據
sz_ch=getchar(); // 取值
boolean=1;
while(sz_ch!=‘\n‘&&boolean)
{
if(sz_ch= =‘(‘)
Push(stack,ch);
if(sz_ch= =‘)‘)
if(gettop(stack)= =‘#‘) // 讀棧頂
boolean=0;
else
Pop(stack); // 出棧
sz_ch=getchar();
}
if(gettop(stack)!=‘#‘) boolean=0;
if(boolean) cout<<"right"; // 輸出判斷信息
else
cout<<"error";
這裏只是介紹腳本的讀取,以後,我們在圖的介紹中,會對腳本結構進行深入的研究。
總之,凡在遊戲中出現先進後出(棧),先進先出(隊列)的情況,就可以運用這兩種數據結構,例如,《帝國時代》中地表中間的過渡帶。
4、二叉樹
樹應用及其廣泛,二叉樹是樹中的一個重要類型。在這裏,我們主要研究二叉樹的一種應用方式:判定樹。其主要應用在描述分類過程和處理判定優化等方面上。
在人工智能中,通常有很多分類判斷。現在有這樣一個例子:設主角的生命值d,在省略其他條件後,有這樣的條件判定:當怪物碰到主角後,怪物的反應遵從下規則:
根據條件,我們可以用如下普通算法來判定怪物的反應:
if(d<100) state=嘲笑,單挑;
else if(d<200) state=單挑;
else if(d<300) state=嗜血魔法;
else if(d<400) state=呼喚同伴;
else state=逃跑;
上面的算法適用大多數情況,但其時間性能不高,我們可以通過判定樹來提高其時間性能。首先,分析主角生命值通常的特點,即預測出每種條件占總條件的百分比,將這些比值作為權值來構造最優二叉樹(哈夫曼樹),作為判定樹來設定算法。假設這些百分比為:
構造好的哈夫曼樹為:
對應算法如下:
if(d>=200)&&(d<300) state=嗜血魔法;
else if(d>=300)&&(d<500) state=呼喚同伴;
else if(d>=100)&&(d<200) state=單挑;
else if(d<100) state=嘲笑,單挑;
else state=逃跑;
通過計算,兩種算法的效率大約是2:3,很明顯,改進的算法在時間性能上提高不少。
一般,在即時戰略遊戲中,對此類判定算法會有較高的時間性能要求,大家可以對二叉樹進行更深入的研究。現在,我們進入本文的最後一節:圖的介紹,終於快要完事了。
5、圖
在遊戲中,大多數應用圖的地方是路徑搜索,即關於A算法的討論。由於介紹A算法及路徑搜索的文章很多,這裏介紹圖的另一種應用:在情節腳本中,描述各個情節之間的關系。
在一個遊戲中,可能包含很多分支情節,在這些分支情節之間,會存在著一定的先決條件約束,即有些分支情節必須在其他分支情節完成後方可開始發展,而有些分支情節沒有這樣的約束。
通過分析,我們可以用有向圖中AOV網(Activity On Vertex Network)來描述這些分支情節之間的先後關系。好了,現在假如我們手頭有這樣的情節:
情節編號 情節 先決條件
C1 遭遇強盜 無
C2 受傷 C1
C3 買藥 C2
C4 看醫生 C2
C5 治愈 C3,C4
註意:在AOV網中,不應該出現有向環路,否則,頂點的先後關系就會進入死循環。即情節將不能正確發展。我們可以采取拓撲派序來檢測圖中是否存在環路,拓撲排序在一般介紹數據結構的書中,都有介紹,這裏便不再敘述。
那麽以上情節用圖的形式表現為(此圖為有向圖,先後關系在上面表格顯示):
現在我們用鄰接矩陣表示此有向圖,請看下面代碼片斷:
struct MGRAPH
{
int Vexs[MaxVex]; // 頂點信息
int Arcs[MaxLen][MaxLen]; // 鄰接矩陣
……
};
頂點信息都存儲在情節文件中。
將給出的情節表示成鄰接矩陣:
0 1 0 0 0
0 0 1 1 0
0 0 0 0 1
0 0 0 0 1
0 0 0 0 0
圖4
我們規定,各個情節之間有先後關系,但沒有被玩家發展的,用1表示。當情節被發展的話,就用2表示,比如,我們已經發展了遭遇強盜的情節,那麽,C1與C2頂點之間的關系就可以用2表示,註意,並不表示C2已經發展,只是表示C2可以被發展了。
請看下面的代碼:
class CRelation
{
public:
CRelation(char filename); // 構造函數,將情節信息文件讀入到緩存中
void SetRelation(int ActionRelation); // 設定此情節已經發展
BOOL SearchRelation(int ActionRelation); // 尋找此情節是否已發展
BOOL SaveBuf(char filename); // 保存緩存到文件中
……
privated:
char* buf; // 鄰接矩陣的內存緩沖
……
};
在這裏,我們將表示情節先後關系的鄰接矩陣放到緩沖內,通過接口函數進行情節關系的修改,在BOOL SearchRelation(int ActionRelation)函數中,我們可以利用廣度優先搜索方法進行搜索,介紹這方面的書籍很多,代碼也很長,在這裏我就不再舉例了。
我們也可以用鄰接鏈表來表示這個圖,不過,用鏈表表示會占用更多的內存,鄰接鏈表主要的優點是表示動態的圖,在這裏並不適合。
另外,圖的另一個應用是在尋路上,著名的A*算法就是以此數據結構為基礎,人工智能,也需要它的基礎。
數據結構在遊戲中的應用