随笔 - 8  文章 - 55  trackbacks - 0
<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

常用链接

留言簿(6)

随笔分类

随笔档案

文章分类

文章档案

朋友的Blog

最新评论

阅读排行榜

评论排行榜

为什么用方块?

在开始埋头编写代码之前,让我们稍微谈谈区块/方块游戏(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推荐使用确定的而且相同的高度和宽度。

芝麻开门-地图切换

你能在一个房子里面呆多久?一张图片能看多久?是的,我们需要提供更多的空间给英雄。那意味着要改变地图、创建新的房间、把英雄放置到合适的位置。

为了创建两个房间,我们声明了两个地图数组:
myMap1 = [
[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, 2],
[1, 1, 1, 1, 1, 1, 1, 1]
];

myMap2 = [
[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],
[2, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
];

在game对象中设置当前的地图序号:
game={tileW:30, tileH:30, currentMap:1}

然后,我们开始扫描地图myMap1,通过buildMap函数把它实现到屏幕上。我们可以把myMap1作为一个参数传递给buildMap函数,这样buildMap函数就具备更好的通用性。

buildMap(_root[“myMap”+game.currentMap]);

接下来,我们还需要一个描绘”门”的对象:

game.Doors = function (newmap, newcharx, newchary)
{
 this.newmap = newmap;
 this.newcharx = newcharx;
 this.newchary = newchary;
};

game.Doors.prototype.walkable = true;
game.Doors.prototype.frame = 3;
game.Doors.prototype.door = true;
game.Tile2 = function () { };
game.Tile2.prototype = new game.Doors(2, 1, 4);
game.Tile3 = function () { };
game.Tile3.prototype = new game.Doors(1, 6, 4);

你可能已经猜到了,Doors对象是可以通行的,它是mc的第3帧,它还有一个属性door,而且值是true.我们后面将会利用这个属性来判断英雄是不是走到了门口.

这里用到一个东西,叫做”继承”,听起来有些恐怖?呵呵,其实很简单也很有用.所有的门对象创建的时候都用了Doors模板,Doors对象拥有的所有的所有属性都传递给了他们.比如他们都是可通行的,而且都是第3帧.

我们创建一个门的时候,必须指定几个必要的信息:
它是通往哪个房间(map)的,还有,英雄的新坐标是多少?

你可能要问,为什么要给英雄指定新坐标呢?因为创建新的房间以后,如果英雄还在原来的坐标,就有可能看起来不正确.还有,要避免英雄的新坐标所在的方块是不可通行的障碍物,或者是另外一个门.最糟糕的结果是英雄不停地被传送于两个房间,天哪~
最合理的应该是把英雄放在新地图中门的旁边。

在创建门方块的时候,我们传递了3个参数:newMap、newCharX、newCharY。
他们分别是新的地图序号,新的X坐标和新的Y坐标。当地图数组中出现数字2的时候,buildMap函数会在相应的地方创建一个Tile2方块。英雄穿过Tile2方块后,他将到达myMap2,他的新坐标是(1,4)。你可以在多个地图中都用2,他们都可以让英雄回到myMap2。

更多的代码

在moveChar函数中插入下面这段代码,放在return语句之前:
if (game["t_" + ob.ytile + "_" + ob.xtile].door and ob == _root.char)
{
 changeMap(ob);
}

他的作用是,我们在移动了对象之后判断是不是站到了门口,而且对象要是英雄才行,如果都满足要求,那么就换个地图。我们利用changMap函数实现更换地图的功能:

function changeMap(ob)
{
 var name = "t_" + ob.ytile + "_" + ob.xtile;
 game.currentMap = game[name].newMap;
 ob.ytile = game[name].newchary;
 ob.xtile = game[name].newcharx;
 ob.frame = ob.clip._currentframe;
 buildMap(_root["myMap" + game.currentMap]);
}

这个函数很好理解,我们从门对象(game[name])中得到newMap、newCharX、newCharY参数,然后调用buildMap函数创建新地图。

这里用到了一个新的属性:frame,它是用来记录英雄当前的方向的,如果没有记录,在新地图里面,英雄总是停在第一帧。我们同时还要加这句as到buildMap函数中(设置了英雄的坐标以后):
char.clip.gotoAndStop(char.frame);

这就是关于门的全部了,好了,多串串门吧~


跳跃

开始之前,我们先把视角从俯视改成侧视,这样英雄才可以跳跃。就像下面的这个,按左右键英雄移动,按空格键跳跃:

跳跃基础

跳跃意味着上升,上升在Flash中意味着_y属性的减少。所以我们需要计算这样的式子:
新的坐标=现在的坐标-上升速度

如果只计算一次,坐标只改变一次,英雄很快就停止了。因此我们需要持续不断的计算新坐标。而且,我们还应该改变速度的值,否则英雄就在空中下不来了。
下落和上升一样,只不过速度值前面加个负号而已。

为了改变速度,我们定义一个新的变量:重力加速度。重力把英雄拉向地面,学过物理吧?重力加速度并不直接改变坐标,他改变的是速度:
速度=速度+加速度

这就是Flash中的表示方法,=可不是表示左右相等,是把右边计算的结果赋给”速度”变量。这个式子也是需要不停计算的,以便保持连贯的运动。你可以改变重力的值,较小的值意味着在空中的时间更长,较大的值很快就会把英雄“拉”下来。
从另外的角度看,重力也意味着跳跃能力,我们可以给不同的对象赋以不同的“重力”属性,这样他们跳得就不至于一样高。

让我们来看一个例子。比如刚开始的速度是-10,重力是2。
那么开始的时候,英雄将会上移10象素,然后速度降低到8;
接着英雄上移8象素,然后速度又变成了6……
如此往复,直到速度等于0的时候,英雄不再上升了。
接着速度成了2,英雄开始下降2象素;
下一步又下降4象素、6象素、8象素……直到落地。

落地后,跳跃自然也应该结束了。但是要是英雄在跳跃过程中顶到障碍物怎么办?
很简单,我们把速度强行改成0就行了,然后英雄就会落下来。

【注意】在区块游戏开发中,不要让速度值超过方块的高度。过大的速度会导致碰撞检测跳过某个方块,导致英雄“穿过”障碍物。或许有些魔法师可以穿过障碍物,但是在普通的区块游戏中,这是个bug。

跳跃并不影响水平方向的运动,在空中我们还可以用方向键控制英雄的水平运动。我们需要做的是,在左移或右移后,判断英雄脚下是不是还有东西,如果是空的,跳跃就开始了(这时候初始速度是0,英雄直接下落)。

会跳的英雄

我们给英雄再添加一些属性:
char = {xtile:2, ytile:1, speed:4, jumpstart:-18, gravity:2, jump:false};
speed属性是水平移动速度,jumpstart是跳跃的初始速度,
granvity是重力值,jump属性用来表示英雄是不是在跳跃过程中。

下面加一句as到buildMap函数中:
char.y = ((char.ytile + 1) * game.tileW) - char.height;

因为我们的视图是侧视的,英雄刚开始的位置可能是“漂”在空中的,我们应该让他站到地面上来。

changeMap函数和getMyCorners函数不需要任何变动。

腾空的感觉

我们先来改造detectKey函数,删除上下键检测,添加空格键检测:

function detectKeys()
{
 var ob = _root.char;
 var keyPressed = false;
 if (Key.isDown(Key.SPACE) and !ob.jump)
 {
  ob.jump = true;
  ob.jumpspeed = ob.jumpstart;
 }
 if (Key.isDown(Key.RIGHT))
 {
  keyPressed = _root.moveChar(ob, 1, 0);
 }
 else if (Key.isDown(Key.LEFT))
 {
  keyPressed = _root.moveChar(ob, -1, 0);
 }
 if (ob.jump)
 {
  keyPressed = _root.jump(ob);
 }
 if (!keyPressed)
 {
  ob.clip.char.gotoAndStop(1);
 }
 else
 {
  ob.clip.char.play();
 }
}

注意看我们怎么避免跳跃过程中空格键触发新的跳跃(听起来很拗口,哈哈),实际上就是当处于跳跃中时,忽略空格键。如果按了空格键,而且英雄没有处于跳跃过程,那就开始跳吧,把jump属性改成true,把jumpspeed改成speedstart属性值。

看一下,在左右方向键的检测语句后面,我们添加了几句。
if (ob.jump)
 {
  keyPressed = _root.jump(ob);
 }

如果jump属性为true(正在跳),那么执行jump函数。只要jump属性为true,jump函数就会不断地被执行,直到jump属性变成了false。这个函数可以在空格键松开后仍然执行,只要jump属性为true。

Jump函数所做的只是改变jumpspeed的值,给他加上重力值。同时做一些处理防止速度值过大,让jumpspeed不会超过方块高度。最后调用moveChar函数移动角色。

function jump (ob)
{
 ob.jumpspeed = ob.jumpspeed + ob.gravity;
 if (ob.jumpspeed > game.tileH)
 {
  ob.jumpspeed = game.tileH;
 }
 if (ob.jumpspeed < 0)
 {
  moveChar(ob, 0, -1, -1);
 }
 else if (ob.jumpspeed > 0)
 {
  moveChar(ob, 0, 1, 1);
 }
 return (true);
}

我们还需要改一下moveChar函数,因为加入了跳跃过程,跳跃时的速度jumpspeed和水平移动的速度speed是不同的:

function moveChar(ob, dirx, diry, jump)
{
 if (Math.abs(jump) == 1)
 {
  speed = ob.jumpspeed * jump;
 }
 else
 {
  speed = ob.speed;
 }
 ...

 

jump参数是从上面的jump函数中传递过来的,它的取值不是1就是-1。如果jump的绝对值(Math.abs)是1,移动的距离就是jumpspeed*jump,否则就是speed属性。

如果这一步移动结束后,角色的顶到了障碍物,就要把jumpspeed值改成0:
ob.y = ob.ytile * game.tileH + ob.height;
ob.jumpspeed = 0;

如果这一步移动结束后,角色站到了地面上,跳跃就该结束了:
ob.y = (ob.ytile + 1) * game.tileH - ob.height;
ob.jump = false;

在左右移动后,我们还要看看角色是不是还站在平台上,如果不是,就应该落下来:
ob.x += speed * dirx;
fall (ob);

所以,我们还需要一个fall函数:
function fall (ob)
{
 if (!ob.jump)
 {
  getMyCorners (ob.x, ob.y + 1, ob);
  if (ob.downleft and ob.downright)
  {
   ob.jumpspeed = 0;
   ob.jump = true;
  }
 }
}

如果角色已经处于跳跃过程中,这个函数就没有必要运行了,它是用来检测“踩空”的情况的,只有当角色站着(!ob.jump)的时候才有用。这时候我们调用getMycorners函数,如果角色下方的两个方块都是可通行的,那就应该落下来了,
起始速度是0,然后把jump属性改成true。

腾云驾雾

到目前为止,我们已经做出了阻止英雄通过墙的效果。很有趣,不是吗?许多游戏还有一类叫做“云”的方块,角色门可以左右穿行他们,甚至可以从下面跳上去,
但是当下落的时候,他们确是不可通行的,英雄会停在上面。看这个例子:

你看到区别了吧?让我们来看看图。
这个是普通的砖墙方块,英雄不能从任何角度穿过它:

再来看云。除了上面,英雄可以从任何方向穿过。
如果英雄从上面掉下来,我们让他停在上面。

首先我们要做一些带有“cloud”属性的方块,
如果cloud属性是true,这个方块就是一块“云”。定义:

game.Tile4 = function () {};
game.Tile4.prototype.walkable = true;
game.Tile4.prototype.cloud = true;
game.Tile4.prototype.frame = 4;

它的walkable属性是true,意味着英雄可以穿行过去。
为了让英雄能站到上面,我们需要创建新的函数。

function checkIfOnCloud (ob)
{
 var leftcloud = game["t_" + ob.downY + "_" + ob.leftX].cloud;
 var rightcloud = game["t_" + ob.downY + "_" + ob.rightX].cloud;
 if ((leftcloud or rightcloud) and ob.ytile != ob.downY)
 {
  return(true);
 }
 else
 {
  return(false);
 }
}

我们检测英雄的左下角和右下角的方块是不是云,只要有一块是,就返回true。否则返回false。

现在我们需要在两个地方调用这个函数:
moveChar函数中往下运动的时候,还有fall函数中检测英雄是不是继续下落的时候。

在moveChar函数中if (diry == 1)的后面原来有这句:

if (ob.downleft and ob.downright)
{
 ..
.

改成这样,加上云的检测:

if (ob.downleft and ob.downright and !checkIfOnCloud (ob))
{
 ...

在fall函数中也一样,把这个:

if (ob.downleft and ob.downright)
{
 ...

换成:

if (ob.downleft and ob.downright and !checkIfOnCloud (ob))
{
 ...

只有左下方和右下方都可通行,而且下面的不是云,英雄才能往下掉。

enjoy :)


梯子

在区块游戏中梯子是很常见的一种东西。英雄可以在梯子上爬上爬下(我打赌你不知道:)。当在梯子上按上下方向键的时候,我们会让英雄上下攀爬。

看起来梯子很简单,实际上又很多东西需要考虑。首先,有多少种梯子?

在图中,有4种不同种类的梯子。
梯子A处于一个不可通行的障碍物中。英雄在上面能做什么呢?他可以上下爬,但是不能左右运动,否则就会卡在墙里,那可不好受。

qhwa注: 有些游戏中在这种梯子上是可以左右移下来的,这个功能可以由你自己添加。

梯子B所在的方块是可通行的,而且它的上面还有梯子,所以英雄可以上下爬,也可以左右移动。但是当他左右移动的时候,就该掉下来了。

梯子C下面没有梯子了,英雄只能在它上面向上爬,或者左右移动。

梯子D并不是在所有的游戏中都会出现。有些人认为这种梯子是设计的失误,因为他们不导向任何地方,在空中就断掉了。英雄可以爬上去然后站到梯子上面吗?如果梯子顶部的左右有方块,英雄可以走过去吗?这些都是容易出现分歧的。

这些都是梯子的一些例子,当然还有其他形式的梯子,但是我希望你能看到在开始写代码之前理一下思绪是多么重要。游戏各不相同,可能这里的东西在有的时候很适用,但是可能在别的地方就未必了,只要你每次写代码之前思考一下,不要硬套,就会事半功倍。

规则

让我们列一下关于梯子的规则:

1. 通过上下方向键,英雄可以在梯子上上下移动
2. 当英雄和梯子接触时,他可以爬上去
3. 当英雄和梯子接触,且下方也有梯子时,他可以爬下来
4. 当英雄在梯子上,且左右没有墙时,他可以左右移动
5. 英雄不能在梯子上跳跃

这些应该够了。

请给我一把梯子

梯子是显示在方块的上面的,所以我们要给他做一个独立的影片夹子。这样我们就不用为上面说的不同类型的梯子创建不同的图像了。确定你的梯子mc被导出到as("Export for as"),并且检查链接名是否为"ladder"。

在ladder影片夹子中,画出如上形状的梯子,梯子水平方向在方块的中间。

和其他方块一样,我们也要定义梯子的原型:

game.Tile4 = function () {};
game.Tile4.prototype.walkable = false;
game.Tile4.prototype.frame = 2;
game.Tile4.prototype.ladder = true;
game.Tile4.prototype.item = "ladder";

game.Tile5 = function () {};
game.Tile5.prototype.walkable = true;
game.Tile5.prototype.frame = 1;
game.Tile5.prototype.ladder = true;
game.Tile5.prototype.item = "ladder";

这两个不同的方块(Tile4和Tile5)都具有frame属性,这是用来表示梯子后面(在屏幕上是下面层)的方块类型。他们还有值为true的ladder属性(用来表示这里有把梯子),值为"ladder"的item属性(用来attachMovie用的,复制ladder影片夹子)

在buildMap函数中复制ladder影片夹到方块中:

game.clip[name].gotoAndStop(game[name].frame);
if (game[name].item != undefined)
{
 game.clip[name].attachMovie(game[name].item, "item", 1);
}

这段代码首先让方块显示正常的帧(由frame属性决定),然后判断item属性是否为空,如果不是(有值)就复制item表示的mc。你可以把item属性设定成别的值,这样就可以复制别的mc,在别的地方也可以用到,只是要注意别在一个方块中复制太多不同的mc。

为了不重复输入代码,我们把moveChar函数的结束部分修改一下,调用一个新函数updateChar:

updateChar (ob, dirx, diry);
return (true);


这是updateChar函数:

function updateChar (ob, dirx, diry)
{
 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);
 if (game["t_" + ob.ytile + "_" + ob.xtile].door and ob == _root.char)
 {
  changeMap (ob);
 }
}

在fall函数中添加:

ob.climb = false;


修改detectKeys函数,添加上下键的监测:

if (Key.isDown(Key.RIGHT))
{
 getMyCorners (ob.x - ob.speed, ob.y, ob);
 if (!ob.climb or ob.downleft and ob.upleft and ob.upright and ob.downright)
 {
  keyPressed = _root.moveChar(ob, 1, 0);
 }
}
else if (Key.isDown(Key.LEFT))
{
 getMyCorners (ob.x - ob.speed, ob.y, ob);
 if (!ob.climb or ob.downleft and ob.upleft and ob.upright and ob.downright)
 {
  keyPressed = _root.moveChar(ob, -1, 0);
 }
}
else if (Key.isDown(Key.UP))
{
 if (!ob.jump and checkUpLadder (ob))
 {
  keyPressed = _root.climb(ob, -1);
 }
}
else if (Key.isDown(Key.DOWN))
{
 if (!ob.jump and checkDownLadder (ob))
 {
  keyPressed = _root.climb(ob, 1);
 }
}

当我们检测了左右键之后,我们判断英雄是不是不在跳跃过程中(!ob.jump),而且利用checkUpLadder函数和checkDownLadder函数判断附近是不是有梯子,
如果一切正常,调用climb函数来移动英雄。

攀爬动作的函数

我们将要创建3个新的函数,1个为了检测是否能往上爬,
1个为了检测是否能往下爬,还有一个是实现攀爬动作的函数。

function checkUpLadder (ob)
{
 var downY = Math.floor((ob.y + ob.height - 1) / game.tileH);
 var upY = Math.floor((ob.y - ob.height) / game.tileH);
 var upLadder = game["t_" + upY + "_" + ob.xtile].ladder;
 var downLadder = game["t_" + downY + "_" + ob.xtile].ladder;
 if (upLadder or downLadder)
 {
  return (true);
 }
 else
 {
  fall (ob);
 }
}

 

这段代码首先计算英雄的上下两个y坐标(头和脚),根据所在的区块的ladder属性就可以判断是否可以往上爬。如果上下都没有梯子,我们检测英雄是否应该掉下来。

function checkDownLadder (ob)
{
 var downY = Math.floor((ob.speed + ob.y + ob.height) / game.tileH);
 var downLadder = game["t_" + downY + "_" + ob.xtile].ladder;
 if (downLadder)
 {
  return (true);
 }
 else
 {
  fall (ob);
 }
}


为了检测往下的攀爬动作,我们需要英雄脚底下方块的ladder属性。
和往上爬不同,我们还要考虑到英雄接下来(移动结束)所在的方块的ladder属性(ob.speed+ob.y+ob.height)。

function climb (ob, diry)
{
 ob.climb = true;
 ob.jump = false;
 ob.y += ob.speed * diry;
 ob.x = (ob.xtile * game.tileW) + game.tileW / 2;
 updateChar (ob, 0, diry);
 return (true);
}

在climb函数中,我们首先设置climb标记为true,jump标记为false。然后计算新的y坐标,把英雄放在梯子方块的中间,
ob.x = (ob.xtile * game.tileW) + game.tileW / 2;

英雄可以在梯子左侧或右侧抓着梯子爬,但是这样不太雅观:)

最后我们利用updateChar函数移动英雄到正确的位置。


愚蠢的敌人

我们的英雄已经很完美了,但是他很无聊,唯一能做的只是来回走。我们需要别的东西。不是食物,不是饮料,也不是美女,我们需要的是——一些敌人。敌人就像加在汤里的盐,缺了它,一切都索然无味。好的游戏中会有聪明的敌人,但是我们从一些最笨的敌人开始做起。他们所做的仅仅是来回走,顺便检测是不是碰上英雄了。

到目前为止,我们已经有了两类对象:英雄和方块。英雄由玩家操纵,方块不会运动。敌人应该类似英雄,唯一不同的是,我们不能操作他们移动,我们将会赋予他们一定的智能。我们将要做两种不同的敌人,第一种上下走,第二种会左右走。他们都会在撞到墙上后回头继续走。(真够笨的:)

在你开始构造你的超级复杂的敌人之前,再想想一些东西。许多游戏实际上没有用到敌人,有的虽然用到了,也不是很聪明。Flash并不是非常强大,如果你的游戏中有100个敌人,他们都聪明地使用A*算法跟踪英雄,我真的怀疑是否有这么强大的机器能运行。如果可以的话,最好让一些敌人愚蠢些,一些聪明些,结果玩家可能就会忽略了他们的差别。另外,我们都想要比别人聪明,所以就让玩家感觉这种乐趣好了 :)

准备敌人

同创建英雄一样,创建一个剪辑放置敌人(如果忘了怎么做,在回头看看)。他们也有4帧,分别是左、上、下、右的动画。同样的,他们也要导出为“enemy1”和“enemy2”(在库面板中设置linkage)。现在我们添加一个enemies数组:

myEnemies = [
[0],
[[1, 6, 1]],
[[2, 1, 3]]
];

 

看得出来,我们在map1放了1个敌人。
[1,6,1]:第一个1代表敌人的类型(hoho~我们有好多中敌人得)
6,1是他开始的位置。创建地图的时候,我们会把他放到x=6,y=1的方块上。同理在map2中也有一个敌人,但是类型是2,位置是1,3。你可在一个地图中放置多个敌人,但是千万记住,不要把他们嵌到墙里面!记得要放在一个可通行的方块上面。

让我们声明一些敌人的模板:

game.Enemyp1= function () {};
game.Enemyp1.prototype.xMove=0;
game.Enemyp1.prototype.yMove=1;
game.Enemyp1.prototype.speed=2;

game.Enemyp2= function () {};
game.Enemyp2.prototype.xMove=1;
game.Enemyp2.prototype.yMove=0;
game.Enemyp2.prototype.speed=2;

他们的代码看起来很相似,但是他们动作就不一样了。Enemyp1会上下走,因为他的yMove属性是1;但是Enemyp2只会水平移动。你可以设置xMove/yMove属性为1或-1或0。不过请不要同时把这两个属性都设置成非零值,除非你希望敌人能走对角线。

你也可以把xMove和yMove都设置成0,这样的敌人没有移动的能力,也许你会用到。

speed属性声明了敌人移动的速度。不同的敌人可以有不同的速度。


摆放敌人

在buildMap函数中,介于创建门和创建英雄的代码之间,加入如下代码:

var enemies = myEnemies[game.currentMap];
game.currentEnemies = enemies.length;
for (var i = 0; i<game.currentEnemies; ++i) {
  var name = "enemy"+i;
  game[name]= new game["Enemyp"+enemies[i][0]];
  game.clip.attachMovie("enemy"+enemies[i][0], name, 10001+i);
  game[name].clip=game.clip[name];
  game[name].xtile = enemies[i][1];
  game[name].ytile = enemies[i][2];
  game[name].width = game.clip[name]._width/2;
  game[name].height = game.clip[name]._height/2;
  game[name].x = (game[name].xtile *game.tileW)+game.tileW/2;
  game[name].y = (game[name].ytile *game.tileH)+game.tileH/2;
  game[name].clip._x = game[name].x;
  game[name].clip._y = game[name].y;
}

这段代码什么意思?首先我们得到当前地图的敌人数组,转存为enemies数组,然后把敌人的个数传给currentEnemies,接着遍历敌人数组,把他们都安置好。
(记住,虽然这里只用到了一个敌人,但是可以有更多的)

变量name的值是新创建的敌人的名字,“enemy0”,“enemy1”,“enemy2”……依次类推。然后我们从相应的模板(刚刚声明过)创建新的对象:

game[name]= new game["Enemy"+enemies[i][0]];
从敌人数组(enemies[i])的第一个元素(enemies[i][0])得到敌人的类型,比如是1,然后通过enemyp1模板创建一个敌人。

下面的几行是取得坐标,然后把敌人放置到该处。ok,这就是buildMap函数该变动的地方。

但是,你也许会大声喊出来,但是他还不会动!好吧,我们就让他动起来


移动敌人

同人一样,敌人也需要脑子,我们就写个enemyBrain函数好了:

function enemyBrain () {
for (var i = 0; i<game.currentEnemies; ++i) {
  var name = "enemy"+i;
  var ob = game[name];
  getMyCorners (ob.x+ob.speed*ob.xMove, ob.y+ob.speed*ob.yMove, ob);
  if (ob.downleft and ob.upleft and ob.downright and ob.upright) {
    moveChar(ob, ob.xMove, ob.yMove);
  } else {
    ob.xMove = -ob.xMove;
    ob.yMove = -ob.yMove;
  }
  var xdist = ob.x - char.x;
  var ydist = ob.y - char.y;
  if (Math.sqrt(xdist*xdist+ydist*ydist) < ob.width+char.width) {
    removeMovieClip(_root.tiles);
    _root.gotoAndPlay(1);
  }
}

}

 

正如你所看到的,我们又要遍历数组了。我们把enemies数组的每个元素存到ob,当i等于0的时候,ob就是enemy0。

然后我们调用getCorners函数,检查敌人是不是碰到了障碍物,如果upleft、downleft、upright、downright都是true,则说明可以行走。我们就可以放心的调用moveChar函数,同时传递xMove、yMove给moveChar函数。我们以前用moveChar都是移动英雄的,现在移动敌人也没有任何区别,我说过,我们可以重复使用许多函数的。

如果英雄碰到了障碍物,我们就让xMove和yMove都取相反数,比如原来xMove是1,就取-1,这样敌人就会掉头移动。如果yMove原来是0,那么相反数还是0,没有影响。

最后一部分检测英雄和敌人的距离,看是不是碰到了一起,如果是,oh,game over。当然,游戏不可能这么简单的结束掉,你可以减少英雄的生命值或者做别的处理,这些都由你自己完成。我们使用的计算距离的公式用的是“勾股定理”,如果你需要精确到象素级别的接触,你应该用hitTest,但是这里没有必要这么精确。别离敌人太近,否则你会死掉的。:-)

我们需要不停的调用这个函数,所以在detectKeys函数中加入:

_root.enemyBrain();

这就是一个敌人,很愚蠢的敌人。接下来我们要做更聪明的敌人,他们象人一样四处巡逻,碰到障碍物后会改变方向继续巡逻。hoho,继续吧~

下载本例的源文件

平台上的敌人

如果你需要在平台上面放一个巡逻的敌人,就像这样:

只需要写几行就行了。敌人可以来回巡逻,碰到边缘的时候会自动折回。这个需要敌人检测下一个落脚点方块的通行性:

getMyCorners (ob.x+ob.speed*ob.xMove, ob.y+ob.speed*ob.yMove+1, ob);
if (!ob.downleft and !ob.downright) {

注意一下这里的一个很重要的数字:1。 (ob.y+ob.speed*ob.yMove+1)加1是为了检查英雄脚下面的方块。还有要注意的是只有当downleft和downright都是true的时候,才能继续向前走,否则就掉头走。

下载本例的源文件


教给敌人一些窍门

碰到障碍物可以改变方向,而不是简单的回头走。

让我们修改一下enemyBrain函数。以前是简单的将ob.xMove和ob.yMove取相反数,现在我们给他一个随机的新方向:

ob.xMove = random(3)-1;
if (ob.xMove) {
 ob.yMove = 0;
} else {
 ob.yMove = random(2)*2-1;
}

一旦敌人接触障碍物,xMove就会得到一个随机的数值(-1,0或1,random(3)会返回0,1或2)。如果生成的xMove值是0,我们就设置yMove为1或-1。
random(2) : 0 或 1
random(2)*2 : 0 或 2
random(2)*2-1 : -1 或 1
如果生成的xMove值是1,那么就把yMove值设置成0,然后重新生成xMove值。

下载本例的源文件

这样就好多了,但是如果我们还可以优化一下,我们应该避免与原来相同或相反的方向。

代码:

} else {
  if (ob.xMove == 0) {
    ob.xMove = random(2)*2-1;
    ob.yMove = 0;
    getMyCorners (ob.x+ob.speed*ob.xMove, ob.y+ob.speed*ob.yMove, ob);
    if (!ob.downleft or !ob.upleft or !ob.downright or !ob.upright) {
      ob.xMove = -ob.xMove;
    }
  } else {
    ob.xMove = 0;
    ob.yMove = random(2)*2-1;
    getMyCorners (ob.x+ob.speed*ob.xMove, ob.y+ob.speed*ob.yMove, ob);
    if (!ob.downleft or !ob.upleft or !ob.downright or !ob.upright) {
      ob.yMove = -ob.yMove;
    }
  }
}

这回我们先检查当前的方向。例如,如果原先是垂直移动(xMove==0)那么我们让xMove为1或-1,然后让yMove为0。但是如果敌人在地图的拐角处,他的新方向可能会让他再一次撞上墙。这就是为什么我们要调用getCorners函数来检测障碍物,如果有障碍物,掉头走。

下载本例的源文件

Ok,这回敌人更加聪明了一些,因为玩家不能预知他遇到障碍物后的行走方向。但是你也注意到了,这个傻小子只会去撞墙然后改变方向。如果你的地图中有大范围的空白区域,玩家很难保证敌人能经过那里。最好的例子是一个大房子,英雄站在中间,那么敌人永远也抓不到他。

我们赋予敌人自动转向的能力,即便他没有遇到障碍物。

我们在他的原型中添加一个turning属性来表征转向的能力。

turning表示的是敌人每行走一步时转向的可能性。0代表永远不会转向,100表示每步都要转向(你可别千万这么做)。

更改一下if判断

if (ob.downleft and ob.upleft and ob.downright
 and ob.upright and random(100)>ob.turning) {

如果random(100)的值小于ob.turning,就换一个新的方向,就算本来能够继续往前走。

下载本例的源文件

射击

你可以用很多方法杀死敌人。你可以通过刀剑、枪或者其他的东西。让我们来看看我们怎样向敌人射击(使用shift键射击)。

当我说到“子弹”的时候,我的意思是那些从英雄处飞出来的、看起来要去杀死敌人的东西。它可以是球、箭头、冰激凌、企鹅等等。

首先,我们还是应该考虑一下,射击应该是怎样的过程,子弹是怎样运动。一旦shift键被按下,首先,子弹对象、子弹剪辑被创建,子弹剪辑的位置应该是英雄所在的位置。而且子弹应该朝英雄面对的方向运动。如果子弹遇到墙或者敌人,它应该自动消失。如果它碰到的是敌人,那么敌人也要消失。

子弹的速度应当比英雄的速度大,除非你准备让英雄可以用某种方式阻挡飞行的子弹。通常那些傻乎乎的敌人不会看到飞行的子弹,但是你也可以创建会躲避子弹的敌人。你也可以做一些能朝英雄射击的敌人。

准备射击

画一个子弹的mc,然后做一个linkage,ID是bullet,以便用AS复制到舞台中。子弹MC中的图像应该居中对齐。

让我们声明一下子弹对象:

game.Bullet= function () {};
game.Bullet.prototype.speed=5;
game.Bullet.prototype.dirx=0;
game.Bullet.prototype.diry=-1;
game.Bullet.prototype.width=2;
game.Bullet.prototype.height=2;

子弹每步移动的距离是5,高度和宽度都是2象素,这已经足够杀死敌人了。

dirx/diry属性确定了子弹移动的方向。实际上我们还是通过moveChar函数移动子弹。如果dirx=1,子弹向右移动,diry=-1则向上移动。我可以从英雄(或敌人)对象中得到方向参数,设定dirx/diry的值,但是在游戏刚开始的时候,英雄还没有移动,这时候玩家如果要射击,就用上面这个默认的方向(dirx=0,diry=-1,朝上移动)。

给game对象添加两个新的属性:

game={tileW:30, tileH:30, currentMap:1, bulletcounter:0};
game.bullets= new Array();

bulletcounter属性用来记录子弹的数目,以便我们给新产生的子弹取不重复的名称。游戏里第一发子弹名称是bullet0,然后是bullet1、bullet2……最多是bullet100,接下来返回到bullet0。我们可以让数目无限增长,但是谁也不能保证到最后会发生什么。

game.bullets数组用来放置舞台上的子弹对象。一开始的时候它是一个空的数组。

然后给角色对象加上一个shootspeed属性,设置每次发弹的最小时间间隔:

char={xtile:2, ytile:1, speed:4, shootspeed:1000};

更高的shootspeed值代表更长的间隔、更慢的射击速度,单位是毫秒。

敌人死亡后,我们还要把他们从game对象中删除。改变buildMap函数的创建敌人的部分:

game.currentEnemies = [];
for (var i = 0; i<enemies.length; ++i) {
  var name = "enemy"+i;
  game[name]= new game["Enemy"+enemies[i][0]];
  game[name].id=i;
  game.currentEnemies.push(game[name]);

然后在enemyBrain函数中,把这句:

var name = "enemy"+i;
换成 :var name = "enemy"+game.currentEnemies[i].id;

我通过currentEnemies数组来放置舞台上活动的敌人。一旦敌人被杀死,我们就会把它从currentEnemies数组中删除。新的属性“id”帮助我们找到敌人在enemies数组中的位置。

在detectKeys函数检查完按键后添加代码:

if (Key.isDown(Key.SHIFT) and getTimer()>ob.lastshot+ob.shootspeed) {
  _root.shoot(ob);
}

如果SHIFT键被按下,而且时间间隔足够长,调用shoot函数。

在moveChar函数的开头添加两行代码,用来保存当前对象的方向:

ob.dirx=dirx;
ob.diry=diry;

我们可以用这个来指定子弹运行的方向。


开枪

为了成功的让子弹按照我们预定的想法飞行,我们要写一个新的函数shoot:

function shoot (ob) {
  ob.lastshot=getTimer();
  game.bulletcounter++;
  if (game.bulletcounter>100) {
    game.bulletcounter=0;
  }
  var name = "bullet"+game.bulletcounter;
  game[name]= new game.Bullet;
  game[name].id=game.bulletcounter;
  game.bullets.push(game[name]);
  if (ob.dirx or ob.diry) {
    game[name].dirx= ob.dirx;
    game[name].diry= ob.diry;
  }
  game[name].xtile= ob.xtile;
  game[name].ytile= ob.ytile;
  game.clip.attachMovie("bullet", name, 10100+game.bulletcounter);
  game[name].clip=game.clip[name];
  game[name].x = (ob.x+game[name].dirx*ob.width);
  game[name].y = (ob.y+game[name].diry*ob.height);
  game.clip[name]._x = game[name].x;
  game.clip[name]._y = game[name].y;
}

首先我们传递了一个obj对象给函数。如果开枪的是英雄,这个obj就是英雄;如果是敌人射击,obj就是敌人对象。

我们通过getTimer()函数把当前的时间保存到lastshot属性中。

接着给game.bulletcounter属性增加1,如果它大于100,我们就让他回到0。

现在用bulletcounter产生一个新的子弹名字,创建一个子弹对象,然后把这个数字存到该对象中,把这个对象的地址放入game.bullets数组中。

下面的if判断用来检测角色对象是否移动过(dirx/diry有值),如果是,则把这两个方向属性传递给新建的子弹对象。否则子弹会有默认的运行方向。

为了让子弹从角色处出现,我们把角色的xtile和ytile属性复制给子弹对象。

代码的最后部分创建了新的子弹剪辑,计算坐标,最后显示到该位置。有趣的地方在于如何计算子弹的象素坐标:

game[name].x = (ob.x+game[name].dirx*ob.width);

首先我们得到角色的位置(ob.x),这是角色的中心坐标。通常子弹并不是从角色的正中心发出,我们给他加上了角色的宽度。这个地方的巧妙之处在于乘了子弹的dirx属性,dirx取-1,0,1都能适合。当dirx为0的时候,子弹的x坐标就是角色的中心,不过这时候子弹是竖直运动的,出现在角色的正上方或正下方。


杀死敌人!

在detectKeys函数的结尾添加一行,调用一个新的函数来移动子弹,并且检查是不是打到了什么东西。

_root.moveBullets();

moveBullets函数:

function moveBullets () {
  for (var i = 0; i<game.bullets.length; ++i) {
    var ob=game.bullets[i];
    getMyCorners (ob.x+ob.speed*ob.dirx, ob.y+ob.speed*ob.diry, ob);
    if (ob.downleft and ob.upleft and ob.downright and ob.upright) {
      moveChar(ob, ob.dirx, ob.diry);
    } else {
      ob.clip.removeMovieClip();
      delete game["bullet"+game.bullets[i].id];
      game.bullets.splice(i,1);
    }
    for (var j = 0; j<game.currentEnemies.length; ++j) {
      var name = "enemy"+game.currentEnemies[j].id;
      var obenemy = game[name];
      var xdist = ob.x - obenemy.x;
      var ydist = ob.y - obenemy.y;
      if (Math.sqrt(xdist*xdist+ydist*ydist) < ob.width+obenemy.width) {
        obenemy.clip.removeMovieClip();
        delete game["enemy"+game.currentEnemies[j].id];
        game.currentEnemies.splice(j,1);
        ob.clip.removeMovieClip();
        delete game["bullet"+game.bullets[i].id];
        game.bullets.splice(i,1);
      }
    }
  }
}

这个函数通过循环来操作放在bullets数组中的每一个子弹对象。

使用getMyCorners函数我们得知子弹下一步是不是打在了障碍物上。如果不是,我们就调用moveChar函数移动子弹。

如果子弹确实击中了障碍物,我们就要删除它了。有3样事情要做:
删除子弹mc (使用removeMovieClip)
删除子弹对象 (使用delete函数)
从子弹数组中删除当前的子弹

尽管我们可以只删除子弹mc而留下子弹对象,这样做不会有大问题。因为子弹mc已经不存在,对它的所有的操作不能被执行。但是这样做会降低效率,子弹数组越来越大,而且里面全是垃圾数据。

当移动了子弹而且子弹没有撞上障碍物,我们开始检查它是不是击中了敌人。循环currentEnemies数组,计算出子弹和敌人的距离。如果他们离得太近,我们就删除他们——子弹和敌人都消失。

如果你要永久清楚这个被杀的敌人(当你再一次进入这个房间的时候,它不会再出现),在这里再添加一行:
myEnemies[game.currentMap][obenemy.id]=0;

你也可以让射击变得更加丰富多彩:

+限制子弹的总数。你可以设置一个变量,每次创建一个子弹,变量就减1,只有这个变量值大于0才能创建对象。
+限制舞台上只有1颗子弹。当子弹数组长度>0时不创建子弹。
+让敌人也能发弹。让敌人射击也很容易做到,方法和他们改变移动方向一样。
+创建不同的武器。你可以声明多种子弹模板,规定不同的伤害值,你可以得到更强的武器,更快的杀死敌人。

玩的开心 :-)

下载上例的源文件

在侧视图中射击:

下载本例的源文件

拾取物品

本章我们将会让英雄从地上拾取各种物品,例如:水晶、金币、死的蜘蛛、生命药水、弹药……

物品可以有很多种,有的用来增加生命值,有的用来增加弹药。这个例子里面的物品只有一个功能,增加你的point值(显示在下面的那个东西)。创建其他种类的物品就由你自己来完成了。

我们从“芝麻开门”这一章的影片开始,这样我们可以去掉许多复杂的代码,或许这样更容易看明白。

首先要画一个物品的影片剪辑。把各种物品的图片都放在一个剪辑的不同帧。和英雄、敌人一样,这个剪辑的注册点也在中心。在第一帧添加stop()动作。导出as链接,ID是items。和梯子类似,把物品单独做成一个剪辑,放置物品到舞台时,不用重新绘制背景方块。

你捡了多少?

为了显示捡起了多少物品,在舞台上添加一个动态文本。把它放到区块的外面,然后把它的“变量(variale)”设成points:

变量point用来记录拾取的物品数。我们可以先把它添加到game对象中。改一下定义game对象的语句,添加一个points属性,值为0(大部分都是从0开始的):

game = {tileW:30, tileH:30, currentMap:1, points:0};


放置物品

和其他东西一样,首先我们要声明一个物品(items)对象,然后创建数组,用来放置位置信息。

myItems = [
[0],
[[1,1,1],[1,1,2],[2,1,3]],
[[2,1,3],[2,6,3],[1,5,4]]
];
game.Item1= function () {};
game.Item1.prototype.points=1;
game.Item2= function () {};
game.Item2.prototype.points=10;

myItems数组的结构和enemies数组一样(见《愚蠢的敌人》)。它包含了每个地图的物品数组,我们没有用到map0,所以第一个数组为空。在map1中我们设置了3个物品:[1,1,1],[1,1,2],[2,1,3]。每个物品都有3个数字,第一个是物品类型(这里的1或2),也就是物品影片剪辑显示的帧数。第二、第三个数是物品的坐标,我们已经很多次用这种形式了。以[2,1,3]为例,它是第二种类型的物品,坐标是x=1,y=3。

最后部分的代码声明了两种物品类型。目前他们只有一个point属性,用来表示玩家得到后增加的point值。Item1只有1点,Item2却有10点。

现在修改buildMap函数,增加一些创建物品的动作,把下面的代码放在创建英雄部分的前面:

game.items = myItems[game.currentMap];
for (var i = 0; i<game.items.length; ++i) {
 var name = "item"+game.items[i][2]+"_"+game.items[i][1];
 game[name]= new game["Item"+game.items[i][0]];
 game[name].position= i;
 game.clip.attachMovie("items", name, 10001+i);
 game[name].clip=game.clip[name];
 game[name].clip._x = (game.items[i][1]*game.tileW)+game.tileW/2;
 game[name].clip._y = (game.items[i][2]*game.tileH)+game.tileH/2;
 game[name].clip.gotoAndStop(game.items[i][0]);
}
_root.points=game.points;

首先我们复制当前地图用到的物品到game.items对象中,然后循环game.items对象(数组)。

从这行开始:

var name = "item"+game.items[i][2]+"_"+game.items[i][1];

我们给新的物品命名。名字由物品的坐标决定,比如一个物品所在的区块坐标是x=1,y=3,那它的名称是item3_1。

创建了物品对象以后,我们给他添加了一个position属性。这个属性就是物品在game.items数组中的索引值。通过这个属性可以很方便地在game.items数组中找到当前的物品,这样就可以在捡到物品后在数组中删除掉它。

放置物品到正确的位置之后,下一步就是让他显示正确的帧。

最后,我们更新一下points变量,在场景的相应地方显示出来。游戏开始的时候点数可能只有0,但是改变地图的时候,可能英雄已经捡到了几个物品。

拾取

我们已经有了英雄、物品,下一步要做的就是让英雄碰到物品的时候捡起它。把这段代码添加到moveChar函数的末尾:

var itemname=game["item"+ob.ytile+"_"+ob.xtile];
if (itemname and ob == _root.char) {
 game.points=game.points+itemname.points;
 _root.points=game.points;
 removeMovieClip(itemname.clip);
 game.items[itemname.position]=0;
 delete game["item"+ob.ytile+"_"+ob.xtile];
}

变量itemname的值由英雄当前的位置决定。比如英雄的坐标是x=4,y=9,那么itemname的值就是"item9_4"。如果这个item9_4确实是存在的,itemname就是物品对象;如果不巧不存在这样的item9_4对象,itemname就是undefined(未定义值)。

如果英雄得到了物品,我们首先把物品的points值加到game.points变量,然后更新_root.points以显示到文本框中。

现在要做的就是把物品从舞台上删除。每一个物品在三个地方都要删除:物品剪辑、物品数组中相应的物品,以及物品对象本身。目前我们不必在数组中删除掉他,仅仅将物品用0替换就行了。另外不要忘了,物品数组仅仅是myItems数组的一部分,如果我们离开地图然后又返回,原先已经拾取的物品又会出现。为了阻止这种现象,我们要更新changeMap函数。

添加下面的代码到changeMap函数的开始:

var tempitems=[];
for (var i = 0; i<game.items.length; ++i) {
  if(game.items[i]) {
    var name = "item"+game.items[i][2]+"_"+game.items[i][1];
    delete game[name];
    tempitems.push(game.items[i]);
  }
}
myItems[game.currentMap]=tempitems;

这里我们用到了一个临时的数组:tempitem,用来复制未被拾取的物品,即myItems数组中的非零元素。删除game[name]对象是我们上面提到的三个删除任务中的一个。

点击这里下载本例的源文件

这里我做了侧视图例子:

点击这里下载源文件

浮动区块

首先来讲一个小故事,关于浮动的方块的故事。或许你已经听说过了“移动平台”这个名字,不要迷惑,他们是一样的东西,一样有趣。

很久很久以前,在区块游戏的世界里,住者一个年轻的方块。他是一个快乐的方块。有一天,他遇到了一个英雄。英雄问他:“年轻人,为什么你不会浮动呢?”

“我不知道怎么移动……” 小方块说道。

“那真遗憾,”英雄说道,“我想要站在你上面,去以前够不到的地方。”

那一天之后,小方块就不再像以前一样开心了。

其实我们可以帮助他浮动的,看:

在我们开工写代码之前,我们还是先来考虑一下规则。

我们应该做什么?如何做?

* 浮动方块应该是云(cloud)类型的方块 (云)
* 方块可以横向或纵向移动
* 英雄可以从上方落到上面
* 当英雄站到上面以后,他会随着方块运动
* 方块上的英雄不能穿过障碍物


踏上浮动的方块

英雄怎么站到运动方块上?第一个也是最简单的一个方法就是跳上去。

在上图中,英雄正处于跌落的过程中,下一步他将会碰到运动方块。我们将会把他放置到方块上去。注意,英雄必须是在运动方块的上方,而且必须是往下运动,否则他就无法站到上面。

但是,这并不是英雄上去的唯一途径。下图中,英雄站在障碍物上,没有移动。

但是方块是移动的,而且接下来就会接触到英雄,若不做任何处理,英雄就会“陷”到方块里面。所以,我们要做的就是让方块带者英雄一起往上移动。


离开运动的方块

一旦我们的英雄可以站到方块上,我们还要保证他能够以某种方式离开。首先,他可以跳开。它可以走到方块的边缘。下图画出了许多可能的情形:

党英雄站在竖直移动的方块上,而且上方有个障碍物时,他应该掉下来,否则就会被压扁。党方块水平移动,党英雄碰到障碍物,他应该被贴着障碍物放置;如果方块移开的话,英雄就会掉下去。

上图中,英雄随着方块往下移动,党他碰到障碍物的时候,他不再随着方块移动,而是停在障碍物上。而方块则继续往下移动。


准备工作

画出移动方块的影片剪辑。你可以做很多种类的移动方块,把他们放在 movingtiles 影片剪辑的不同帧,然后将剪辑连接为movingtiles

定义 movingtiles 对象:

game.MovingTilep1= function () {};
game.MovingTilep1.prototype.speed=2;
game.MovingTilep1.prototype.dirx=0;
game.MovingTilep1.prototype.diry=1;
game.MovingTilep1.prototype.miny= 0;
game.MovingTilep1.prototype.maxy=2;
game.MovingTilep1.prototype.width=game.tileW/2;
game.MovingTilep1.prototype.height=game.tileH/2;
game.MovingTilep2= function () {};
game.MovingTilep2.prototype.speed=2;
game.MovingTilep2.prototype.dirx=1;
game.MovingTilep2.prototype.diry=0;
game.MovingTilep2.prototype.minx= -2;
game.MovingTilep2.prototype.maxx=2;
game.MovingTilep2.prototype.width=game.tileW/2;
game.MovingTilep2.prototype.height=game.tileH/2;

我们有两种类型的可移动方块: MovingTilep1可以竖直移动(它的diry属性为非0数值),MovingTilep2可以水平移动(它的dirx值非0)。speed属性,你一定猜到了,代表方块一次移动的像素距离。

miny/maxy/minx/maxx 属性设置了方块运动的边界。我们当然可以把边界坐标设置成绝对的数值范围,但是如果需要把方块放到别的地方的话,就要改动边界的范围了。而在这里,我们用的是相对于方块起始位置的数值。这样我们可以把方块放在任意的位置,而不用修改边界范围。需要注意的是,移动的方块不会检测碰撞,所以你应该确保他们运动时不撞到障碍物。或者你也可以允许他们穿过障碍物。你在做游戏,你就是上帝。

来看一个例子。方块起始的位置是(x=2,y=5),竖直运动,miny=-1,maxy=4。它会怎么运动呢?起始位置-miny=5+(-1)=4,所以最小可以到达的位置是(x=2,y=4),最大可以到达的位置是(x=2,y=9)。

方块起始位置的数组和敌人起始位置的数组类似:

//浮动方块数组 [方块类型, x位置, y位置]
myMovingTiles = [
[0],
[[1, 4, 2]],
[[2, 4, 4]]
];

在地图1中,我们定义了一个浮动方块,它的类型编号是1(从MovingTile1模版创建),起始位置是(x=4,y=2)。地图2中同样有1个浮动方块。你也可以在一个地图中放置多个浮动方块。

接下来是要在buildMap函数中添加浮动方块的生成代码。在创建敌人部分后面加入:

game.movingtiles = myMovingTiles[game.currentMap];
for (var i = 0; i<game.movingtiles.length; ++i) {
 var name = "movingtile"+i;
 game[name]= new game["MovingTilep"+game.movingtiles[i][0]];
 game.clip.attachMovie("movingtiles", name, 12001+i);
 game[name].clip=game.clip[name];
 game[name].clip.gotoAndStop(game.movingtiles[i][0]);
 game[name].xtile = game.movingtiles[i][1];
 game[name].ytile = game.movingtiles[i][2];
 game[name].x = game[name].xtile *game.tileW+game.tileW/2;
 game[name].y = game[name].ytile *game.tileH+game.tileH/2;
 game[name].clip._x = game[name].x;
 game[name].clip._y = game[name].y;
 game[name].minx=game[name].minx+game[name].xtile;
 game[name].maxx=game[name].maxx+game[name].xtile;
 game[name].miny=game[name].miny+game[name].ytile;
 game[name].maxy=game[name].maxy+game[name].ytile;
}

首先还是取得当前地图的浮动方块数组(第一句)。变量game.movingtiles保存了当前地图的浮动方块数据,包括数目和方位。然后创建新的对象,放置mc到舞台正确的位置,跳转到相应的帧。类型1的方块是第一帧,类型2的方块则是第二帧。代码的最后一部分是计算浮动方块的运动范围。虽然变量名称还是miny/maxy/minx/maxx,但是这些属性变成了确定的数字,和原先的含义(相对起始位置的坐标)已经不同了(或者说他们是绝对的坐标,使用的时候不需要再参考起始位置)。

在moveChar函数中,需要添加一行代码,用来保存y坐标:

ob.lasty=ob.y;

在moveChar函数中还需要改写移动功能的代码:

if (diry == 1) {
  if (ob.downleft and ob.downright and !checkMovingTiles(speed*diry)) {
    ob.y += speed*diry;
  } else {
    ob.jump = false;
    if(ob.onMovingTile){
      ob.y=ob.onMovingTile.y-ob.onMovingTile.height-ob.height;
    }else{
      ob.y = (ob.ytile+1)*game.tileH-ob.height;
    }
  }
}

我们使用了checkMovingTiles函数,如果英雄将会降落在浮动方块上,这个函数会返回true。如果英雄马上就要落在浮动方块上,我们设置他的y坐标为刚好在方块上面。


英雄在浮动方块上面吗?

或许你已经从moveChar函数中增加的部分看出来了,没错,我们需要创建一个新函数,用来检测角色是不是站在浮动方块上面。checkMovingTiles函数不仅仅返回答案(是或者不是),而且还把英雄所在浮动方块的名字保存到char对象中。

function checkMovingTiles (y) {
  if(char.diry<>-1){
    var heroymax=char.y+char.height+y;
    var heroxmax=char.x+char.width;
    var heroxmin=char.x-char.width;
    foundit=false;
    for (var i = 0; i<game.movingtiles.length; i++) {
      var ob=game["movingtile"+i];
      var tileymax=ob.y+ob.height;
      var tileymin=ob.y-ob.height;
      var tilexmax=ob.x+ob.width;
      var tilexmin=ob.x-ob.width;
      if(char.lasty+char.height<=tileymin){
        if (heroymax<=tileymax and heroymax>=tileymin) {
          if (heroxmax>tilexmin and heroxmax<tilexmax) {
            char.onMovingTile=ob;
            foundit=true;
            break;
          } else if (heroxmin>tilexmin and heroxmin<tilexmax) {
            char.onMovingTile=ob;
            foundit=true;
            break;
        }
      }
    }
  }
  return(foundit);
  }
}

让我们看看发生了什么。如果角色不是往上运动(diry值不是-1),我们就计算出角色的边界。然后遍历浮动方块数组,看角色是否和当前的浮动方块接触:

带有“lasty”属性的if语句是用来确定角色的上一个位置是在浮动方块的上方,下面的两个if语句则判断角色是不是和方块有接触。如果有碰撞,那就意味着我们找到了正确的移动方块,于是onMovingTile属性就会纪录下找到的方块对象。

让他也动起来

请准备好看史上最丑陋最冗长最小气的函数!它很长,因为它要很多东西。首先,它移动所有的浮动方块,然后检查这些方块是不是需要反过来运动了,这些还不够,它还要处理英雄在浮动方块上面的动作,检查是不是应该掉下来。

function moveTiles () {
  for (var i = 0; i<game.movingtiles.length; i++) {
    var ob=game["movingtile"+i];
    getMyCorners (ob.x + ob.speed*ob.dirx, ob.y + ob.speed*ob.diry, ob)
    if (ob.miny>ob.upY or ob.maxy<ob.downY) {
      ob.diry=-ob.diry;
    }
    if (ob.minx>ob.leftX or ob.maxx<ob.rightX) {
      ob.dirx=-ob.dirx;
    }
    ob.x = ob.x + ob.speed*ob.dirx;
    ob.y = ob.y + ob.speed*ob.diry;
    ob.xtile = Math.floor(ob.x/game.tileW);
    ob.ytile = Math.floor(ob.y/game.tileH);
    ob.clip._x = ob.x;
    ob.clip._y = ob.y;
    if(ob.diry==-1){
      checkMovingTiles(0);
    }
  }
  //check if hero is on moving tile
  if(char.onMovingTile){
    getMyCorners (char.x, char.y+char.onMovingTile.speed*char.onMovingTile.diry, char);
    if (char.onMovingTile.diry == -1) {
      if (char.upleft and char.upright) {
        char.y=char.onMovingTile.y-char.onMovingTile.height-char.height;
      } else {
        char.y = char.ytile*game.tileH+char.height;
        char.jumpspeed = 0;
        char.jump = true;
        char.onMovingTile=false;
      }
    }
    if (char.onMovingTile.diry == 1) {
      if (char.downleft and char.downright) {
        char.y=char.onMovingTile.y-char.onMovingTile.height-char.height;
      } else {
        char.onMovingTile=false;
        char.y = (char.ytile+1)*game.tileH-char.height;
      }
    }
    getMyCorners (char.x+char.onMovingTile.speed*char.onMovingTile.dirx, char.y, char);
    if (char.onMovingTile.dirx == -1) {
      if (char.downleft and char.upleft) {
        char.x += char.onMovingTile.speed*char.onMovingTile.dirx;
      } else {
        char.x = char.xtile*game.tileW+char.width;
        fall (char);
      }
    }
    if (char.onMovingTile.dirx == 1) {
      if (char.upright and char.downright) {
        char.x += char.onMovingTile.speed*char.onMovingTile.dirx;
      } else {
        fall (char);
        char.x = (char.xtile+1)*game.tileW-char.width;
      }
    }
    updateChar (char);
  }
}

和上面说的一样,第一部分的代码用来移动浮动方块。遍历所有的浮动方块,把它们下一步的坐标和miny/maxy(minx/maxx)属性对照,看是否需要反过来运动。

这几行代码:
if(ob.diry==-1){
  checkMovingTiles(0);
}

用来检查是否要载上英雄,注意满足的条件是英雄站在障碍物的边缘上不动,而且方块是朝上运动(diry是-1)。

在这行以下的函数部分:
if(char.onMovingTile){
用来处理英雄在浮动方块上的动作。当onMovingTile值不是false,意味着英雄站在某个浮动的方块上面,而且onMovingTile属性值就是所在的浮动方块对象。这里的代码和moveChar函数比较相似。我们利用getMyCorners函数计算英雄下一步的位置,如果没有碰到任何障碍物,就让英雄和方块一起运动;反之则不能把英雄移动过去。

使用函数

在detectKeys函数的开头加入这行语句,用来移动所有的浮动方块(即使英雄没有踩在它们上面):

moveTiles();

另外,当英雄起跳的时候,我们还要让他的onMovingTile属性变回false:

ob.onMovingTile=false;

下载源文件

卷屏

在我们开始这部分之前,有件事得先解释清楚。Flash很慢。Flash非常慢。卷屏意味着以每秒20-30次的速度移动屏幕上数以百计的方块。而太多移动的方块意味着Flash不能及时地完成重绘,于是速度下降。你的游戏将慢得像条睡着的蜗牛在爬。
“什么?”你可能会问,“难道不用卷屏吗?怎么会慢得像睡着的蜗牛在爬呢?”
你可以用移动的方块,但是你得小心,别让卷屏的区域太大,而且移动的方块别太多。多大算“太大”,多少又算“太多”,这就得你自己体会了。记住,Flash游戏大多数时候是在浏览器上玩的,游戏者可能同时打开了多个窗口,后台可能有多个程序在同时运行,而且不是所有游戏者都有那么好的电脑配置。可以用低配置的电脑来测试你的游戏,如果觉得速度慢,就把它改小点。
来看看我们接下来要做的:

原理

左边的图中是非卷屏游戏。主角向右移动,而其它的东西原地不动。现在看右边的图,这是个卷屏游戏。主角仍然是要向右移动,但是我们实际上并没有移动主角,为了让他看起来是在向仍然右移动,我们将其它东西向左移了。

所以,原理很简单:
当需要变换主角的位置时,向反方向移动其它部分就行了。但是,由于我们习惯于将主角与其它背景区块一起放入MC中,主角会跟他们一起往反方向移动。为了固定主角的位置,我们仍然将所有物体向反方向移,同时,将主角向正确的方向移动相同的距离。

来看个卷屏游戏的例子。假设主角要向右移10px。我们先将所有方块(包括主角)向左移10px,并将主角向右移10px。最后看起来主角并没有动,而其它方块向左移了。

卷屏的最简单的方法是将所有方块都放到屏幕上,但是只显示其中一小部分,然后整个一起移动。这可能会让你的游戏速度非常慢,因为没显示出来的那上千个方块也在移动。另一个办法是将移出可见区域的方块删除,并在另一头添加新的方块。这会好一点儿,但是Flash花费在删除/复制MC上的时间又太多了。
最后一个办法是:只把可见部分的方块放到舞台上,当它们移出可见区域时,我们将相同的方块移动到另一端,重命名,并再次使用相同的MC。这被称作“gotoAndStop”卷屏引擎:

如图所示,当方块从右边消失,我们将它移动到左边。同时我们要重命名MC,因为所有的MC都是以“t_3_4”(表示它位于y=3,x=4)这样的名字命名的。而在新位置的方块可能要显示不同的帧(图片)。这就是为什么我们要将它发送到含有正确图片的帧,也是这种方法叫做“gotoAndStop”的原因。

准备卷屏

在大多数卷屏游戏中,主角的位置是始终在屏幕中央的。

你可以看见,主角左边的方块数和右边的方块数是相等的。这就是说,总的列数是3、5、7、9、11……而绝不会是2、4、6、8。行数也是一样。
来声明一个游戏对象:
game = {tileW:30, tileH:30, currentMap:1, visx:7, visy:5, centerx:120, centery:90};
属性 visx 代表可见的列数,visy 代表可见的行数。我们还需要属性centerx/centery来标记影片的中心点。

当我们移动方块时,可能需要显示一些在地图数组中没有声明的方块。例如,当主角高高兴兴地走到地图数组中已声明的最左边的方块上以后,就需要显示更左边的方块。对于这类方块,我们会建立新的不含图片的方块类型(这样对Flash来说负担比较小)。在方块MC的第20帧创建关键帧。以下是声明这类方块的代码:
game.Tile4 = function () { };
game.Tile4.prototype.walkable = false;
game.Tile4.prototype.frame = 20;

你可能还想部分地遮住一些方块,这就需要新建含有名为“frame”的关联的新MC,该MC的中间挖空,只显示经过挖空处的方块。

建立卷屏的世界

让我们从 buildMap 函数开始。
function buildMap (map) {
_root.attachMovie("empty", "tiles", 1);
game.halfvisx=int(game.visx/2);
game.halfvisy=int(game.visy/2);

我们将为game对象计算两个新的属性。halfvisx和halfvisy被分别用来存放主角与可见区域边缘之间的列数和行数。当总列数visx=5时,halfvisx=2,就是说主角右边有2列、左边有2列,中间就是主角所在的那一列。

game.clip = _root.tiles;
game.clip._x = game.centerx-(char.xtile*game.tileW)-game.tileW/2;
game.clip._y = game.centery-(char.ytile*game.tileH)-game.tileH/2;

我们得根据以下两个变量的值来决定包含主角和所有方块的MC的位置:游戏的中心点和主角的位置。游戏的中心点在game对象中由centerx/centery声明;主角的位置在char对象中由xtile/ytile声明。当主角被放在x坐标的(char.xtile*game.tileW)+game.tileW/2处时,方块MC必须被放在相反的坐标上,所以我们要用centerx减去这个值。

for (var y = char.ytile-game.halfvisy; y<=char.ytile+game.halfvisy+1; ++y) {
    for (var x = char.xtile-game.halfvisx;
    x<=char.xtile+game.halfvisx+1; ++x) {
        var name = "t_"+y+"_"+x;
        if(y>=0 and x>=0 and y<=map.length-1 and x<=map[0].length-1){
            game[name] = new game["Tile"+map[y][x]]();
        }else{
            game[name] = new game.Tile4();
        }
        game.clip.attachMovie("tile", name, 1+y*100+x*2);
        game.clip[name]._x = (x*game.tileW);
        game.clip[name]._y = (y*game.tileH);
        game.clip[name].gotoAndStop(game[name].frame);
   }
}

这个循环创建了所有的可见方块对象,并且复制该方块对象相应的MC到舞台上。如你所见,这个循环并不是从0开始,而是从ytile-halfvisy开始。同理它也不是在map数组的末尾结束的,而是在ytile+halfvisy+1结束。然后,if条件句检查将被创建的方块是否已经在map数组中。如果不在,我们就使用tile4模版中的空白方块。
为了使卷屏的过程平滑,我们得在舞台右边和底部分别增加一行和一列方块。这些方块可以保证,哪怕我们把半个方块移到舞台的另一边,也不会出现任何空白区域。
_root.attachMovie("frame", "frame", 100);
最后一行复制帧来覆盖影片中你不想显示的区域。

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;
char.clip.gotoAndStop(char.frame);
char.xstep=char.x;
char.ystep=char.y;

char的创建方法与前面一样。仅有的不同是增加了两个新的属性:xstep和ystep。我们将在后面用它们来检查当前是否是移动方块行或列到另一端的合适时间。

卷屏!卷屏!

既然卷屏的世界已经建好了,下面就该让它真正“卷”动起来了。在为主角计算出ob.xtile/ob.ytile的值以后,在moveChar函数的末尾加上如下代码来进行卷屏:
game.clip._x = game.centerx-ob.x;
game.clip._y = game.centery-ob.y;
if(ob.xstep<ob.x-game.tileW){
    var xtile = Math.floor(ob.xstep/game.tileW) + 1;
    var xnew=xtile+game.halfvisx+1;
    var xold=xtile-game.halfvisx-1;
    for (var i = ob.ytile-game.halfvisy-1; i<=ob.ytile+game.halfvisy+1; ++i) {
       changeTile (xold, i, xnew, i, _root["myMap"+game.currentMap]);
    }
    ob.xstep=ob.xstep+game.tileW;
} else if(ob.xstep>ob.x){
    var xtile = Math.floor(ob.xstep/game.tileW);
    var xnew=xtile+game.halfvisx+1;
    var xold=xtile-game.halfvisx-1;
    for (var i = ob.ytile-game.halfvisy-1; i<=ob.ytile+game.halfvisy+1; ++i) {
        changeTile (xold, i, xnew, i, _root["myMap"+game.currentMap]);
    }
    ob.xstep=ob.xstep-game.tileW;
}
if(ob.ystep<ob.y-game.tileH){
    var ytile = Math.floor(ob.ystep/game.tileH)+1;
    var ynew=ytile+game.halfvisy+1;
    var yold=ytile-game.halfvisy-1;
    for (var i = ob.xtile-game.halfvisx-1; i<=ob.xtile+game.halfvisx+1; ++i) {
        changeTile (i, yold, i, ynew, _root["myMap"+game.currentMap]);
    }
    ob.ystep=ob.ystep+game.tileH;
} else if(ob.ystep>ob.y){
    var ytile = Math.floor(ob.ystep/game.tileH);
    var yold=ytile+game.halfvisy+1;
    var ynew=ytile-game.halfvisy-1;
    for (var i = ob.xtile-game.halfvisx-1; i<=ob.xtile+game.halfvisx+1; ++i) {
        changeTile (i, yold, i, ynew, _root["myMap"+game.currentMap]);
    }
    ob.ystep=ob.ystep-game.tileH;
}
return (true);

首先,我们根据中心点的位置和主角的坐标值将包含主角和所有方块的game.clip放到相应的位置。然后我们写了4段相似的代码,每一段定义一个方向。当主角的移动距离大于方块的大小时,循环会调用含有相应变量的changeTile函数。循环结束,方块被移动/重命名/修改以后,就该更新属性ystep/xstep的值了。

现在,让我们来编写changeTile函数:

function changeTile (xold, yold, xnew, ynew, map) {
 var nameold = "t_"+yold+"_"+xold;
 var namenew = "t_"+ynew+"_"+xnew;
 if(ynew>=0 and xnew>=0 and ynew<=map.length-1 and xnew<=map[0].length-1){
  game[namenew] = new game["Tile"+map[ynew][xnew]]();
  game.clip[nameold]._name = namenew;
  game.clip[namenew].gotoAndStop(game[namenew].frame);
  game.clip[namenew]._x = (xnew*game.tileW);
  game.clip[namenew]._y = (ynew*game.tileH);
 }else{
  game[namenew] = new game.Tile4();
  game.clip[nameold]._name = namenew;
  game.clip[namenew].gotoAndStop(game[namenew].frame);
 }
}

这样,我们会分别得到方块的2个旧的和2个新的坐标值。为了检查方块是否还在map数组之内,我们还增加了map参数。 “nameold”是这些方块的旧名字,而“namenew”是它们的新名字。在我们用新名字创建了新的tile对象以后,语句:
game.clip[nameold]._name = namenew;
将方块MC重命名。新的MC是空的,所以我们不必将它放到新的_x/_y,让它留在原处就行了。

下载源文件

posted on 2006-05-18 18:10 blog搬家了--[www.ialway.com/blog] 阅读(1781) 评论(0)  编辑  收藏 所属分类: AS2.0

只有注册用户登录后才能发表评论。


网站导航: