来至:http://www.blueidea.com/tech/multimedia/2004/1892_20.asp
为什么用方块?
在开始埋头编写代码之前,让我们稍微谈谈区块/方块游戏(tile based games)。为什么要使用方块?是区块游戏更容易制作吗?或许还是他们比基于艺术的游戏(art based games)更复杂?Flash适合区块游戏吗?
在很久很久以前,方块技术已经被应用到游戏制作中。那时候,电脑还没有上GHz的cpu,没有上百MB的内存。缓慢的速度、有限的内存意味着,游戏制作者不得不使用他们的脑子来发明聪明的办法,让游戏看起来更棒,而且更快。
比如,你想在你的游戏中加入漂亮的背景,但是图片太大了,而且使得你的游戏变得很慢。怎么办?把图片切成方块!
在上图中,你可以看到图片的某些部分是完全一样的。1和2是一模一样的,3和4是一样的,5到7都是完全一样的。如果你把图片切割开来,重复使用相同的部分,你就已经在应用方块了。这个大图片比方块的文件大小大多了。实际上,你用4块不同的方块就可以画出了这个图片。
方块还有其他一些不错的特性,当你想要替换部分背景,那么你不需要重新绘制所有的东西,你只要改变1个方块就行了。你还可以重复使用方块,创建不同的对象。比如,你可能有草地的方块,还有花的方块,当你需要在草地的背景上放几朵花时,只需要把原来地方的草换成花就行了。
Flash 和方块
我们都知道,Flash是基于矢量的,所以Flash生成的文件体积更小,而且可以无限缩放。因此,我们一点都不需要方块来制作游戏吗?好吧,用Flash你可以很容易地做一个基于艺术的游戏(art based games),但是当你的游戏区域增大时,或者你想要更多的特性时,你可能会遇到麻烦。许多东西用区块游戏来做是如此简单(立体视角,寻找路径和深度排序)。不要忘记,区块游戏已经存在了很长一段时间,许多理论对于Flash来说依然适用。
用Flash做区块游戏也有不太舒服的地方,我们用不上许多绘图功能和时间线的部分,我们的游戏是通过actionscripot制作的,基本上,我们要写大量的代码来创建、移动、修改舞台上的图片。
用位图作为区块也是一个好主意。是的,我们可以在Flash中绘制所有的东西,用矢量图也可以,但是当游戏运行的时候,播放器需要计算屏幕上的矢量数据,我们可不希望有什么东西弄慢了我们的游戏。位图在播放以前是预先渲染的,而且通常情况下他们更好看。如果你想在Flash中导入位图作为方块,通常最好的做法是把图像存为带透明背景的GIF文件(用于各种对象,比如花等)
枯燥的讲话到此结束,让我们做点东西吧 :-)
首先,我们来看看怎样存储我们的地图。
地图的格式
我们将用Flash提供给我们的一个美妙的格式表示地图:数组。如果你不知道什么是数组,打开Flash的帮助,先看看。
二维数组
我们需要一个二维数组表示地图,不,他不是什么空间、时间的维数,它是说一个数组的每一个元素还是数组。迷惑了?让我们来看看。
通常,这是大家经常看到的简单的数组:
myArray=["a", "b", "c", "d"];
这很简单。你可以用myArray[0]得到第一个元素,就是”a”,用myArray[1]得到第二个元素”b”,等等。
现在换个聪明的法子! 如果我们不用”a”,”b”和”c”放在数组中,但是我们把另外的数组放进去呢?是的,我们可以这么做的。看这里,让我们做个这样的数组:
a=["a1", "a2", "a3"];
b=["b1", "b2", "b3"];
c=["c1", "c2", "c3"];
myArray=[a, b, c];
现在我们已经定义了一个数组,而且他的每一个元素都是数组。那么,myArray[0]的值现在就是一个数组 [“a1”,”a2”,”a3”],第二个元素值就是 [“b1”,”b2”,”b3”],等等。如果你这样写:
myVar=myArray[2];
那么myVar得到的值是 ["c1", "c2", "c3"].
OK,那又怎么样?现在你也许会问。我们不会停止在这里的。如果你这样写
myVar=myArray[2][0];
那么他得到的值就是myArray第三个元素的第一个元素的值”c1”。
让我们试试更多的。
myVar=myArray[0][1]
取得myArray的第一个元素(a)的第二个元素(”a2”)。
myVar=myArray[1][0] 得到值”b1”
你想得到整个图片? 继续看……
创建地图
首先我们写出这个地图的数组,这个数组包含了每个方块的信息
myMap = [ [1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 1], [1, 0, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1] ];
正如你所看到的,我们的地图有6行8列。如果我们的英雄(主角)从左上角开始,他可以往右移动8格,往下移动6格,超出这个范围,他就会走出这个地图,走入未知的空间。
但是一些聪明的人已经想到了一个重要的问题:“数组中的这些数字是做什么用的呢?”好吧,我们会使用一些OOP(那是面向对象,不过不要逃跑,他们并不是像他们听起来那样可怕)来创建方块,并且管理我们的游戏(可以参阅Flash的OOP教程的链接部分)。在开始的时候,我们会定义多种方块,他们就像模板一样放到游戏中。然后我们遍历整个地图数组,检测每个数字。
例如,如果我们得到数字1,那么我们就从Tile1模板中创建一个新的方块,这个方块的特点我们都事先在模板中定义好了。在游戏中,我们会检查那个方块对象的属性。他可以有许多属性,最基本的方块只有2个属性:walkable(通行性)和frame(帧)。
Walkable是表示一个方块是不是允许角色从他上面走过去,如果可以,就是true(真);如果不是,就是false(假)。我们不使用hitTest,因为hitTest很慢,而且在区块游戏中使用hitTest并不是很好。
Frame是表示显示方块的第几帧。当放置方块到舞台时会用到这个参数。因为我们使用同一个方块movie clip(影片夹子,检查mc)来存放不同的方块。使用时复制这个mc。他们默认是显示第一帧。在“创建方块”部分会有更多这方面的内容。
所以,如果我们声明下面的方块:
//wall tile Tile1= function () {}; Tile1.prototype.walkable=false; Tile1.prototype.frame=2;
那么我们每次在地图数组中遇到1的时候,就会创建一个类似的对象(Tile1),我们还定义了这个方块不能被通行(walkable=false),而且在那个点上的方块mc显示第二帧。
关于地图的东西
你或许会考虑,为什么我要选择这种方式呢?我可以告诉你这是最好的方法。我可以说这个地图格式可以最快地创建地图,可以产生最小的文件。我只能说在和区块游戏大了多年交道之后,我发现这个格式最适合我的需要。但是我们也可以看看其他可能的方法,保存地图数据的方法。
JailBitch的方法
这是原始的OutsideOfSociety的教程所采用的格式,非常简单。他以同样的方式把某个点的帧数字保存到二维数组中。每次你都需要检测下一个方块是不是墙(或者是可以捡起来的东西,或者是门,或者任何东西),你可以从地图数组中查找数字。
(这里显示的数组并不是全部的,下面还有没有显示出来)
当检测碰撞时,你能够可以让某部分的帧作为墙(或者可拾取的东西,或者门)。例如,你可以让所有的帧数在0到100的方块都作为可通行的方块,所有的从101到200的是墙,大于200的是特殊的方块。
当你只有很少的方块类型,而且方块不会变化很多时,这是一个很好的很简单的方式。
OutsideOfSociety的文章: http://oos.moxiecode.com/tut_01/index.html
沙漠中的树
一些地图具有许多不同的方块,一些只有很少的几种。例如,想象在沙漠中,方圆几百公里都是沙子,如果你很幸运,你可以看到一片绿洲。或者在海上,除了水还是水,然后出现一个海岛。
如果你的地图大部分是相同的方块(沙子),而且只有少量的变化(树),那么二维数组并不是很好的选择。他会产生许多“死信息”,许多行的0,直到一些其他的frame数字出现。在这种情况下,你可以单独声明非沙子的方块,然后让剩下的方块都是沙子。
让我们假设你有一个100×100的地图,有3个树。你可以这样写:
当创建地图的时候,你遍历这个trees数组,放置trees方块,让其他的方块显示沙子。那样比写100×100的二维数组要简单多了。
当然,当你有更多的对象(树、灌木、草、石头、水……),这个方法的速度不是很快,而且你也很难记住什么地方放了什么方块。
S,M,XXXL
如果你有Flash MX或更新版本,估计你已经听到过XML。他的格式和HTML很像,他允许声明许多东西。你也可以用XML来保存你的地图数据。
下面的XML地图基于Jobe Makar的《Macromedia Flash MX Game Design Demystified》。看看这个XML的地图:
<map>
<row>
<cell type="1">
<cell type="1">
<cell type="1">
</row>
<row>
<cell type="1">
<cell type="4">
<cell type="1">
</row>
<row>
<cell type="1">
<cell type="1">
<cell type="1">
</row>
</map>
这里我们设定了3×3的地图。首先是头部”map”,然后设置了3个”row” 结点。每个row结点有3个cell结点。
如果从外部文件中载入地图,XML可能是很好的方案,因为大部分的XML解析可以有Flash MX内建的函数完成。从外部文本文件中载入二维数组可没有那么简单,你经常要靠loadVariables得到字符串,然后又不得不把字符串分割成数组,这个过程是很慢的。
XML也有缺点:他会导致更大的文件大小(不过对于现在的网络,这种大小可以忽略),而且你需要Flash Player 6以上。
下面的所有例子都使用二维数组来存储地图数据,而且使用对象的方法来创建方块,就像在“地图的格式”中介绍的那样。
创建方块
现在我们将会让方块在屏幕上显示出来、定位到合适的地方,然后显示正确的帧。就像这个:
首先我们先定义一些对象和值:
myMap = [
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
];
game={tileW:30, tileH:30};
//可通行的方块
game.Tile0= function () {};
game.Tile0.prototype.walkable=true;
game.Tile0.prototype.frame=1;
//墙
game.Tile1= function () {};
game.Tile1.prototype.walkable=false;
game.Tile1.prototype.frame=2;
你可以看到,我们把地图保存在myMap数组中。
地图定义的下一行,定义了一个叫game的对象。
我们会把所有用到的其他对象(方块、敌人……)都作为game的子对象,我们也可以不这样做,直接把所有的对象都放在主场景_root或者其他任何地方,
但是这样做(把对象都放在一个固定的地方)更加清晰一些。
注意我们给了game对象2个属性:tileW和tileH,
两个属性的值都是30。那表示我们的方块的宽度(tileW)和高度(tileH)。方块不一定是个正方形,你也可以使用长宽不相等的矩形。
一旦我们想要知道,方块的宽度和高度,我们可以这样写:
game.tileW
game.tileH
而且如果你想要改变方块的大小,只要在一行代码中改就行了。
下面的几行代码在game对象里面构造了Tile0对象,然后用prototype构造了他的2个属性。
game.Tile0= function () {};
game.Tile0.prototype.walkable=true;
game.Tile0.prototype.frame=1;
第一行的 game.Tile0=function(){} 声明了一个新的对象类型。
当我们从地图的二维数组中得到0时,我们就会使用Tile0作为相应方块的类型。
下面2行告诉我们Tile0对象和所有用Tile0创建的对象都具有的一些属性。我们会设置他们的walkable为true(意味着无法通行)还有frame为1(复制方块mc后,显示在第一帧)。
显示地图
你准备好做整个地图了吗?我们将要写个函数来布置所有的方块了,函数名取做buildMap。如果你要显示别的地图,你也可以使用这个函数。buildMap将要做的是:
+复制一个空mc作为容器(放置各种对象)
+遍历地图数组
+为每个小格子创建相应的方块对象
+复制所有的方块mc
+定位所有的方块mc
+让所有的方块mc显示在正确的帧
这是代码:
function buildMap (map) {
_root.attachMovie("empty", "tiles", ++d);
game.clip=_root.tiles;
var mapWidth = map[0].length;
var mapHeight = map.length;
for (var i = 0; i < mapHeight; ++i) {
for (var j = 0; j < mapWidth; ++j) {
var name = "t_"+i+"_"+j;
game[name]= new game["Tile"+map[i][j]];
game.clip.attachMovie("tile", name, i*100+j*2);
game.clip[name]._x = (j*game.tileW);
game.clip[name]._y = (i*game.tileH);
game.clip[name].gotoAndStop(game[name].frame);
}
}
}
第一行声明了buildMap作为一个函数,并且参数是map。当我们调用这个函数的时候,我们会同时传递地图数组给他。
下面的一行复制了一个空mc到舞台上:
_root.attachMovie("empty", "tiles", ++d);
你需要一个空mc(里面没有任何东西)在库中。
在库面板中右键单击这个mc,选择”Linkage…”(链接),选择”Export this symbol”(导出这个符号),在ID栏填上”empty”。
现在,attachMovie命令会在库中查找链接名称是empty的mc,
找到之后他会在舞台上复制一个这样的mc,并给他一个新的名字tiles。
这个mc将会收容舞台上所有的方块,就相当于一个容器。
使用这样的容器有个很美妙的事情,就是每当我们想要删除所有的方块时(比如游戏结束),
我们只需要删除tiles这个mc就行了,然后所有的方块都消失了。
如果你不用容器,直接把方块都复制到_root(主场景)中,
那么当你进入下一场景的时候(比如游戏结束),这些复制的方块不会消失,你不得不使用更多的actionscript来删除他们。
复制了这个tiles之后,我们还要把他连接到我们的game对象中:
game.clip = _root.tiles
现在,当我们需要访问tiles时,我们只需要使用game.clip,这很便利。
如果我们需要把tiles放到别的地方,我们只需要改一下这行就行了,不需要改动整个代码。
然后我们创建了两个新的变量:mapWidth和mapHeight。
我们通过这两个变量遍历整个地图数组。
mapWidth的值是地图数组的第一个元素的长度。
如果你忘了地图数组什么样子,回头看看。
地图数组的第一个元素是一个数组[1,1,1,1,1,1,1,1],mapWidth就是他的长度值(数组的长度就是数组的元素个数),在这里就是8。
现在我们从地图数组中知道了地图的宽度。
同理,mapHeight的值就是地图数组的长度值,他是数组的行数,也是地图的行数。
我们这样遍历地图数组:
for (var i = 0; i < mapHeight; ++i) {
for (var j = 0; j < mapWidth; ++j) {
我们让变量i从0开始,每次自加1,直到他比mapHeight大。
变量j从0循环到mapWidth。
var name = "t_"+i+"_"+j
变量name的到的值是和i、j的值有关的。
假设i=0,j=1,那么name=”t_0_1”;
如果i=34,j=78,那么name=”t_34_78”。
现在我们创建新的方块
game[name]= new game["Tile"+map[i][j]]
左边的game[name]表示新的方块对象将会放置在game对象里面,就像其他对象一样。
map[i][j]的值告诉我们这个点(i,j)的方块类型,
如果是0,就创建一个Tile0对象;如果是1,就创建一个Tile1对象。
这个点的方块具有的属性在相应的Tile对象中都事先定义好了。
当i=0,j=0时,相当于这样的形式:
game[“t_0_0”]=new game[“Tile0“]
记住:所有的方块都作为game对象的子对象。
在下一行中,我们复制了一个新的mc到舞台上,并使用game.clip[name]来访问他。
mc的坐标可以通过i,j值乘以方块宽度和方块高度得到。
我们通过gotoAndStop命令让他跳到正确的帧,借助他继承得到的frame属性。
当我们需要创建地图时,我们这样调用buildMap函数就行了:
buildMap(myMap);
再谈谈区块原型的定义
既然我们把区块作为对象处理,我们可以利用对象的许多优点。对象有个美丽的特性是他们可以继承属性。如果你认真阅读了上一章,你会记得我们这样写区块的原型:
game.Tile0= function () {};
game.Tile0.prototype.walkable=true;
game.Tile0.prototype.frame=1;
这些让我们写一次原型就可以在其他地方应用,创建新的方块时候就使用这个模板。我们还可以深入研究一下逻辑,再减少一些工作量。
让我们声明一个通用的区块类:
game.TileClass = function () {};
game.TileClass.prototype.walkable=false;
game.TileClass.prototype.frame=20;
这里我们用了一个假设。假设每个区块都是不可通行的,而且都显示在第20帧。当然了,实际的区块不全是不可通行的,否则我们不能移动。而且他们也不会都显示在第20帧。问题看起来很严重,实际上不然。我们只是定义了这两个通用属性而已,我们会让他完美工作的。
现在我们创建新的区块类型:
game.Tile0 = function () {};
game.Tile0.prototype.__proto__ = game.TileClass.prototype;
game.Tile0.prototype.walkable=true;
game.Tile0.prototype.frame=1;
game.Tile1 = function () {};
game.Tile1.prototype.__proto__ = game.TileClass.prototype;
game.Tile1.prototype.frame=2;
game.Tile2 = function () {};
game.Tile2.prototype.__proto__ = game.TileClass.prototype;
通过使用聪明的__proto__,我们不需要重复写同样的属性了。我们的区块从TileClass类中获得了所有必要的材料。当我们这样创建新的区块类型后:
game.Tile2 = function () {};
game.Tile2.prototype.__proto__ = game.TileClass.prototype;
所有后来创建的Tile2区块都继承了属性walkable=false、frame=20。这是不是很美妙呢?但是还没有结束,我们可以改变这两个属性。看:
game.Tile0 = function () {};
game.Tile0.prototype.__proto__ = game.TileClass.prototype;
game.Tile0.prototype.walkable=true;
game.Tile0.prototype.frame=1;
我们在继承了TileClass类的属性之后,又改写了walkable、frame的值。最后的结果是Tile0区块的walkable为true,frame为1。
所有这些可能太复杂了些,毕竟我们只有少量的区块类型和属性。但是你如果要做一个复杂的区块游戏,每个区块都有很多属性,那么单单定义这些重复的属性就已经够繁的了
英雄
每个游戏都有英雄。英雄要打败坏蛋、拯救公主,还要拯救全世界。我们也要加一个英雄,不过他暂时还不会拯救世界,他什么也干不会,但是他已经来了:
看到了吗,就是那个是红色的方块:)
什么,看起来不够帅?你当然可以自己画一个呀。他就是库中那个名字是“char”的那个mc,而且他已经被导出为“char”连接。注意不要让英雄mc比方块大!
另外还要注意,英雄mc的注册点是在中心,而方块的注册点则在左上角:
要来些代码?好吧,加上这句:
char={xtile:2, ytile:1};
这句代码定义了一个char对象。这个char对象将会被赋予所有的关于英雄的信息:
他如何移动、他感觉怎么样、他吃什么……等等。
不过这一次我们只给他两个属性:xtile和ytile。他们记录英雄所处的方块。当他四处走动的时候,我们将会更新xtile/ytile属性,这样我们总能知道他站在那个方块上面。
例如当xtile=2,ytile=1时,他脚下的方块就是“t_1_2”。实际上,他是站在左数第3块、上数第2块方块上,记得坐标是从0开始数的。
我们以后会给他增加更多属性。
为了让英雄站到舞台上,在buildMap函数中,在for循环外(下面),添加这几行代码:
game.clip.attachMovie("char", "char", 10000);
char.clip = game.clip.char;
char.x = (char.xtile * game.tileW)+game.tileW/2;
char.y = (char.ytile * game.tileW)+game.tileW/2;
char.width = char.clip._width/2;
char.height = char.clip._height/2;
char.clip._x = char.x;
char.clip._y = char.y;
第一行又复制了一个mc到game.clip这个mc中(你还记得我们用game.clip代表_root.tiles吧?),然后给他实例名“char”。
然后我们把char的路径保存到char对象中,这样当我们需要访问char这个mc时,
不用再敲入mc的完整路径_root.tile.char了。这样做的好处是,如果我们要把char这个mc放到另外的地方,改动代码就会方便许多。
接下来我们要计算char对象的两个属性:x和y。你也许会纳闷,为什么还要两个属性,我们不是有xtile和ytile这两个属性了吗?记住,xtile和ytile只是脚底下方块的位置,不是我们需要的确切的象素值。英雄当然可以在同一块方块上面走动,x和y属性才可以给出正确的坐标。
还有,当x和y的值计算正确后再赋给_x和_y,这样做是有好处的,尤其是碰撞检测的时候。
我们通过英雄所在的方块计算出英雄的实际位置(象素值)。首先,char.xtile*game.tileW得到所在方块的实际坐标,在加上方块大小的一半,这样英雄就站到了方块的中间。如果你有些迷糊的话,可以对照他们的注册点自己画一下。
接着我们把英雄mc的宽度(宽度)的一半记为char对象的width(height)属性。这样做是很有用的,尤其是计算英雄的边界的时候。你也可以自己定义char的这两个属性的值。有些英雄可能有长长的头发,而且允许头发碰到墙上,身体却不行,这样你就应该按需要自己定义。
最后两行把英雄放到我们计算好的位置上去。
char.clip._x = char.x;
char.clip._y = char.y;
下载代码和源文件
按键和移动
在这一章中我们将用四个方向键控制英雄的移动。在移动过程中,他会面朝移动的方向,并且会显示走动的动画。一旦他停止移动,动画也会停止。试试这个:
因为没有碰撞检测,所以英雄可以走出舞台外面,不过不要担心这个,我们以后会解决这个问题。
首先,让我们完善英雄角色。建立3个新的mc。我们需要一个mc表示角色向左走(或者向右,我选择了左),一个表示向上走,最后一个朝下走。在这些mc中,做角色走动的动画。
这些mc里不需要写代码。
现在,编辑char影片夹子(mc),在时间线上创建5个关键帧:
在关键帧1放置char_up影片夹子,关键帧2放置char_left影片夹子,关键帧4放char_right,关键帧5放char_down。许多时候向左移动和向右移动只是简单的水平翻转关系,所以你可以用一个mc表示这两个方向的动画。现在确认一下,这几帧动画mc的实例名称都是char,检查每一帧。他们都叫char?是的,不用担心。如果你不理解为什么是这样的排列方式,我们将会在代码的部分讲解这个问题。
ok,该写点代码了。
代码
首先,移动需要有个速度,所以先给英雄添加一个速度属性:
char={xtile:2, ytile:1, speed:4};
速度表示英雄每一步移动的象素值,更大的值意味着更快的移动,很小的值将会使英雄像个蜗牛。在这里使用整数是个好习惯,否则你会得到怪异的结果,实际上10象素和10.056873象素之间也看不出什么区别。
你还记得吧,我们创建了_root.char这个对象来保存英雄的信息(如果忘记了,请回头看看)?并且我们把char影片夹子放在tiles影片夹子里面了。为了让我们的英雄醒来并开始移动,我们需要添加两个函数来检查按键和控制mc。拖一个空的影片夹子empty到舞台上。你可以把它放到可视区域外,他只是用来放些代码,所以在哪里都无所谓。在这个mc上面写这些代码(选中mc,然后打开代码面板):
onClipEvent (enterFrame) {
_root.detectKeys();
}
你可以看到我们在每一帧调用detectKeys这个函数。现在写这个函数:
function detectKeys() {
var ob = _root.char;
var keyPressed = false;
if (Key.isDown(Key.RIGHT)) {
keyPressed=_root.moveChar(ob, 1, 0);
} else if (Key.isDown(Key.LEFT)) {
keyPressed=_root.moveChar(ob, -1, 0);
} else if (Key.isDown(Key.UP)) {
keyPressed=_root.moveChar(ob, 0, -1);
} else if (Key.isDown(Key.DOWN)) {
keyPressed=_root.moveChar(ob, 0, 1);
}
if (!keyPressed) {
ob.clip.char.gotoAndStop(1);
} else {
ob.clip.char.play();
}
}
首先我们定义了两个变量:ob 和 keyPressed。设置ob变量指向_root.char (记住,那是我们保存英雄所有信息的对象),设置变量keyPressed为false。keyPressed变量用来表示是否有四个方向键之一被按下去。
下面有4个相似的 if 判断,每个 if 都检测相应的键是不是被按下了。如果键被按下,他们就这样调用另外的一个函数moveCha:
keyPressed=_root.moveChar(ob,1,0);
这一行调用moveChar函数的时候带了3个参数。第一个参数就是ob变量,就是我们的英雄对象。后两个的取值我们总是让他们为-1,1或者0。这些数字决定对象移动的方向,第二个参数表示水平移动的方向(-1:左;1:右),第三个参数代表垂直移动的方向(-1:上;1:下)。最后我们把moveChar的返回值交给变量keyPressed。
你在后面就可以看到moveChar函数总是返回true,
所以任何方向键被按下后,变量keyPressed值都是true。
现在来看看第二个函数moveChar:
function moveChar(ob, dirx, diry) {
ob.x += dirx*ob.speed;
ob.y += diry*ob.speed;
ob.clip.gotoAndStop(dirx+diry*2+3);
ob.clip._x = ob.x;
ob.clip._y = ob.y;
return (true);
}
看第一行,moveChar函数接收了3个参数,变量ob表示要移动的对象,dirx、diry分别表示x、y方向的移动。这是一个很通用的函数,我们可以用它移动游戏中所有东西。例如我们要让子弹飞行,我们就可以调用moveChar函数,同样,我们也可以用这个函数移动敌人。
接下来的两行我们给对象的x和y加上相应的值。同样,如果使用不同的对象(子弹、敌人),这些对象可以有不同的speed属性。所以,当我们检测到右箭头键时,我们调用moveChar函数时的参数是1,0 ,此时dirx=1,diry=0。所以x值会在原来的基础上增加(speed),而y则保持不变。如果我们调用moveChar函数的参数是0,-1(意味着上箭头键),那么y值就会在原来的基础上减小(speed),而x保持不变。
注意,如果我们还有其他的动作,比如碰撞或者跳跃,我们应该将这些动作单独计算。这样比简单的mc.hitTest方法要好不少。
这一句:
ob.clip.gotoAndStop(dirx+diry*2+3);
他使得角色mc跳到正确的帧上,让角色面对正确的方向。你可以算出所有的dirx/diry组合(这里只有4种情况),如果你的角色mc时间线是和我们以前所说的设置一样的话,这里就不会出问题。你可以拿计算器算算看:)
没有计算器?那我们还是看看吧:假设按了方向键右,那么 dirx=1,diry=0,
结果 dirx+diry*2=4。那么角色mc会跳到第4帧,那里正好是我们角色朝右走的动画。
接下来的两行,设置角色mc的_x/_y属性的值等于x/y的值。最后,我们返回一个true值,这样keyPressed就得到了正确的值。下一章我们将会介绍碰撞检测,很有趣的哦:)
下载源文件
碰撞检测
像上面这个,英雄可以穿墙而过,那就没什么意思了。我们要想办法让英雄感受到障碍物的存在。
在第一章中,我们给每个方块都设置了一个“walkable”属性,当某个位置方块的walkable属性是false的时候,英雄就无法穿过它。当值为true的时候,英雄就可以从上面走过(这个东西叫做“逻辑”:)。
为了让这个逻辑起作用,我们将会这样做:
当方向键被按下以后,我们首先检查下一个方块是不是可通行的。
如果是,我们就移动英雄。如果不是,那么就忽略掉按键事件。
这是完美的墙的碰撞:
英雄贴着墙站着,而且下一步他就会进到墙里面。我们不会让它发生的。
但是这个世界总是不够完美,要是英雄只和墙接触一部分呢?
这就要求我们检测英雄的全部四个角是否和墙接触了。只要任意一个角和墙接触(上图中是左下角),移动就是不合理的。
或者,英雄没有贴着墙站,但是下一步就要跑到墙里去了,虽然只是一部分:
我们不得不让他这样贴着墙站着:
“这么难?!”,你也许会喊,“不太可能办到吧?”不用担心,实际上很简单的~
检查四个角
我们不希望英雄的任何一部分能进到墙里面去,只要四个角没有进去就行了,这是假设英雄的大体形状是个长方形(他们确实是的)。
为了实现这个功能,让我们写个函数:getMyCorners
function getMyCorners (x, y, ob) {
ob.downY = Math.floor((y+ob.height-1)/game.tileH);
ob.upY = Math.floor((y-ob.height)/game.tileH);
ob.leftX = Math.floor((x-ob.width)/game.tileW);
ob.rightX = Math.floor((x+ob.width-1)/game.tileW);
//检测他们是否是障碍物
ob.upleft = game["t_"+ob.upY+"_"+ob.leftX].walkable;
ob.downleft = game["t_"+ob.downY+"_"+ob.leftX].walkable;
ob.upright = game["t_"+ob.upY+"_"+ob.rightX].walkable;
ob.downright = game["t_"+ob.downY+"_"+ob.rightX].walkable;
}
这个函数接收了3个参数:对象中心的x/y位置(象素值)、对象的名称。
“等一下”,你也许会迷惑,“我们不是已经在英雄对象中保存了他的当前位置了吗?”是的,但是我们当时存的是当前的位置,这里处理的是将要达到位置(先假定英雄可以移动)。
首先,我们根据这个x/y坐标计算出英雄所处的方块。可能英雄的中心在一个方块上面,但是左上角在另外一个方块上面,左下角又在第三个方块中,这是有可能的。
(y+英雄的高度)/方块高度=英雄下面的两个角所在区块的行值。
最后的四行使用了我们计算出的方块的可通行性。例如,左上角使用upY行leftX列的方块的walkable属性。你可以看到,得到的四个结果(upleft、downleft、upright、downright)被保存到ob对象中了,所以我们以后还可以用到它。
我要再一次指出的是,getMyCorners函数不仅可以用在英雄上面,这里的ob也可以是任何可移动的对象。做区块游戏要多考虑函数的通用性,在后面的章节中你会体会到这种思想的正确性。
移动
当我们检查了四个角以后,现在就可以很简单地移动了:
如果4个角都是可以通行的,那么就移动,否则不移动。但是要让最后英雄贴着墙站着,还得多写几个字。修改后的moveChar函数处理4个可能的方向的移动,它看起来可能有些长,实际上仅仅是4段类似的代码。让我们看看:
function moveChar(ob, dirx, diry) {
getMyCorners (ob.x, ob.y+ob.speed*diry, ob);
if (diry == -1) {
if (ob.upleft and ob.upright) {
ob.y += ob.speed*diry;
} else {
ob.y = ob.ytile*game.tileH+ob.height;
}
}
if (diry == 1) {
if (ob.downleft and ob.downright) {
ob.y += ob.speed*diry;
} else {
ob.y = (ob.ytile+1)*game.tileH-ob.height;
}
}
getMyCorners (ob.x+ob.speed*dirx, ob.y, ob);
if (dirx == -1) {
if (ob.downleft and ob.upleft) {
ob.x += ob.speed*dirx;
} else {
ob.x = ob.xtile*game.tileW+ob.width;
}
}
if (dirx == 1) {
if (ob.upright and ob.downright) {
ob.x += ob.speed*dirx;
} else {
ob.x = (ob.xtile+1)*game.tileW-ob.width;
}
}
ob.clip._x = ob.x;
ob.clip._y = ob.y;
ob.clip.gotoAndStop(dirx+diry*2+3);
ob.xtile = Math.floor(ob.clip._x/game.tileW);
ob.ytile = Math.floor(ob.clip._y/game.tileH);
//---------下面两行由qhwa添加--------
ob.height = ob.clip._height/2;
ob.width = ob.clip._width/2;
//---------------------------------
return (true);
}
像以前一样,moveChar函数通过键盘检测函数传递过来的值得到对象和方向。
这一行:
getMyCorners (ob.x, ob.y+ob.speed*diry, ob);
计算垂直移动(当diry不等于0时)后的四个角的可行性,
随后,通过四个角walkable的值检查是不是合法的移动:
if (diry == -1) {
if (ob.upleft and ob.upright) {
ob.y += ob.speed*diry;
} else {
ob.y = ob.ytile*game.tileH+ob.height;
}
}
这块代码是用来检测向上的移动的。当上箭头键被按下去后,diry的值等于-1。
我们使用了getMyCorners函数得到的ob.upleft和ob.upright值,如果他们都是true,那就意味着上面两个角所在方块都是可通行的,我们就给角色的y坐标加上ob.speed*diry,让角色朝上移动。
但是如果这两个角任何一个碰巧是不可通行的,即ob.upleft或者ob.upright是false,
我们就要把角色放到墙边上。为了让角色贴着它上面的墙,他的中心点必须距离当前方块的上边缘char.height象素,如图:
ob.ytile×game.tileH得到的是当前方块的y坐标,也就是上边缘的y坐标,再加上角色的height值,就是正确的位置了。同样的道理,另外三个方向的部分也可以这样分析出来。
最后一行的把实际的mc放到计算出来的坐标处,让角色显示正确的动画帧,并且更新角色的属性。同以前一样,函数返回true值。
Qhwa注:我在这里加了两行
ob.height = ob.clip._height/2;
ob.width = ob.clip._width/2;
这是因为当clip这个影片夹子跳转相应的帧后,原来的_width和_height可能会发生变化,如果还用初始化时的值,可能就会出错。如果英雄的高度和宽度是一样的,就没有必要这么做了。Tony推荐使用确定的而且相同的高度和宽度。
下载源文件