Phaser3 ES6+入门教程

最近,在寻找一些HTML5的游戏引擎,发现一些比较知名的引擎还是原来那些,不过经过长久的时间发展,这些引擎也都更新了很多内容。
最初,还是比较喜欢cocos的,但是cocos3.x虽然依然支持JavaScript,但是编辑器中只能新建TypeScript了。所以,最终还是选择了Phaser。
在Github上,Phaser的Stars是cocos的3倍。然而,Phaser的中文资料少之又少,很多英文资料也没有使用ES6中的class,因此,我阅读了很多官方的、非官方的中英文的文档、问答等,最终决定写几篇基于Phaser3和ES6+的中文教程。
由于能力有限,文章中可能存在不足和错误,敬请指正。
本教程中的代码使用JavaScript(ES6+标准),为什么不使用TypeScript呢?因为不想用。虽然Phaser也支持TypeScript。

Phaser 简介

Phaser是一个基于MIT协议开源的HTML5、2D游戏开发框架,Phaser 3是其中的一个版本,支持JavaScript和TypeScript。
虽然没有官方的编辑器,但是有一些第三方的编辑器(免费或付费),当然你也可以直接写代码或自己做一个编辑器。本教程中,我们直接写代码。
Phaser的官方网站:https://phaser.io

环境准备

我们可以安装http-server或自己写或在electron中运行,都没什么问题,为了方便,这里就是用http-server了。
因此,需要安装NodeJS和http-server。
接着我们可以新建一个文件夹,npm初始化一下,然后安装phaser。

Phaser的安装

可以直接使用CDN引用js文件,也可以到官方网站下载下来,或者通过npm来安装。

npm install --save phaser

如果是通过npm安装的话,需要的文件在node_modules中的对应目录下的dist文件夹中,我们主要用到的是phaser.js和phaser.min.js,用一个就可以了,一般开发时,用前面那个,开发好后就用后面那个,因为前面那个只是打包好了,但没压缩,这样使用一些IDE开发时,有语法提示。

Phaser3的一些概念

在Phaser3中,所有的类都在Phaser命名空间中,对于一个游戏而言,它需要实例化一个Phaser.Game类或其子类。
我们当然可以直接new Phaser.Game(config)来实例化,这样也是可以的,但是毕竟我们想要用JS中的class语法糖,那么我们完全可以定义一个继承自Phaser.Game的类,然后实例化该类。
在实例化Phaser.Game类时,需要传入一个配置参数,如果我们写一个派生类,当然我们可以在实例化这个派生类对象时,传入配置参数,在派生类的构造方法中直接使用传入的参数,当然我们也可以不传入,而是直接写在派生类的构造方法中。虽然在一个网页中,可以有多个Phaser.Game类对象,但是一般就一个,所以,我想,你也是不会想在实例化的时候传入配置的,那么我们可以直接在派生类的构造方法中直接写想要的配置。
在只有一个场景的情况下,你也可以直接在Phaser.Game的派生类中进行处理,但是大多数情况是多场景的,不过即使只有一个场景,我们也可以写一个场景类并将其添加到Phaser.Game中。运行后,默认启动第一个被添加的场景。
图片(Image)、精灵(Sprite)、文本(Text)等可以被绘制在场景中的物体在Phaser3中被称为GameObject。
GameObject可以被添加到场景中。例如,Phaser.GameObjects.Sprite类就是精灵类。
在Phaser.Scene类中,会自动地运行preload()、create()和update()函数,但是Phaser.Game类和Phaser.GameObjects中的各种类不会,不过,我们可以在Phaser.Scene类中遍历添加到该场景中的所有GameObject,然后在上述三个方法中手动调用。
preload()函数用于预加载,例如图片等,只会运行一次,之后运行create()函数,该函数也只会运行一次,然后每帧运行一次update()函数。简而言之,update()函数会在每一帧运行一次。且update()函数有两个参数,第一个参数代表当前时间(后面我也没看懂具体是什么意思),第二个参数代表两帧之间的时间差(单位毫秒)。
默认情况下,Phaser的帧率是每秒60帧,即一秒会运行60次update()函数。

简单代码

1.首先准备好一个HTML文件,引入phaser.js或者phaser.min.js。接着就可以开始写代码了

