1. 前言
这是我们第一个用Phaser3
开发游戏的教程。我们将学习如何创建一个小游戏,包括玩家在平台上跑和跳,收集星星并避免一些不好的行为。
什么是Phaser?
Phaser
是一个HTML5旨在帮助开发者创建强大、跨浏览器的HTML5游戏的框架。它是利用现代浏览器(桌面版和移动版)的优势而专门创建的。 对浏览器的唯一要求是对Canvas
标签的支持。
每步的完整代码和资源可以在此处下载。在继续之前,建议先看看Phaser3入门。
在我们的编辑器中打开part1.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(){
}
config
对象表示我们想如何配置Phaser
游戏。可以在此对象中设置许多选项,随着我们对Phaser
了解加深将其进一步拓展。上述代码中我们设置了渲染器,尺寸和默认Scene。
config
对象被传给Phaser.Game
对象的一个实例并赋值给game
这个局部变量。这是Phaser
游戏生命周期的开始。
type
属性可以是Phaser.CANVAS
,Phaser.WEBGL
或者Phaser.AUTO
。
这是要用于游戏的渲染上下文。 推荐值为Phaser.AUTO
,它会自动尝试使用WebGL,但是如果浏览器或设备不支持WebGL,它将使用Canvas。 Phaser创建的Canvas元素将在调用脚本时添加到HTML文档中,但也可以根据需要在游戏配置中指定一个父容器。
width
和height
属性用于设置Phaser创建的Canvas元素的尺寸。上面代码是800x600像素,可以设置成任何想要的尺寸,也代表游戏的分辨率。
配置对象的scene
属性将在之后的内容更加详细的介绍。
2. 加载资源
让我们加载游戏所需要的资源。这个可以通过放在称为preload
的Scene函数中来实现。 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}
);
}
这将加载5个资源:4张图像和1个精灵表。load.image
函数的第一个参数是个字符串,相当于资源的键值。
显示图像
为了显示一张我们已经加载过的图像资源,我们可以将下列代码放到create
函数中:
this.add.image(400,300,'sky');
在浏览器中访问part3.html
文件,可以看到有个蓝色的天空背景:
400和300是图像的x和y坐标。 为什么是400和300? 这是因为在 Phaser3
中,默认情况下所有游戏对象都基于其中心位置。 背景图像的尺寸为800x600像素,因此,如果我们将其设置为0x0为中心,只会看到其右下角。如果我们设置为400x300,将会看到整个图像。
提示:我们可以使用setOrigin
函数改变这种设置。比如,this.add.image(0,0,'sky').setOrigin(0,0)
。
游戏对象的显示顺序与创建它们的顺序一致。 因此,如果想在背景上放置一个星星,只要将创建星星的代码放在天空之后:
function create(){
this.add.image(400, 300, 'sky');
this.add.image(400, 300, 'star');
}
3. 平台
让我们给游戏添加一些平台,
function create(){
this.add.image(0, 0, 'sky').setOrigin(0,0);
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
物理系统,在使用之前需要通过配置对象告诉Phaser
我们需要用它。
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
}
};
在浏览器中访问part4.html
文件可以看到,
我们用
platforms = this.physics.add.staticGroup();
创建了一个静态物理组,并将其赋值给platforms
。 在Arcade物理系统中,有两种类型的物理实体:动态和静态。 动态物体是可以由于受到速度或加速度之类的力而移动的物体。它可能会由于和其它物体碰撞而弹开。
而静态物体仅具有位置和大小。 它不会受到重力的影响,我们无法在其上设置速度,并且当物体与其碰撞时,它不会因此而移动。由于这个特性,它非常适合我们游戏中需要的地面和平台。
那什么是组呢?顾名思义,它将类似的对象组合在一起并将它们作为一个整体加以控制。比如,我们可以检查组和其他游戏对象是否发生碰撞。
创建了平台组后,我们就以用其创建平台了:
platforms.create(400, 568, 'ground').setScale(2).refreshBody();
platforms.create(600, 400, 'ground');
platforms.create(50,250,'ground');
platforms.create(750,220,'ground');
现在,我们可以看到上图所示的内容了。代码的第一行我们创建了地面,之后将其放大两倍。因为原始的ground
图像是400x32像素的,而我们的游戏界面的尺寸是800x600,所以我们需要将图像放大两倍,也就是变成800x64,否则玩家就会从地面掉下去。
4. 玩家
创建个名为player
的变量,并将下列代码写在create
函数中:
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
});
主要做了两件事:创建物理精灵和创建可以使用的一些动画。
物理精灵
代码的第一部分创建了精灵:
player = this.physics.add.sprite(100,450,'dude');
player.setBounce(0.2);
player.setCollideWorldBounds(true);
我们创建了一个精灵并赋值给player
,并让其显示在游戏的底部(100x450)。精灵创建是通过Physics Game Object Factory(this.physics.add)
,意味着它是动态物体。
创建精灵后,将反弹值设为0.2。 这意味着当它跳下后着陆时,它会反弹得比较小。 然后将精灵设置会与游戏边界碰撞。 默认情况下,游戏边界在游戏尺寸的之外。 当我们将游戏尺寸设置为800x600时,玩家将无法在该区域之外活动,将避免玩家跑出屏幕边缘或跳出顶部。
动画
回头看一下preload函数,会看到dude
是作为精灵表而不是图像加载的。 那是因为它包含动画帧。 完整的精灵表如下所示:
总共有9帧,4帧向左跑,1帧面向镜头,4帧向右跑。我们定义了两个动画left
和right
。这是left
动画:
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('dude', {start:0, end:3}),
frameRate: 10,
repeat: -1
});
left
动画使用帧0、1、2和3,以每秒10帧的速度运行。repeat: -1
指的是让动画循环运行。
查看一下现在游戏的运行情况,我们会发现玩家会从地面掉下来……
为了让玩家与平台碰撞,我们可以创建一个Collider
对象。 让该对象监视两个物理对象(可以包括组),并检测它们之间是否存在碰撞或重叠。 若发生这种情况,则可以选择调用相应的函数,仅仅为了检测与平台碰撞,我们并不需要调用什么函数:
this.physics.add.collider(player, platforms);
5. 键盘控制
现在我们开始给我们的游戏加入一些键盘控制。Phaser
有内置的键盘管理器,一大好处就是我们可以用下面这样方便的函数:
cursors = this.input.keyboard.createCursorKeys();
cursors
对象有四个属性:up
,down
,left
,right
,它们都是Key
对象的实例。我们需要做的就是在update
函数中进行处理:
cursors = this.input.keyboard.createCursorKeys();
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);
}
当我们按下键盘<-
键时,玩家的运动速度设为-160,并播放left
动画,其它按键也进行类似处理。值得注意的是,我们在最后的if
语句中加入的player.body.touching.down
这个条件,若不加的话,玩家将可以上演“纵云梯”,我们不希望这样,所以加入了这个条件。详情请看Phaser.Physics.Arcade.Body。
6. 收集星星
是时候给我们的游戏定个小目标了!我们将在游戏场景中添加一些星星,并让玩家可以去收集它们。在create
函数中添加如下代码:
stars = this.physics.add.group({
key: 'star',
repeat: 11,
setXY: {x: 12, y: 0, stepX: 70}
});
stars.children.iterate(function(child){
child.setBounceY(Phaser.Math.FloatBetween(0.4,0.8));
});
与创建平台组类似,只不过这次我们创建的是动态物理组,而不是静态物理组。
代码将迭代组中的所有元素,并为他们提供介于0.4和0.8之间的随机Y反弹值。 反弹范围介于0(完全没有反弹)和1(完整反弹)之间。 所有的星星都是在y=0
处产生的,由于重力它们会掉落,直到它们与平台或地面碰撞。 反弹值表示它们将随机反弹,直到最终稳定下来。
但如果我们现在运行代码,星星会掉到游戏的底部而不见了。 为此,我们需要检查它们是否与平台发生碰撞。 我们可以用另一个Collider对象来做到这点:
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);
}
collectStar
函数让与玩家重叠了的星星不可见。
7. 计分
我们可能还希望让游戏计分,并在游戏中显示。
创建两个变量:
var score = 0;
var scoreText;
score
用来记录分数,scoreText
将是个Text
对象,用于在游戏中显示分数。在create
函数中创建Text
对象的一个实例,并将其赋值给scoreText
。
scoreText = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000' });
每次我们收集到星星,分数就会增加,于是我们可以将其代码加入到之前的collectStar
函数中。
score += 10;
scoreText.setText('Score: ' + score);
8. 炸弹
为了完善我们的游戏,是时候给游戏增加一点挑战了。想法是这样的:当我们收集完所有星星时,它将放出一颗炸弹,所有星星都会重现。 炸弹只会在水平方向随机反弹,玩家触碰到炸弹就会死。而我们可以再次收集星星,收集完它将释放另一枚炸弹。 所以,要在不死的情况下获得尽可能高的分数。
首先创建一个炸弹组和两个Collider
对象:
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;
}
然后,我们修改一下hitStar
函数,以便释放炸弹:
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.setCollideWorldBounds(true);
bomb.setVelocity(Phaser.Math.Between(-200,200), 20);
}
}
我们用了Group
的countActive
方法来查看还剩多少星星。如果为零,表示玩家已收集了所有星星,因此我们迭代地启用所有星星,并将其y坐标置为零。这将使所有星星再次从屏幕顶部掉落。
接下来的代码将创建炸弹。 首先,我们选择一个随机的x坐标,总是在玩家相对的另一侧。 然后创建炸弹,将其设置为与游戏边界碰撞,具有随机速度。
总结
本文我们学习了如何使用Phaser3
开发第一个游戏,主要包括加载资源、创建精灵、键盘控制、物体碰撞等等一些知识点,之后我们可以参考PHASER 3 EXAMPLES将这个简单实例丰富起来。