C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(二十) 第一部分拓展小結篇
寫了20節,一路向追著鬼子打一樣都沒停過,索性也想暫時休息一下整理整理思緒好完成後面的第二部分更為精彩的內容:諸如跟隨式地圖移動模式、NPC & 怪物 與主角的互動、對象AI、攻擊與魔法、各種類型傷害計算、完美的RPG游戲界面……等等等等,激動嗎?講實話:我很激動!
讀者聲音:還沒寫就開始激動了,典型的傻子。
^_^||言歸正傳,本節就先來個承上啟下的的小結吧,我打算分4個部分對前20節內容進行補充拓展:
一、完美的改進型A*尋路移動模式
在C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十九) 完美精靈之八面玲珑(WPF Only)③中,我們雖然實現了精靈的全方向與動作,但是細心的朋友就會發現,精靈在走路的時候一直使用著A*;這將導致兩個問題:1、性能上的損失,每次移動不管中間是否有障礙物都啟動尋路算法,造成資源的白白浪費;2、在C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十二)神奇的副本地圖 的結尾我曾輕描淡寫的敘述了如何實現改進型A*,雖然通過副本地圖簡單實現了,但是暫時並不完美。那麼,下面我將向大家講解通過地地道道的方法實現改進型完美A*移動模式。
何謂改進型完美A*移動模式?即主角每次移動時,首先並不啟動A*尋路而是直接建立兩點間的直線移動;接下來即進行時時的障礙物判斷,如果沒有碰撞到任何障礙物或對象則將該直線移動保持到終點;但是中途一旦碰到障礙物,則以目的地為終點即時啟動A*尋路。
原理很簡單,關鍵技術就是如何對碰撞進行檢測?
傳統的方法有兩種:
第一種我且稱之為坐標還原法:即時時記錄精靈未碰撞障礙物時的坐標(Old_X,Old_Y),在精靈移動時一旦檢測到精靈此時站到了障礙物上,則將精靈此時的坐標進行還原(X=Old_X,Y= Old_Y),然後啟動A*尋路。此方法的優點是使用簡單,不需要復雜的判斷邏輯;缺點是效果不好,在畫面上將造成精靈一瞬間被彈開的情況,雖然那一刻非常的短暫且距離微小,但是對於精靈移動動畫平滑性的影響是嚴重的,因此我們最好不要采用此方法。
第二種為啟發式預測法:該方法的原理為時時對精靈前方的區域進行預測,一旦發現前方有障礙物,則即時啟動A*尋路直到目的地。該方法可謂絕對皇室血統,一個字“正”,集所有優點之大成者;優點多相對的實現起來難度就大些。在WPF/Silverlight中如何實現之?先來看下圖:
上圖中已經給了很詳細的說明,即在直線移動過程中,精靈時時判斷此時朝向前方的單元格是否為障礙物,如果是則啟動A*尋路饒過它。充分理解了原理後,我們可以通過如下方法來返回精靈是否將要遇到障礙物了:
//判斷是否將要碰撞到障礙物(障礙物預測法)
private bool WillCollide() {
switch ((int)Spirit.Direction) {
case 0:
return Matrix[(int)(Spirit.X / GridSize), (int)(Spirit.Y / GridSize) - 1] == 0 ? true : false;
case 1:
return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize) - 1] == 0 ? true : false;
case 2:
return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize)] == 0 ? true : false;
case 3:
return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize) + 1] == 0 ? true : false;
case 4:
return Matrix[(int)(Spirit.X / GridSize), (int)(Spirit.Y / GridSize) + 1] == 0 ? true : false;
case 5:
return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize) + 1] == 0 ? true : false;
case 6:
return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize)] == 0 ? true : false;
case 7:
return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize) - 1] == 0 ? true : false;
default:
return true;
}
}
WillCollide()方法依據精靈的朝向判斷精靈前方是否為障礙物(即判斷障礙物數組Matrix[,]此時是否為0)。
有了它以後,我們同樣還需要像C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十二)神奇的副本地圖 一樣建立一個名為NormalMoveTo()的方法用於精靈直線移動,此時我們只需要在第十二節代碼的基礎上增加精靈朝向部分即可:
//直線移動
private void NormalMoveTo(Point p) {
//總的移動花費
int totalcost = (int)Math.Sqrt(Math.Pow(p.X - Spirit.X, 2) + Math.Pow(p.Y - Spirit.Y, 2)) / GridSize * UnitMoveCost;
……
//創建主角朝向屬性動畫
double direction = Super.GetDirectionByTan(p.X, p.Y, Spirit.X, Spirit.Y);
doubleAnimation = new DoubleAnimation(
direction,
direction,
new Duration(TimeSpan.FromMilliseconds(totalcost))
);
Storyboard.SetTarget(doubleAnimation, Spirit);
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Direction"));
storyboard.Children.Add(doubleAnimation);
//動畫播放
storyboard.Begin();
}
這裡要特別注意的是我用黃色背景注明的totalcost這個變量,它的值代表精靈在兩點間移動所需要花費的時間,計算它的目的是因為Storyboard動畫是基於時間軸的動畫(即在一個規定時間內完成指定動畫),第一節中也有相應的說明。因此,為了讓精靈在全角度(不僅僅是8個方向,是360度全方位)的任意兩點間直線移動時均使用統一速度(每移動一個單元格固定花費UnitMoveCost毫秒),這樣不論兩點間是30度、40度、55度、76.3度、87.6度等等隨意多少角度,精靈均能進行平滑的均速移動。
OK,一切就緒,接下來就是在游戲窗口中的鼠標左鍵點擊事件中啟動精靈的直線移動:
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p = e.GetPosition(Map); //點擊的地方在Map中的坐標點
//假如點擊的地點不是障礙物
if (Matrix[(int)p.X / GridSize, (int)p.Y / GridSize] != 0) {
Spirit.Destination = p; //設置主角的最終移動目的地
Spirit.Action = Actions.Run; //主角動作切換成跑步狀態
Spirit.IsAStarMoving = false; //非尋路模式
NormalMoveTo(p); //兩點間建立直線移動
}
}
看完上面代碼有朋友就要問了:IsAStarMoving是什麼東西?簡單講,它是精靈直線移動與A*尋路移動的樞紐。雖然我們實現了NormalMoveTo()和AStarMoveTo()這兩種移動方式,但是如何在邏輯中對其進行很好的銜接,這裡就必須加入IsAStarMoving這個精靈屬性,有了它,我們就可以在窗口刷新事件中這樣寫:
//游戲窗口刷新主線程間隔事件
private void Timer_Tick(object sender, EventArgs e) {
……
//判斷主角是否移動到了目標,如果是則動作切換成停止
if (ArriveTarget()) {
Spirit.Action = Actions.Stop;
} else if (!Spirit.IsAStarMoving && WillCollide()) {
//在尋路移動模式中,主角100%會饒過障礙物的,
//因此只有在非尋路模式中才需要時時判斷主角是否將要碰撞障礙物
AStarMoveTo(Spirit.Destination);
Spirit.IsAStarMoving = true;
}
}
通過黃色背景代碼部分的邏輯我們可以輕松實現精靈的直線移動與A*移動的轉換。即精靈首先進行直線移動,在它沒有到達目的地之前(ArriveTarget()==false)我們需要時時判斷它是否將要碰撞到障礙物(判斷WillCollide()是否==True),並且前提是精靈在此移動中還沒啟動過A*尋路(IsAStarMoving==false),因為一旦在直線移動中啟動過A*尋路,結果100%會引導精靈饒過障礙物到達終點,A*尋路過程中不需要額外再判斷是否還會碰撞到障礙物,那是多此一舉(如果出現偶然,不要怪別人,怪自己沒把A*算法寫正確)。如果此兩個條件都符合了,則以精靈的移動目標(Destination)為終點啟動A*尋路模式,這樣就順利的由直線移動轉入到A*尋路移動,完美的銜接與樞紐。
二、完美遮罩層
在C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十一)地圖遮罩層的實現 中我曾經講解了如何實現地圖遮罩層。雖然是實現了,但是還有一些小小的瑕疵,如果不屏蔽它,那麼這會很大幅度影響到游戲的畫面效果。
首先我們按照第十一節中說到的方法來截取我們地圖中心的這個標志物,並取名為Mask3.png並且加載到項目Map文件夾中:
接下來我們同樣在後台代碼中初始化它:
//創建遮罩層
Image Mask = new Image();
private void InitMask() {
Mask.Width = 202;
Mask.Height = 395;
Mask.Source = new BitmapImage((new Uri(@"Map\Mask3.png", UriKind.Relative)));
Mask.Opacity = 0.7;
Carrier.Children.Add(Mask);
Canvas.SetZIndex(Mask, 612); //其中的612 = Mask的高 + Mask的Y值,由於還沒引進地圖控件暫時這樣寫
}
最後就是在游戲窗口刷新線程中對它也進行時時的更新:
private void Timer_Tick(object sender, EventArgs e) {
……
//遮罩層跟隨移動
Canvas.SetLeft(Mask, MapLeft + 793);
Canvas.SetTop(Mask, MapTop + 217);
//主角跟隨地圖同時移動
Canvas.SetLeft(Spirit, Spirit.X - Spirit.CenterX + MapLeft);
Canvas.SetTop(Spirit, Spirit.Y - Spirit.CenterY + MapTop);
Canvas.SetZIndex(Spirit, (int)Spirit.Y); //時時的更新它的層次(畫家算法)
……
}
這裡關鍵的代碼就是黃色部分代碼,我通過Canvas.SetZIndex(Mask, 612)設置了遮擋物的層次處於ZIndex:612這個位置,弄過網頁的同志對Z-Index應該不陌生,它們的意義是一樣的。最後還需要精靈的ZIndex與之配合才能實現完美默契的遮罩效果,因此我們在線程中時時根據精靈的Y坐標來更改它的ZIndex:Canvas.SetZIndex(Spirit, (int)Spirit.Y),這樣就算以後增加了其他的NPC、怪物之類的對象物體,只要同樣的設置它們的ZIndex=Y,即可以完美的實現時時的層次關系。
三、完美換裝
這個就簡單多了,因為之前的章節中已經將相關參數與實現方法都定義好了,那麼剩下的就是如何調用的問題。這裡為了演示方便,我在xaml裡面添加了兩個下拉列表(ComboBox)分別對應衣服及武器的代號,和一個換裝啟動按鈕(本教程目錄中有源碼下載,這裡就不列出來了)。然後我們在此按鈕的點擊事件中只需要3行代碼就可以輕松的實現換裝:
//換裝
private void ChangeEquipment(object sender, RoutedEventArgs e) {
Spirit.Equipment[0] = Convert.ToInt32(comboBox1.SelectionBoxItem.ToString());
Spirit.Equipment[1] = Convert.ToInt32(comboBox2.SelectionBoxItem.ToString());
Spirit.Source = Super.EquipPart(Spirit.Equipment, Spirit.DirectionNum, Spirit.DirectionFrameNum, Spirit.TotalWidth, Spirit.TotalHeight,Spirit.SingleWidth, Spirit.SingleHeight);
}
這三行代碼實在太簡單因此不再多做說明。需要提的是在更換衣服與武器的時候,如果內存中不存在該新衣服與武器搭配,則需要時時的進行合成,這會根據您CPU的速度來決定游戲卡殼的時間,畢竟是一個較大量圖片合成計算。這在網絡游戲中同樣經常會遇到,例如你是否有過這樣的經歷:當你到了一定級別可以更換更高級的武器時,你雙擊該武器的時候,游戲會卡住那麼以下,然後“哐铛”一聲,武器才安裝上去;但是如果你再把這把武器取下,再重新換上時卻一點也不卡,這就是裝備的緩存起到了作用。
四、A*尋路之大補充
鑒於目前很多朋友反饋說A*難度過高而無法理解,因此,我打算就A*尋路的使用及相關要點做一次重大補充說明:
使用A*,首先需要引用QX.dll程序集;接著在程序中創建IPathFinder PathFinder = new PathFinderFast(Matrix);這裡有個重要的參數Matrix,它是用來描述尋路坐標系中(即縮小後的坐標系)障礙物的二維矩陣,我們這樣創建它byte[,] Matrix = new byte[1024, 1024]; Matrix[x,y]與尋路坐標系中的坐標是一一對應的關系,例如Matrix[150,266]即對應尋路坐標系中的(150,266)這個點,假設GridSize=10,那麼尋路坐標系中的(150,266)此點即對應游戲窗口中的Canvas.LeftProperty(1500)、Canvas.TopProperty(2660)。並且如果該點是障礙物,則我們設置Matrix[150,266]=0,如果不是障礙物而是可以通行的地點,則我們設置Matrix[150,266]=1,Matrix[,]數組中的其他所有的點依此類推均設置後即完成了地圖中障礙物的布局。這裡出現了GridSize這個很重要的概念,它起到縮放坐標系的作用,如何來理解它呢?這裡我以byte[,] Matrix = new byte[1024, 1024]為例,1024*1024像素地圖僅僅是一張大約我們一個電腦桌面尺寸,但是要在它上面構建障礙物卻需要我們對1024*1024=1048576個點進行設置,簡單有規律的障礙物布局還好,要是遇到復雜的地圖該怎麼辦?這還是小事,要是地圖的尺寸為10000*10000像素(這在MMORPG中再常見不過了),它帶來的不僅是一個大內存數組int[,] Matrix = new int[10000, 10000],更可怕的是在沒有制作地圖編輯器之前去設置其中的100000000個障礙物,簡直就是一件讓人崩潰致極的事。
因此,我引入GridSize(單位格尺寸)這個參數來對坐標系進行縮放操作,從而達到簡化地圖構建過程。例如,我設置GridSize=20,那麼游戲坐標系中的坐標都是窗口中坐標的1/20,例如窗口中Canvas.getLeft(Spirit)=123;Canvas.getTop(Spirit)=353;則對應游戲坐標系中(6,17)(可以直接用整數相除,結果會取整數部分)。這樣的話,一張10000*10000的地圖只需要Matrix[500,500]來實現障礙物構造,並且一個角色占據一個20*20尺寸的單元格是非常合理的。以下為關於引入GridSize這個參數概念的幾大優勢總結:
1、簡化障礙物數組,並且使得地圖構造伸縮自如:關於簡化數組提高性能在上面已經說了,至於伸縮自如是因為我只需要通過改變GridSize的值,其他代碼均不變即可以實現不同精度的游戲坐標系。不信?在前面章節中我的GridSize均為20,本節我將之設置成了10,大家可以很明顯的看到障礙物精度提高了1倍(如下圖):
大家也不妨將GridSize分別設置成1、5、30等,然後相應的修改障礙物(不同GridSize下,障礙物的位置肯定不同)再運行程序看看在不同GridSize下,游戲地圖界面是一樣的,但是單元格精度卻是不同的。特別值得一提的是,當GridSize=1時,此時的尋路坐標系==窗口坐標系,這或許也能讓朋友們更好的理解GridSize的意義。
2、提高尋路算法速度。例如我們設置GridSize=20,此時在40*40像素的地圖區域內只有2*2=4個游戲坐標系單元格即(0,0)、(0,1)、(1,0)、(1、1);如果你需要讓角色從區域左上角移動到右下角,則只需要在這4個點內進行尋路計算出從點(0,0)到點(1,1)的路徑;而如果GridSize=1,即不進行游戲坐標系中單元格縮放而以窗口中的像素點作為基礎單元格,那麼在40*40像素區域內有40*40=1600個坐標點;同樣的如果你需要將角色從區域左上角移動到右下角,就必須在這1600個點內進行尋路計算出從點(0,0)到點(40,40)的路徑。因此我們將GridSize設置為<=20的值,即不失定位的精度,又大幅度簡化及優化地圖構造及性能,何樂而不為?
3、SLG、回合制等類型游戲地圖引擎制作中的必定參數。如果你說RPG(ARPG、MMORPG等)類型的游戲肯定都有自己的地圖編輯器,從而能輕松實現以像素為單位(精確度達到GridSize<=5)的高精度障礙物構造及地圖編輯,這我100%贊同(前提:必須有地圖編輯器,否則後果就如我上文中提到的,一張大且無規律的地圖將讓你痛不欲生)。但是,在SLG、回合制等基於N*N尺寸基礎單元格的游戲中,就如同它們往往被大家通俗的描述為走格子(戰棋類)游戲一樣,地圖格子的概念無處不在。無論是垂直地圖或是斜度地圖,通過設置GridSize都可以輕松的將之實現。
歸納補充了那麼多關於A*的相關使用,大家是漸漸進入狀態了?
本節即將結束,同樣標志著第二部分的開始。第二部分我將就本文開頭用彩色字所提到的相關知識進行講解,或許那才是您真正想要了解的,它將引領我們進入一個真正2D游戲制作中,敬請關注。