Phaser3初体验
译者说:
1、本文讲述的是作者初次使用Phaser3(之前都用Phaser2)制作游戏的一些心得和碰到的坑。
2、为了跟代码对应,有些单词不译成中文(比如scene、image)。
3、初次翻译教程,加上水平所限,疏漏之处欢迎指教。
5、译者:大吃货,转载请注明。
6、Phaser2还没学会就出了Phaser3…不带这么坑爹的。
===========================================================================
Part1:
项目介绍这个项目故意做得比较小,所以我不会在学习过程中讲得过于详细。我将它设定为一个休闲益智游戏,可通过触屏输入(或单击)。这是一个很多人从小就知道的纸牌游戏----翻牌记忆大作战。游戏的名字就叫Pixel Memory吧。
概述:挑战Phaser3
本文的重点是向大家展示我使用Phaser3的过程,并分享我的一部分代码。在我使用Phaser3的第一天,我遇到了以下的挑战,我将在下文一一阐述。
1、Phaser 3 游戏设置
2、设置全局变量
3、添加scene
4、游戏的闭包(原文enclosing,个人觉得“闭包”比“打包”更适合)
5、包含场景和预设的独立文件
6、Canvas的居中和尺寸调整
7、全屏化
8、设置Bitmap Text 的原点
1、Phaser 3 游戏设置
第一个挑战是:我该怎样用一种首选的配置来启动游戏?
在Phaser3中用一个对象作为游戏的构造器,这个对象包含了游戏的各种配置属性。
这些属性是很重要的,从官网的例子就可以看出来。
var config = {
// ...
};
var game = new Phaser.Game(config);
你会发现很多的新属性,它们非常酷!其中之一就是游戏的name。parent属性就是包含了游戏canvas的div的ID。
下面就是我的游戏设置对象
var config = {
type : Phaser.AUTO,
width : 9 * 64, // 576
height : 15 * 64, // 960
parent : 'phaser-app',
scene : scenes,
title : 'PixelMemory'
};
var game = new Phaser.Game(config);
2、设置全局变量
在Phaser2中我常做的一件事就是设置一个可以从每个scene调用的全局变量(在Phaser3中state变成scene)。如果我需要从许多场景中访问一个属性,我选择设置全局变量,而不是在场景之间传递它们。此外,在场景之间传值目前还存在bug。见github。
下面看看我是怎样设置全局变量的:
game._URL = 'http://localhost/PhaserGames/PixelMemory/'
game._USER_ID = 0;
稍后在任意场景中调用这个变量的方法:
var url = this.sys.game._URL;
var u_id = this.sys.game._USER_ID;
3、添加scene
在Phaser3中,我们把scene添加到一个数组中,再把这个数组添加到config对象的scene属性中。虽然我喜欢这种用数组传递的方式,但最开始我还是有点困惑:scene的启动顺序是否跟它在数组中的顺序有关呢?
此外,如果你添加了很多的scene,这种方式的可读性就会变差。为了提高代码的可读性,我通过这种方式添加scene:
var scenes = [];
scenes.push(BootScene);
scenes.push(PreloadScene);
scenes.push(IntroScene);
var config = {
// ...
scene: scenes,
// ...
};
除了你添加的第一个scene,其他的scene顺序无关紧要。默认情况下,游戏将自动从数组的第一个scene启动。请注意你添加的第一个scene。当然,你也可以手动设定为从任意的scene启动游戏。
4、游戏的闭包
浏览器端游戏跟原生应用不同之处在于,前者的代码很容易在浏览器上暴露出来。我喜欢尽可能的把函数闭包,这样任何人想要在游戏运行时修改游戏就变得不那么容易(破解、修改)。基于以上原因,我一直喜欢把游戏进行闭包。我不知道这能多大程度保护游戏,但聊胜于无。我相信,你做出的这些努力,会让玩家感受到你对你的游戏有多在乎。(我总是想看看自己能否轻易地破解H5游戏,像我这样的人肯定不少)
在Phaser2中,我用这种简单的方式实现游戏闭包:
var App = function() {};
App.prototype.start = function()
{
// Scenes
var scenes = [];
scenes.push(BootScene);
scenes.push(PreloadScene);
scenes.push(IntroScene);
// Game config
var config = {
type : Phaser.AUTO,
width : 9 * 64, // 576
height : 15 * 64, // 960
parent : 'phaser-app',
scene : scenes,
title : 'PixelMemory'
};
// Create game app
var game = new Phaser.Game(config);
// Globals
game._URL = 'http://localhost/PhaserGames/PixelMemory/'; // this.sys.game._URL
game._USER_ID = 0;
game._CONFIG = config;
};
window.onload = function()
{
'use strict';
var app = new App();
app.start();
}
5、包含场景和预设的独立文件
出于代码组织的原因(也可能是个人喜好),我喜欢为每个scene和预设建立单独的js文件。闭包和原型(prototyping)帮助我保持一个干净的命名空间。此外,如果我的代码整齐地组织在这样的文件中,我会更好地专注于当前的任务。
根据官网的例子,最好在场景中使用静态方法,而不是把它们原型化。之前传给scene对象的name字符串,以后可以在启动这个scene时直接调用(或切换,Phaser3的新功能)。
下面就是我组织scene的方式:
var PreloadScene= new Phaser.Scene('Preload');
PreloadScene.preload = function()
{
'use strict';
// ...
};
PreloadScene.create= function()
{
'use strict';
// ...
};
PreloadScene.update= function()
{
'use strict';
// ...
};
这个是预设文件:
var Helper = function() {};
Helper.prototype.createText = function(ctx, x, y, string, size, anchor)
{
'use strict';
};
如何从当前场景开始一个新场景:
this.scene.start('Preload');
6、Canvas的居中和尺寸调整
在Phaser2中我们可以很方便地配置canvas的位置和尺寸。但Phaser3中还没有类似的方法。所以我得用老办法,在index.html中添加一些css和js代码。
对于这个游戏,我希望它始终在屏幕居中,并随着浏览器窗口调整大小,并且可以适配移动端。
下面是我的css代码(‘phaser-app’是我在config对象中配置的parent属性)
body {
margin: 0;
overflow: hidden;
background-color: black;
}
canvas {
height: 100%;
}
#phaser-app {
margin: 0 auto;
}
js代码:
// Resize
function resizeApp()
{
var div = document.getElementById('phaser-app');
div.style.width = window.innerHeight * 0.6;
div.style.height = window.innerHeight;
}
window.addEventListener('resize', function(e)
{
resizeApp();
});
resizeApp();
7、全屏化
在Phaser2中我们可以方便地使游戏全屏启动。但在Phaser3Beta20中似乎没有这样的功能(至少原文作者没找到这个功能),所以我必须手写全屏代码。
此代码在我的index.html文件中:
// Fullscreen
function fs_status()
{
if(document.fullscreenElement)
{
return true;
}
else if(document.webkitFullscreenElement)
{
return true;
}
else if(document.mozFullScreenElement)
{
return true;
}
else
{
return false;
}
}
function goFullscreen()
{
if(fs_status())
{
return;
}
var el = document.getElementsByTagName('canvas')[0];
var requestFullScreen = el.requestFullscreen || el.msRequestFullscreen || el.mozRequestFullScreen || el.webkitRequestFullscreen;
if(requestFullScreen)
{
requestFullScreen.call(el);
}
}
document.getElementsByTagName('div')[0].addEventListener('click', goFullscreen);
8、设置Bitmap Text 的原点
目前为止,我们还无法改变bitmap text的原点,但是可以改变image的原点(没试过其他对象)。
Phaser3舍弃了‘anchor’,取而代之的是‘origin’。
Phaser3默认情况下,image的原点就设置在了0.5(Phaser2是0),但是bitmap text的原点默认还是0。
设置一个image的原点:
// You can chain it directly
this.add.image(0, 0, 'bg-main').setOrigin(0);
// Or you can add it later to an image object
this.bg_main.setOrigin(0);
为了把bitmap text的原点设置在中心,我写了这个辅助方法:
// My helper prefab
var Helper = function() {};
// Method to create a bitmap text
// I am still calling it "ancho"r instead of "origin"; old habit that will change going forward
Helper.prototype.createText = function(ctx, x, y, string, size, anchor)
{
'use strict';
var text;
var font = 'supermercado';
var size = size || 64;
// Text
text = ctx.add.bitmapText(x, y, font, string, 64);
// Anchor...
// ...center
if(!anchor || anchor === 0.5)
{
text.x -= (text.width * 0.5);
text.y -= (text.height * 0.5);
}
// ...1
if(anchor === 1)
{
text.x -= text.width;
text.y -= text.height;
}
// ...x & y different
else if(typeof anchor == 'object')
{
if(anchor.x === 0.5)
{
text.x -= (text.width * 0.5);
}
if(anchor.y === 0.5)
{
text.y -= (text.height * 0.5);
}
if(anchor.x === 1)
{
text.x -= text.width;
}
if(anchor.y === 1)
{
text.y -= text.height;
}
}
// Return
return text;
};
===========================================================================
Part2:
概述:
1、在场景之间传值。
2、改变对象的宽高。
3、配置tween(补间动画)。
4、精灵的子对象:带标签的按钮。
1、在场景之间传值
2018.2.25更新:这个bug在3.1.2中已被修复。详情参考这个帖子,特别是对全局变量的使用。
如上文所说,Phaser3Beta20在场景中传递数据会有bug。正常来说,我们是这样在场景中传值的:
// Here we are in the "Level" scene
// We start the "play" scene and send some data
this.scene.start('Play', { level: 3, difficulty: Medium });
// In the init or create method of the "Play" scene you receive the data as follows
PlayScene.init = function(data)
{
this._LEVEL = data.level;
this._DIFF = data.difficulty;
};
不幸的是,在PlayScene场景接收到的值是空的。
在我的小游戏中,玩家可以选择难度,还可以指定卡片的颜色。由于我无法把这个值传给PlayScene,我只好使用全局变量来解决这个问题。
在DecksScene场景选择卡片颜色:
DecksScene.clickBlue = function()
{
'use strict';
if(this.flagClick() === false)
{
return;
}
this.sys.game._DECK = 0;
this.hideUnselected();
this.startTransitionOut(this.goPlay);
};
在DifficultyScene场景选择难度:
DifficultyScene.clickEasy = function()
{
'use strict';
if(this.flagClick() === false)
{
return;
}
this.sys.game._COLS = 4;
this.sys.game._ROWS = 7;
this.startTransitionOut(this.goDecks);
};
初始化PlayScene场景时,给它的配置赋值:
PlayScene.init = function()
{
'use strict';
// ...
this._COLS = this.sys.game._COLS;
this._ROWS = this.sys.game._ROWS;
this._DECK = this.sys.game._DECK;
// ...
this.sys.game._COLS = undefined;
this.sys.game._ROWS = undefined;
this.sys.game._DECK = undefined;
};
这个想法很简单,只要你设置的值正确,程序就可以正常运行。
重要提示:如果你想在全局空间保存一个变量,就必须把它保存在this.sys.game里面。
2、改变对象的宽高
Phaser2中你可以直接设置一个游戏对象的宽高,这些数值也会确实地应用在游戏中。
在Phaser3中可就没这么简单了。在Phaser3中,你必须弄清楚sprite.width和sprite.displayWidth的区别。比如,我想动态改变一个玩家配置文件中的经验条宽度,你就必须设定displayWidth属性。
现在width属性被保留为image的宽度。如果一个sprite sheet含有width属性,则它表示的是sprite sheet一帧的宽度值。你也可以通过缩放来改变精灵的宽度。但是请记住,这个操作会缩放整个image!由于我的经验条有长短不同的边,所以我不能缩放整个image。我需要动态地改变宽度,于是操作displayWidth值就可以实现了。
// This does nothing
this.ui.profile.xpbar.width = Math.round(this.ui.profile.xpbar.width * ratio);
// This works
this.ui.profile.xpbar.displayWidth = Math.round(this.ui.profile.xpbar.width * ratio);
// This also changes the width, but it scales the full width of the image!!
this.ui.profile.xpbar.setScale(ratio, 1);
3、配置tween
Phaser3的补间动画简直棒极了!
我们只是简单地给一个对象传递了补间动画的配置属性,比如duration、ease,还有callback和callbackScope。
下面这个例子对于了解补间动画属性非常有用,如callbackScope。试试吧!
var scope = this;
var tween = this.tweens.add({
targets: [ myImage, myGraphic, mySprite ],
x: 600,
ease: 'Linear',
duration: 3000,
yoyo: true,
repeat: 1, // -1 for infinite repeats
onStart: function () { console.log('onStart'); console.log(arguments); },
onComplete: function () { console.log('onComplete'); console.log(arguments); },
onYoyo: function () { console.log('onYoyo'); console.log(arguments); },
onRepeat: function () { console.log('onRepeat'); console.log(arguments); },
callbackScope: scope
});
有时你可能创建了一些非常相似的补间动画,它们只有很小的差别。在这种情况下,你就可以创建一个方法,来为你返回补间动画的配置对象。
function getTweenConfig(ctx, delay, col, row, pos)
{
return {
targets: ctx.decks[col][row],
delay: delay,
duration: 500,
x: pos.x,
y: pos.y,
angle: -720,
ease: 'Linear',
// play sfx
onStart: function() { ctx.time.delayedCall(delay, ctx.helper.playSfx, [ctx, 'tap_card'], ctx); },
// normal callback
onComplete: function() { completeIntro.call(ctx, col, row); },
callbackScope: ctx
}
}
4、精灵的子对象:带标签的按钮
我喜欢为对象创建辅助方法,我会在我的游戏中多次这么做。
这个对象就是带标签的按钮。虽然很多时候按钮是相同的,但它们的标签从来都不一样。
在Phaser2中,你可以创建一个按钮,并将一个文本作为它的子对象。无论你改变按钮的什么属性,都会应用到它的子对象(文本)上。不幸的是,在Phaser3Beta20中,你不可以给一个对象添加子对象。我希望在以后的版本中可以实现这个功能。虽然你依旧可以创建Phaser组,但是组对象也没有提供我的按钮需要的所有功能。所以,我只好将文本添加到按钮的data对象中。
我会在下面贴出方法的完整代码,你可以用在自己的代码中并进一步完善它:
Helper.prototype.createBtnWithLabel = function(ctx, x, y, img, callback, label_config, frames, data)
{
'use strict';
var btn;
var text;
var label_config = label_config || { string: '[n/a]', size: 64, color: '0xFFFFFF', x: 0, y: 0 };
// Label position
if(!label_config.x)
{
label_config.x = 0;
}
if(!label_config.y)
{
label_config.y = 0;
}
// Create...
// ...sprite
btn = ctx.add.sprite(x, y, img);
// ...label
text = this.createText(ctx, x + label_config.x, y + label_config.y, label_config.string, label_config.size, label_config.color);
// ...data
btn.data = data || {};
btn.data.label_obj = text;
// Inputs...
// ...activate
btn.setInteractive();
// ...callback
btn.on('pointerup', function(e)
{
ctx.helper.playClickSfx(ctx);
callback.call(ctx);
});
// Frames...
// ...hover
if(frames && frames.over)
{
btn.on('pointerover', function(e)
{
this.setFrame(frames.over);
});
btn.on('pointerout', function(e)
{
this.setFrame(0);
});
}
// ...click
if(frames && frames.down)
{
btn.on('pointerdown', function(e)
{
this.setFrame(frames.down);
});
}
// Return group
return btn;
};
如你所见,这种方法的一个妙处在于,你可以轻松地为所有的按钮添加或者改变点击音效。你还可以为移动设备的播放器添加回调函数,使得你的一个触屏动作不会触发退出状态。举个例子,执行注册的pointerdown输入时,你可以运行一个回调方法,稍后(比如250毫秒)将按钮重置到“out”帧。希望这个方法可以为你的按钮提供很好的参考。
===========================================================================Part3
今天官方放出了Phaser3,尽管只是一个beta版本。请继续关注我用Phaser3Beta20制作我的第一个Phaser3游戏。
虽然我现在提及的一些东西以后可能会被修复或者改变,但我会尽量为你们讲解一些以后不太可能改变的知识点。
概述:
1、没有补间的Preload
2、Graphics对象的特性
3、重新开始同一场景4、播放音频
1、没有补间的Preload
这个第一段讲述的是我的发现,而不是教程。
我想为我的游戏创建一个自定义的加载动画,而不是使用从左到右加载的标准进度条。我想在游戏加载时慢慢地把卡片翻转过来。
补间一开始运行正常,可是在加载资源时出错了。这是因为补间动画只能在场景的preload方法执行完毕才能运行。
问题是,所有的资源都是在preload方法中加载的,而preload方法在create方法之前执行。所以当执行create方法时,再运行加载动画就没有意义了。即使我在preload方法中创建补间动画,也得等到场景执行create方法时才能触发动画。手动迫使补间动画在preload方法中执行也失败了。
所以我就在想,在执行preload方法期间,进度条的宽度是如何变化的呢?因为它不是一个补间动画!实际上,当图像、音频、字体这些文件加载时,每次加载后的回调函数都会触发进度条图像的宽度改变。
this.load.on('progress', function(value)
{
txt_percent.text = Math.round(value * 100) + ' %';
});
这意味着,只要能调用进度回调函数,就能创建自定义加载动画。
Phaser的加载进度表示为0到1之间的浮点数,再把它乘以100,就可以表示当前的加载进度。因此,如果你能想到一个不同的动画,并且把它跟数值联系起来,就能做出自定义加载动画。就像进度条图像一样,只需要数值参数就可以改变它的displayWidth属性。
但是补间动画只能在所有资源加载完成后才能运行。
2、Graphic对象的特性
我对Phaser2的Graphic对象太熟悉了,以至于我发现Phaser3的Graphic对象是如此惊人的不同。
2.1 X、Y坐标
让我们从创建一个graphic对象开始。我的大多数graphic对象都是UI背景或类似的东西。
我习惯在x,y位置创建graphic,而不是像许多例子一样在0, 0。
我的理由是:如果你在0,0创建了一个graphic对象,并在稍后把它拖拽到x,y,但是对象的实际位置依然在0, 0。如果你想在graphic对象中添加一个文本,并且这个文本的位置是参考graphic对象的,那么这个文本的坐标设为0,0是无效的。
对于整个canvas的原点来说也是一样。当你在Phaser3中创建一个graphic对象时,就再也不能给它传递x和y作为单独的参数了。你必须传递给对象一个x和y的属性。如果你像这样创建graphic对象,那么x和y属性将与对象的位置一致:
// We want our rectangle to be at 25, 100.
var x = 25;
var y = 100;
var bg = this.add.graphics({ x: x, y: y });
// We make it solid black
bg.beginFill(0x000000, 1);
// When you draw the rectangle, you have to start drawing at 0,0 now because
// this position is relative to what you have set in the create method above
var width = 350;
var height = 200;
bg.drawRect(0, 0, width, height);
// We are done!
bg.endFill();
2.2 Graphic的宽度和高度
现在你可以在graphic对象的相对坐标系创建对象了。
不幸的是,we cannot base anything off the graphic’s width and height properties anymore.(这句话实在是没看懂,-_-|)因为某些原因,Phaser3Beta20的graphic对象没有宽度和高度值。因此,如果你需要相对定位,你必须自己设置。
但是我会小心地设置属性名称,以防止跟以后发布的新版本Phaser属性名冲突。我不会使用bg.width,像bg.my_width这样类似的名称就很安全,不会跟未来可能会有的属性发生冲突。
// I wouldn't set these properties
bg.width = 350;
// This is probably never going to be needed by the Phaser 3 framework
bg.my_width = 350;
2.3 Graphic的补间和alpha
最后一个问题是,对于一个已经存在的graohic对象,你无法为它创建补间,或者改变它的alpha。我不知这是否是一个bug,又或者是在Beta20版本中遗漏了。但是如果它很快加入了,那就太棒了。
如果你想改变graphic对象的alpha,你只能先把它清空–重置fillStyle–重绘。
// We create the graphics object
var bg = this.add.graphics({ x: 25, y: 100 });
bg.fillStyle(0x000000, 1);
bg.drawRect(0, 0, 350, 200);
bg.endFill();
// Neither of these will work
bg.setAlpha(0.5);
bg.alpha = 0.5;
// You have to clear & re-draw
bg.clear();
bg.fillStyle(0x000000, 0.5);
bg.drawRect(0, 0, 350, 200);
bg.endFill();
这使得它不适合补间动画。也许一个可能的解决办法就是做一个包含数次循环的补间,然后每次使用onRepeat回调函数来手动清除和重绘。
3、重新开始同一场景
在Phaser2中你可以从一个场景内部启动这个场景本身,也就是场景调用自身的init方法重启。这听起来可能有些乱,但实际上就是这样:重启场景scene(从Phaser2的state变成Phaser3的scene)。
Phaser3中你是无法在当前场景进行‘重启’的。
// If you are currently in the PlayScene, this will not work
this.scene.start('Play');
所以我想出一个办法:创建一个像这样的重定向场景:
var RedirectScene = new Phaser.Scene('Redirect');
RedirectScene.init = function()
{
'use strict';
// ...
};
RedirectScene.create = function()
{
'use strict';
this.bg = this.add.image(0, 0, 'bg-main').setOrigin(0);
this.scene.start('Play');
};
如果场景之间可以传值我们就可以很容易地传递场景的name属性,并让RedirectScene变得更加灵活。如果你想在RedirectScene场景中重启不同的场景,首先你必须设置一个全局变量。
幸运的是,在我的游戏中,我只需要重启PlayScene场景。因此,我只要在RedirectScene中start(‘Play’)就行了。
4、播放音频
在教程临近尾声之时,我想说,在Phaser3中播放音频的功能也很棒。一旦音频文件加载到游戏中,你就可以随时播放它们。不用像在Phaser2中那样先创建一个sound对象了。这虽然是一个小变化,却使得添加音频变得更有乐趣。
// PreloadScene
this.load.audio('level_won', [ 'levelwin.ogg', 'levelwin.m4a' ]);
// PlayScene
this.sound.play('level_won');