从零到一:用 Phaser.js 写意地开发小游戏(Chapter 4 - 游戏即将开始)

2,259 阅读7分钟

回顾

上一节我们介绍了加载场景,并利用加载好的资源,丰富了开始界面。现在点击屏幕后仍是一片黑暗,那么,这一节我们就来完成游戏最核心的场景——play。我们要做的是一个接苹果的游戏,为此我们会加入物理引擎,会使用一些过渡动画以及监听触摸事件等等。

物理引擎

几乎每一个游戏框架都必须具备一个甚至多个物理引擎供开发使用,使用物理引擎可以实现例如碰撞、加减速运动、摩擦力等效果。Phaser非常人性化,提供了3个物理引擎供开发者使用,每个引擎各有自己的特点。下面来简单介绍一下:

Arcade

最简单快速的物理引擎,因为只支持AABB式的碰撞,计算速度最快,实现简单的物理碰撞、接触、重力等效果最佳。

关于AABB下面有几个链接可以让你去理解,全称是Axis-Aligned Bounding Box,直译就是轴对称盒。例如一张星星的图片,尽管边上很多透明的部分,但如果使用AABB来计算碰撞的话,则会用一个矩形将星星框住,这样计算起来非常方便,但精度就比较低。如此一来我们也可以想到,用Arcade构建的body是不可以发生形变的。

相关概念:


P2

如果说Arcade是小而精,P2引擎则是大而全了。各种物理模型均可实现,诸如多边形、弹簧、摩擦力、碰撞物体的材质、反弹系数等等都可以实现。尽管在性能上有一定消耗,毕竟要做更多复杂的运算,但为了效果,我们也很常用P2,作者引进P2也是由于它的全面。


Ninja

至于Ninja,则是比较专注精确的多种模式的碰撞检测。例如凹凸面的碰撞、平面和球的碰撞等等。平常比较少用,有兴趣的可以查看官方示例,另外,作者给出引进Ninja的理由是:

It's a really nice little physics system, supporting AABB and Circle vs. Tile collision, with lots of defs for sloping, convex and concave tile types. But that's all it does, it's not trying to be anything more really.

Stack Exchange - Game Development上关于Phaser三个引擎的介绍
Difference between Arcade, P2 and Ninja physics in Phaser

Phaser作者写的,关于物理引擎的介绍:
Explaining Phaser 2's multiple physics systems


Box2D

咦?为什么没有上面没有提到Box2D?很遗憾,这个引擎是收费的,40刀,如果没有特别大的需求,估计也用不上。

附上:非常有趣的官方示例,车掉坑里就爬不起来了。


正式开始

第一步:先布置好场景
  • 添加背景

  • 添加主角

  • 添加分数

  • 播放背景音乐

背景音乐的使用:

// 创建背景音乐实例
var bgMusic = game.add.audio('bgMusic');
// 循环播放
bgMusic.loopFull();

注意:

场景的布置和开始场景差不多,在开始场景中我们添加了背景音乐。会有人问,为什么不进入游戏就自动播放,是因为移动端的浏览器,必须要用户操作才能播放音频。

Safari HTML5 Audio and Video Guide
Warning: To prevent unsolicited downloads over cellular networks at the user’s expense, embedded media cannot be played automatically in Safari on iOS—the user always initiates playback.

另外可参考:

segmentfault上的讨论:HTML5的audio标签设置了autoplay属性在手机端出现的问题

示例代码:jsfiddle.net/Vincent_...点击预览


第二步:让你的主角动起来
  • 监听滑动事件

  • 移动主角位置

主要使用的就是input的addMoveCallback方法:

// 监听滑动事件
game.input.addMoveCallback(function(pointer, x, y, isTap) {
    if (!isTap) man.x = x;
});

第四个参数非常有用,可以判断是否为点击事件,如果是点击就不移动主角。

示例代码:jsfiddle.net/Vincent_...点击预览


就这么简单?NO,NO,NO

很快你就会发觉在PC上主角一直跟着鼠标移动,根本无法停下来!

其实原因也很简单,在PC端,Phaser的move事件对应的是mousemove;在移动端,对应的是touchmove。这两个事件有什么区别?主要区别就是touchmove必须手指触摸屏幕并滑动才会触发,而mousemove则不需要点住鼠标,只需要移动鼠标就会触发。

于是,我们来修改一下代码:

// 是否正在触摸
var touching = false;
// 监听按下事件
game.input.onDown.add(function() {
    touching = true;
});
// 监听离开事件
game.input.onUp.add(function() {
    touching = false;
});
// 监听滑动事件
game.input.addMoveCallback(function(pointer, x, y, isTap) {
    if (!isTap && touching) man.x = x;
});

Good!加入了触摸标记以后,我们监听了按下和离开事件,在PC端和移动端的表现一致了!

