C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(二十二)重構 – 讓代碼插上翅膀自由飛翔
上一節,我將游戲地圖模式進行了一次重大的變動,這在實際開發中意味著項目大規模重置,雖然表面上顯得游刃有余,僅僅一個AllMove()方法的改變即實現了完美轉型,這全得歸功於前20節所搭建起的相對高度可擴展平台。但是,隨著開發不斷深入,我慢慢的感到些許的不安,因為代碼上的日益松散與結構的漸漸稀疏如同Windows系統的磁盤碎片與日俱增,未來維護時的煩瑣與痛心疾首已歷歷在目。代碼向我發出了求救信號,用什麼來拯救你-我的代碼?是時候亮劍了 –- 我的第一次親密重構。
下面我將分幾點對上一節中的代碼進行重構:
一、統一化代碼格式,讓代碼可讀性發揮到極至:
我以上一節中創建地圖地表層的代碼為例:
private void InitMapSurface() {
MapSurface.Width = 1750;
MapSurface.Height = 1440;
MapSurface.Source = BitmapFrame.Create((new Uri(@"Map\0\0.jpg", UriKind.Relative)));
Carrier.Children.Add(MapSurface);
Canvas.SetLeft(MapSurface, -320);
Canvas.SetTop(MapSurface, -200);
MapSurface.SetValue(Canvas.ZIndexProperty, -1);
}
大家先看InitMap()方法中的前3行代碼,它們均以MapSurface打頭進行賦值書寫;接著看倒數2、3行,卻又是以Canvas.Set…()模式來設置MapSurface的屬性;更可怕的是最後一行,明明可以寫成Canvas.SetZIndex(…)的形式,好歹也與它前面兩行湊合湊合,可是這個作者趕著寫項目,五花八門的寫法都出來了。盡管,這達到實現功能的要求;可是僅僅不上十行的代碼可讀性已達到了“神出鬼沒”的地步,你是否曾想過如此類似的代碼一旦多起來,除了你,還有誰能進行維護?上帝知道。
其實這段代碼改造起來是很簡單的,不外呼統一書寫格式,從而提高代碼的可讀性。下面且看我是如何操作的:
1、分析,Canvas.SetLeft、Canvas.SetTop與Canvas.ZindexProperty這3個東西設置的是地表層圖片左上角距離游戲窗口的X距離、Y距離以及它在游戲窗口中的深度(層次)。並且,它們的賦值范圍均為整數(正、負、0皆可)。
2、思考,WPF與Silverlight同屬於不同形式的應用,一個桌面,一個浏覽器;但是它們卻可以使用相同的xaml進行表現層設計,是MS刻意拉近兩者的距離?這無從考證,未來可以回答我們,但是從兩著的共性讓我不禁聯想起了網頁。
3、比較,網頁對象的Style(Css樣式表)屬性中有3個屬性與上面的3個屬性名稱與作用驚人的相似:left、top、z-index,只是它們必須在position:absolute模式下使用,但這又切中了我們下懷,Canvas畫布的內部布局同樣是采用基於點的絕對定位,兩者看來仿若一物。
為了更清晰的比較,大家先看上圖。會做網頁的朋友再熟悉不過了,其中的網頁播放器與廣告均為浮動層,我們通過設置播放器div的left、top樣式值來使之在網頁中絕對定位到相對於網頁左上角的(A,B)這個像素坐標位置;並且播放器的z-index值大於廣告的z-index值,因此前者的顯示層次高於後者。
接下來再看下圖:
大家可以將主角比做網頁中的廣告,將這頭熊雕像遮擋物比做播放器,然後將游戲窗口的布局畫布Canvas(Carrier)比做網頁,這樣再根據圖中的注釋,是否覺得兩者幾乎完全一樣。那麼此時大家肯定會想,為何不用類似left、top、z-index這樣簡單的屬性來替代書寫復雜且不易懂的Canvas.getLeft(…)、Canvas.getTop(…)、Canvas.getZIndex(…)等寫法呢。
4、實現,在清晰的理論思路下屬性訪問器呼之欲出。對,就是它了,接下來我們只需為QXMap地圖控件添加如下3個屬性:
/// <summary>
/// 地圖位於父容器中的Canvas.Left位置
/// </summary>
public double Left {
get { return (double)this.GetValue(Canvas.LeftProperty); }
set { this.SetValue(Canvas.LeftProperty, value); }
}
/// <summary>
/// 地圖位於父容器中的Canvas.Top位置
/// </summary>
public double Top {
get { return (double)this.GetValue(Canvas.TopProperty); }
set { this.SetValue(Canvas.TopProperty, value); }
}
/// <summary>
/// 地圖深度(層次)
/// </summary>
public int ZIndex {
get { return (int)this.GetValue(Canvas.ZIndexProperty); }
set { this.SetValue(Canvas.ZIndexProperty, value); }
}
此時我們再回到地表層的初始化方法體中進行相應的替換,結果如下:
private void InitMapSurface() {
MapSurface.Width = 1750;
MapSurface.Height = 1440;
MapSurface.Source = BitmapFrame.Create((new Uri(@"Map\0\0.jpg", UriKind.Relative)));
MapSurface.Left = -320;
MapSurface.Top = -200;
MapSurface.Zindex = -1;
Carrier.Children.Add(MapSurface);
}
比起原先的代碼,改進後的不僅書寫優雅,而且更易於理解,這就是重構的重要手法之一。
二、通過加載配置文件,進行系統參數設置:
在游戲設計中,很多參數是在啟動游戲時就必須加載的,即游戲的初始化讀取(Loading Data…),例如當前的地圖、障礙物、遮擋物、聲音等資料數據。這些數據往往在游戲開發初期習慣性的被程序員放在代碼中(內存中),目的是方便頻繁的修改及調試;但是,當項目進展到需要實現具體功能的實質性階段,此時迫切需要將這些數據進行歸類並統一放到一些配置文件中,這樣我們可以通過修改外圍配置文件實現不同的游戲啟動配置而不必再重新編譯,從而極大幅度的提高設計的拓展性且易於維護和更新。舉個最簡單的例子,網絡游戲在運營中如果服務器地址發生變更,由IP:145.10.6.8換成IP:167.10.8.9,那麼你會怎麼做?在游戲代碼中更改服務器連接IP,然後重新編譯發布後告訴所有的玩家:“請重新下載游戲新版本客戶端,否則將無法登陸服務器。”這是極其愚蠢的做法不是嗎?因此,網絡游戲在啟動時均會檢測更新,通過接收更新服務器傳來的新版文件替換掉每個客戶端的舊配置文件,這樣游戲啟動時即可以加載新的配置參數連接上新的服務器地址。
那麼,在WPF/Silverlight中我們應該以什麼作為配置文件載體?ini文件?不,那太原始了。xml文件才是.NET開發者的追求。下面我以設置地表層與遮罩層配置為例,向大家講解在WPF/Silverlight中如何加載xml配置文件。
首先我們需要在項目中添加一個名為System的文件夾,然後在其中新建一個名為Config.xml的配置文件並寫入如下內容:
<?xml version="1.0" encoding="utf-8" ?>
<Config>
<Maps>
<Map Sign="0">
<Surface Src="Map\0\0.jpg" Width="1750" Height="1440" X="0" Y="0"></Surface>
<Mask Src="Map\0\0.jpg" Width="55" Height="73" X="1040" Y="179" CenterY="73" Opacity="0.7"></Mask>
<Mask Src="Map\0\0.jpg" Width="202" Height="395" X="793" Y="612" CenterY="395" Opacity="0.7"></Mask>
……
</Map>
<Map Sign="1">
……
</Map>
<Map Sign="2">
……
</Map>
……
</Maps>
</Config>
從上面代碼可以看到,它配置了地圖集合節點<Maps>,在此節點下是不同代號的地圖節點:<Map Sign="0">、<Map Sign="1">、<Map Sign="2">等,以代號為0(Sign=”0”)的地圖節點為例,在它下面有一個Surface節點和若干個Mask節點,它們描述的是0號地圖的一個地表層與若干遮擋物,而這些節點中的屬性,如Src、Width、Height、X、Y等等,均是以它們自身的屬性名來命名,這樣在調用的時候可以很方便的對應上。
設置完配置文件後,接下來的任務就是在代碼中調用之。目前加載xml文件的方法很多,我選擇XLINQ(LINQ TO XML),為什麼?因為我喜歡LINQ,它是我見過最具藝術感的語法尤物。
話不多說,先看本節的精華方法GetTreeNode():
/// <summary>
/// 獲取XML文件樹節點
/// </summary>
/// <param name="xml">XML文件載體</param>
/// <param name="mainnode">要查找的主節點</param>
/// <param name="attribute">主節點條件屬性名</param>
/// <param name="value">主節點條件屬性值</param>
/// <returns>以該主節點為根的XElement</returns>
public static XElement GetTreeNode(XElement XML, string newroot, string attribute, string value) {
return XML.DescendantsAndSelf(newroot).Single(X => X.Attribute(attribute).Value == value);
}
該方法僅僅一行代碼,卻可以高速查找出xml樹中任意節點,強悍得只能用“了得”兩個字來形容。接著就是使用它來加載地圖表層Surface配置:
/// <summary>
/// 初始化游戲物件對象
/// </summary>
private void InitializeGameObject() {
//獲取代號為0的地圖數據
XElement mapdata = Super.GetTreeNode(Super.SystemConfig, "Map", "Sign", "0");
//抽離地圖數據中的地表層參數並用其來初始化地圖地表層
InitMapSurface(mapdata.Element("Surface"));
……
}
這裡我們通過GetTreeNode()方法得到Sign==”0”的Map節點,然後以該節點為參數初始化地圖表層:
private void InitMapSurface(XElement args) {
MapSurface = new QXMap();
MapSurface.Source = BitmapFrame.Create(new Uri(string.Format(@"{0}", args.Attribute("Src").Value), UriKind.Relative));
MapSurface.Width_ = Convert.ToDouble(args.Attribute("Width").Value);
MapSurface.Height_ = Convert.ToDouble(args.Attribute("Height").Value);
MapSurface.X = Convert.ToDouble(args.Attribute("X").Value);
MapSurface.Y = Convert.ToDouble(args.Attribute("Y").Value);
Carrier.Children.Add(MapSurface);
}
最後按照args.Attribute(“屬性名”).Value這樣的方式,我們將從此節點中獲取的屬性值對應賦予到MapSurface相應的屬性中,從而完成了從設置配置文件到成功加載的整個流程。其他的如地圖遮罩、障礙物數組等配置的加載如出一轍,源碼中有這裡就不累述了。
這樣,我們在游戲中換地圖時只需重新加載相應代號地圖節點,然後讀取其中的地表層與遮罩層相關信息即可實現場景輕松切換。並且,如果游戲客戶端需要添加幾張新地圖,或是要對現有地圖配置進行修改,那麼我們只需更新xml文件,然後讓對方(客戶端)下載替換即可以進行版本的升級,這就是典型的面向對象的分層開發模式。
三、取其精華,去掉糟粕,讓代碼質量得到質的飛躍:
在WPF/Silverlight中,大家是否有發現一個比較古怪的情況,每個控件都有這樣兩個屬性:x:Name和Name,它們的區別到底在哪?我可以謹慎的告訴大家,其實使用起來兩者效果是一模一樣的。例如我設置x:Name=”A”,或設置Name=”A”,在Behind代碼中兩種方式均可以將”A”值識別。這可頭大了,難道MS在搞飛機?其實區別僅僅是上帝創造的先後問題,這對於絕大多數人來說毫無意義。因此我們可以將之歸納到重復屬性的范疇,其他的類似情況在WPF/Silverlight中還有很多,連帶頭老大哥都這樣龌龊,我們的開發中出現類似情況也算情有可原。所以,在重構時,我們還需要對所有的屬性進行理性的審視,是否有重復的,是否有不合理的,是否有沒用到卻還凳在那的,這些統統得回爐再造。惟有如此,才能給程序的擴展提供更便利的支持。同樣的,我以一個活生生的例子給大家講解。
是否還記得上一節中,要實現9區域的主角移動,首先得定義WindowCenterX與WindowCenterY這兩個變量,然後通過讓它倆參與到范圍判斷中從而得到主角當前所處的區域。但是大家有沒想過,如果游戲窗口尺寸是可變的,為了兼容前面實現的功能,每次窗口尺寸改變(如拖動邊緣、最大化、窗口化等)時,我們都得重新設置WindowCenterX和WindowCenterY這兩個值,不但增加了代碼量,而且毫無擴展性而言,這是相當糟糕的。因此,我使用游戲窗口現有變量:ActualWidth與ActualHeight來取代WindowCenterX與WindowCenterY,即ActualWidth /2=WindowCenterX,ActualHeight/2=WindowCenterY,然後替換掉全部其他所有調用到WindowCenterX與WindowCenterY的地方。結果是,我們不論如何調整窗體尺寸,都不需要再更改任何代碼,ActualWidth與ActualHeight就好比心有靈犀的得力助手,為您提供時時的游戲窗口實際寬度與高度。
當然,重構的方式還有很多很多,但是它們的最終目的都只有一個:讓代碼插上翅膀自由飛翔。可以這麼說,本節的代碼在保證前一節功能不變的前提下我對其進行了大幅度的代碼重構,不僅優化結構,更可貴的是將整個架構提升到極具拓展性的高度。當然,嘴上說的沒有一點價值,事實將勝於雄辯:下節我將給您演示短短十幾行代碼輕松實現WPF下窗口及其內部所有對象的任意縮放,完美比擬MMORPG中的全屏與窗口模式切換,敬請關注。