在当前的网络游戏中,地图基本都是采取一定斜度的拼装地图,这其中存在两种斜度地图的构造方式:
第一种我称之为伪斜度地图:该类型地图表现层图片为斜度的,但地图基底障碍物等的构造则实为正方形,如下图:
其实最典型的例子就是上一节所演示的内容了,地图是斜的,但是我们却用垂直的障碍物对其进行基底布局,这就是典型的伪斜度地图了。
这样的地图优点在于可以使用简单直接的地图构造算法(上一节中有详细的讲解),同样也可以拥有漂亮的画面。但是,当大家将之运用到实际游戏运行中将会发现人物在饶过不规则障碍物时会很别扭。当然,如果您能制作出优秀的地图编辑器并且拥有与之默契匹配的地图的话,这些或许不会成为大问题。
第二种即为真实的:斜度α地图。下面我将就该类型地图的构造基本原理及其在WPF/Silverlight中的基本实现及算法进行讲解。
首先解释一下关于α角度。通常来讲,对局式或战棋类回合制网络游戏钟爱于60度、45度角的地图构造;而2D-MMORPG网络游戏则无一定规律,可以是任意角度(根据地图开发策划设定进行统一的约束与规范)。下面我们先来看一张图:
该图以梦幻古龙对局战斗时的场景为例进行了非常详细的分析标注。首先我们要讲解实际对应我们WPF窗口的坐标系W坐标系。图中的W(x),W(y)即对应我们窗口坐标系的X轴(Canvas.LeftProperty)和Y轴(Canvas.TopProperty) (当然这其中有相对偏移量,我们后面会讲到)。这两轴是垂直的,也是我们最最常见的直角坐标系了,这很好理解。而该游戏的界面坐标系G坐标系,我在图中用蓝色的线进行了标识,其中G(x)正方向与G(y)负方向的夹角就是α了(在该游戏中为60度)。上图我为了方便演示及说明,假设它的两个坐标系均相交于一个点,这个点我将之定义为坐标原点(0,0)。大家回忆一下前两节讲解的关于障碍物数组Matrix[,]。该数组参数是无法有负值的,如Matrix[-1,5]、Matrix[6,-7]等,这些都是语法中非法的。所以假设按照坐标与障碍物等值对应原理(后面章节还会讲到非等值对应—参数集体偏移量),如Matrix[5,5]对应G坐标系(5,5)、Matrix[8,9]对应G坐标系(8,9),那么构建的地图布局将如上图:红色和蓝色的菱形均代表G坐标系下的坐标点(按照GridSize放大过的),菱形上方也有标识它们在G坐标系下的坐标。很清晰的可以看见,只要x或y值中有负值的,均为红色,此区域为角色无法移动到的区域(在上图中我用浅绿色区域进行标识)。而在其他正值区域中,菱形则均为蓝色的。
如上图,下部份那大片蓝色的区域(G系正值区域)就是我们最终的游戏真实场景所在了,在斜度的游戏世界里,所有人物角色的移动范围均在其中。上一节中有讲过,WPF窗口的左上角为原点(0,0)。但是上图的W坐标系的原点(0,0)却在中上部(已经标识出来,该点与左上角的x距离为a,y距离为b,图中有标注)。如果我们需要在WPF窗口中构造出与上图一模一样的场景效果,就涉及到关于坐标偏移量的计算了。就拿这个例子来说,该游戏此场景中的W(0,0)其实就是WPF的(Canvas.Left(a),Canvas.Top(b));同理,点W(40,60)则为(Canvas.Left(a+40),Canvas.Top(b+60)),以此类推。这样就很简单了不是吗?只要将所有的人物角色对象它们自身的坐标按以上方式进行换算,那么就可以在WPF中实现以上的地图坐标系构造了。这与上一节中讲解到的关于将主角的坐标定位到它的脚底如出一辙。所以在大多数的游戏中都会存在一个关键点,比如MMORPG最典型了,主角始终处于屏幕的正中间(除非他位于地图的8个边缘,后面的章节会讲到相关内容),显而易见它的脚底坐标就是游戏的关键点,其他所有的物体都以之为参照物进行相对于它的位移。关于地图和物体的移动问题需要大量的篇幅,相关内容我将放在后面的章节中再进行讲解。那么下面的内容就暂时以WPF窗口左上角为W系的(0,0)坐标原点,进行简单演示在此基础上构建的斜度α的地图。
有了以上的基础知识作铺垫,后面的内容可谓小儿科了。
首要任务:构造W坐标系与G坐标系的换算公式。假设W坐标系下某点坐标为(W(x),W(y)),该点在G坐标系中的坐标为(G(x),G(y)),那么它们之间的换算公式即为:
W(x)=(G(x)-G(y))*sinα
W(y)=(G(x)+G(y))*cosα
G(x)=(W(y)*sinα+W(x)*cosα)/(2*sinα*cosα)
G(y)=(W(y)*sinα-W(x)*cosα)/(2*sinα*cosα)
这乃本节之精华所在,好比上帝的右手,阿拉丁的神灯无所不能、天下无敌!汗一个。。。好了,有了该法宝,那么我们开始练练手吧,看看一个斜度60的地图是如何构造的。
首先我将该公式用代码来表示写成两个方法,方法名很明确,它们的作用是分别获取某点在G坐标系和W坐标系中的坐标:
//将窗口坐标系中的坐标换算成游戏坐标系中的坐标(缩小操作)
private Point getGamePosition(double x, double y) {
return new Point(
(int)((y + (x / 1.732)) / GridSize),
(int)((y - (x / 1.732)) / GridSize)
);
}
//将游戏坐标系中的坐标换算成窗口坐标系中的坐标(放大操作)
private Point getWindowPosition(double x, double y) {
return new Point(
(x - y) * 0.886 * GridSize,
(x + y) * 0.5 * GridSize
);
}
这里我进行了简单的正弦与余弦的取值,即sin60=0.886,cos60=0.5。一张地图中是不可能存在两个α值的,所以本例在定义好α=60度后,我直接取它的正弦与余弦值这将有效的提高运算效率。
接下来就是构建障碍物了,只有通过它我们才能非常直观的看到这个斜度α地图的构造:
//构建障碍物
for (int x = 10; x < 20; x++) {
for (int y = 1; y < 10; y++) {
Matrix[x, y] = 0;
rect = new Rectangle();
//构建菱形
TransformGroup transformGroup = new TransformGroup();
SkewTransform skewTransform = new SkewTransform(-10, -25);
RotateTransform rotateTransform = new RotateTransform(54);
transformGroup.Children.Add(skewTransform);
transformGroup.Children.Add(rotateTransform);
rect.RenderTransform = transformGroup;
rect.Fill = new SolidColorBrush(Colors.GreenYellow);
rect.Opacity = 0.3;
rect.Stroke = new SolidColorBrush(Colors.Gray);
rect.Width = GridSize;
rect.Height = GridSize+2;
Carrier.Children.Add(rect);
Point p = getWindowPosition(x, y);
Canvas.SetLeft(rect, p.X);
Canvas.SetTop(rect, p.Y);
}
}
这里我用菱形方块真实的模拟障碍物视觉效果。接下来就是在上一节代码的基础上将窗口鼠标左键事件中相关的坐标值通过上面写的两个方法getGamePosition(double x, double y)和getWindowPosition(double x, double y)进行替换,实际上改动的地方不过4处,我用黄色背景色进行了标识(…….号表示该段代码与上一节不变),具体如下:
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p = e.GetPosition(Carrier);
//进行坐标系缩小
Point start = getGamePosition(Canvas.GetLeft(Spirit) + SpiritCenterX,
Canvas.GetTop(Spirit) + SpiritCenterY);
Start = new System.Drawing.Point((int)start.X, (int)start.Y); //设置起点坐标
Point end = getGamePosition(p.X, p.Y);
End = new System.Drawing.Point((int)end.X, (int)end.Y); //设置终点坐标
…….
if (path == null) {
MessageBox.Show("路径不存在!");
} else {
Point[] framePosition = new Point[path.Count]; //定义关键帧坐标集
for (int i = path.Count - 1; i >= 0; i--) {
//从起点开始以GridSize为单位,顺序填充关键帧坐标集,并进行坐标系放大
framePosition[path.Count - 1 - i] = getWindowPosition(path[i].X, path[i].Y);
}
…….
//用白色点记录移动轨迹
for (int i = path.Count - 1; i >= 0; i--) {
rect = new Rectangle();
rect.Fill = new SolidColorBrush(Colors.Snow);
rect.Width = 4;
rect.Height = 4;
Carrier.Children.Add(rect);
Point target = getWindowPosition(path[i].X, path[i].Y);
Canvas.SetLeft(rect, target.X);
Canvas.SetTop(rect, target.Y);
}
}
}
如果大家能将上一节中讲解的内容都吸收的话,那么可以将修改的部分与上一节的代码进行对比,再结合本节前部分内容的讲解就会慢慢的理解了(请大家发散自己的思维吧)。
到这我们就完成了该斜度60的地图构造。按Ctrl+F5看看我们的成果吧:
嘿嘿,A*寻路将我们的路径描绘得非常明显,显然主角是沿着这样一条斜度60的路线饶过这个片菱形障碍物区域的。而因为此例我将W(0,0)点和G(0,0)都定位在窗口的左上角,所以根据本节前部分关于G坐标系的讲解,上图中红色的区域即为含有负值的区域,所以不被寻路方法所识别。您可以尝试对该区域进行点击,它将告诉您路径不存在,从而也证明了我们这个坐标系的构建是成功的。
最后为了让朋友们能更好的理解比较,我将本节例子中的障碍物代码拷贝替换掉上一节的障碍物代码,并将菱形换回成正方形,代码如下:
//构建障碍物
for (int x = 10; x < 20; x++) {
for (int y = 0; y < 10; y++) {
Matrix[x, y] = 0;
rect = new Rectangle();
rect.Fill = new SolidColorBrush(Colors.GreenYellow);
rect.Opacity = 0.3;
rect.Stroke = new SolidColorBrush(Colors.Gray);
rect.Width = GridSize;
rect.Height = GridSize;
Carrier.Children.Add(rect);
Point p = getWindowPosition(x, y);
Canvas.SetLeft(rect, p.X);
Canvas.SetTop(rect, p.Y);
}
}
然后大家可以尝试运行一下新的Window9.xaml,运行效果图如下:
同样的障碍物代码在第九节的直角地图坐标系中是垂直方型显示的,而在本节中则为菱形方式显示。同样证明了本节斜度α地图的成功构造!
Good idea!难道不是吗?嘿嘿,比较复杂也是非常重要的一节。如果你能掌握它,想想A*寻路在不同模式地图中可以完全忽略基本单元格的样式(无论是正方形的,或是菱形的,甚至六边形的)可谓无所不能,想想斜α地图在实际游戏开发中的运用几乎无处不在,这难道不是莫大的成就吗?
至此,关于地图表层的基础知识基本都讲解完了,地图构造原理涉及的知识方方面面,有人就打这样的比方:一个好的地图编辑器决定着一款游戏的成功与否,这毫不为过。所以我们离真正完成它还有很长的路要走。下一节我将介绍如何实现地图的遮罩效果,敬请关注。
出处:http://alamiye010.cnblogs.com/
教程目录及源码下载:点击进入(欢迎加入WPF/Silverlight小组 WPF/Silverlight博客团队)
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面显著位置给出原文连接,否则保留追究法律责任的权利。