使用phaser3进行游戏开发系列二

Posted rysefgfzh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用phaser3进行游戏开发系列二相关的知识,希望对你有一定的参考价值。

写在前面

需要用到网页链接

先贴几个常用的链接:

phaser3官网地址

phaser-3.50.0下载地址

phaser3官方demo实例地址

phaser3官方文档地址

游戏用到的图片资源包
资源包提取码:oa2t

入门教程书籍
提取码:3l4s

这篇文章是接着上一篇文章继续的,这里附上上一篇文章的地址。
使用phaser3进行游戏开发系列一

其他废话不多说,我们继续上一篇的编码工作。

开始编码

创建游戏场景Game.js文件

在我们的 /src/scenes 目录下创建Game.js文件

import Phaser from '../lib/phaser.js'

export default class Game extends Phaser.Scene{
    constructor(){ // Game的构造函数
        super('game'); // 调用父类 Phaser.Scene的构造函数
    }
	// init(){} 
    preload(){} // 一般用于加载资源的函数
    create(){ // 创建游戏元素的函数
    	console.log('hello');
    } 
    update(){} // 更新游戏逻辑的函数,会在游戏主循环中一直调用
}

先说一下构造函数 constructor() ,这就是普通的构造函数,你想怎么构造都行,主要是 super() ,这里直接传递了一个字符串 ‘game’ ,这个字符串在 phaser 全局中,唯一标识了当前的场景,相当于它的身份证。每个场景都要有一个唯一的 key ,因为你切换场景时需要用到这个 key。而且 super 函数可以接受一个Object对象作为配置对象,有很多可以配置的选项,可以看看文档,自己改一改。

然后介绍一下四个函数。假如你的类实现了这四个方法,那么这四个方法的调用顺序是 init() --> preload() --> create() --> update()

init() 函数: 主要用来做一些初始化工作,它是最先被执行的。至于想初始化什么都随你…

preload() 函数: 主要用来做资源加载的,配合phaser的加载模块 scene.load 使用。如果是你自己写的加载方法,那么将无法保证资源何时加载完成。因为phaser是基于事件机制的,加载模块将资源加载完成,会发出 “加载完成” 事件,然后才会继续调用 create 函数。如果自己写,那么将无法利用这个事件机制,有可能在 create 函数创建元素时,资源还没加载完,导致出现bug。另外,你在 preload 进行的资源加载,会被自动加入加载队列,如果你在其他函数中加载资源,则需要手动添加到加载队列,这些信息都可以翻阅文档得到,提前在这里说一下我踩的坑…另外如果想把phaser3移植到小游戏平台,可能得自己写加载模块,因为小游戏无法使用 phaser 的 load 模块,它阉割了XHR对象。

create()函数: 主要用来创建游戏元素。你也可以做其他的逻辑,都随你…它是第三个被执行的函数。

update()函数: 游戏循环将会在每一帧 (一般16ms一次,可以调整) 调用这个方法,你可以在这个函数里进行游戏逻辑判断,更新游戏对象的状态,等等。

修改入口文件main.js

修改我们之前创建的入口文件,如下:

import Phaser from './lib/phaser.js'
import Game from '../src/scenes/Game.js' // 引入我们的Game场景文件
export default new Phaser.Game({
    type: Phaser.CANVAS,
    parent: 'game',
    width: 480, // 修改游戏区域宽度
    height: 640, // 修改游戏区域高度
    backgroundColor: '#FFFFFF',
    scene:[Game], // 使用我们引入的Game场景
    physics:{
        default:'arcade',
        arcade:{
            gravity:{
                x:0,
                y:200, // 修改竖直方向重力
            },
            debug:true,
        }
    }
});

这里只对改动的位置增加了注释。
第二行代码引入了我们上面创建的 Game.js 。
第六行、第七行修改了游戏区域的大小。
第十行代码修改了 scene 字段的值,改成了数组。这里介绍一下 scene 配置字段,这个字段的值可以是Object,可以是数组,可以翻阅一下文档,看看它的形式。当它的值是数组时,则只有第一个场景会被激活,除非其他场景显示声明需要被激活。
第15行代码修改了重力。
现在重新加载页面,应该还是正常显示一个白块,控制台输出 “hello” 字符串。假如刷新页面没有效果,可以试试清空缓存,强制刷新。后面更新游戏内容后没有出现预期的效果,有可能也是缓存的缘故。不过目前应该没什么问题。我们继续编码。

加载资源并显示

首先,在 bunny_jump 根目录创建 assets 文件夹,然后我们先把资源包解压,挑出我们需要的图片资源,放到 assets 文件夹内,资源包下载地址在上面。
文件夹结构调整后如下:

bunny_jump
	|--assets  存放图片和其他资源
		|--bg_layer1.png
		|--ground_grass.png
		|--carrot.png
		|--bunny1_stand.png
	|--src
	|--index.html
	|--jsconfig.json

