上一节,我将游戏地图模式进行了一次重大的变动,这在实际开发中意味着项目大规模重置,虽然表面上显得游刃有余,仅仅一个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距离以及它在游戏窗口中的深度(层次)。并且,它们的赋值范围均为整数(正、负、皆可)。
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="">
<Surface Src="Map\0\0.jpg" Width="1750" Height="1440" X="" Y=""></Surface>
<Mask Src="Map\0\0.png" Width="55" Height="73" X="1040" Y="179" CenterY="73" Opacity="0.7"></Mask>
<Mask Src="Map\0\1.png" 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="">、<Map Sign="1">、<Map Sign="2">等,以代号为0(Sign=” 0” )的地图节点为例,在它下面有一个Surface节点和若干个Mask节点,它们描述的是号地图的一个地表层与若干遮挡物,而这些节点中的属性,如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() {
//获取代号为的地图数据
XElement mapdata = Super.GetTreeNode(Super.SystemConfig, "Map", "Sign", "0");
//抽离地图数据中的地表层参数并用其来初始化地图地表层
InitMapSurface(mapdata.Element("Surface"));
……
}
这里我们通过GetTreeNode()方法得到Sign==””的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中的全屏与窗口模式切换,敬请关注。