这里游戏制作用到的所有代码都被放到这个文件里,可以打包下载查看
https://phaser.io/tutorials/making-your-first-phaser-3-game/phaser3-tutorial-src.zip
工程的结构及其核心函数
在前面的工作中,我们已经搭建好了完整的环境,也完成了 hello world 的展示,现在,可以开始创建一个Phaser工程的结构及其核心函数。除去一些HTML样板代码,以下是核心代码的实现。
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
scene: {
preload: preload,
create: create,
update: update
}
};
var game = new Phaser.Game(config);
function preload ()
{
}
function create ()
{
}
function update ()
{
}
完整的代码在part1.html
中。
打开我们写好的文件,可以看到渲染出来了一块黑色的区域,这块区域是我们的画布,这块画布区域的大小,正如我们在代码中的config配置一样。
这个config(配置)对象意味着你怎么配置Phaser游戏。目前只涉及到渲染器(renderer)、尺寸和默认Scene(场景)。一个Phaser.Game
对象实例(instance)赋值给一个叫game
的局部变量,上述配置对象传给这个实例。
在Phaser 2 中,对象
game
用作几乎所有内部系统的入口,并常常是通过全局变量访问它。在Phaser 3 中不再如此,在全局变量中存储游戏实例不再有用。
config
中属性type
可以是Phaser.CANVAS
,或者Phaser.WEBGL
,或者Phaser.AUTO
。这是你要给你的游戏使用的渲染环境(context)。推荐值是Phaser.AUTO
,它将自动尝试使用WebGL,如果浏览器或设备不支持,它将回退为Canvas。
Phaser生成的画布元素(canvas element)将径直添加到文档中调用脚本的那个节点上,如果需要的话,也可以在游戏配置中指定一个父容器,
config
中属性width
和height
设定了Phaser即将生成的画布元素的尺寸,在此例中是800 x 600 像素。这是游戏显示所用的分辨率,而游戏世界(world)可以是任意尺寸。
加载资源
在之前的代码中,我们给出了一个工程的最小实例,现在,为了能让我们在工程中,使用图片等资源,我们需要先加载资源。
要做到这个,我们需要使用一个叫preload
(预加载)的函数内部,调用Phaser的Loader(加载器)。Phaser启动时会自动找到这个函数,并加载里面定义好的所有资源。
目前preload
函数是空的。把它改成这样:
function preload ()
{
this.load.image('sky', 'assets/sky.png');
this.load.image('ground', 'assets/platform.png');
this.load.image('star', 'assets/star.png');
this.load.image('bomb', 'assets/bomb.png');
this.load.spritesheet('dude',
'assets/dude.png',
{ frameWidth: 32, frameHeight: 48 }
);
}
完整的代码在part2.html
中。
这样就可以加载5个资源:4张图(image)和一个精灵表单(sprite sheet)。其中加载资源的第一个参数叫做资源的key(键值,即sky
,bomb
这种)。这个字符串是一个链接,指向已加载的资源,你在代码中生成游戏对象时将用到它。你可以随意使用任何有效的JavaScript字符串作为键值。
显示图像
要显示已经加载的一张图像,我们把下面的代码到create
(生成)函数中:
this.add.image(400, 300, 'sky');
可以在part3.html
中看到这行代码,如果我们打开写好的这个文件,我们可以看到一个游戏画面,如下图所示:
其中参数中的400
和300
是图像坐标的x值和y值。为什么是400和300呢?这是因为,在Phaser 3 中,所有游戏对象的定位都默认基于它们的中心点。这个背景图像的尺寸是800 x 600像素,所以,如果我们显示它时将它的中心定在0 x 0,你将只能看到它的右下角。如果我们显示它时定位在400 x 300,你能看到整体。
也就是说在Phaser 3中,所有游戏对象的定位都是基于它的中心点的。
提示: 你可以用
setOrigin
(设置原点)来改变这种情况。比如代码this.add.image(0, 0, 'sky').setOrigin(0, 0)
,将把图像的绘制定位点重置为左上角。在Phaser 2 中,定位点是通过属性anchor
(锚点)获取的,但在Phaser 3 中则通过属性originX
和originY
。
游戏对象的显示顺序与你生成它们的顺序一致。所以,如果你想放一个星星的精灵在背景上,你就要保证在添加天空(sky)图像之后才添加星星(star)图像:
function create ()
{
this.add.image(400, 300, 'sky');
this.add.image(400, 300, 'star');
}
如果先放star
(星星)图像,它将被sky
(天空)图像盖住。
完整的代码在part3.html
中
建立游戏世界
在底层,代码this.add.image
生成一个新的Image(图形)类游戏对象,并把它添加到当前场景的显示列表(display list)中。显示列表是游戏世界里面所有生效的游戏对象。你的所有游戏对象都活在这个列表中。你可以把图像放置在任何位置,当然,如果图像位于0x0到800×600这个区域之外,那么你视觉上看不到它,因为它已“脱离画面”,但它仍旧在场景中存在。
场景(Scene)自身没有确定的尺寸,在所有方向上都是无限延展的。镜头(Camera)系统控制着你观看场景的视野,你可以随意移动、推拉已激活的镜头。你还可以另外生成一些镜头,用于别的观看场景的视野。Phaser 3 的镜头系统,能力大大地超过Phaser 2的。能够实现很多功能。
现在让我们搭建场景,添加一张背景图像和几个平台。这是更新后的create
函数
var platforms;
function create ()
{
this.add.image(400, 300, 'sky');
platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground').setScale(2).refreshBody();
platforms.create(600, 400, 'ground');
platforms.create(50, 250, 'ground');
platforms.create(750, 220, 'ground');
}
在这段代码中,你可以看到一个对this.physics
的调用。这意味着我们在使用Arcade(游乐场)物理系统(Physics system),不过在此之前我们还需要把它添加到游戏配置中,引入对物理系统的支持。这是修订后的游戏配置:
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false
}
},
scene: {
preload: preload,
create: create,
update: update
}
};
新加的是physics
属性,完整的代码在part4.html
中
完成这些之后打开文件就可以看到如下图所示的场景。
create 函数
在create
函数中,我们在里面加入了大量的代码,接下来对这些代码进行解释。首先,这一部分:
platforms = this.physics.add.staticGroup();
这一句生成一个静态物理组(Group),并把这个组赋值给局部变量platforms
。在Arcade物理系统中,有动态的和静态的两类物体(body)。动态物体可以通过外力比如速度(velocity)、加速度(acceleration),得以四处移动。它可以跟其他对象发生反弹(bounce)、碰撞(collide),此类碰撞受物体质量和其他因素影响。
与此明显不同的是,静态物体只有位置和尺寸。重力对它没有影响,你不能给它设置速度,有东西跟它碰撞时,它一点都不动。名副其实,完全是静态的。所以用作地面和平台很完美,我们打算让玩家在上面来回跑动。
那么什么是组呢?如其名所示,是把近似对象组织在一起的手段,控制对象全体就像控制一个统一的个体。你也可以检查组与其他游戏对象之间的碰撞。组能够生成自己的游戏对象,这是通过便利的辅助函数如create
实现的。物理组会自动生成已经开启物理系统的子项(children),省得你处理时跑腿。
平台组做好了,我们现在可以用它生成平台:
platforms.create(400, 568, 'ground').setScale(2).refreshBody();
platforms.create(600, 400, 'ground');
platforms.create(50, 250, 'ground');
platforms.create(750, 220, 'ground');
这就生成了场景,正如前面的图片所示。
在预加载时,我们输入了图像’ground’。它是个简单的绿色长方形,尺寸是400 x 32像素,将用于我们的基础平台。
上述代码的第一行,添加一张新的地面图像到400 x 568的位置(请记住,图像定位基于中心点)——问题是,我们需要这个平台撑满游戏的宽度。否则玩家就会掉出边界。要做到这一点,我们用函数setScale(2)
把它按x2(两倍)缩放。现在它的尺寸是800 x 64了,恰好符合我们的要求。这是因为我们缩放的是一个 静态 物体,必须把所作变动告诉物理世界(physics world),所以要调用refreshBody()
。
platforms.create(600, 400, 'ground');
platforms.create(50, 250, 'ground');
platforms.create(750, 220, 'ground');
这个步骤跟前面完全相同,只是不需要缩放。
玩家
我们已经有了平台,但还没有人在上面跑动。让我们改一下以添加玩家。
做一个新的变量player
,并把下面的代码添加到create
函数中。你可以在part5.html
中看到这些代码:
player = this.physics.add.sprite(100, 450, 'dude');
player.setBounce(0.2);
player.setCollideWorldBounds(true);
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'turn',
frames: [ { key: 'dude', frame: 4 } ],
frameRate: 20
});
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
frameRate: 10,
repeat: -1
});
在这块代码中,我们一共做了两件事情
- 生成物理精灵(sprite)
- 生成精灵能用到的几个动画
物理精灵
player = this.physics.add.sprite(100, 450, 'dude');
player.setBounce(0.2);
player.setCollideWorldBounds(true);
这一部分生成精灵
这样生成一个新的精灵,叫player
(玩家),位于100 x 450像素,在游戏的下部。精灵是通过物理游戏对象工厂函数(Physics Game Object Factory,即this.physics.add
)生成的,这意味着它默认拥有一个动态物体。
精灵生成后,被赋予0.2的一点点反弹(bounce)值。这意味着,它跳起后着地时始终会弹起那么一点点。然后精灵设置了与世界边界(bound)的碰撞。——边界默认在游戏尺寸之外。我们(通过player.setCollideWorldBounds(true)
)把游戏(的世界边界)设置为800 x 600后,玩家就不能不跑出这个区域了。这样会让玩家停下来,不能跑出画面边界,或跳出顶边。
动画
如果回顾一下preload
函数,你会看到’dude’是作为精灵表单(sprite sheet)载入的,而非图像。这是因为它包含了动画帧(frame)。完整的精灵表单是如下图这个样子的:
总共有9帧,4帧向左跑动,1帧面向镜头,4帧向右跑动。
注意:Phaser支持翻转精灵,以节省动画帧。
我们定义两个动画,叫left
和right
。这是left
动画:
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }), // 使用 0 ~ 3 帧的动画
frameRate: 10, // 每秒 10 帧
repeat: -1 // 循环播放
});
left
动画使用0, 1, 2, 3帧。
跑动时每秒10帧。repeat: -1
告诉动画要循环播放。
这是我们的标准跑动周期。反方向的动画把这些重复一下,键值用right
。最后一个动画键值用turn
(转身)。
补充信息: 在Phaser 3 中,动画管理器(Animation Manager)是全局系统。其中生成的动画是全局变量,所有游戏对象都能用到它们。它们分享基础的动画数据,同时管理自己的时间轴(timeline)。这就使我们能够在某时定义一个动画,却可以应用到任意多的游戏对象上。这有别于Phaser 2,那时动画只属于据以生成动画的特定游戏对象。
添加物理系统
Phaser支持多种物理系统,每一种都以插件形式运作,任何Phaser场景都能使用它们。在本文写作时,已经装有Arcade, Impact, Matter.js三种物理系统。针对本教程,我们将给我们的游戏使用Arcade物理系统,它简单,轻量,完美地支持移动浏览器。
物理精灵在生成时,即被赋予body
(物体)属性,这个属性指向它的Arcade物理系统的Body。它表示精灵是Arcade物理引擎中的一个物体。物体对象有很多属性和方法,我们可以玩一下。
比如,在一个精灵上模仿重力效果,可以这么简单写:
player.body.setGravityY(300)
这是个随意的值,但逻辑讲,值越大你的对象感觉越重,下落越快。如果你把这些加到你的代码里,或者运行part5.html
,你会看到玩家不停地往下落,完全无视我们先前生成的地面:
原因在于,我们还没有做玩家与地面之间的碰撞。
要想玩家能与平台碰撞,我们可以生成一个碰撞对象。该对象监控两个物体(可以是组),检测二者之间的碰撞和重叠事件。如果发生事件,这时它可以随意调用我们的回调函数。不过仅仅就与平台间的碰撞而言,我们没必要那么做:
this.physics.add.collider(player, platforms);
碰撞器(Collider)是施魔法的地方。它接收两个对象,检测二者之间的碰撞,并使二者分开。在本例中,我们把玩家精灵和平台组交给它。它可以执行针对所有组成员的碰撞,所以这一个调用就能处理与组合以及所有平台的碰撞。结果就有了一个稳固的平台,不再崩塌:
完整的代码可以在part6.html
中看到。
键盘控制
在上面的例子中,我们完成了碰撞检测,目前碰撞很棒了,不过我们非常想玩家动起来。你可能想到了,去找文档,搜一搜怎样添加事件监听器,但这里不需要。Phaser有内置的键盘管理器,用它的一个好处体现在这样一个方便的小函数:
cursors = this.input.keyboard.createCursorKeys();
这里把四个属性up, down, left, right(都是Key对象的实例),植入光标(cursor)对象。然后我们要做的就是在update
循环中做这样一些轮询:
if (cursors.left.isDown)
{
player.setVelocityX(-160);
player.anims.play('left', true);
}
else if (cursors.right.isDown)
{
player.setVelocityX(160);
player.anims.play('right', true);
}
else
{
player.setVelocityX(0);
player.anims.play('turn');
}
if (cursors.up.isDown && player.body.touching.down)
{
player.setVelocityY(-330);
}
它做的第一件事,是查看方向左键是不是正被按下。如果是,我们应用一个负的水平速度,开动奔跑动画’left’。如果是方向右键正被按下,我们按字面意思做反向动作。通过清除速度值,再如此设置,一帧一帧,形成一个“走走停停”(stop-start)式的运动。
玩家精灵只有键被按下时才移动,抬起时立即停止。Phaser也允许你用动量(momentum)和加速度(acceleration)生成更为复杂的动作,不过这里已经得到我们的游戏所需要的效果了。键盘检测的最后部分,如果没有键被按下,就设置动画为turn
,水平速度为0。
跳跃
if (cursors.up.isDown && player.body.touching.down)
{
player.setVelocityY(-330);
}
代码的最后部分添加了跳起功能。方向up键是跳起键,我们检查它有没有被按下。不过我们同时也检测玩家是不是正与地面接触,否则在半空中还会往上跳。
如果所有这些条件都符合,我们应用一个垂直速度,330像素每秒。玩家会自动落回地面,因为有重力。控制已经就位,我们现在有了一个可以探索的游戏世界。请加载part7.html
,玩一玩。尝试调整各个值,比如跳起值330,调低,调高,看看分别会有什么效果。
完整的代码可以看下 part7.html
。
目标:收集星星
给我们的小游戏定个目标了。让我们撒几颗星星到场景中,让玩家来收集。要做到这一点,我们会生成一个新的组,叫’stars’,再充实它。在生成函数中,我们加入如下代码(这些可以在part8.html
中看到):
stars = this.physics.add.group({
key: 'star',
repeat: 11, // 获得12个星星, 因为他会自动生成一个子项
setXY: { x: 12, y: 0, stepX: 70 } // 设置子项的位置
});
stars.children.iterate(function (child) {
child.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8)); // 添加随机的反弹
});
这个过程跟我们生成平台组近似。因为需要星星移动、反弹,我们生成动态物理组,而不是静态的。
组可以接收配置对象,以便于设置。在本例中,组配置对象有3个部分:首先,它设置纹理key(键值)为星星图像。这意味着配置对象生成的所有子项,都将被默认地赋予星星纹理。然后,它设置重复值为11。因为它自动生成一个子项,重复11次就意味着我们总共将得到12颗,这正好是我们的游戏所需要的。
最后的部分是setXY
——这用来设置组的12个子项的位置。每个子项都将如此放置:初始是x: 12,y: 0,然后x步进70。这意味着第一个子项将位于12 x 0;第二个离开70像素,位于82 x 0;第三个在152 x 0,依次类推。’step’(步进)值用于组生成子项时加以排布,真是很方便的手段。选用值70是因为,这意味着所有12个子项将完美地横跨着布满画面。
下一段代码遍历组中所有子项,给它们的bounce.y
赋予0.4到0.8之间的随机值,反弹范围在0(不反弹)到1之间(完全反弹)。因为星星都是在y等于0的位置产出的,重力将把它们往下拉,直到与平台或地面碰撞为止。反弹值意味着它们将随机地反弹上来,直到最终恢复安定为止。
如果现在我们这样就运行代码,星星会落下并穿过游戏底边,消失不见了。要防止这个问题,我们就要检测它们与平台的碰撞。我们可以再使用一个碰撞器对象来做这件事:
this.physics.add.collider(stars, platforms);
与此类似,我们也将检测玩家是否与星星重叠:
this.physics.add.overlap(player, stars, collectStar, null, this); // 碰到了就算重叠了
这会告诉Phaser,要检查玩家与组中任何一颗星星的重叠。如果检测到,他们就会被传递到collectStar
函数:
function collectStar (player, star)
{
star.disableBody(true, true);
}
简单来说,星星带着个已关闭的物体,其父级游戏对象被设置为不活动、不可见,即将它从显示中移除。现在运行一下游戏,我们得到一个玩家,它左冲右突的,跳起,从平台反弹,收集头顶上落下的星星。
完整的代码在part8.html
中
计算分数
最后我们打算给游戏增加两处改进:一个需要躲避的敌人,它会杀死玩家;收集到星星时得分。首先是得分。
为了做这个,我们将使用游戏对象Text(文本)。在此我们生成两个新的变量,一个持有实际得分,一个文本对象本身:
var score = 0;var scoreText;
scoreText
在create
函数中构建:
scoreText = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000' });
16 x 16是显示文本的坐标位置。’score: 0’是要显示的默认字符串,接下来的对象包含字号、填充色。因为没有指定字体,实际上将用Phaser默认的,即Courier。
下一步我们要调整collectStar
函数,以便玩家捡到一颗星星时分数会提高,文本会更新以反映出新状态:
function collectStar (player, star)
{
star.disableBody(true, true);
score += 10;
scoreText.setText('Score: ' + score);
}
这样一来,每颗星星加10分,scoreText
将更新,显示出新的总分。如果运行part9.html
,你可以看到星星掉下来,收集星星时分数会提高。
增加敌人
该添加一些坏蛋,以此给我们的游戏收尾。这将给游戏增添很棒的挑战因素,这是此前缺乏的。
想法是这样的:你第一次收集到所有星星后,将放出一个跳跳弹。这个炸弹只是随机地在平台上各处跳,如果收集它,你就死了。所有星星会重新产出,以便你可以再次收集,如果你完成了,又会放出另一个炸弹。这将给玩家一个挑战:别死掉,取得尽可能高的分数。
我们首先需要的东西是给炸弹用的一个组,还有几个碰撞器:
bombs = this.physics.add.group();
this.physics.add.collider(bombs, platforms);
this.physics.add.collider(player, bombs, hitBomb, null, this);
炸弹当然会跳出平台,如果玩家碰到它们,我们将调用hitBomb
函数。这个函数所作的就是停止游戏,使玩家变成红色:
function hitBomb (player, bomb)
{
this.physics.pause();
player.setTint(0xff0000);
player.anims.play('turn');
gameOver = true;
}
现在看来还不错,不过我们要放出一个炸弹。要做到这一点,我们改一下collectStar
函数:
function collectStar (player, star)
{
star.disableBody(true, true);
score += 10;
scoreText.setText('Score: ' + score);
if (stars.countActive(true) === 0)
{
stars.children.iterate(function (child) {
child.enableBody(true, child.x, 0, true, true);
});
var x = (player.x < 400) ? Phaser.Math.Between(400, 800) : Phaser.Math.Between(0, 400);
var bomb = bombs.create(x, 16, 'bomb');
bomb.setBounce(1);
bomb.setCollideWorldBounds(true);
bomb.setVelocity(Phaser.Math.Between(-200, 200), 20);
}
}
我们使用一个组的方法countActive
,看看有多少星星还活着。如果没有了,那么玩家把它们收集完了,于是我们使用迭代函数重新激活所有星星,重置它们的y位置为0。这将使所有星星再次从画面顶部落下。
下一部分代码生成一个炸弹。首先,我们取一个随机x坐标给它,始终在玩家的对侧画面,以便给玩家个机会。然后生成炸弹,设置它跟世界碰撞,反弹,拥有随机速度。
最终结果是个很棒的小炸弹精灵,它在画面上跳呀跳。尺寸小,开始的时候易于躲避。不过数量增加后就变得比较棘手!
结论
现在你已经学会怎样生成有物理属性的精灵,学会控制它的动作,学会使它与其他对象在一个小小的游戏世界里互动。你还可以做很多事情,以便增强它。为什么不扩展平台的尺寸并允许镜头摇动呢?也许可以增加不同类型的坏蛋,不同分值的收集活动,或者给玩家一个血条(health bar)。
或者,为了做个非暴力型的,你可以把它做成比快游戏(speed-run),仅仅挑战人们去尽可能快地收集星星。