Phaser学习笔记

精灵(Sprite)

了解如何绘制和移动游戏中的精灵。

精灵基础

绘制子弹精灵

让我们从基础开始,在游戏的舞台上绘制一个物体。在Pyaser中最基本的对象是精灵(Sprite)。因此,我们首次的代码段就是加载并绘制一个子弹精灵。

在preload.js中,增加如下代码:

preload: function() {
    .......
    this.load.image('sea', 'assets/sea.png');
    this.load.image('bullet', 'assets/bullet.png');
  },

在play.js中,增加如下代码

create: function() {
      this.game.physics.startSystem(Phaser.Physics.ARCADE);
       this.sea = this.add.tileSprite(0, 0, 800, 600, 'sea');
       this.bullet = this.add.sprite(400, 300, 'bullet');
    },

我们调用了如下函数:

  • load.image():加载一个图片(如:assets/bullet.png)并指定它一个名字(如:bullet),以便以后使用。
  • add.sprite():参数为我们精灵指定的xy坐标,以及我们在load.image中指定的图片的名字。

增加子弹精灵

屏幕坐标 vs 笛卡尔坐标

在数学里,笛卡尔坐标系(Cartesian坐标系),也称直角坐标系,是一种正交坐标系。二维的直角坐标系是由两条相互垂直、0 点重合的数轴构成的。中心点是(0,0),x轴右边为正,y轴向上为正。 笛卡尔坐标

然而,计算机的显示不能如笛卡尔坐标那样显示,取而代之的是另外一个变种:(0,0)点在左上角,而不是在中心。另外,Y轴向下为正。下图显示了游戏的当前坐标情况。

游戏坐标

绘制带动画的敌人

让我们处理更加复杂的对象,带动画的精灵。首先我们在preload.js中加载一个精灵图(sprite sheet),一个包含多帧的图片。

 preload: function () {
     ......
     this.load.spritesheet('greenEnemy', 'assets/enemy.png', 32, 32);
 }

我们没有使用load.image()函数,相反我们使用load.spritesheet()来精灵图。两个额外的参数是单独帧的宽度和高度。由于我们定义了每帧的宽度和高度均为32px,Phaser将加载精灵图并分割成如许的单独帧。

对手的精灵图现在已经被加载,现在我们将其加入到我们的游戏场景中:

 create: function() {
         ......
         this.sea = this.add.tileSprite(0, 0, 800, 600, 'sea');

        this.enemy = this.add.sprite(400, 200, 'greenEnemy');
        this.enemy.animations.add('fly', [ 0, 1, 2 ], 20, true);
        this.enemy.play('fly');

        this.bullet = this.add.sprite(400, 300, 'bullet');
    }

animations.add(name, frames, frameRate, loop, useNumericIndex): name:动画的名字 frames:显示的帧数组 frameRate:动画的速度(每秒的帧数) loop:是否循环播放 useNumericIndex:是否使用数组作为帧的索引

上面代码定义了一个叫fly的动画,循环显示精灵表中0,1,2帧 动画的演示规则如下图示

顺序: 细心的读者已经发现,我们增加敌对精灵代码在子弹精灵之后。正如我们后边看到的,这样做的目的是让子弹精灵在敌对精灵之上显示。

有很多办法来重新对精灵的显示顺序进行安排。但是最简单的方法就是在创建它们的时候,就按从后到前的顺序进行添加。

设置锚点

精灵同样共享和屏幕一样的坐标系。因此其锚点也是默认在左上角。

然而在游戏开发中,我们大多数时间让精灵的0点坐标为精灵的中心。我们可以通过Phaser修改默认的锚点:

    this.enemy = this.add.sprite(400, 300, 'greenEnemy');
    this.enemy.animations.add('fly', [ 0, 1, 2 ], 20, true);
    this.enemy.play('fly');
    this.enemy.anchor.setTo(0.5, 0.5);

    this.bullet = this.add.sprite(400, 400, 'bullet');
    this.bullet.anchor.setTo(0.5, 0.5);

(0.5,0.5)是精灵的中心,(0,0)点代表精灵的左上角,(1,1)代表精灵的右下角。

游戏循环

