Flash 区块游戏开发-2

芝麻开门-地图切换

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

为了创建两个房间,我们声明了两个地图数组:
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 @ 2010-11-19 17:49  模西的哥哥  阅读(408)  评论(0编辑  收藏  举报