下面使用的文件名,默认都已经放到了assets文件夹下,如果读取时遇到404,可以查看是否忘了添加图片了。
修改 Game.js 的 preload 和 create 函数,新增资源加载和资源显示代码。

	preload(){
        this.load.image('background','assets/bg_layer1.png'); // 第一个参数是资源的唯一标识,第二个参数是资源的加载地址
    }
    create(){
        this.add.image(240,320,'background'); // 第一个参数是横坐标,第二个参数是纵坐标,第三个参数是资源的唯一标识
    } 

这里使用 this.load.image ,这个是phaser的加载模块,可以查看文档的 Scene 下拉框 load 条目,有关于加载器的介绍。
this.add.image 用来创建一个图片,并添加到显示列表。这个方法的文档也在 Scene 下拉框,add 条目。这里多说一点,phaser3 默认图片的锚点在图片的中心,而不是左上角。锚点就是对象的中心点…可以自己查一下。
this.add.image 中的坐标,(240,320) 是游戏区域的中心点,这样相当于把图片的中心放在了游戏区域的中心,如果图片比较小,显示的效果就是图片居中。但是我们的这张背景图太大了,看不出来,先理解意思就可以了。
下面我们将会加快速度了,把游戏实现的代码先贴出来,然后慢慢解释。

整个游戏场景的代码

import Phaser from '../lib/phaser.js'
const bunnySpeedY = 300; // bunny 向上跳跃的速度
const bunnySpeedX = 5; // bunny 水平移动的速度

export default class Game extends Phaser.Scene {
    constructor() { // Game的构造函数
        super('game'); // 调用父类 Phaser.Scene的构造函数
    }
    init() {
        this.score = 0; // 分数变量
    }
    preload() {
    	// 加载图片资源
        this.load.image('background', 'assets/bg_layer1.png');
        this.load.image('platform', 'assets/ground_grass.png');
        this.load.image('bunny_stand', 'assets/bunny1_stand.png');
        this.load.image('carrot', 'assets/carrot.png');
    }
    create() {
        // 添加背景图
        this.add.image(240, 320, 'background').setScrollFactor(0);
        // 胡萝卜组
        this.carrots = this.physics.add.group();
        for (let j = 0; j < 5; j++) {
            let carrot = this.carrots.create(-50, 0, 'carrot').setScale(0.5); // 默认把创建的胡萝卜放在游戏区外
            carrot.visible = false; // 是否可见
            carrot.active = false; // 激活状态
        }
        // 跳台组
        this.platforms = this.physics.add.staticGroup(); // 添加一个静态组,存放跳台
        for (let i = 0; i < 5; i++) {
            const x = Phaser.Math.Between(80, 400); // x取值范围是 80 -400,闭区间
            const y = 150 * i; // y取值间隔是 150
            let platform = this.platforms.create(x, y, 'platform'); // 组内创建元素
            platform.scale = 0.5; // 对新建元素进行缩放
            platform.body.updateFromGameObject(); // 缩放后更新元素的物理检测边框
            let carrot = this.carrots.getFirst(false, false, x, y - 50) // 取一个胡萝卜放在跳台上面,-50是为了让胡萝卜在平台上面
            if (carrot) {
                carrot.visible = true;
                carrot.active = true;
            }
        }
        // 游戏的主角 bunny
        this.bunny = this.physics.add.sprite(240, 280, 'bunny_stand').setScale(0.5);
        this.bunny.body.checkCollision.up = false; // 是否检测 bunny 的上边界碰撞?
        this.bunny.body.checkCollision.left = false; // 是否检测 bunny 的左边界碰撞?
        this.bunny.body.checkCollision.right = false; // 是否检测 bunny 的右边界碰撞?
        this.bunny.body.checkCollision.down = true; // 是否检测 bunny 的下边界碰撞?
        // 显示分数
        this.scoreText = this.add.text(0, 0, `Score:${this.score}`, { color: '#0000FF', fontSize: '16px' });
        this.scoreText.setScrollFactor(0);
        // 快速创建键盘上的四个方向键
        this.cursor = this.input.keyboard.createCursorKeys();
        // 添加碰撞检测
        this.physics.add.collider(this.platforms, this.bunny); // bunny与跳台检测碰撞
        this.physics.add.collider(this.platforms, this.carrots); // 胡萝卜与跳台检测碰撞
        this.physics.add.overlap(this.bunny, this.carrots, this.collectCarrot.bind(this)); // 玩家与胡萝卜检测重叠
        // 设置相机
        this.cameras.main.startFollow(this.bunny);// 设置摄像机跟随我们的主角
        this.cameras.main.setDeadzone(this.scale.width * 1.5); // 设置摄像机静态区域 (可以只设置宽度 或者宽高一起设置),在此区域内,玩家移动摄像机将不跟随,只有超出这个边界,相机才会移动跟随
    }
    update() {
        if (this.cursor.left.isDown) { // 左方向键按下
            this.bunny.x -= bunnySpeedX;
            if (this.bunny.x <= this.bunny.displayWidth / 2) {
                this.bunny.x = this.bunny.displayWidth / 2;
            }
        }
        if (this.cursor.right.isDown) { // 右方向键按下
            this.bunny.x += bunnySpeedX;
            if (this.bunny.x >= 480 - this.bunny.displayWidth / 2) {
                this.bunny.x = 480 - this.bunny.displayWidth / 2;
            }
        }
        if (this.bunny.body.touching.down) {
            this.bunny.body.velocity.y = -bunnySpeedY;
        }
        this.platforms.children.iterate((platform) => {
            if (platform.y > this.cameras.main.scrollY + 640 + platform.displayHeight / 2) { // 这里的700是游戏区域的高度+跳台高度的一半,没有进行精确计算
                // this.platforms.killAndHide(platform); // 回收超出游戏区的跳台,这一步也可以不写,直接重新设置跳台的位置即可
                let x = Phaser.Math.Between(80, 400);
                let y = this.getHighestPlatform().y - 150;
                platform.setPosition(x, y);
                platform.body.updateFromGameObject();

                let carrot = this.carrots.getFirst(false, false, x, y - platform.displayHeight / 2 - 50);
                if (carrot) {
                    carrot.visible = true;
                    carrot.active = true;
                }
            }
        }, this);
        this.carrots.children.iterate((carrot) => {
            if (carrot.y > this.cameras.main.scrollY + 640 + carrot.displayHeight / 2) { // 游戏区域的高度+胡萝卜高度的一半
                this.carrots.killAndHide(carrot); // 回收超出游戏区的胡萝卜
            }
        }, this);

        if (this.bunny.y >= this.getLowestPlatform().y) {
            this.scene.start('over');
        }
    }
    getHighestPlatform() { // 获取游戏区中最上面的跳台
        let positionY = 640; // 从游戏区底部开始找
        let temp = null;
        this.platforms.children.iterate((platform) => { // 遍历全部的跳台
            if (platform.y < positionY) {
                positionY = platform.y;
                temp = platform;
            }
        });
        return temp;
    }
    getLowestPlatform() { // 获取游戏区中最下面的跳台
        let positionY = this.getHighestPlatform().y; // 从最高的跳台开始找
        let temp = null;
        this.platforms.children.iterate((platform) => { // 遍历全部的跳台
            if (platform.y >= positionY) {
                positionY = platform.y;
                temp = platform;
            }
        });
        return temp;
    }
    collectCarrot(player, carrot) {
        this.carrots.killAndHide(carrot);
        carrot.x = -50; // 把胡萝卜的body转移到屏幕外,防止一直检测到重叠
        this.score += 10;
        this.scoreText.setText(`Score:${this.score}`);
    }
}