下图是Phaser游戏运行期的简单流程图

  • Preload 游戏开始于预先加载部分,在该阶段所有的资产都预先加载。如果没有预先加载,游戏在运行过程会口吃或挂起的游戏因为它加载的资产。
  • Create 加载完所有资源后,设置游戏的初始化状态。
  • Update
    在设定的间隔时间内(通常是每秒60帧),该函数被被调用并更新游戏的状态。游戏的更新逻辑都在这里实现。如:检查冲突,随机出现敌人,检测用户的输入,对精灵进行移动等。
  • Render 更新完成后,绘制整个的屏幕。

更新-绘制就是所谓的游戏循环(Game-loop)模式,也是游戏引擎的核心。你可以在游戏编程模式网站上查看更多的内容。

通过update()方法移动子弹

现在我们已经知道在Phaser中游戏循环的实现。让我们在update()函数内通过减少y轴坐标的来实现对子弹精灵的垂直移动。

 update: function () {
    this.sea.tilePosition.y += 0.2;
    this.bullet.y -= 1;
  },

正如上面提到的,Phaser引擎将定时的调用update()函数,以每秒60个像素的速度向上移动。

通过这种方式,大多数的游戏引擎或框架实现了精灵的移动。但是在Phaser中,我们通过物理引擎为我们做了大部分的工作。

render()函数漏掉了吗

在我们讨论如何使用物理引擎之前,我们先解释一下没有render()函数,游戏仍能自个绘制合适的游戏状态。

首先,一下细心的读者可能已经注意到。在main.js,我们对游戏中被称为状态的一部分进行了编码,状态,顾名思义,就是一个游戏的场景。

 //global variables
window.onload = function () {
  var game = new Phaser.Game(800, 600, Phaser.AUTO, 'shootem');

  // Game States
  game.state.add('boot', require('./states/boot'));
  game.state.add('gameover', require('./states/gameover'));
  game.state.add('menu', require('./states/menu'));
  game.state.add('play', require('./states/play'));
  game.state.add('preload', require('./states/preload'));

  game.state.start('boot');
};

状态是在Phaser的游戏循环在,被更新和绘制的事物之一。如:game对象调用逻辑如下(略去了pre-和 post- 更新的钩子):

  this.state.update();
  this.tweens.update();
  this.sound.update();
  this.input.update();
  this.physics.update();
  this.particles.update();
  this.plugins.update();

下边是绘制的部分:

  this.renderer.render(this.stage);
  this.plugins.render();
  this.state.render();
  this.plugins.postRender();

我们不需要写代码来绘制我们的精灵,是因为在绘制的代码第一行:this.renderer.render(this.stage);已经替你完成了。this.stage包含了在游戏中所有的精灵。

我们在后边为了调试的目的,编写一些绘制代码

物理引擎的应用

Phaser内置了3个物理引擎系统,Arcade,P2。Arcade是默认的也是最简单的,因此我们采用它。

(此外,在Phaser官方的基本模板的版本中的phaser-arcarde-physics.min.js,只包含Arcade物理引擎,以便减少下载文件的大小)

速度

一旦我们把我们的子弹精灵放入到Arcade物理引擎,我们现在可以通过设置它的速度,让系统处理所有其它计算(例如下一帧中的位置)。

我们将代码从:

 update: function () {
    this.sea.tilePosition.y += 0.2;
    this.bullet.y -= 1;
  },

改为:

    this.physics.enable(this.bullet, Phaser.Physics.ARCADE);
    this.bullet.body.velocity.y = -500;

随着物理引擎和速度的设置,我们精灵的坐标通过this.physics.update();代码进行更新,而不是我们自个的update()代码。在本例中“velocity.y = -500” 含义是每秒钟500像素向前,每次更新将使子弹移动8-9个像素。

显示body的调试信息

Arcade物理引擎局限于AABB包围盒树碰撞检测算法。简单的说,就是所有的对象都是矩形。

精灵的边界框(hitboxes)用红色表示;右边的图的精灵是互相碰撞的示意图。

我们能通过调速器对这些区域的绘制查看这些矩形。首先我们增加敌人精灵到物理系统中:

   .....
    this.enemy.anchor.setTo(0.5, 0.5);
    this.physics.enable(this.enemy, Phaser.Physics.ARCADE);

    this.bullet = this.add.sprite(400, 300, 'bullet');
    .......

