Phaser.js开发小游戏之洋葱头摘星星

今天我们讲一下官方提供的一个经典案例:洋葱头摘星星。游戏内容是很精彩。

因为宇宙大爆炸,天上的经常会有一些星星掉到地球上,洋葱头为了收集星星中的能量,就会很勤奋的收集落到地球上的每一棵星星。但是也需要注意安全,因为从天上掉下除了星星,还有发射性物质,如果不小心收集到了放射性物质,就会有生命危险。

OK,现在开始制作游戏。

制作游戏的过程其实和我们平时做网站一样的:

  1. 了解需求
  2. 设计师设计页面
  3. 切图布局
  4. 游戏功能制作

需求我们已经了解了,设计师我们暂时没有,也暂时不用切图,先用官网提供的素材资源,也就是游戏中的图片,素材源码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

widthheight 就是告诉游戏世界的面积有多大,我们这里是 800px * 600px。也就是地球的大小。

scene

scene 是开启游戏场景,也就是我们需要在世界中创建游戏的生态。可以看成是地球的生态圈物质:空气、水、动物、植物....

scene.preload

scene.preload 是预加载资源的函数,在这里面先把游戏资源加载好,以备后期使用

scene.create

scene.create 主要用来创建场景,将资源加载进去,并且处理游戏中的逻辑,比如物体碰撞之后的处理之类的

scene.update

scene.update 每一帧的时候就会执行,有两个参数:time,delta,time 是执行了多长时间,单位是 msdelta 是间隔时间,默认是 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 移动背景图一样。

frameWidthframeHeight 就是展示的宽高

preload 函数就是预加载了游戏中的各种资源,并且对每个资源设置了别名,方便后面引用,同时设定精灵元素,方便后面动态切换图片位移。

做完这些工作后,页面上还是啥都没有,要想页面中有东西,就需要在 create 函数中将资源添加到场景中

添加元素到场景

添加背景图
function create(){
    this.add.image(400,300,"sky");
}

this.add 返回的是 Phaser.GameObjects.GameObjectFactory 对象。

this.add.image 创建一个游戏图片对象,并添加到场景中。它有四个参数,我们这里主要就用到前三个,第一个参数是 x 轴,第二个参数是 y 轴,第三个参数就是在 preload 中预加载的资源别名。

在这里我们要重点注意的是 xy 轴的方式,在 Phaser3 中,资源的坐标不是从左上角定位的,而是从元素的中心开始定位的

因此我们代码中设置的 400 和 300,就是 800/2600/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 主要有两个参数:xy。如果没有设置 yy 就会用 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.animsPhaser.Animations.AnimationManager 实例。 Phaser.Animations.AnimationManager它是一个全局的对象,它主要用来创建动画以及配置动画,它可以将动画直接绑定全局的 Game 对象上,可以在游戏中的任意 Scene 中用到这个动画。

this.anims.create 它会创建一个动画并将它添加到动画管理对象上。创建完成后,就可以到任意 Scene 中调用这个动画了。如果创建动画的名称已经存在了,它就会返回 false,如果不存在就会返回一个 Phaser.Animations.Animation 实例。在 this.anims.create 中需要传入动画的配置对象 config,用来设置不同的动画规则。

在这里我们创建了三个动画:leftturnright,在三个动画中,我们都传入了不同的配置:

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 的图片。

其实和 CSSbackground-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.inputPhaser.Input.InputPlugin 实例

this.input.keyboardPhaser.Input.Keyboard.KeyboardPlugin 实例

this.input.keyboard.createCursorKeys 返回一个 Phaser.Types.Input.Keyboard.CursorKeys 实例,这个实例里面包含了 updownleftrightspaceshift 6个键。

同时上面 6 个键属于 Phaser.Input.Keyboard.Key 实例,有一个 isDown 属性,用来判断是否被按下了。

首先我们代码里面做了一个逻辑判断,如果没有按左右键就会将水平速度设为 0,并执行 turn 动画:

player.setVelocityX(0);
player.anims.play('turn');

对于按了左右键要执行的事情,也就很简单了,left 就是将水平速度设为 -160right160

按上键的时候,就会将垂直速度改为 -330330 像素每秒,由于设置了重力,他就会自动落回地面。

再按上键的时候,我们还做了一个边缘监测,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.Types.GameObjects.Group.GroupCreateConfig

key

key 引用的游戏资源别名

repeat

repeat 表示重复使用多少次 key 资源,从 0 开始,我们设置 11,就是生成 12 颗星星

setXY

setXY 是用来设置资源在游戏中的位置

setXY.x

资源的水平位置

setXY.y

资源的垂直位置

setXY.stepX

每个资源在水平位置从 0 开始递增的值。例如我们代码里面设置的是 70,那么第一颗星星 x0,第二颗 x70,第三颗 x140,以此类推

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 个参数:

  1. 前面两个是覆盖的游戏元素对象
  2. 第三个是覆盖的回调函数
  3. 第四个也是覆盖的回调函数,但是必须返回一个 boolean 值。
  4. 第五个是函数执行的作用域对象指向

然后我们在覆盖的回调函数中将星星隐藏了。

最后的效果就是下面的图:

加上得分数据

现在我们加上一个得分的文字显示,每次获取一个星星就将得分 +10

create 函数中加上下面的代码:

scoreText = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000' });

collectStar 函数中添加下面的代码:

score += 10;
scoreText.setText('Score: ' + score);

scorescoreText 添加到全局代码中。

this.add.text 是创建一个文字对象并添加到当前的场景中。返回一个 Phaser.GameObjects.Text 实例,将实例赋值给 scoreText

它传入 4 个参数:

  1. 文字 x 坐标
  2. 文字 y 坐标
  3. 文字内容
  4. 文字的样式配置

然后在覆盖监听回调中,做了两个事情:

  1. score 加 10,也就是每次碰到一个星星就加 10
  2. 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,让它一直弹,如果不弹那还有什么难度。

然后设置边界碰撞,不让炸弹掉出去了。

再设置炸弹在 xy 轴上的速度

我们在两个地方创建炸弹,一个在 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包

这篇文章写了三天左右,内容有点多,大家可以收藏后分开看。

posted on 2020-11-24 17:02  松鼠闹IT  阅读(719)  评论(0编辑  收藏  举报

导航