这里对原书中的内容做了一些简化,所以与书上的代码有一点出入。一共131行代码,我们来深入了解一下。资源加载的部分就不说了。

第21行,我们添加一张背景图,最后调用了 setScrollFactor 函数,这个方法是为了让背景图不滚动。游戏中,我们能看到的画面,都是由 phaser 的相机组件进行渲染的,相机可以移动,来展示游戏区中不同位置的景象。如果不加限制,相机可以无限制移动,而我们的背景图是有限的,相机移动到背景图外,就会出现意料之外的空白画面。所以使用这个方法,把背景图固定在相机展现的区域内,这样不管怎么相机怎么移动,图片就像固定在可视区中一样。

第23-28行,30-42行使用 “物理系统” 创建了两个组。一个 group,一个 staticGroup。“组”相当于一个池子,是很多相同对象的集合,可以重复使用对象,减小开销。从组的名字可以看出来,一个是普通的,一个是静态的。这两个区别在于他们的 body 不同。body 是物理引擎进行检测时使用的对象轮廓。静态组的元素 body 是固定的,惰性的,而普通的 group内元素的body则可以根据当前设置进行自动变化。如果改变了静态组中的元素,则需要手动调用 updateFromGameObject() 函数进行更新。这两者的详细说明可以查一下文档。获取组内元素有多种方法,在上面的代码中,使用 getFirst 这个方法获取组内元素,这个方法的具体参数需要查阅文档,这里只说明37行用的参数,第一个 false 是指 active 状态,第二个 false 是指假如没有可用的元素,是否创建新的,第三和第四个则是设置的新坐标。每个元素都有一个布尔值 active 状态,这代表元素是否处于激活状态。由“物理系统”创建的组,创建组内元素时直接会给元素添加 body ,而像我们21行创建的背景图就不会有 body 属性。

