2015年2月6日
欢迎!
在连续几周讨论了平台跳跃游戏的机制之后,我们也应该休息一下了。因此,本周的教程将基于论坛上经常谈论的特性:网格运动,或者可以说得更加明确一点:如何像Pacman(译者注:在红白机上叫做吃豆子,或者小精灵)那样在网格中移动。
我们本周讨论的代码可以让玩家优雅地在瓦片地图中移动,在很小的空间中转弯。我们还将构建Pacman游戏的核心代码。
获取源代码
在这里我只会着重高亮最重要的部分代码。所以请先浏览一下代码。如果你对一些代码有疑问,你可以去论坛询问。
运行/编辑汽车游戏代码可以到 jsbin 或者 codepen (译者:国内用户可访问runjs)
运行/编辑Pacman代码可以到jsbin or codepen (译者:国内用户可访问runjs)
克隆Phaser编程技巧代码可以到git repo。
基础设施的构建
我们需要一个玩家精灵和一张瓦片地图。瓦片地图包含来了关卡的布局。在这里,我们在Tiled(译者注:Tiled官网 http://www.mapeditor.org/)中画了一个简单的关卡:
这个软件导出了一个JSON文件,然后和tileset一起导入游戏中。在 create 方法中我们生成了所有的对象:
this.map = this.add.tilemap('map'); this.map.addTilesetImage('tiles', 'tiles'); this.layer = this.map.createLayer('Tile Layer 1'); this.map.setCollision(20, true, this.layer); this.car = this.add.sprite(48, 48, 'car'); this.car.anchor.set(0.5); this.physics.arcade.enable(this.car); this.cursors = this.input.keyboard.createCursorKeys(); this.move(Phaser.DOWN);
我们创建了地图和图层。碰撞被设置到20号瓦片上(暗灰色的砖块)。汽车摆放在地图的左上角,并且一开始就向下移动。.
在 update 函数中,我们检测与瓦片地图的碰撞:
this.physics.arcade.collide(this.car, this.layer);
当前的问题
玩家的地图中的移动是一件需要解决的游戏的挑战。 尽管表面上这个看起来很简单,但是实际上这需要玩家对象对周边的情况有一个基本的了解。
玩家通过上下左右方向键来控制,当按键按下的时候,玩家开始向着对应的方向不停的移动,直到撞到墙壁或者按下其他方向键转到其他方向。
在上面这个截图中,汽车即将接近一个交叉路口。如果用户不按任何键,它将继续向下移动。然而,如果正好在到达墙的右边的空隙时按下右键,汽车就会向右开。
你被包围了
所以,汽车如何知道他是否可以转弯,什么时候它应该转弯呢?为了弄清楚这个,我们在汽车四周创建了4块临时瓦片。
在 update 函数中,我们将使用Phaser Tilemap类中内建的特性,此特性可用于扫描特殊的瓦片:
this.marker.x = this.math.snapToFloor(Math.floor(this.car.x), 32) / 32; this.marker.y = this.math.snapToFloor(Math.floor(this.car.y), 32) / 32; var i = this.layer.index; var x = this.marker.x; var y = this.marker.y; this.directions[Phaser.LEFT] = this.map.getTileLeft(i, x, y); this.directions[Phaser.RIGHT] = this.map.getTileRight(i, x, y); this.directions[Phaser.UP] = this.map.getTileAbove(i, x, y); this.directions[Phaser.DOWN] = this.map.getTileBelow(i, x, y);
Tilemap.getTileLeft
及其同类函数正是用于实现我们上面所说的功能:返回给定坐标左边的瓦片(如果没有找到,则范围null
)。
因为这些函数是基于瓦片坐标而不是像素的,所以我们首先要找出我们的汽车的地图中的确切位置。我们对汽车的x和y值调用 Math.floor
函数,然后使用 Phaser.Math.snapToFloor
转换为网格坐标。这几步就会得到汽车所在的确切瓦片。我们把结果存放在 marker
变量中。
周围四块瓦片存放在 directions
数组中。启用渲染调试开关,我们能够看到汽车知道他四周的相关信息:
绿色的瓦片表示汽车可以安全的移动过去。红色的表示是障碍物,白色的表示当前的移动方向。
放置一个转弯标识
确认汽车是否可以转弯是第一个要解决的事情。第二个要解决的是要告诉它什么时候转弯,因为我们想要它在快达到地图合适的位置的时候转弯,否则,它就会撞到墙上停止向前。
可以利用 checkDirection
函数来实现这个。
这个函数需要传入一个参数:汽车将要转弯的方向。这是一个Phaser方向常量,例如Phaser.LEFT
或 Phaser.DOWN
。
这个函数做的第一件事就判断是否满足以下任意一个条件:
-
汽车的运动方向就是要转向的那个方向
-
那个方向上没有瓦片
-
那个方向上的瓦片不是一个“安全瓦片”(例如是一堵墙)
如果不满足上面任意一个条件,接下来就会设置一个转向标志。这个标志保存在 turnPoint
中,这是个 Phaser.Point
对象,保存了我们要汽车改变方向时所在的点的坐标。
if (this.current === this.opposites[turnTo]) { this.move(turnTo); } else { this.turning = turnTo; this.turnPoint.x = (this.marker.x * this.gridsize) + (this.gridsize / 2); this.turnPoint.y = (this.marker.y * this.gridsize) + (this.gridsize / 2); }
理想的转弯点就是汽车当前正驶入的那个方块的中心。在图中可以看到一个黄点:
Warm and Fuzzy Inside
当 car.x 和 car.y 与转弯点的值匹配时,汽车将转向新的方向。如果汽车以每帧一个像素的速度移动的话,那没问题。但是如果我们想要应用加速效果,或者其他改变汽车速度的方法,就会出问题。因为汽车的x/y坐标将只会是与转弯点接近而不是正好相等。
为了解决这个问题,我们使用了 Phaser.Math.fuzzyEqual
这个函数。它接受两个值和一个阈值。它将比较两个值,如果他们在差在阈值范围内,他们被认为是相等:
this.math.fuzzyEqual(a, b, threshold)
这正满足我们的需求。对于速度为150,我们的阈值可以设置为3。这个值足够可以保证汽车不会跳过转弯点。在 update
函数中,我们将检查汽车是否已经到达了转弯点。
当汽车到达时,我们将重新设置它的坐标,同时重新设置汽车的刚体的坐标,然后转向新的方向:
this.car.x = this.turnPoint.x; this.car.y = this.turnPoint.y; this.car.body.reset(this.turnPoint.x, this.turnPoint.y); this.move(this.turning); this.turning = Phaser.NONE;
通过增加上面的这最后一部分代码,你现在可以自由的在地图上行驶,在拐角转弯,随意的的撞墙。
进入Pacman
让我们来用上面的工作机制去克隆一个Pacman。一开始的构建是一样的:一个玩家精灵和一幅瓦片地图。不同的在于我们的Pacman精灵是动态的:
动画由 Phaser Animation Manager 来处理:
this.pacman.animations.add('munch', [0, 1, 2, 1], 20, true); this.pacman.play('munch');
然而,在 move
函数中,我们需要将它的脸转向新的方向:
this.pacman.scale.x = 1; this.pacman.angle = 0; if (direction === Phaser.LEFT) { this.pacman.scale.x = -1; } else if (direction === Phaser.UP) { this.pacman.angle = 270; } else if (direction === Phaser.DOWN) { this.pacman.angle = 90; }
我们重设了它的水平缩放和角度,然后基于它的方向进行调整。它的脸默认是朝右的,所以我们可以通过调整 scale.x
来让它朝左。要让它朝上或者朝下,就要进行旋转。
另外一个重要的部分就是:Pacman实际上比网格要大,所以我们需要调整它的刚体以网格大小:
this.pacman.body.setSize(16, 16, 0, 0);
这一句是在精灵(32x32大小)的中心位置设置了一个16x16大小的刚体,这就完美的适应了16x16大小的网格。
豆子
Pacman 需要一些豆子来吃。豆子已经使用了7号瓦片画到了瓦片地图上。所以我们将使用Phaser特性来将所有的7号瓦片转变为精灵。
this.dots = this.add.physicsGroup(); this.map.createFromTiles(7, this.safetile, 'dot', this.layer, this.dots); this.dots.setAll('x', 6, false, false, 1); this.dots.setAll('y', 6, false, false, 1);
这里首先获取豆子瓦片,用空白瓦片(安全瓦片)来代替,然后为每一个空白瓦片在 dots
组中添加一个精灵。 setAll
函数用来调整豆子精灵的位置,添加6个像素的偏移,因为他们只有4x4大小,这可以让他们处于瓦片的中心。
Pacman与豆子之间的碰撞检测在 update
函数中:
this.physics.arcade.overlap(this.pacman, this.dots, this.eatDot, null, this);
如果他们重叠了,就调用:
eatDot: function (pacman, dot) { dot.kill(); if (this.dots.total === 0) { this.dots.callAll('revive'); } },
豆子被销毁了。如果豆子组中的豆子数量变为零,我们要让豆子重生。所以你可以重新把豆子吃光!
希望你可以看到,我们只用很少的代码,我们现在实现了一个最基本的吃豆子游戏。
Star Bug
基于上面这些代码,我们利用这些概念将游戏进一步演变。增加更多的关卡、会飞的敌人和大量的收藏品。最终的结果就是Star Bug游戏:
这个游戏将会出现在我们即将面世的 Phaser Book of Games 这本书中。