然后增加我们目前还不存在的render()函数中增加调试代码:

    render: function() {
         this.game.debug.body(this.bullet);
        this.game.debug.body(this.enemy);
      },

碰撞

一旦我们将精灵假如物理系统,检查碰撞和重叠的方法是调用正确的函数:

    update: function () {
        this.sea.tilePosition.y += 0.2;
        this.physics.arcade.overlap(
          this.bullet, this.enemy, this.enemyHit, null, this
        );
  },

overlap()函数要求一个回调函数,用于在精灵重叠时调用。当前的回调函数enemyHit()代码如下:

 enemyHit: function (bullet, enemy) {
        bullet.kill();
        enemy.kill();
  },

游戏在正常的情况下,Phaser提供给我们一个sprite.kill()函数用于“消除”精灵。调用该函数标识精灵死去或不可见,并有效的将精灵从游戏中移去。

下图是碰撞时发生的示意图:

通过debug信息,我们能看到精灵仍旧在碰撞位置,但是它不可见但是物理引擎忽略了它。

移去调试

但你完成测试后,记得将debug代码移去或住宿掉。

爆炸

在开始下一节的课程之前,我们通过增加爆炸动画来增加我们的碰撞效果。在preload.js文件中预加载爆炸资源。

preload: function () {
    ......
    this.load.spritesheet('explosion', 'assets/explosion.png', 32, 32);
}

然后增加实际的爆炸效果:

enemyHit: function (bullet, enemy) {
    bullet.kill();
    enemy.kill();
    var explosion = this.add.sprite(enemy.x, enemy.y, 'explosion');
    explosion.anchor.setTo(0.5, 0.5);
    explosion.animations.add('boom');
    explosion.play('boom', 15, false, true);
  },

在这里我们使用了不同的方式来设置动画。这一次我们使用animations.add(),函数只需要动画的名字。没有使用其它参数,boom动画使用精灵表中的所有帧,速度60fps,但不循环。

我们想调整这个动画的设置,所以我们在explosion.play()调用时增加了额外参数:

* 15 -每秒播放的帧数
* false——不要循环动画
* true——动画结束后删除精灵

最后一个参数对我们优为方便,不需要我们再次注册一个回调的事件处理程序来删除精灵,关于事件处理我们稍后再讲。先欣赏一下我们改进的“击落敌人”的动画吧:
![](collision_explosion.png)

玩家操作

我们已经学会了精灵的绘制和移动,现在我们开始实现一个在游戏中代表玩家的对象。在preload.js中增加加载代表玩家的精灵。

 preload: function() {
 .......
 this.load.spritesheet('player', 'assets/player.png', 64, 64);
 }

在play.js的增加:

 create: function() {
     .......
     this.sea = this.add.tileSprite(0, 0, 800, 600, 'sea');

    this.player = this.add.sprite(400, 550, 'player');
    this.player.anchor.setTo(0.5, 0.5);
    this.player.animations.add('fly', [ 0, 1, 2 ], 20, true);
    this.player.play('fly');
    this.physics.enable(this.player, Phaser.Physics.ARCADE);

    this.enemy = this.add.sprite(400, 200, 'greenEnemy');
 }

键盘控制

在Phaser中实现对键盘的输入控制是非常简单的。本例中我们采用能四键操作的工具函数。

create: function() {
    .......
    this.cursors = this.input.keyboard.createCursorKeys();
    }

因为玩家的速度在整个游戏过程中多次使用,让我们将玩家的初始速度在创建时就作为玩家对象的一个属性。同时也允许我们有机会针对不同的飞机设置不同的速度,提高游戏的可玩性。

create: function() {
    .......
  this.physics.enable(this.player, Phaser.Physics.ARCADE);
    this.player.speed = 300;

    this.enemy = this.add.sprite(400, 200, 'greenEnemy');
    ......
    }

一旦这些都完成后,我们现在就可以通过如下代码设置玩家精灵的速度:

