http://school.ogdev.net/listshow.asp?page=4&typeid=0&categoryid=5&id=0&ListType=2
目 录
1.1 地图和地表
1.2 斜视角游戏中的视角
1.3 Tile图片的拼接
1.4 不同地表间的过渡
1.5 地图数据结构的定义
--------------------------------------------------------------------------------
这篇关于斜视角游戏的文章节选于前段时间我根据自己的实践体会写的一篇关于游戏制作的文章。对于其中的观点或者说算法,有的是我自己想出来的,有的是借鉴了其他人的。特别要指出的是“斜视角游戏中的视角”和遮挡问题中的图片修正思想是云风提出的,我只不过是借用了一下,在他的个人网站“云风工作室”上有更详细的说明。另外图1.1是从“风魂”(也是由云风开发)游戏制作库的一个演示程序中抓下来的。
一直都想写一些关于游戏制作的东西,但直到最近才完成了一点。想写的原因有很多,但最主要的是自己对游戏制作的热爱。因为热爱它所以我才用心去了解它,因为想要了解它我才知道在中国要想学习游戏制作是多么的困难。没有什么系统、完整的资料和书籍可以参考,完全靠自己零零碎碎得从各种可以获得的资料中拼凑。直到有一天我上了网,我才直到在中国有那么多和我一样有共同志向的人在默默无闻地努力着。最让我感动的是他们能把自己的想法和经验毫无保留地写出来,是其他人从中受益,包括我。这就是为什么我也愿意把我所知道的告诉大家的原因。
文章中的想法可能是不成熟的或者低效的,仅仅给有志于游戏制作的人参考。你可以自由地阅读和复制这篇文章,但如果你想把文章发布到你的网站上或者别的什么地方,请保持文章的完整(包括这部分)并事先通知我,我想知道我的文章都到了什么地方。另外如果你有什么好的建议或者想法,欢迎来信讨论。
1.1、地图和地表
许多斜视角游戏中都使用数组来描述游戏中的地图,在每个地图坐标处的地面情况则称为地表,常见的地表有草地、沙漠、水、石板等等。由于每一块地表都拥有同样的大小,所以地表通常也称为Tile(瓦块)。一个完整的地图就是由许多各种各样的Tile组成的。图1.1就是一个典型的斜视角游戏地图的一部分。
可以清楚地看到,在斜视角游戏中,每个Tile并不是正方形的,而是有点扁的菱形。正是这种有点扁的菱形使地面出现了立体的感觉,就好象我们站在空中斜向下一样,斜视角游戏也是因此而得名。这个菱形到底有多扁是由游戏使用的视角大小来决定的。因此在更加深入的讨论地图和地表之前,需要先解释一下斜视角游戏中的视角。
图1.1 典型的斜视角游戏
1.2、斜视角游戏中的视角
从图1.1 中我们已经可以感觉到该图体现的视觉效果并不是从空中垂直向下看,而是视线和地面之间有一个角度,这个角度就是斜视角游戏中的视角。在不同的游戏中,这个角度的大小可以是不同的。在演示程序中每个Tile的宽是62,高是32,因此使用了30度的视角,如图1.2所示。
图1.2 斜视角游戏中的视角
为什么宽为62,高为32的Tile对应的视角是30度呢?可以这样假设,如果从空中垂直俯视地面,此时投影面(就是显示屏对应的平面,该平面永远与视线垂直)与地面重合,因此每个Tile的宽和高都是实际值62。现在视线和地面的夹角是30度,但投影平面仍然和视线垂直,所以在投影面上每个Tile的高符合下面的公式:
投影面上Tile的高 = Tile实际的高 * sin (a);
其中a代表视角。于是可以推算如果在投影面上Tile的宽为62,高为32,则视角的大小约为30度。
1.3、Tile图片的拼接
在前面已经说明了在斜视角游戏中表示地表的Tile是菱形的,但计算机中处理的图片都是矩形的,也就是说只能使用矩形的图片来存放菱形的Tile。这样就出现了一个问题,如何对存放Tile图片进行拼接,才能出现象图1.1那样的效果。图片拼接时出现重叠部分是不可避免的,就象图1.3中显示的一样,但是可以想办法隐藏每个Tile图片的多余部分。
图1.3 Tile图片的拼接
一种方法是在制作每个Tile图片是将多余的部分使用黑色(颜色值为 0)填充,在将Tile绘制到屏幕上前将整个地图区域也使用黑色填充,然后使用异或方式将每幅Tile图片绘制到屏幕上,也就是绘制前先让Tile图片中的每个像素和屏幕上对应像素进行异或运算。由于任何值和0进行异或运算的结果是保持值不便。所以使用这种方式绘制Tile可以保证Tile图片中多余的部分不影响最终的图像。还有一种方法就是将Tile图片作为带有透明颜色的位图处理, 使用镂空位图绘制函数或RLE位图绘制函数进行绘制,这样就可以去除Tile图片中多余部分。在演示程序中使用了后一种方法,而在为演示程序制作地图的地图编辑器中使用了前一种方法来拼接Tile图片。
1.4、不同地表间的过渡
经常可以看到对一些游戏的评价中有场景逼真这样一条,而在斜视角游戏中要做到场景逼真,除了美工对图片的处理要力求真实以外,还有许多其它难点,其中有一个就是地表间的过渡处理。更通俗的说就是怎样处理草地、沙漠、浅滩、雪地等地表之间的过渡问题,才能使它们之间衔接更加地自然,更加接近真实的情况。
仔细观察《帝国时代II》的画面,可以发现该游戏在这个问题上处理地非常好。该游戏使用的方法是专门制作各种地表间衔接的Tile图片,在地图放置某个坐标处的地表时对其周围的地表进行判断,从而决定是否需要处理地 表的衔接,如果要,则选择合适的Tile图片,在必要时还需要改变周围已经放置好的Tile图片。说起来容易,做起来难 。如何决定的Tile图片在内存中的排放顺序和如何根据周围的地表对其进行选择是最大的难点。
一是因为Tile的衔接是有方向性的,即草原地表在左边, 沙漠地表在右边和草原地表在右边,沙漠地表在左边是不同的。二是当新放置一个地表时,通常需要改变周围已经放置好的Tile图片,而改变它们时也应该考虑到它们自己四周的Tile图片是否应该发生相应变化。三是当多种类型的地表相互接近时,它们之间的过渡问题将变得更加复杂。
在制作斜视角游戏的演示程序时我没有直到一个很好的方法可以解决上面叙述的这几个地表过渡问题。而是采用了一个相对比较简单的方法,使用它虽然不能达到象《帝国时代II》中那样的地表效果,但也能比较自然地处理地表间的过渡。在这种方法中一个很重要的思想就是去除地表衔接时的方向性,而使用混合两种地表内容的Tile图片来表现地表过渡,并根据两种地表占用的百分比使用不同的Tile图片。下面使用草原和荒漠两种地表来说明一下这种处理地表过渡的方法。
在地图上,除了边沿一圈的坐标,其它每个坐标的周围都有另外8个坐标,也就是说处理某个坐标处使用的Tile图片时必须考虑周围8个坐标中的地表。为此,我们首先准备10幅Tile图片,一幅表示是100% 的草原,一幅是100% 的荒漠,其余8幅分别表示草原的占有量从1/9变化到8/9,而荒漠的占有量从8/9下降到1/9。现在假设要在某个坐标处放置一个草原地表,先对周围8个坐标中的地表进行判断,如果这8个坐标中的地表也是草原,那么该坐标处就使用100% 的草原地表;如果周围8个坐标中有的是荒漠地表,那么就计算它们的个数,最后根据这个数选择一个草原占用适当比例的Tile图片,比如8个坐标中有3个是荒漠地表, 那么就选择草原占6/9的那个Tile图片作为该坐标的图片。如果周围8个坐标中不止包含了草原和荒漠两种地表,那么我们将选择其中占用坐标数量最多的那种地表来作为判断的依据。
使用这种方法大大简化了地表过渡的问题,可以较好地处理普通地表之间的衔接。但同时该方法也存在着很大的局限,在处理象水这种地表的过渡问题时很难取得令人满意的效果。
1.5、地图数据结构的定义
地图数据结构的定义是斜视角游戏的地图设计中非常重要的一个部分。在斜视角游戏中每个地图坐标都涉及到许多内容,如该坐标处的地表类型、Tile图片等,所以通常使用结构体对地图中的每个坐标进行描述,而所有的地图信息存放在一个二维数组中,也就是说可以使用一个二维结构体数组来存放地图信息。在定义此结构体的内容时应该注意两个问题,一是结构体不能太大,因为在游戏中地图的范围通常都很大,有的可以达到256X256或者更多,如果每个坐标使用的结构体占用过多字节的话,整个地图将占用巨大的内存空间;另外一个应该注意的问题是:在定义每个坐标使用的结构体时应该充分考虑地图上的物体和精灵,它们和地图之间的关系非常紧密。定义得好的地图结构体可以十分方便地描述地图上的物体和精灵。
图1.4 地图数据结构的定义
在斜视角游戏中,因为每块Tile都是菱形的,所以整个地图看起来也就是一个大的菱形。游戏中地图坐标系一般是象图1.4中那样定义的,即将最高的那个菱形顶角处的坐标作为坐标系的原点,其地图坐标就为(0,0)。X轴增长的方向是右下方,Y轴增长的方向是左下方。
按照一般的思想,在地图中最小体积的精灵占一个地图坐标,大一些的占两个或更多的地图坐标。但按照这种想法,在想增大地图时中就会出现一个的矛盾。要么通过增加地图数组的大小来增加地图范围,这样必然会造成地图占用的内存迅速增加;要么通过增加每块Tile图片的大小来增加地图范围,但这样将使单个精灵占用的面积和移动时的跨度变得过大,看上去很不自然。为了解决这个矛盾,许多游戏都使用了自己的方法,我在演示程序中也因此提出了逻辑坐标系的概念。
图 1.5 逻辑坐标系和地图坐标系的关系
所谓逻辑坐标是专门为描述精灵的位置而提出的一个假想的坐标系统。精灵每时每刻的位置都是基于该坐标系的,它和地图坐标系相重合,但使用地图坐标系2倍的刻度单位。就是说地图坐标系中1个刻度单位对应逻辑坐标系中2个刻度单位,图1.5清楚地说明了这一点。使用逻辑坐标系只需在每个地图坐标结构体中增加很少的字节,带来的好处就是可以通过增大每个Tile图片的大小,从而扩大地图画面的面积,而且不会有每个精灵占用面积或移动跨度过大的情况,因为精灵可以停留在同一Tile上的不同位置(如果象图1.5中那样建立逻辑坐标系,精灵可以停留在同一Tile中的4个不同位置)。
前面已经说过,由于地图通常比较大,所以每个存放每个坐标处的内容的结构体不能占用太多的字节,但对每个坐标上内容描述得越详细,对地图、物体以及精灵的操作就会越方便。为了使每项内容占用尽可能少的空间,演示程序中使用了对内容进行压缩存储的方法。举例来说就是如果某项内容的值不会超过15,那么就只使用4位(bit)来表示它;如果不超过63,就只使用6位来表示它。这样可以去除每项内容的冗余信息,将本来需要几个字节存放的内容压缩到1个或2个字节中来。
2.1、游戏中的物体
游戏中物体最明显的特征就是其在地图上位置是不会发生改变的,诸如树、房屋、巨石等都属于物体的范畴。物体的位置不会发生改变并不就是说物体是完全静态的,在许多游戏中经常可以看到树和草等物体随风摆动等情景,不过这些只是表示物体的图片在不断改变而已,物体的位置并没有改变。
在定义描述物体的数据结构时应该注意斜视角游戏中物体的几个特性:
1) 物体在地图中可能占用不只一个的地图坐标,而且这些坐标组成的可能并不是规则的矩形。处理占用不规则区域的物体是经常遇到的问题,在演示程序中采用了这样的方法:将物体按占用的坐标分为几个部分(占用几个坐标就分成几个部分),并且指定其中的一个为基准部分,然后给每个物体都分配一个数组用于记录物体中每个部分相对于基准部分的坐标偏移。当物体放到地图上后,要修改地图数组中物体占用坐标对应的数组元素内容,以便确定每个坐标上对应的物体部分,对于基准部分,在对应的地图数组元素中要设置相应的标志。
2) 物体每个部分对应的高度可能是不同的。在有的游戏中对于地图的描述除了使用X和Y两个坐标轴之外,还使用了一个用于表示每个地图坐标处高度的坐标轴。在这样的游戏中,物体通常也是有高度属性的,而且每个部分的高度可能不同,于是在定义物体的数据结构时还应该包含物体各部分的高度属性。而当物体放到地图上后,还应该改变物体占用的地图坐标处的高度值。
3) 物体是有阴影的,所以在描述物体的数据结构中不仅需要有物体本身图片的信息,还要有阴影图片的信息。
以上是斜视角游戏中物体的一些基本的特征。对于不同的游戏,物体还应该包含许多属性,比如:物体是否会被毁坏、物体是否有动画效果、物体是否有伤害性等等。
2.2、游戏中的精灵
与物体最大的区别就在于精灵会在地图上移动,而且精灵使用的是逻辑坐标而不是地图坐标。精灵同样有有些自己的特征,在定义描述精灵的数据结构时应该加以注意。
1) 精灵有可能不只占用了一个逻辑坐标,但是考虑到移动精灵时的方便性,精灵占据的通常是一个规则的矩形。这样就可以使用一个基准部分属性和两个边长属性来描述精灵占有的区域,而不用象物体中那样使用一个相对坐标数组。
2) 由于精灵可以移动,所以必须有各个方向上精灵运动的图片和对应的阴影图片。在制作精美的游戏中,精灵的运动都伴随着动画,这样各个方向上的动画图片和对应的阴影图片也就必不可少了。
3) 不同精灵可到达的区域是不同的。这是由精灵自身的属性决定的。对于可以飞的精灵,它可能可以到达地图上的任何一个地方;而在水中生活的精灵当然不能移动到陆地上,相反也是一样。精灵可到达区域的不同就决定了在移动精灵时必须对前面的地形进行判断,从而决定精灵是否可以移动。
4) 精灵的移动虽然是基于坐标系统的,但在进行精灵的移动处理时却不能将精灵的图片一下就移动到下一个坐标处,因为这样会使精灵图片的移动幅度过大从而出现不连贯的感觉。演示程序中使用了一个较好的方法,先求出在下一个逻辑坐标处精灵图片应该显示的位置,然后在该位置和当前精灵图片显示的位置之间使用画直线的算法来确定精灵图片的移动轨迹。在真正移动时将精灵在两个逻辑坐标间移动的过程分成多步进行,每一步都按照计算所得的直线轨迹显示精灵的图片,从而实现精灵在坐标间的平滑移动。
在实际的游戏中精灵的控制要比物体复杂得多,这不仅因为要根据精灵不同的状态使用不同的图片,而且因为在游戏中通常为精灵融入了思维,也就是通常所说的人工智能。因此精灵拥有更多的属性,需要对所处的环境进行更多的判断。
在上面对地表、物体和精灵的叙述中都没有说明是怎么确定它们的图片在屏幕上的位置的,这是因为这要涉及到一个新的概念,就是绝对坐标系。它是为了方便地确定游戏中各种图片在屏幕上的显示位置和解决物体遮挡问题而引入的一个坐标系。下面首先结合图3.6来说明确定Tile图片在屏幕中显示位置的方法。
图3.6 绝对坐标系
在绝对坐标系中使用像素作为刻度单位,坐标原点就是地图坐标为(0,0)的Tile的图片的左上角,即图中的A点,X和Y坐标轴(图中没有标出) 就是穿过A点的水平直线和垂直直线,X轴的正方向是水平向右,而Y轴的正方向是垂直向下。由图中可以看出,地图横坐标(X)每增加一个单位,Tile图片的输出位置就在水平方向上增加width/2个像素,在垂直方向上增加height/2个像素;而地图纵坐标(Y)每增加一个像素,Tile图片的输出位置就在水平方向上减少width/2个像素,在垂直方向上增加height/2个像素。所以,地图坐标为(x,y)的Tile的图片在绝对坐标系中的输出位置(ax,ay)可以用下面的公式得到:
ax = (width / 2) * (x – y),ay = (height / 2) * (x + y)
如图中灰色Tile的图片在绝对坐标系中的输出位置B点的坐标为:
Bx = (width / 2) * (3 – 1) = width,By = (height / 2) * (3+1) = height * 2
为了计算出Tile图片在屏幕上的输出位置,还必须记录每个时刻屏幕区域(视口)在绝对坐标上的位置。假设当前视口的左上角的绝对坐标为(vleft, vtop),那么Tile图片在屏幕上的输出位置(px, py)就是:
px = ax – vleft,py = ay – vtop
有了Tile图片的输出位置以后,确定物体图片的输出位置也就不难了。输出物体图片时可以先计算物体基准坐标处Tile图片的输出位置,然后根据物体形状再加上一个偏移量就可以了。
对于精灵图片输出位置的确定,其基本思想和物体是一样的,但是应该注意,精灵使用的是逻辑坐标系,而不是地图坐标系。换句话说,精灵可能停留在同一块Tile上的不同位置上。所以应该先求出精灵基准坐标是处于哪个地图坐标中,并以次求出该地图坐标Tile图片的输出位置。然后根据精灵基准坐标处于该Tile的哪个部分加上一个偏移量,最后再根据精灵的形状加上另外一个偏移量即得到精灵图片的输出位置。
斜视角为2D游戏带来了立体感,增加了游戏的视觉效果,但同时也为游戏的制作带来了新的困难。其中一个就是如何解决物体和精灵之间的遮挡问题。众所周知,在真实的世界中对于人的眼睛来说,前面的物体会完全或部分地遮挡住后面的物体。但是在2D游戏中,所有的物体和精灵都是使用图片来表示的,而不象3D世界中是一个个的实体,所以在2D游戏中,尤其是在斜视角游戏中,解决物体和精灵的遮挡问题是游戏制作中的一个难点。即使在许多成功的商业游戏中,在这一点上也没有做到十全十美。
既然在2D游戏中所有的物体和精灵都是以图片来表示的,那么物体和精灵间的遮挡问题必然涉及到图片的绘制顺序问题。所以解决遮挡问题的核心也就是确定图片的绘制顺序。首先可以将精灵分为两类,一类是在地面上移动的,另外一类是在空中飞行的。在空中的精灵,由于它们比地面的物体都高,而且就算它们重叠在一起,也不会给人不真实的感觉,所以对于它们可以不进行特别的遮挡处理,只需要在绘制完其它的物体和精灵之后再绘制它们就可以了。对于在地面上的精灵,由于每一时刻它们只能在地图的某个位置上,所以在遮挡问题上可以将它们作为物体来看待。因此,我们最终的问题归结到如何解决物体间的相互遮挡上来。
通过仔细观察斜视角地图的结构不难发现这样的规律,对于只占用了一个地图坐标的物体来说,使用画家算法对进行绘制就可以保证它们之间正确的遮挡关系。所谓画家算法就是指先画远的物体再画近的物体,这样近的物体必然会遮住远的物体。在斜视角游戏中可以这样确定物体的远近:物体在地图上的Y坐标越大,物体就越近,如果Y坐标值一样,那么X坐标越大,物体就越近。按通常的习惯,在二维的地图数组中使用行表示Y坐标,使用列表示X坐标,所以对于只占用一个坐标的物体来说, 可以按这样的顺序进行绘制:先以列值从小到大的顺序绘制第一行中的物体,然后以同样的顺序绘制第二行和以后各行,正如下面的程序段中的一样:
S_MAPITEM Map[n][n]; // n行n列的地图数组
int i, j;
for (i = 0; i < n; i++)
{
for (j=0; j < n; j++)
{
绘制 Map[i][j] 处的物体
}
}
对于占用了不止一个地图坐标来说,情况就远没有怎么简单了。在演示程序中解决这种物体遮挡问题时利用了物体的基准坐标和绝对坐标。先看图4.1中的三幅图片:
(a)
图4.1 物体间的遮挡
(c)
图4.1显示了解决树(物体1)和房屋(物体2)两个物体之间遮挡问题的过程。在演示程序中使用物体的基准坐标来判断是否该绘制某个物体,图(a)中显示了两个物体在地图上的位置和它们各自的基准坐标(灰色部分),可以看出房屋的基准坐标比树的“远”,所以当使用画家算法绘制物体时,根据前面的程序段,房屋应该比树先被绘制,于是结果就如图(b)中所示的那样,树画到了房屋的前面,这时需要对画面进行修正。修正的方法是当绘制树的时候应该判断一下它的图片是否和已经绘制的物体图片有相交的部分(图(b) 中清楚地显示了树的图片和房屋的图片有相交的部分),判断时使用的就是两个图片的绝对坐标。如果有,而且先画的物体应该遮挡后画的物体,则需要重新绘制先画的图片中相交的那一部分。经过这样的处理,就可以出现象图(c)中所示的正确结果。
上面的这种方法对形状比较规则的物体进行遮挡处理是非常有效的。对于更加复杂的情况,如两个物体间互相都有被对方遮住等,可以通过将一个物体分割成多个物体,从而使物体的形状变得比较规则,然后再使用上面的方法来解决遮挡问题。
在大多数斜视角游戏中地图范围都比较大,单靠一个显示屏是无法同时显示地图的全部区域的,而且在游戏中通常只使用显示屏的一部分显示地图,剩下的部分作为和用户交互时使用。于是就有了视口的概念,视口指的就是屏幕上显示的地图区域。为了清楚地表示出视口在整个地图上的位置,换句话说就是当前屏幕上显示的地图是整个地图区域的哪个部分,在游戏中经常使用微缩地图。微缩地图是在屏幕上一个形状同整个地图相似,并且使用一些不同颜色的像素来表示地表、物体、精灵等的区域,通常在微缩地图上使用一个矩形的方框来表示当前视口在整个地图中的位置。
图5.1 微缩地图
图5.1是一个大小为96×96的地图的微缩地图,在此微缩每个像素代表实际地图中的一个Tile,不同的颜色分别表示地图中的草原、沙漠、水等地表区域和树、房屋等物体,而白色的矩形框表示当前视口的位置。在微缩地图中, 虽然使用一个像素表示实际地图中的一个Tile,但它们的排列是有所不同的,图5.2中说明了这一点。在微缩地图中将实际地图中交错的两排Tile使用一行像素来表示。造成这种不同的一个原因是像素不能象Tile一样。
图5.2 实际地图到微缩地图的映射方法
交错地排列,还有就是为了使微缩地图看起来更美观和更容易理解。为什么说更加美观和更容易理解呢?先看一下在演示程序中曾经尝试使用的另一种微缩地图的表示方法,见图5.3:
5.3 实际地图到微缩地图的映射方法(已经被淘汰)
可以想象,使用这种像素排列方式形成的微缩地图是一个宽明显小于高的菱形,明显不如图5.1中那样能够让人接受。所以显示程序中使用了图5.2中那样的像素排列方式,这也是为什么图5.1中微缩地图的宽是191个像素,而高是96个像素。
要想在微缩地图中表示出视口的位置,就必须要有能够求出实际地图中每个Tile对应于微缩地图中哪个像素的方法。为了描述的方便,在微缩地图中建立了图5.1中所示的坐标系,假设地图坐标(0,0)在微缩地图上对应像素的坐标为(a,0),那么根据图5.2可以推算出地图坐标(mx,my)在微缩地图上对应的像素坐标(x,y)为:
x = a + mx - my; y = (mx+my) / 2
应该注意的是mx和my都是整数,对于整数运算,(mx+my)/2并不等于mx/2+my/2。a的值是很好求的,对于96×96的地图来说,它就是96。有了这个算法,就可以在微缩地图上确定当前视口左上角的位置了,那么在微缩地图的视口宽和高分别是多少呢?为了说明这个问题,借助一下图4.1(a),假设它表示的就是当前的视口,使用图5.2中像素的对应方法,可以得出该视口在微缩地图上的宽为7个像素,而高为6个像素。好象宽和高的比例发生了变化,这是因为实际地图中的两排Tile变成了微缩地图中的行像素。现在,视口左上角在微缩地图上的坐标以及视口在微缩地图上的宽和高都已经知道,就可以在微缩地图上绘制代表它的矩形框了。
关于微缩地图还需补充一点的是:究竟是应该使用地表的代表色还是使用物体和精灵的代表色来表示微缩地图上对应像素的颜色呢?
通常可以使用这样的优先级:精灵 > 物体 > 地表。