Phaser.js开发小游戏之洋葱头摘星星
今天我们讲一下官方提供的一个经典案例:洋葱头摘星星。游戏内容是很精彩。
因为宇宙大爆炸,天上的经常会有一些星星掉到地球上,洋葱头为了收集星星中的能量,就会很勤奋的收集落到地球上的每一棵星星。但是也需要注意安全,因为从天上掉下除了星星,还有发射性物质,如果不小心收集到了放射性物质,就会有生命危险。
OK,现在开始制作游戏。
制作游戏的过程其实和我们平时做网站一样的:
- 了解需求
- 设计师设计页面
- 切图布局
- 游戏功能制作
需求我们已经了解了,设计师我们暂时没有,也暂时不用切图,先用官网提供的素材资源,也就是游戏中的图片,素材源码zip包。
注意:在开发 Phaser.js 小游戏项目的时候,一定要注意一点的就是要开启一个服务器来运行游戏,因为 Phaser.js 内部有用到 ajax 请求本地资源,如果不开始本地服务器就会加载不到静态资源
引入资源
拿到素材后,把里面的 assets
放到项目根目录下面,然后新建一个 index.html
,在 html
文件中引入 phaser.js
文件
<script src="../../lib/phaser.min.js"></script>
第一步:创建游戏主程序
let game = new Phaser.Game({
type:Phaser.AUTO,
width: 800,
height: 600,
scene: {
preload: preload,
create: create,
update: update
},
physics:{
default:"arcade",
arcade:{
gravity:{
y:300
},
debug:true
}
}
})
//预加载资源
function preload (){}
//创建场景
function create (){}
//实时监听每帧更新
function update (){}
首先我们用 Phaser.Game
创造出我们的游戏世界,并将游戏中的配置传入到游戏世界中。例如地球是我们世界
type
type
是告诉游戏世界是用 canvas
还是用 WebGL
渲染,用 Phaser.AUTO
它将自动尝试使用WebGL,如果浏览器或设备不支持,它将回退为Canvas。可以看成我们地球是什么物质创建的。
width、height
width
和 height
就是告诉游戏世界的面积有多大,我们这里是 800px * 600px
。也就是地球的大小。
scene
scene
是开启游戏场景,也就是我们需要在世界中创建游戏的生态。可以看成是地球的生态圈物质:空气、水、动物、植物....
scene.preload
scene.preload
是预加载资源的函数,在这里面先把游戏资源加载好,以备后期使用
scene.create
scene.create
主要用来创建场景,将资源加载进去,并且处理游戏中的逻辑,比如物体碰撞之后的处理之类的
scene.update
scene.update
每一帧的时候就会执行,有两个参数:time,delta
,time
是执行了多长时间,单位是 ms
,delta
是间隔时间,默认是 16ms,也就说是每个 16ms 就会执行一次 update
。
physics
physics
开启游戏中的物理引擎,就像我们地球上的重力、摩擦力...各种物理元素
physics.default
一定要加上这个属性,不然物理环境不会生效。这个属性的作用就是配置采用什么样的物理引擎。有三个配置:'arcade'
、 'impact'
、 'matter'
。三个物理引擎区别后面再细说。一般我们都用的是 'arcade'
physics.arcade
arcade
物理引擎配置
physics.arcade.gravity
设置重力加速度,px/s 为单位
physics.arcade.gravity.y
设置 y
轴上的重力加速度
physics.arcade.gravity.debug
是否开启调试模式,如果开启了,就会给每个元素加上边框,还有移动的方向。
预加载资源
function preload(){
this.load.setBaseURL("./assets/");
this.load.image("sky","sky.png");
this.load.image("ground","platform.png");
this.load.image("star","star.png");
this.load.image("bomb","bomb.png");
this.load.spritesheet("dude","dude.png",{
frameWidth:32,
frameHeight:48
})
}
在这里 this
指向的是 Phaser.Scene
对象,this.load
返回一个 Phaser.Loader.LoaderPlugin
对象,然后用 setBaseURL
设置静态资源的基本路径。
image
方法两个参数:资源别名、资源路径。例如:sky
就是这个资源的快捷名称,sky.png
就是资源路径。这个方法就是用来预加载对应的图片,这个图片是静态的,就是不会动的那种。
spritesheet
方法他们称为是精灵表单,就和我们 CSS sprite
差不多,将一些图片合并放到一张大图上,但是区别在于每一张图片的尺寸大小是一样的。一般这个方法用到的地方就是需要经常变动的元素,比如一个人走路的不同状态,就会将不同的状态放到一张图上,并且根据帧的变动移动图片的位置,就像 CSS 中 backgroun-position
移动背景图一样。
frameWidth
和 frameHeight
就是展示的宽高
preload
函数就是预加载了游戏中的各种资源,并且对每个资源设置了别名,方便后面引用,同时设定精灵元素,方便后面动态切换图片位移。
做完这些工作后,页面上还是啥都没有,要想页面中有东西,就需要在 create
函数中将资源添加到场景中
添加元素到场景
添加背景图
function create(){
this.add.image(400,300,"sky");
}
this.add
返回的是 Phaser.GameObjects.GameObjectFactory
对象。
this.add.image
创建一个游戏图片对象,并添加到场景中。它有四个参数,我们这里主要就用到前三个,第一个参数是 x 轴,第二个参数是 y 轴,第三个参数就是在 preload
中预加载的资源别名。
在这里我们要重点注意的是 x
和 y
轴的方式,在 Phaser3
中,资源的坐标不是从左上角定位的,而是从元素的中心开始定位的。
因此我们代码中设置的 400 和 300
,就是 800/2
和 600/2
,也就是游戏世界的中心坐标。
当宽高和游戏世界一样,中心坐标也是游戏世界一样,那么四个角的坐标也就和游戏世界对齐了。
当然我们也可以改变原点坐标,this.add.image(0, 0, 'sky').setOrigin(0, 0)
,这样我们就可以从左上角对齐了。
添加背景图之后游戏世界就变成下面这样了:
添加平台
背景图添加完了之后,我们就要添加几个平台,添加一点游戏难度。
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
,使用这个属性必须在配置中添加 physics
,不然就会报错。
this.physics
返回 Phaser.Physics.Arcade. ArcadePhysics
对象。
this.physics.add
返回 Phaser.Physics.Arcade.Factory
对象。
this.physics.add.staticGroup()
主要作用是创建一个静态物理组,并返回一个 Phaser.Physics.Arcade.StaticGroup
。我们在这里使用这个方式的原因将同一种静态物体组织到一起,只要控制全体就像控制个体一样。最后我们将返回的 StaticGroup
对象赋值给 platforms
。
platforms.create
主要就是创建一个游戏对象并添加到静态物理组中。它原本有 6 个参数,我们这里用到了三个。
前三个参数的作用和上面的 this.add.image
一样。
platforms.create
返回的是一个 Game Object
,一般返回的是 Phaser.Physics.Arcade.Sprite
对象。
我们在代码中先是添加了一个平台对象,并用 setScale
将平台放大 2 倍。setScale
主要有两个参数:x
和 y
。如果没有设置 y
,y
就会用 x
值。setScale(2)
相当于 setScale(2,2)
。setScale
返回的是一个 Phaser.Physics.Arcade.Sprite
对象。
当我们放大平台后,需要用 refreshBody
方法将游戏场景刷新一下。
我们一共添加了 4 个平台,添加完之后的样子是这样的:
添加英雄
好了,地球已经被我们创建完成了,现在我们要添加英雄了,让他在游戏中诞生。
我们需要在 create
函数中添加下面的代码:
let 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
});
this.physics.add.sprite
我们将英雄添加到游戏场景中。返回的是 Phaser.Physics.Arcade.Sprite
实例
然后用 setBounce
设置英雄的弹跳能力,如果是 0
就不会跳起来,如果是 1
就会一直跳来跳去。我们在这里设置的是 0.2
。
setCollideWorldBounds(true)
这个方法的作用就是让英雄是否与世界边界碰撞,也就是我们平常写拖拽的时候,是否会超过浏览器的边界。如果为 false
那么英雄就会掉下去看不到了。我们这里设置的是 true
,英雄不会掉到世界外面。
OK,这个时候我们就可以看到英雄在游戏中了
下面我们开始设置英雄的动作动画了。这些动画操作其实和预加载一样,是我们先声明在 create
方法中,当我们要使用的时候才会用里面的 key
值。
this.anims
是 Phaser.Animations.AnimationManager
实例。 Phaser.Animations.AnimationManager
它是一个全局的对象,它主要用来创建动画以及配置动画,它可以将动画直接绑定全局的 Game
对象上,可以在游戏中的任意 Scene
中用到这个动画。
this.anims.create
它会创建一个动画并将它添加到动画管理对象上。创建完成后,就可以到任意 Scene
中调用这个动画了。如果创建动画的名称已经存在了,它就会返回 false
,如果不存在就会返回一个 Phaser.Animations.Animation
实例。在 this.anims.create
中需要传入动画的配置对象 config
,用来设置不同的动画规则。
在这里我们创建了三个动画:left
、turn
、right
,在三个动画中,我们都传入了不同的配置:
key
key
的作用是动画的名称
frames
frames
的作用是告诉动画使用精灵图片中的哪一个索引位置上的图片,上面我们说了精灵图片是将每一帧的图片放到一张大图上,而这个配置的作用就是让动画使用精灵图片中的哪一帧图片了。
frames
的值是一个数组,数组中的每一项都是一个 Phaser.Types.Animations.AnimationFrame
对象
例如:left
中传入的是 this.anims.generateFrameNumbers('dude', { start: 0, end: 3 })
, this.anims.generateFrameNumbers
返回的就是一个 Phaser.Types.Animations.AnimationFrame
数组对象。它第一个参数接受的是精灵图的 key
名,第二个参数是一个 Phaser.Types.Animations.GenerateFrameNames
配置。告诉动画选择精灵图片中的哪些位置的图片,像 left
中选择的就是 0-3
索引上的图片,right
中传入的是 5-8
索引上图片
turn
里面传入的是一个 Phaser.Types.Animations.AnimationFrame
数组,frame:4
就是说用的是精灵图中索引为 4
的图片。
其实和 CSS
中 background-position
差不多,只是 background-position
需要我们自己计算,这里用 frames
自动帮我们选择了
frameRate
frameRate
的作用是帧率,也就是每秒钟播放多少张图片,例如 left
里面就是每秒播放 10
张图片,就是不停的切换 0-3
索引上的图片
repeat
repeat
重复的次数,-1
表示无限重复
让英雄站到平台上
现在英雄还不能站到平台上,我们要再 create
中加一个碰撞监测,让英雄站到平台上:
this.physics.add.collider(player, platforms);
键盘控制
一般我们游戏都是用键盘在操作,大部分都是上下左右,我们这游戏也是一样,用键盘上面的上下左右键来控制。
但是这个时候有个问题,就是上下左右键的事件逻辑写在哪里呢?这里和我们浏览器里面的监听有点不听,一般浏览器里面的监听是监听 window
下的按的是哪一个键,然后根据按的键来做对应的操作。当然,这个逻辑也一样,也是根据按的键来做对应的操作,只是并不是监听 window
下的事件,而是在 update
回调中监听你按的是哪个键,也就是每一帧更新的时候就会判断你按了哪一个键。
OK,我们了解了这个方式之后,我们就在 update
回调中开始写对应的逻辑
function update(time,detla){
let 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);
}
}
这里的逻辑很简单:
this.input
是 Phaser.Input.InputPlugin
实例
this.input.keyboard
是 Phaser.Input.Keyboard.KeyboardPlugin
实例
this.input.keyboard.createCursorKeys
返回一个 Phaser.Types.Input.Keyboard.CursorKeys
实例,这个实例里面包含了 up
、down
、left
、right
、space
、shift
6个键。
同时上面 6 个键属于 Phaser.Input.Keyboard.Key
实例,有一个 isDown
属性,用来判断是否被按下了。
首先我们代码里面做了一个逻辑判断,如果没有按左右键就会将水平速度设为 0
,并执行 turn
动画:
player.setVelocityX(0);
player.anims.play('turn');
对于按了左右键要执行的事情,也就很简单了,left
就是将水平速度设为 -160
,right
是 160
按上键的时候,就会将垂直速度改为 -330
,330
像素每秒,由于设置了重力,他就会自动落回地面。
再按上键的时候,我们还做了一个边缘监测,player.body.touching.down
这个是判断英雄是否和地面接触了,否则他就会一直往上跳的。
加上 player.body.touching.down
后,就正常了,现在游戏画面就是这样的了:
让星星落下来
我们现在要做的一个事就是让星星从天上掉下来了。
我们先要计划要在我们游戏中放几颗星星,在我们例子中暂时定 12
颗,颗数无所谓了,你们可以自己定。
既然有 12
颗星星,那么我们肯定不可能一颗一颗的去生成以及管理。所以我们就要用 组 来管理。上面的平台用的是 静态组,因为星星要动,所以就用 组。
在 create
函数中添加下面的代码:
let 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));
});l
this.physics.add.group
创建一个 物理组 对象,可以传入两个参数:
- 要添加到组里面游戏对象,这个参数可以传入三种配置对象
- 数组,数组里面可以是 Phaser.GameObjects.GameObject,如果传入的数组,就会添加游戏对象到物理组里面
- Phaser.Types.Physics.Arcade.PhysicsGroupConfig
- Phaser.Types.GameObjects.Group.GroupCreateConfig
- 物理组的配置,这个参数可选两种配置对象:
在这里我们传入的是 Phaser.Types.GameObjects.Group.GroupCreateConfig:
key
key
引用的游戏资源别名
repeat
repeat
表示重复使用多少次 key
资源,从 0
开始,我们设置 11
,就是生成 12
颗星星
setXY
setXY
是用来设置资源在游戏中的位置
setXY.x
资源的水平位置
setXY.y
资源的垂直位置
setXY.stepX
每个资源在水平位置从 0 开始递增的值。例如我们代码里面设置的是 70
,那么第一颗星星 x
为 0
,第二颗 x
为 70
,第三颗 x
为 140
,以此类推
this.physics.add.group
返回的是 Phaser.Physics.Arcade.Group
对象实例,然后将实例赋值给 stars
。
stars.children
是 物理组中的 成员,它是一个 Phaser.Structs.Set
对象实例,其实可以看成是一个 Set
类型的数据。
stars.children.iterate
就是遍历 Phaser.Structs.Set
,在这里就是对每一颗星星进行遍历处理。
child.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
就是设置每一颗星星的垂直弹力值。
这样我们游戏就会是下面这样的画面:
将星星放到平台上
上面的图片好像有点奇怪,居然直接落下去了,本来我们打算让星星放到平台上的。
既然如此,我们只需要让星星和平台产生碰撞就好了:
this.physics.add.collider(stars, platforms);
游戏就成为下面这样的画面了:
摘星星
总算到了最关键一步了。
要摘星星,其实我们可以看成是英雄和星星碰撞的时候让星星隐藏。在 Phaser3
中物体碰撞除了有碰撞检测事件 collider
之外,还有物体覆盖事件 overlap
,就像下图一样。
我们在 create
中添加覆盖检测代码:
this.physics.add.overlap(player, stars, collectStar, null, this);
function collectStar (player, star){
star.disableBody(true, true);
}
this.physics.add.overlap
用来创建一个碰撞覆盖对象,可以监听到两个物体是否发生了碰撞覆盖。它会返回一个 Phaser.Physics.Arcade.Collider
对象实例。
它可以传入 5 个参数:
- 前面两个是覆盖的游戏元素对象
- 第三个是覆盖的回调函数
- 第四个也是覆盖的回调函数,但是必须返回一个
boolean
值。 - 第五个是函数执行的作用域对象指向
然后我们在覆盖的回调函数中将星星隐藏了。
最后的效果就是下面的图:
加上得分数据
现在我们加上一个得分的文字显示,每次获取一个星星就将得分 +10
在 create
函数中加上下面的代码:
scoreText = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000' });
在 collectStar
函数中添加下面的代码:
score += 10;
scoreText.setText('Score: ' + score);
score
和 scoreText
添加到全局代码中。
this.add.text
是创建一个文字对象并添加到当前的场景中。返回一个 Phaser.GameObjects.Text 实例,将实例赋值给 scoreText
。
它传入 4 个参数:
- 文字 x 坐标
- 文字 y 坐标
- 文字内容
- 文字的样式配置
然后在覆盖监听回调中,做了两个事情:
- 给
score
加 10,也就是每次碰到一个星星就加 10 - 用
setText
设置scoreText
的文字内容,将最新的score
显示上去。
最后游戏就会成为下面的样子,在左上角显示我们的星星:
加上炸弹
现在我们加个炸弹,让游戏添加点难度,增加游戏可玩性。
首先我们在 create
中添加一个 炸弹物理组
bombs = this.physics.add.group();
在全局设置 bombs
变量
然后我们添加一个创建炸弹的方法
function createBomb(bombs){
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);
}
传入刚才添加的炸弹物理组 bombs
。
我们先设置一下炸弹的 x
轴位置,判断英雄的 x
位置是否小于 400
,这个 400
就是游戏宽度的一半,就是判断英雄在左边还是右边,如果小于 400
就是在左边,如果在左边,炸弹就从 400-800
也就是右边落下,如果英雄在左边就从右边落下。
再然后就创建一个炸弹元素,y
轴设为 16
,引用 bomb
资源
将炸弹的弹力值设为 1
,让它一直弹,如果不弹那还有什么难度。
然后设置边界碰撞,不让炸弹掉出去了。
再设置炸弹在 x
和 y
轴上的速度
我们在两个地方创建炸弹,一个在 create
中创建,也就是刚进游戏的时候就创建炸弹。还有一个就是当星星都获取完了之后,再创建一次。
当星星都获取完的代码,我们需要修改一下 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);
});
createBomb(bombs)
}
}
stars.countActive(true) === 0
这个就是判断星星激活的数量是否为 0
,如果为 0
还需要将星星用 enableBody
重新添加到场景中。然后创建炸弹。
OK,现在创建炸弹完成,我们要开始处理英雄碰撞炸弹的情况
this.physics.add.collider(player, bombs, hitBomb, null, this);
function hitBomb (player, bomb){
this.physics.pause();
player.setTint(0xff0000);
player.anims.play('turn');
gameOver = true;
}
首先我们创建一个英雄和炸弹的碰撞检测
然后再碰撞回调中处理逻辑。
先让物理环境暂停,再将英雄变成红色,然后运行 turn
动画,最后将 gameOver
变量改成 true
。
这样我们游戏就处理完了。
现在游戏就是这样了:
最后,如果要下载游戏,可以下载上面的素材源码zip包。
这篇文章写了三天左右,内容有点多,大家可以收藏后分开看。