從零開始重寫KOK1(萬王之王1) —— (4)遮擋、阻擋與尋路

0. 上篇文章,人物已經可以正確的朝向和移動了,這次我們要添加幾個石頭,並且達到以下效果

1) 人物和石頭根據站位可以正確產生遮擋效果

2) 人物被石頭阻擋住,即不能穿過石頭

3) 當鼠標點擊石頭後面時,人物可以自動繞過石頭走過去

效果截圖:

尋路

可直接運行版本下載:>>點擊進入下載頁面<<

 

1. 物件管理器與遮擋

因爲我們之前只有玩家一個物體,所以在畫物體的時候直接調用player.draw()就可以了,但是當有很多物體的時候,我們就需要有一個管理器,來自動幫我們畫當前窗口中的所有物體。爲此我們可以利用C++的繼承特性,給所有物體定義一個基類,這個基類負責定義統一的draw()方法,那麼物件管理器就可以忽略這些物體的具體類型而統統調用draw()就行了。

下面的代碼是所有物體的基類定義:

 

對於物件管理器,則非常的簡單,就是把所有的物件的初始化、繪製、清理都在這個管理器裏面去做,所以他的定義也是4個方法,另外,爲了簡單,我們沒有采用從文件來讀物件數據的方式,所以玩家和石頭直接在物件管理器中硬編碼了。下面是頭文件:

其中ListObj存放的是基類型的指針,用來執行他們共有的Init、Logic、Draw、Release方法。在private中的東西就是上面我說的硬編碼的物體。

 

下面我們來看看ObjManager的實現,非常簡單:

我們一個一個函數來看,首先是ObjManager::Init(),他首先把所有的石頭都加載到了統一的物件列表裏,然後再初始化玩家並放入列表。這裏有個小細節,就是先初始化石頭,則地圖上面很多地方就不可以放置玩家了,所以要先放石頭再放玩家,否則可能玩家當期的座標是在石頭的內部,就會卡住了。具體如何進行判斷落地點的,在後面會有介紹。

 

然後我們會發現,這裏有一個int CompareGameObj(GameObj *obj1, GameObj *obj2)的函數,這個是來做什麼的呢。不知道大家有沒有聽說過畫家算法,原理很簡單,就是先畫背景,再畫前景,這樣就會產生層次效果。因爲我們的遊戲是斜45度,所以相當於在座標Y大的(即屏幕靠下的)是離大家近的,Y小的就是離大家遠的,所以我們可以根據Y的值來判斷先畫誰後畫誰。這也就是遮擋的原理了。所以這個函數的作用就不言自明瞭,是提供給list的sort方法用的,讓他們根據Y來排序。如圖所示:

物件管理器

接下來的函數一看就知道了,除了Logic方法對ListObj排了一下序,其他的方法都是直接循環調用ListObj中GameObj的相關方法,沒有什麼好說的了。

 

2. 道路管理器WayManager與阻擋

說起尋路,估計大家都聽過A*尋路算法,所以我第一感覺就是學習一下這個算法,再來看看我怎麼去用。學習之後發現,果然可用,對於A*尋路算法我這裏就不多講了,隨便搜搜就好多文章,這裏簡單說說他是幹什麼的,如下圖:

A*尋路

A*尋路就是在幹這麼一件事:將地圖分爲一堆格子,那麼我們已知起點格(玩家當前格)、終點格(鼠標點擊格)和格子是否是障礙(地圖信息),求從起點到終點,途經的格子隊列,即路徑。

 

所以在遊戲中,我們相當於將一個大的網格附加到我們的地圖上,形成一個WayTable數組,其實這個網格和地圖沒什麼關係,有聯繫的只有他們所佔的像素多少。然後我們會注意到一個問題,玩家所處的點,就是一個像素的座標點,而一個格子佔用了很多像素,這個怎麼轉換呢。我這裏使用的是這種方式,因爲將這個網格看成是一個數組WayTable,那麼我就可以通過玩家的座標點和每個網格的長寬佔用多少像素爲條件,求得當前玩家所處的WayTable的Index。反之,我可以通過一個WayTable[n],來求得n所在的網格的像素中心點的座標。由於數組是一維的,不方便只管的表示網格的位置,所以我經常把一個索引看成爲這種形式:n = x + y*TableWidth,這裏x是網格的列數,y是網格的行數,TableWidth是每行有多少個網格。而且一套X,Y正好可以看成是一個POINT類型。還有一個問題,就是當我們任意給定一個點,我們要找到距離這個點最近的不是障礙的網格,這樣當玩家點到了石頭上,仍然可以走到石頭的附近去,而且前面講的ObjManager的Init方法,要計算玩家的落地點,也是有這個需求。

綜上所述,這個WayManager的定義如下:

 其中有一個PathPoint的結構,現在不用管它,他是爲了實現A*尋路算法而用的結構。大家翻閱A*尋路算法時自然能明白其用處。同樣,在private中的變量也都是用來尋路的中間變量。

 

上面的幾個轉換的輔助函數很簡單,實現如下:

 

A*尋路算法的實現代碼也貼出來,裏面有註釋,我的這個函數將會返回計算出的路點的WayTable的一系列Index,直接調用後,就能拿這些Index來用了,很方便:

 

那麼就剩一個小問題了,計算落地點,我畫了個圖,想法很簡單:

尋找可用路點

即,假如搜索起點不可用,比如石頭已經把這個路點(格子)佔用了,那麼我將在一個半徑R內找一個可用的格子,我們通過圖片可以發現一個特點,所有的紅色實現長度L均滿足 L = 2R。讀者可以試試半徑更大的情況,仍然如此。所以這個搜索函數實現如下:

 

至此,遊戲已經達到文章開始時預期的效果。

 

其實還有很多方法可以在這個基礎上進行優化,我能想到的就有不少:

1) 在A*尋路算法時維護開啓列表的順序,使其不用每次find時都需要再排序

2) 以後在加上小地圖時,當玩家在小地圖上點擊一個位置時,可以用較大的網格來計算一個粗糙值,減少性能開銷

3) 以後如果出現地圖上有孤島,導致要便利所有路點時,要把這些孤島的索引放在一個列表裏管理,防止大量遍歷

4) 地圖沒有探出來之前,應該是無法尋路的,比如小地圖是黑的,那麼不應該點一下小地圖就自動去尋找最佳路線,那樣太假了。

5) 其實現在最大的問題在於,我們每個鼠標按下的的幀中,都會去算個路徑,實際上沒有必要,對於直線間沒有障礙的情況,應該讓他直接走過去而不去尋路,對於這點的發現,是我玩Diablo2的無意間,請看下面兩張圖:

尋路

 

不尋路

 

在沒有障礙物的情況下,人物會非常平滑的移動,而在有障礙物的情況下,人物即使繞過障礙物後走直線,仍然有所輕微抖動。

 

終於寫完了,完整項目代碼下載地址,和上一個文章一樣,因爲那時已經都寫完了。

>>點擊進入下載頁<<

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章