class Game extends Phaser.Game{
constructor(){
super({type:Phaser.AUTO,width:window.innerWidth,height:window.innerHeight});
}
}
new Game(); // 运行

运行上述代码之后,phaser会自动地在DOM中,添加canvas元素。当然也可以在配置中,指定添加到哪个元素中。然后我们使用http-server,然后在浏览器中查看,就可以看到一个黑色的框框了,这就代表成功运行了,然后我们可以打开开发者工具看一看有没有什么报错。
在使用http-server时,建议加上-c-1选项关闭缓存。
先解释一下上面的代码,在Game类中,构造方法中,调用基类构造方法,传入参数,这个参数是一个基本的配置,Phaser.Game类需要有这个配置。
在这个配置中,type是渲染类型,可以指定使用canvas还是WebGL,Phaser.AUTO代表自动选择(即一般为WebGL,只有当不支持使用WebGL时,使用canvas),然后就是宽度和高度。
Phaser3中,也可以配置物理引擎,默认关闭物理,物理我们之后再说。
上面这个代码没有什么用,我们再新建一个场景类

class Scene extends Phaser.Scene{
constructor(){
super({active:true})
}
create(){
this.add.text(0,0,"hello,123");
}
}

接着,我们在Game类的构造方法中,添加以下代码

let s1=new Scene();
this.scene.add("S1",s1);

然后,运行,我们看到在黑色的背景上的左上角显示了文本。
首先,解释一下场景的添加,
在Game类中,scene的add()方法可以用于添加一个场景,第一个参数为用于识别场景的字符串(这被称为键),不同场景的键不能重复,第二个参数为实例化的Phaser.Scene或其派生类对象。
接着,我们来看Scene类,
在Scene类中,构造方法中,先调用了基类的构造方法,传入一个参数,在Phaser中,active是bool类型,代表是否启用当前的这个对象,启用就会显示出来且有各种效果,不启用就不会显示,也不会有任何的效果,它与visible是不同的,visible只是用来设置是否显示出来的。
Phaser.Scene.add属性是用于添加各种GameObject的,它的各种方法都是以类型来命名的,例如要添加图片,则是调用Phaser.Scene.add.image()方法。那么,添加文本就是使用Phaser.Scene.add.text()方法。这些方法中,前两个参数基本都是x坐标和y坐标,第三个参数开始就是字符串或者键或者其它参数,具体请见文档。
然后,我们来在Scene类中,添加图片吧,先在根目录下准备一张图片,假设相对路径是img.jpg
图片需要在使用前先预加载,因此,我们可以添加preload()方法,
里面写:

this.load.image("bg","img.jpg");

预加载都在load属性中,方法的命名同上,这个方法中,第一个参数给这个图片取得一个键名,第二个参数就是路径(可以是相对路径)。以后,要使用这张图片时,就要使用该图片的键,而不能使用路径。
然后,可以在Scene类中添加以下代码:

this.add.image(0,0,"bg");

然后,就会显示图片了。
在调用添加GameObject的函数后会返回新实例化的GameObject对象,因此我们引用它,然后来看一些效果。
我们把上述Scene类中的create()函数代码改为如下:

this.img=this.add.image(0,0,"bg");

然后新增一个update()函数:

update(){
this.img.x++
}

然后我们看到图片正在向右移动,对了,这个函数就是每帧调用一次的,而create()函数只会调用一次,preload()函数会在create()函数之前调用一次。

坐标系

在Phaser中,和大多数2D引擎一样,以画布左上角为原点,水平向左为x轴正方向,竖直向下为y轴正方向。
GameObject的位置(Position,即x和y)指的是该对象应该放在画布上的哪里,这是以上述坐标系进行描述的。
但是,我们需要一个坐标来描述这个位置指的是这个GameObject上的哪一个点。这个点被称为Origin,该坐标是由该对象的originX和originY属性指定的,Origin的坐标是以该对象(图片等)的左上角为(0,0)、右下角为(1,1)来进行描述的,默认情况下,对于文本是(0,0)即左上角,对于Image和Sprite而言,是(0.5,0.5)即图片的中心。
接着,我们来在场景中添加一个sprite,纹理是路径为img2.jpg的图片

preload(){
this.load.image("p","img2.jpg");
}
create(){
this.add.sprite(0,0,"p")
}

