最近,在寻找一些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)
}
}
下一篇文章,我们来看看输入处理。