Phaser学习笔记

setupaproject

安装官方的最佳实践

下载npm

安装Yeoman

npm install -g yo

安装Yeoman生成器

npm install -g generator-phaser-official

初始化生成器

yo phaser-official

创建项目目录

mkdir flappy-bird-reborn
cd flappy-bird-reborn
yo phaser-official

按提示一一回答:

You're using the fantastic Phaser generator.  
[?] What is the name of your project? flappy bird reborn ---项目名称
[?] Which Phaser version would you like to use? 2.0.1    ---使用Phaser的版本号
[?] Game Display Width: 288                     --游戏的宽度
[?] Game Display Height: 505                    --游戏的高度

最终的结果如下:

├── Gruntfile.js
├── assets
│   ├── preloader.gif
│   └── yeoman-logo.png
│──bower_components  
│──node_modules
├── bower.json
├── config.json
├── css
│   └── styles.css
├── game
│   ├── main.js
│   └── states
│       ├── boot.js
│       ├── gameover.js
│       ├── menu.js
│       ├── play.js
│       └── preload.js
├── package.json
└── templates
    └── _main.js.tpl

进入flappy-bird-reborn目录下,执行命令

 grunt

该命令将编译资源,启动服务器,打开浏览器,当编写的代码发生变化时,自动刷新浏览器。

资源

菜单

每一个游戏都需要一个菜单,我们的也不例外。首先,我们加载菜单所需要的资源。打开game/states/preload.js.代码如下:


'use strict';
function Preload() {
  this.asset = null;
  this.ready = false;
}

Preload.prototype = {
  preload: function() {
    this.asset = this.add.sprite(this.width/2,this.height/2, 'preloader');
    this.asset.anchor.setTo(0.5, 0.5);

    this.load.onLoadComplete.addOnce(this.onLoadComplete, this);
    this.load.setPreloadSprite(this.asset);
    this.load.image('yeoman', 'assets/yeoman-logo.png');

  },
  create: function() {
    this.asset.cropEnabled = false;
  },
  update: function() {
    if(!!this.ready) {
      this.game.state.start('menu');
    }
  },
  onLoadComplete: function() {
    this.ready = true;
  }
};

module.exports = Preload;

原创的Flappy Bird游戏,在菜单界面上包含有背景,地面,游戏标题,小鸟,一个开始按钮,和一个共享按钮。因此我们加载这些图片资源:

preload: function() {

    this.load.onLoadComplete.addOnce(this.onLoadComplete, this);
    this.asset = this.add.sprite(this.width/2, this.height/2, 'preloader');
    this.asset.anchor.setTo(0.5, 0.5);
    this.load.setPreloadSprite(this.asset);

    this.load.image('background', 'assets/background.png');
    this.load.image('ground', 'assets/ground.png');
    this.load.image('title', 'assets/title.png');
    this.load.image('startButton', 'assets/start-button.png');

    this.load.spritesheet('bird', 'assets/bird.png', 34, 24, 3);
}

开始的四行代码是样本代码,用于在加载我们需要的资源时,显示加载进度条。紧跟着的四行用于加载单个的图片文件。加载图片的API如下

this.load.image(key,url);

简单的来说,就是告诉Phaser的缓存系统通过指定的url加载图片并保存起来,以便通过key参数指定过的唯一名字进行引用。

最好一行,稍微有些不同。其主要目的是用于加载具有动画帧的小鸟图片。小鸟动画图片如下。

小鸟图片

你可以看到图片有三帧,用行话说就是每帧宽34像素,高24像素。API如下:

this.load.spritesheet(key, url, frameWidth, frameHeight, numberOfFrames);

保存代码,刷新浏览器。应当看到在屏幕上有一个绿色的立方体。这是因为我们删除了以前在该区域显示的资源。这是正确的,我们通过一行代码进行改观。

对菜单文件编码

打开game/states/menu.js 原始代码如下


'use strict';
function Menu() {}

