是否期待了很久?本节就来个重量级的做为开场白吧:主位式地图移动模式。何谓主位式地图移动模式,即以主角为中心,它的移动带动着所有对象包括地图、物体对象、其他玩家、怪物等等的相对移动,这些对象的移动都是以主角为参照物的。最典型例子莫过于当前流行的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=290,那么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
并为其添加如下属性:
// 地图关键点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()方法,其他的代码一个也不用改,结果就像变魔术一样,地图的移动模式转眼由牵引式地图移动模式转变成主位式地图移动模式,地图、遮罩、就连障碍物都同样的被无缝移植了,这难道不是奇迹吗?欣赏一下自己的劳动成果吧:
瞬间的模式转换是否让大家感到措手不及,匆忙中让太多的代码与属性显得臃肿冗余且无章可循,那么下一节我将对本教程源码进行第一次大规模重构,从设计升华到艺术,这是每一位开发者无上的追求,敬请关注。