Phaser.Scene.add.sprite()方法的第一、二个参数是坐标,第三个参数是纹理图片的键。
虽然Phaser.Scene.add和Phaser.Scene.load属性的一些方法的名称是相同的,但它们是不一样的,例如我们添加sprite时,需要纹理,纹理是一张图片,因此需要使用Phaser.Scene.load.image()方法加载其所需纹理,同时也没有Phaser.Scene.load.sprite()方法,但是添加时要使用Phaser.Scene.add.sprite()方法,因为add中的image方法和sprite方法返回的是不同的类的对象,它们是不同的。
我们也可以写一个继承自Image、Sprite等GameObject的类,然后添加到场景中,在这些类中,我们依然可以使用在场景类中已经预加载的图片等资源的键,但是在场景类中添加自定义的GameObject类,及其实例化不能在Phaser.Scene类的构造函数中,一般在create()函数中,我们先写一个继承自Phaser.GameObjects.Text的类,这就是一个普通文本

class Hello extends Phaser.GameObjects.Text{
constructor(scene){
super(scene); // 无论怎样,scene这个参数是必须的,代表它会被添加到哪个场景中,当然,如果它不是一个变量,你也可以固定地写下来

this.setPosition(0,0); // 不设置坐标也可以,默认是(0,0)
this.setText("hello"); // 不设置文本也可以,但既然继承自文本类了,不设置文本就没有意义了
}
}

然后就可以在Scene类中添加了,如果只需添加一次,写在create()方法中就行了。

create(){
this.add.existing(new Hello(this));
}

Phaser.Scene.add.exsiting()方法用来添加自定义类对象,后面的this就代表当前Scene类对象,如果不想写呢,我们就要修改Hello类中的构造方法的super()方法了。这样的话,只有在暴露Game类对象的情况下才可以使用,假设Game类的对象叫做game,则可以改为如下:

super(game.scene.keys["S"]);

这个方括号中的S,就是我们刚才添加场景时,给它取的一个名字。不过,为了更好的扩展性,还是建议用this。
为了更好地结构化,我们可以在场景类中,通过children的list属性获得所有在场景中的GameObject对象,然后就可以场景类的一些方法中遍历,调用各个GameObject类的对应方法了。这样,我们就可以把具体的GameObject的逻辑写在各自的类中了。
比如我们可以把Scene类的update()方法改成如下:

update(t,d){
for(let i=0;i<this.children.list.length;i++){
this.children.list[i].update(t,d);
}
}

基本形状

Phaser中,也提供了一些基本形状,当然和文本、图片等一样也可以直接添加,也可以自定义派生类。在本教程的后面文章中,大多数情况下,都写自定义派生类,不直接使用,可以直接在Phaser.Scene类中使用add.existing()方法添加,以后不再赘述。
Phaser提供的基本形状类有很多,这里写一些,其它的请看文档。
一个Phaser.Game类对象的默认画布背景是黑色的,文本默认是白色的,有些基本形状默认是黑色的,所以需要设置基本形状的颜色才能看得出来

// 矩形
class Shape1 extends Phaser.GameObjects.Rectangle{
    constructor(s){
        super(s);

        console.log("Shape1")
        this.setPosition(100,100);
        this.setSize(50,50);

        this.setFillStyle(0xfffffff); // 设置填充颜色
        this.setStrokeStyle(5,0x00ff00); // 设置轮廓,当然也可以没有轮廓
    }
}
// 柱体
class Shape2 extends Phaser.GameObjects.IsoBox{
    constructor(s){
        super(s);

        this.setPosition(300,300)
        this.setSize(130,150)
    }
}
// 三棱柱
class Shape3 extends Phaser.GameObjects.IsoTriangle{
    constructor(s){
        super(s);
        this.setPosition(500,500);
        this.setSize(300,300);
    }
}
// 直线
class Shape4 extends Phaser.GameObjects.Line{
    constructor(s){
        super(s,0,0,100,100,200,200,0x0000ff);
        this.setLineWidth(5)
    }
}
// 三角形
class Shape5 extends Phaser.GameObjects.Triangle{
    constructor(s){
        super(s,30,30,100,100,50,150,150,150,0x00ff00);
    }
}
// 多边形
class Shape6 extends Phaser.GameObjects.Polygon{
    constructor(s){
        super(s,0,0,[100,200,300,200,50,50],0xffff00)
    }
}

下一篇文章,我们来看看输入处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容