Menu.prototype = {
  preload: function() {

  },
  create: function() {
    var style = { font: '65px Arial', fill: '#ffffff', align: 'center'};
    this.sprite = this.game.add.sprite(this.game.world.centerX, 138, 'yeoman');
    this.sprite.anchor.setTo(0.5, 0.5);

    this.titleText = this.game.add.text(this.game.world.centerX, 300, '\'Allo, \'Allo!', style);
    this.titleText.anchor.setTo(0.5, 0.5);

    this.instructionsText = this.game.add.text(this.game.world.centerX, 400, 'Click anywhere to play "Click The Yeoman Logo"', { font: '16px Arial', fill: '#ffffff', align: 'center'});
    this.instructionsText.anchor.setTo(0.5, 0.5);

    this.sprite.angle = -20;
    this.game.add.tween(this.sprite).to({angle: 20}, 1000, Phaser.Easing.Linear.NONE, true, 0, 1000, true);
  },
  update: function() {
    if(this.game.input.activePointer.justPressed()) {
      this.game.state.start('play');
    }
  }
};

module.exports = Menu;

删除在create和update函数中的内容,我们从头开始。

  'use strict';
  function Menu() {}

  Menu.prototype = {
    preload: function() {
    },
    create: function() {
    },
    update: function() {
    }
  };
  module.exports = Menu;

增加背景

第一件事就是为我们的游戏设置背景图片。我们在menu.js中的create方法中增加如下代码:

create: function() {  
     // 增加背景
    this.background = this.game.add.sprite(0, 0, 'background');
},

保存代码,在浏览器中你将看到背景被正确的加载。

背景图片

加载一个精灵是相当容易的。

var sprite = this.game.add.sprite(x, y, key);

x和y是指定加载的坐标,key是在预加载过程中指定的资源名字。

增加地面

在原始版本中,在菜单界面,地面也是移动的。为了实现这一点,我们使用一个称为TileSprite的对象,这样就能够在不需要实例化一组精灵的情况下,当它们离开屏幕的时候,通过切换纹理,实现让地面自动滑动。在menu.js中的create方法中增加如下代码:

   // 增加地面,并沿着x轴的反方向滚动
    // and start scrolling in the negative x direction
    this.ground = this.game.add.tileSprite(0, 400, 335, 112, 'ground');
    this.ground.autoScroll(-200, 0);

保存,效果如下。

滑动的地面

API如下:

var tileSprite = this.game.add.tileSprite(x, y, width, height, key);
tileSprite.autoScroll(xSpeed, ySpeed)

这行代码告诉tileSprite按照指定的速度进行滑动 xSpeed:X轴速度。负值代表沿着x轴的负方向移动。 ySpeed:Y轴速度。负值代表沿着y轴的负方向移动。

增加标题和小鸟

这是一个较大的主题。我们先讨论一下我们想要做成的效果。在原创的游戏中,当小鸟扇动翅膀时,标题和鸟都上下震动。为了模仿这种效果,我们需要编写如下代码:

1.创建一个组,用于保存标题和小鸟 2.创建一个标题精灵,增加到标题组 3.创建一个小鸟精灵,增加到标题组 4.使用动画精灵表创建一个会动的小鸟,并开始动画 5.设置组的在屏幕中的最初位置 6.为组增加动画,以便它们在震动时能一起移动。 7.谈论它

我们先列出全部代码,然后在逐一讲解。

如下代码仍旧添加在menu.js的create方法中:

     /** 步骤 1 **/
    // 创建一个组,用于保存标题和小鸟
    this.titleGroup = this.game.add.group();

    /** 步骤 2 **/
    // 创建一个标题精灵,增加到标题组
    this.title = this.game.add.sprite(0,0,'title');
    this.titleGroup.add(this.title);

    /** 步骤 3 **/
    // 创建一个小鸟精灵,增加到标题组
    this.bird = this.game.add.sprite(200,5,'bird');
    this.titleGroup.add(this.bird);

    /** 步骤 4 **/
    // 使用动画精灵表创建一个会动的小鸟,并开始动画
    this.bird.animations.add('flap');
    this.bird.animations.play('flap', 12, true);

    /** 步骤 5 **/
    // 设置组的在屏幕中的最初位置
    this.titleGroup.x = 30;
    this.titleGroup.y = 0;

    /** 步骤 6 **/
    // 为组增加动画,以便它们在震动时能一起移动
    this.game.add.tween(this.titleGroup).to({y:15}, 350, Phaser.Easing.Linear.NONE, true, 0, 1000, true);

分解

步骤1:创建一个组,用于保存标题和小鸟

this.titleGroup = this.game.add.group();

用于创建一个简单的游戏组。