update: function () {
    .........

    this.player.body.velocity.x = 0;
    this.player.body.velocity.y = 0;

    if (this.cursors.left.isDown) {
      this.player.body.velocity.x = -this.player.speed;
    } else if (this.cursors.right.isDown) {
      this.player.body.velocity.x = this.player.speed;
    }

    if (this.cursors.up.isDown) {
      this.player.body.velocity.y = -this.player.speed;
    } else if (this.cursors.down.isDown) {
      this.player.body.velocity.y = this.player.speed;
    }
  },

  请注意,我们在update中设置速度为零而不是在create方法中,这样当输入停止后飞机就能立马停止。当然我们也允许玩家同时控制水平和垂直方向的运动。
![](movement.png)

Arcade物理引擎也使得如同堵强一样,将玩家局限于舞台上相当容易。

```javascript
create: function() {
    .......
    this.physics.enable(this.player, Phaser.Physics.ARCADE);

    this.player.body.collideWorldBounds = true;
    .....
    }

鼠标触屏移动

基于像素的移动通常要求手动处理数学计算。庆幸的是,Phaser 已经提供了基于输入的点来计算角度和速度的函数。

以下是基于手势的输入来对对象进行移动。

update: function() {
    .......

    if (this.input.activePointer.isDown) {
            this.physics.arcade.moveToPointer(this.player, this.player.speed);
          }
}

基于对象的位置和速度,Arcade物理引擎函数moveToPointer()以输入的速度计算移动到目标点所需的角度和速度。调用这个函数本身已经修改对象x轴y轴的速度,这正是我们在这种情况下需要的。

但这个函数不会旋转精灵,因此如果你想对应的对精灵做成旋转,你可以利用该函数的返回值-旋转角的弧度。我们将在后边的教程中看到这样的例子。

仅仅是一个提醒,在一个框架的运动可能超过目标(即移动5像素虽然点只2像素)使你的玩家精灵抖动,而不是原地不动。触摸屏对坐标不准确也会产生类似的效果。最粗暴的解决办法是在距离压点一定距离时停止运动,像这样:


if (this.input.activePointer.isDown &&
        this.physics.arcade.distanceToPointer(this.player) > 15) {
      this.physics.arcade.moveToPointer(this.player, this.player.speed);
    }

如果你需要更精确的输入,你可能会更好实现屏幕上全方位的补充。

发射子弹

为了能够实时的创建子弹,我们首先删除原来旧的子弹代码。

 create: function() {
  ......
        // this.bullet = this.add.sprite(400, 300, 'bullet');
        // this.bullet.anchor.setTo(0.5, 0.5);
        // this.physics.enable(this.bullet, Phaser.Physics.ARCADE);
        // this.bullet.body.velocity.y = -100;

        this.bullets = [];
        .....
        }

我们设置按Z键或者点击屏幕时发射子弹:

update: function ()
{
   ......
     this.physics.arcade.moveToPointer(this.player, this.player.speed);
    }

    if (this.input.keyboard.isDown(Phaser.Keyboard.Z) ||
        this.input.activePointer.isDown) {
      this.fire();
    }

}

我们创建一个新的函数在玩家精灵的鼻部发射子弹。

fire: function() {
       var bullet = this.add.sprite(this.player.x, this.player.y - 20, 'bullet');
       bullet.anchor.setTo(0.5, 0.5);
       this.physics.enable(bullet, Phaser.Physics.ARCADE);
       bullet.body.velocity.y = -500;
       this.bullets.push(bullet);
     },

最后我们修改我们的冲突检测代码为迭代整个子弹数组:

update: function() {
        this.sea.tilePosition.y += 0.1;
        //this.bullet.y -= 1;

        // this.physics.arcade.overlap(
        // this.bullet, this.enemy, this.enemyHit, null, this);

        for (var i = 0; i < this.bullets.length; i++) {
            this.physics.arcade.overlap(
              this.bullets[i], this.enemy, this.enemyHit, null, this
            );
          }
    ........
    }

发射频率

当你测试新的发射子弹代码时,一个明显的问题就是子弹的发射频率太高了。我们可以通过指定一定的时间间隔后才允许下一发子弹发射来控制。

在create()函数中增加变量nextShotAt和shotDelay(设置为100毫秒)。

    this.bullets = [];
    this.nextShotAt = 0;
    this.shotDelay = 100;

然后修改fire函数,周期性的检查和设置nextShotAt变量。

fire: function() {
    if (this.nextShotAt > this.time.now) {
      return;
    }

    this.nextShotAt = this.time.now + this.shotDelay;
    .......
    }