示例代码:jsfiddle.net/Vincent_...点击预览


这样算完事了?NO,NO,NO

细心的你多测试了几下,这时候发现了一个非常诡异的情况,当开始点击的时候,不是点在主角身上,主角就会瞬移过去!不难理解,因为我们是直接设置主角的x坐标,等于触摸位置的x坐标的。如果开始时x坐标不在主角身上,就会在一瞬间移动到手指的位置。

于是,我们又来修改一下代码,非常简单的一个方法:

// 监听按下事件
game.input.onDown.add(function(pointer) {
    // 要判断是否点住主角,避免瞬移
    if (Math.abs(pointer.x - man.x) < man.width / 2) touching = true;
});

上述代码的意思就是,开始触摸的位置必须在主角的最左边到最右边的x坐标范围内,才算作开始触摸,否则不算。

Excellent!现在可以随心所欲地操控你的主角了!

示例代码:jsfiddle.net/Vincent_...点击预览


第三步:让苹果掉下来吧
先来创造几个苹果看看

这里用到了Phaser的group,实际上可以理解成是一个数组,只不过更形象,组的常用方法:

  • add/addChild/addChildAt - 创建成员

  • countDead/countLiving - 统计成员

  • forEach/forEachAlive/forEachDead - 遍历成员

  • remove/removeAll/removeChildAt - 删除成员

  • create - 创建成员

  • bringToTop - 整个组的元素的图层提到最上层

另外组本身也有x,y等属性,也就是说,整个组的成员都可以根据组的偏移值而一起偏移!另外组还提供了很多丰富的方法,活用组可以达到事半功倍的效果。

// 添加苹果组
var apples = game.add.group();
var green = apples.create(50, 0, 'green');
var red = apples.create(150, 0, 'red');
var yellow = apples.create(250, 0, 'yellow');

示例代码:jsfiddle.net/Vincent_...点击预览


定时随机创造苹果

上面我们看到有三种苹果,那么下面我们来实现:每隔一段时间,随机创建三种苹果中的一种,并且摆放到不同的位置。

为此我们用到Phaser的timer,用于创建定时任务。会有人问为什么不用setInterval,setTimeout这些,是因为Phaser只要焦点离开了页面,就会自动暂停游戏,包括定时任务也会暂停,而setInterval和setTimeout则不会。

一般会用到addloop两个方法,分别对应setTimeout和setInterval:

我们修改一下代码:

// 添加苹果组
var apples = game.add.group();
// 苹果类型
var appleTypes = ['green', 'red', 'yellow'];
var appleTimer = game.time.create(true);
appleTimer.loop(1000, function() {
    var x = Math.random() * game.world.width;
    var y = Math.random() * game.world.height;
    var type = appleTypes[Math.floor(Math.random() * appleTypes.length)];
    apples.create(x, y, type);
});
appleTimer.start();

现在每隔1秒就会在屏幕随机位置出现一个苹果了,而且种类是随机的。

示例代码:jsfiddle.net/Vincent_...点击预览


最后,我们让苹果掉下来

这里就要使用到物理引擎了,考虑到接苹果的游戏对碰撞精度要求不是很高,我们选择使用Arcade,也就是Phaser默认的物理引擎。

关键代码:

// 开启物理引擎
game.physics.startSystem(Phaser.Physics.Arcade);
game.physics.arcade.gravity.y = 300;
// 设置苹果加入物理运动
game.physics.enable(apple);

于是,我们继续修改上面的代码:

// 添加苹果组
var apples = game.add.group();
// 苹果类型
var appleTypes = ['green', 'red', 'yellow'];
var appleTimer = game.time.create(true);
appleTimer.loop(1000, function() {
    var x = Math.random() * game.world.width;
    var type = appleTypes[Math.floor(Math.random() * appleTypes.length)];
    var apple = apples.create(x, 0, type);
    // 设置苹果大小
    var appleImg = game.cache.getImage(type);
    apple.width = game.world.width / 8;
    apple.height = apple.width / appleImg.width * appleImg.height;
    // 设置苹果加入物理运动
    game.physics.enable(apple);
});
appleTimer.start();
// 开启物理引擎
game.physics.startSystem(Phaser.Physics.Arcade);
game.physics.arcade.gravity.y = 300;

Perfect!现在满天苹果都会有了重力,加速掉向地上了。

示例代码:jsfiddle.net/Vincent_...点击预览


小结

这一节内容比较多,我们首先布置了游戏场景,加入了背景音乐。然后实现了对主角的操作,最后实现了苹果的随机掉落。经过这一节,万事俱备只欠东风,下一节我们就来完成这个游戏的剩余逻辑,比如接苹果加分,接到炸弹或苹果掉到地上游戏结束,还有加入更丰富的音效。

游戏截图:

未完待续

回顾:
预告:Chapter 5 - 游戏大功告成