*什么是游戏组 组一般用于保存一组能够拉动的精灵,如子弹或者敌人。但是,它更强大。在组中的每一个精灵能被很容易的枚举,并同时有一个方法能调用它们,所有的属性能够被同时设置,包括事件。如我们设置的变形动画。

步骤 2:创建一个小鸟精灵,增加到标题组

this.title = this.game.add.sprite(0,0,'title');  
this.titleGroup.add(this.title);

步骤 3:创建一个小鸟精灵,增加到标题组

this.bird = this.game.add.sprite(200,5,'bird');  
this.titleGroup.add(this.bird);

步骤 4:使用动画精灵表创建一个会动的小鸟,并开始动画

this.bird.animations.add('flap');  
this.bird.animations.play('flap', 12, true);

相当炫,我们引入一些新内容。

基于精灵表简单的动画

把某些相关的小图组成的图形叫做spriteshee,你可以很容易的通过AnimationManager让图形动起来。AnimationManager可以通过具有动画效果的sprite对象的获取到。我们在这里创建了一个简单的动画。假如我们的spritesheet有更多的帧和不同的动画(如:跳,向左走,向右走,溜达,或者射击),我们可以创建更逼真的动画,这是另外一个不同的教程。我们的代码非常简单,让我们逐行解释

sprite.animations.add(animationKey);

告诉sprite的AnimationManager使用在spritesheet中的所有帧创建一个被命名为animationKey的动画。

sprite.animations.play(animationKey, frameRate, loop);

让sprite的AnimationManager对象开始以给定的frameRate速度执行animationKey命名的动画。假如loop为true,将不停的执行动画,直到sprite.animations.stop()方法被调用。我们再次回顾一下我们的代码:

1. this.bird.animations.add('flap');  
2. this.bird.animations.play('flap', 12, true);

1.我们告诉bird的AnimationManager使用在spritesheet中所有的帧创建一个名为flap的动画。

2.我们告诉sprite的AnimationManager以每秒12帧的速度开始flap的动画,并且一直执行。

步骤 5:设置组的在屏幕中的最初位置

this.titleGroup.x = 30;  
this.titleGroup.y = 100;

我们分别在坐标(0,0) 和 (200,5)创建了我们的sprite,如果我们没有设置组的位置,屏幕的显示如下:

未设定组之前的效果

然而,当你改变一个组的位置,它变换在组中的所有sprite的位置的偏移量。因此,通过将titleGroup的坐标设置为(30,100),我们实际上将组中包含的sprite右移30个像素,下移100个像素,最终的效果如下:

设定组位置之后的效果

正如瓦片说的,组使用起来超级方便。我们在以后的教程中进一步介绍它的高级功能。

步骤 6:为组增加动画,以便它们在震动时能一起移动

this.game.add.tween(this.titleGroup).to({y:115}, 350, Phaser.Easing.Linear.NONE, true, 0, 1000, true);

这一行代码看起来有点多。让我们一点点的看到底实际上干了那些事情。

this.game.add.tween(object).to(properties, duration, ease, autoStart, delay, repeat, yoyo);
  • object: 渐变的对象。它可以是一个sprite,组或者任何一个游戏对象。
  • properties: 渐变的对象的呈现属性。
  • duration: 每一个渐变过程需要的时间,单位毫秒
  • ease: 使用的渐变函数 (内置的函数Phaser.Easing)
  • autoStart: 是否自动开始渐变
  • delay: 在开始执行渐变之前的等待时间,单位毫秒
  • repeat: 执行多少次渐变
  • yoyo: 是否执行完后,重新开始执行。

关于讨论渐变有很多文档,建议查看官方文档。 Phaser.Tween

回过头来看看我们的代码:

this.game.add.tween(this.titleGroup).to({y:115}, 350, Phaser.Easing.Linear.NONE, true, 0, 1000, true);

属性列表如下:

  • object: this.titleGroup
  • properties: { y: 115 }
  • duration: 350
  • ease:Phaser.Easing.Linear.NONE
  • autoStart: true
  • delay: 0
  • repeat: 1000
  • yoyo: true

用白话描述如下: 350毫秒内从屏幕的上方开始,沿y轴方向下移115个像素,并且不带渐变。立马开始执行,不需等待,重复1000次后重复从头开始重复执行。

第一个交互:开始按钮

菜单屏幕看起来相当不错,但是不能开始游戏,我们的菜单是完全无用的。

