C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(二十一)主位式地圖移動模式
是否期待了很久?本節就來個重量級的做為開場白吧:主位式地圖移動模式。何謂主位式地圖移動模式,即以主角為中心,它的移動帶動著所有對象包括地圖、物體對象、其他玩家、怪物等等的相對移動,這些對象的移動都是以主角為參照物的。最典型例子莫過於當前流行的MMORPG了,你控制的角色在地圖中永遠是處於窗口正中心的位置(除了8個角落外),這就是主位式地圖移動模式(如下圖)。
有朋友開始焦躁了:我的媽呀,才剛學完牽引式地圖移動模式,還沒完全吸收呢,又來個什麼鬼主位式地圖移動模式,頭都大冽!
其實一點也不用擔心,一個結構賊清晰的程序只需要您一次輕輕的手術即可以實現功能的革新。是否對謙謙的魔術記憶猶新?神奇的時刻即將降臨,Let me show you the principle first:
游戲窗口尺寸仍然暫定為800 * 600,如上圖,我將游戲地圖(尺寸1750 * 1440)分為9個部分;當主角處於左上(Spirit.X<=400 && Spirit.Y<=390)、右上(Spirit.X>=950 && Spirit.Y<=390)、左下(Spirit.X<=400 && Spirit.Y>=1050)、右下(Spirit.X>=950 && Spirit.Y>=1050)這4個角落區域時,地圖靜止,主角如第九節中的一樣可以任意在窗口中移動,直接講就是主角在窗口中的顯示位置即是它的圖片左上角點(X-CenterX,Y-CenterY);
當主角處於中上(Spirit.X>400 && Spirit.X<950 && Spirit.Y <=390)、中下(Spirit.X>400 && Spirit.X<950 && Spirit.Y >=1050)這2個邊緣區域時,主角在水平方向上始終居中,移動時它在窗口中只會做上下移動,水平方向上通過地圖相對反向移動形成主角水平方向前進的視覺效果;
當主角處於左中(Spirit.>390 && Spirit.Y<1050 && Spirit.X<=400)、右中(Spirit.>390 && Spirit.Y<1050 && Spirit.X>=950)這2個邊緣區域時,主角在垂直方向上始終居中,移動時它在窗口中只會做左右水平移動,垂直方向上通過地圖反向移動形成主角垂直方向前進的視覺效果;
當主角處於正中區域,也就是游戲中主角最多的時候,主角此時始終處於游戲窗口的正中位置(定位到腳底),當它移動時,在窗口中通過地圖的反向移動從而在視覺上形成主角在移動(實際上主角是靜止的,只做方向動畫移動),這與第二十節中的牽引式地圖移動模式有異曲同工之處,只是兩者剛好相反:前者主角不動,地圖反向移動;後者為地圖隨鼠標的牽引而移動,主角不動。最後得出結論:我們只需將第二十節中的AllMove()方法進行修改,即可以實現完美的模式轉換。
原理就這麼簡單,至於其中的數字是如何得到的,我們只需要預先定義好兩個參數WindowCenterX,WindowCenterY。它們其實就是游戲窗體尺寸的5折(如果有標題欄則需要減去標題欄的高度約20像素),以800*600的窗口模式游戲窗體為例,那麼它的WindowCenterX=800/2 =400,WindowCenterY=(600-20)/2=390,那麼1024*768呢?以此類推。理請了思路,接下來就讓我們來實現主位式地圖移動模式下的AllMove()方法,這裡我以主角位於左上這個區域為例:
private void AllMove() {
if (Spirit.X <= WindowCenterX && Spirit.Y <= WindowCenterY) {
//地圖左上
//所有精靈以主角為參照相對移動
for (int i = 0; i < Carrier.Children.Count; i++) {
if (Carrier.Children[i] is QXSpirit) {
//假如子控件為精靈類型,則獲取之
QXSpirit spirit = Carrier.Children[i] as QXSpirit;
//設置精靈在游戲窗口中的顯示位置
Canvas.SetLeft(spirit, spirit.X - spirit.CenterX);
Canvas.SetTop(spirit, spirit.Y - spirit.CenterY);
//畫家方法,使所有精靈之間的遮擋關系由近及遠
Canvas.SetZIndex(spirit, Convert.ToInt32(spirit.Y);
} else if (Carrier.Children[i] is Image) {
//假如是地圖/遮罩
Image Map = Carrier.Children[i] as Image;
Canvas.SetLeft(Map, 0);
Canvas.SetTop(Map, 0);
}
}
}
……
}
我們首先判斷主角是否在左上的區域(Spirit.X <= WindowCenterX && Spirit.Y <= WindowCenterY),如果是,那麼我們循環遍歷畫布中的所有子控件,假如某個控件是精靈類型(QXSpirit),那麼我們捕獲它。由於此時主角處於的是地圖左上區域,按我們前面的分析,它在此區域內的顯示位置就是它的坐標減去中心點值(CenterX,CenterY),因為精靈坐標是定位到腳底的,而窗口顯示它的位置時是定位到精靈圖片左上角點的。那麼其他方向以次類推(源碼中有這裡就不再列羅列)。
做到這,有朋友忍不住要問了:
對於遍歷子控件,我可拿手了,用Foreach不是更能勝任,為何還要用老土的For呢?
深藍色:這涉及到在Foreach中動態添加和刪除子控件的問題。舉個最簡單的例子,游戲中有一個怪物(monster),你一個如來神掌不小心把它給掛了(monster.Life=0),那麼畫布就需要對其控件進行移除(Carrier.Children.Reomve(monster));好,此時問題來了,Carrier.Children這個Collection集合的內容發生了變化(少了一個monster),這將導致系統十分的不高興:*的!誰動了我的怪!(拋出InvalidOperationException異常),這就是臭名昭著的在Foreach遍歷中由於對Collection內容進行更改而引發的血案!如何屏蔽它?用Try{}Catch{}?我非常拒絕在我的代碼中出現這對兄弟,還剩下誰?惟有善良且和諧的For能肩此重任。
又有朋友問了:我們先判斷了子控件是否為QXSpirit類型,恩,這很好很強大;但是後面接著將地圖和遮罩當作Image來判斷是不是有些太牽強?
深藍色:嘿嘿!等你多時了。偉大的地圖控件華麗登場:
有了第十四節關於創建精靈控件的知識,這地圖控件只需要依葫蘆畫瓢,整一個輕松。那麼我們依照第十四節中創建QXSpirit控件的方法,在Controls文件夾上點右鍵添加一個用戶控件,取名叫QXMap
並為其添加如下屬性:
#region (地圖表層/遮罩)屬性
// 地圖關鍵點X定位到左上角0>
public int CenterX { get{…}; set{…}; }
// 地圖關鍵點Y定位到左上角0
public int CenterY { get{…}; set{…}; }
// 地圖X坐標
public double X { get{…}; set{…}; }
// 地圖Y坐標
public double Y { get{…}; set{…}; }
// 地圖寬
public double Width_ { get{…}; set{…}; }
// 地圖高
public double Height_ { get{…}; set{…}; }
// 地圖圖片源
public ImageSource Source { get{…}; set{…}; }
// 地圖透明度
public double Opacity_ { get{…}; set{…}; }
由於地圖與遮罩擁有幾乎一樣的屬性,因此為了簡單且統一化,我只建立一個名為QXMap的控件進行實現(當然,您將之分成QXMap和QXMask兩個控件亦可),下文中為了區分,我均稱地圖為地表圖層(簡稱地表),遮罩為遮罩圖層(簡稱遮罩),這樣可以讓大家更好的理解QXMap的不同實現。回到它的屬性上,其中的X,Y代表坐標,如果是地表則為0,因為它自己相對於自身的坐標當然是(0,0);如果是遮罩,那麼它的X,Y則是它圖片左上角位於地表中的坐標。CenterX,CenterY目前暫時不會用到,因此均默認為0即可;至於其他屬性都很好理解這裡就不再講解。
地圖控件創建完成,接下來我們將原先的:Image Map = new Image();用QXMap MapSurface = new QXMap();代替,Image Mask = new Image(); 用QXMap Mask = new QXMap();代替,並設置好相應的屬性,這樣就完成了通過地圖控件對地表與遮罩的初始化。
到此,第二位朋友的問題已經雲開見日,我們只需輕輕一掃鍵盤:
……
else if (Carrier.Children[i] is QXMap) {
//假如是地圖/遮罩
QXMap Map = Carrier.Children[i] as QXMap;
Canvas.SetLeft(Map, 0);
Canvas.SetTop(Map, 0);
}
……
這樣完美多了不是,嘿嘿,得瑟一下。
深藍色!我還有問題!
更加深邃了我心中的理念:青春就是熱血與激情!
深藍色!我發誓這是最後一個問題:
你前面不是說游戲後期還會加入怪物(monster)、NPC(npc)等亂七八糟的東西,那麼在判斷的時候不是要這樣寫:
……
for (int i = 0; i < Carrier.Children.Count; i++) {
if (Carrier.Children[i] is QXSpirit) {
……
} else if (Carrier.Children[i] is QXMap) {
……
} else if (Carrier.Children[i] is QXMonster) {
……
} else if (Carrier.Children[i] is QXNpc) {
……
}
}
這不是沒完沒了了呀?而且這還是左上區域的實現代碼,還有其他8個區域呢?維護起來不成了是典型的牽一發而動全身?
不提我還真差點給忘記了,如何將這些對象物體控件進行一個歸類呢?分析:首先這些控件均為用戶控件,用戶控件繼承自UserControl類;這道好了,在C#中只能單類繼承,UserControl類在用戶控件出生的時候就已經將這個尊位給踞為己有,哎,雜辦可好??郁悶之時,接口天籁般的魔音再次缭繞於我的耳邊:老大,還有我們捏!對呀!差點把軟哥賜予我們神聖的接口姐妹給忘了。使用接口即可以對這眾多的對象物體用戶控件進行規范,又能被類一對多繼承,很酷不是嗎?
那麼接下來我們添加一個接口取名叫:QXObject.cs,並對其進行如下設定:
interface QXObject {
int CenterX { get; set; }
int CenterY { get; set; }
double X { get; set; }
double Y { get; set; }
……
}
如此一來,只要對繼承此接口的類設定好如上屬性,再對現有的QXSpirit與QXMap兩個控件添加對此接口的繼承:
public partial class QXSpirit : UserControl, QXObject { …… }
public partial class QXMap : UserControl, QXObject { …… }
最後再次對前面的方法進行如下修改:
……
for (int i = 0; i < Carrier.Children.Count; i++) {
if (Carrier.Children[i] is QXObject) {
QXObject Object = Carrier.Children[i] as QXObject;
Canvas.SetLeft(Object, Object.X - Object.CenterX);
Canvas.SetTop(Object, Object.Y - Object.CenterY);
Canvas.SetZIndex(Object, Convert.ToInt32(Object.Y));
}
}
……
忽忽,大功告成!
當我們將AllMove()的9區域代碼均補充完整後,替換掉第二十節中的AllMove()方法,其他的代碼一個也不用改,結果就像變魔術一樣,地圖的移動模式轉眼由牽引式地圖移動模式轉變成主位式地圖移動模式,地圖、遮罩、就連障礙物都同樣的被無縫移植了,這難道不是奇跡嗎?欣賞一下自己的勞動成果吧:
瞬間的模式轉換是否讓大家感到措手不及,匆忙中讓太多的代碼與屬性顯得臃腫冗余且無章可循,那麼下一節我將對本教程源碼進行第一次大規模重構,從設計升華到藝術,這是每一位開發者無上的追求,敬請關注。
源碼下載:http://flashview.cnblogs.com//2009_07/78.rar