精华 Phaser3初体验(译文)
发布于 1 年前 作者 enoz56 2537 次浏览 来自 分享

Phaser3初体验

译者说: 1、本文讲述的是作者初次使用Phaser3(之前都用Phaser2)制作游戏的一些心得和碰到的坑。 2、为了跟代码对应,有些单词不译成中文(比如scene、image)。 3、初次翻译教程,加上水平所限,疏漏之处欢迎指教。 4、原文分为三部分,链接在此:原文1原文2原文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');
1 回复

好棒啊,感谢分享

回到顶部