第44-48行使用“物理系统”创建了我们的游戏主角 bunny ,然后设置了bunny的边界碰撞检测,phaser 的arcade 物理引擎支持只检测某一个边的碰撞。我们这里只检测底部,因为我们需要让 bunny 从跳台下面穿透到上面,如果上边界也检测,就会发生碰撞,无法穿透。

50-51行创建了一个文本对象,用来显示分数,同时也设置了文本不随相机移动,相当于固定在屏幕中。文本对象可配置的属性很多,翻阅文档查看。

53行使用 createCursorKeys 创建了一个检测键盘输入的对象,这是 phaser 为了开发方便内置的一个方法,这个对象会检测4个方向键、空格键和shift键。当然,你也可以自己分开来写,输入相关的文档都在Scene.input部分。

55-57行告诉“物理系统”对哪些对象添加碰撞检测或者重叠检测。其中 collider 是碰撞检测,overlap 是重叠检测。这两个方法的参数相同,只是collider 会让两个对象发生碰撞,overlap不会导致两个对象碰撞,他们可以互相穿过,但是会触发重叠检测。这两个函数支持检测的对象类型有多种,详细读一下文档吧。

59-60行对相机进行了一个简单的设置,主要是跟随 bunny,还有设置一个相机静态区,这个静态区我给出一张图比划一下,因为我觉得可能不太容易说明白。
在这里插入图片描述

“假如我们在60行同时设置了静态区宽高”,注意是假如,为上图绿色区域,那么蓝圈在绿色区域移动时,绿色框不会跟随蓝圈移动。但是实际上,实际上我们只设置了宽度,所以当蓝圈 “水平移动” 时,锚点不超过绿色框的宽度,绿框也不会水平移动。不过一旦蓝圈 “竖直” 移动,绿框将一直跟随,保持蓝圈的锚点位于绿框高度一半的水平线上。

63-74行代码,是检测是否按下了键盘的方向键,如果按下,则移动bunny。同时限制 bunny 不能移动出游戏区的左右边界。这里与原书中不同。原书中是检测bunny的位置,超出则重新设置,这里进行了简化。

75-77行,检测 bunny 的底部是否触碰到了其他对象,如果碰到了,则给 bunny 一个y方向的负的的速度,这个速度将会导致 bunny 向游戏区上方移动,因为游戏区左上角为坐标原点 (0 ,0)。这里有一点问题,就是当 bunny 碰到胡萝卜也会触发跳跃,这个问题怎么解决?其实只需要把这个 if 语句改成与跳台的碰撞检测就可以了,你来试试吧。

78-92行,遍历跳台组内的每一个跳台的y坐标,检测是否超出了游戏区,超过了说明已经看不到了,则进行回收,重复使用。这里注释的回收的代码,因为可以直接调整跳台的坐标,把不可见的跳台挪到上方。然后从胡萝卜组中取出一个胡萝卜放在跳台上。如果没取到胡萝卜,那么跳台上将不会有萝卜。

93到97行是胡萝卜的回收逻辑。这里有一个 killAndHide 函数,这个函数相当于快速修改 visible 和 active两个属性。你也可以自己手动写回收逻辑。

99-101行用来检测 bunny 是否已经掉到最下面的跳台之下了,这时已经无法起跳,所以会跳转到游戏结束场景。

103-124行,主要是获取最高的跳台和最低的跳台,方便修改跳台位置。这是自定义的函数。

125-130行,是 bunny 和胡萝卜重叠检测的回调函数。这里只说明一点,回调函数接受的参数顺序,与你传递的参数顺序一致。也就是与57行的参数顺序一致。可以打印 arguments 验证一下。
Game场景到此结束,我们来看看游戏结束场景怎么写。

游戏结束场景

在 /src/scenes 目录下新建 GameOver.js文件,整个工程结构调整如下,只列出新增的部分:

bunny_jump
	|--assets 
	|--src
		|--scenes
			|--GameOver.js
			|--Game.js
		...
	|--index.html
	|--jsconfig.json

贴出游戏结束场景的代码:

import Phaser from '../lib/phaser.js'
export default class Over extends Phaser.Scene {
    constructor() {
        super('over');
    }
    create() {
        this.add.image(240, 320, 'background'); // 添加背景图
        this.add.text(240, 300, 'GameOver', { color: '#FFFF00', fontSize: '20px' }).setOrigin(0.5, 0.5); // GameOver 文字提示
        let replay 以上是关于使用phaser3进行游戏开发系列二的主要内容,如果未能解决你的问题,请参考以下文章

使用phaser3进行游戏开发系列一

使用phaser3进行游戏开发系列一

使用Phaser3进行微信小游戏开发

使用 Phaser3+Matter.js 实现“合成大西瓜”游戏

phaser3开发微信小游戏样例

phaser3开发微信小游戏样例