增加一个按钮

再一次我们在mene.js的create方法中增加如下代码:

   //增加一个代回调函数的开始按钮
    this.startButton = this.game.add.button(this.game.width/2, 300, 'startButton', this.startClick, this);
    this.startButton.anchor.setTo(0.5,0.5);

除了最后的两个参数,代码看起来很眼熟。

var button = this.game.add.button(x, y, key, callback, callbackContext);

如调用game.add.sprite一样,我们增加一个按钮,指定X,Y坐标,以及一个我们以及预加载的key代表的图片。然而我们必须指定一个callback 方法来执行我们按钮的交互。callbackContext 是callback方法执行的上下文。

Callbacks 和CallbackContexts简介

在Phaser中具有回调函数的每一个函数都有一个callbackcontext参数。假如你忘记传递callbackcontext参数,Phaser将设置其为null。一般情况下,都将这个参数设置为this,因为我们想让我们的回调函数的上下文能够取到游戏中的所有对象。

细心的读者注意到,我们通过非常熟悉的小技巧来设置按钮的坐标,而不是具体的数值。

var centerX = this.game.width/2;  
var centerY = this.game.height/2;

这将坐标设置游戏的屏幕中央。

紧跟这代码如下:

this.startButton.anchor.setTo(0.5,0.5);

设置按钮的锚位为高度的1/2,宽度的1/2.或者更简洁的说是其中心。

锚位

一个sprite的锚位是在sprite的内部,被Phaser当做圆点。意味着当你定位一个sprite,锚位将移动到这个位置,并且sprite的图像将围绕该点进行变换。当一个sprite被第一次创建时,锚位被设置为(0,0)。意味着sprite将其定位为左上脚。设置为(0.5,0.5)意味着sprite将其设置为显示区域的中心。

默认锚位的显示如下:

默认锚位

修改为(0.5,0.5)锚位的显示如下:

中心锚位

锚位也是sprite的旋转中心。也就是说,默认的旋转中心是左上脚。

在create方法下,增加新的方法startClick,代码如下:

create: function() {  
    /* ....... */
},
startClick: function() {  
    // 开始按钮的回调函数,开始play场景
    this.game.state.start('play');
}

该函数只做了一件事。调用了game.state.start()。该方法只有一个参数:一个代表开始状态的字符串。在该实例中,我们开始‘play’状态,它才是游戏的主角。

状态(state)

一个状态,对于游戏来说通俗的讲就是一个不同的屏幕,或展示。在我们这个游戏中,我们有5个状态:boot状态,加载屏幕,菜单状态,play状态,和游戏结束状态。虽然我们只是讨论了menu界面(稍微的提到了加载pigment),但是生成器以及给我们提供了所有的5个状态。

打开main.js.你能看到如下代码:

'use strict';

var BootState = require('./states/boot');  
var GameoverState = require('./states/gameover');  
var MenuState = require('./states/menu');  
var PlayState = require('./states/play');  
var PreloadState = require('./states/preload');

var game = new Phaser.Game(288, 505, Phaser.AUTO, 'flappy-bird-reborn');

// 游戏状态
game.state.add('boot', BootState);  
game.state.add('gameover', GameoverState);  
game.state.add('menu', MenuState);  
game.state.add('play', PlayState);  
game.state.add('preload', PreloadState);


game.state.start('boot');

生成器自动给我们生成了这个文件,每增加一个状态,修改这个文件一次。了解这段代码最重要的是:

game.state.add(key, Phaser.State);

和加载器一样,游戏的StateManager 允许你增加一个游戏状态,并用一个key定义,以便以后检索它。Phaser.State代表状态本身。每一个状态继承了能被Phaser 自动识别并知道在游戏的生命周期中在合适时机调用的方法。你已经看到了它们的一部分(preload,craete,update)

在menu.js的底部有这么一行代码

module.exports = Menu;

因为我们使用 Browserify定义模块,并且通过require()管理依赖,module.exports告诉Browserify当这个文件被要求时,应当返回的内容。在本例中,我们告诉我们的模块输出Menu对象,它包含了我们真的这个状态写的全部代码。

因为生成器在main.js中为我们创建了以下几行代码:

var MenuState = require('./states/menu');  
...
game.state.add('menu', MenuState);

游戏的StateManager将知道当‘menu'的状态开启时,menu.js代码将被执行。