了解如何绘制和移动游戏中的精灵。
让我们从基础开始,在游戏的舞台上绘制一个物体。在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');
},
我们调用了如下函数:
在数学里,笛卡尔坐标系(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游戏运行期的简单流程图
更新-绘制就是所谓的游戏循环(Game-loop)模式,也是游戏引擎的核心。你可以在游戏编程模式网站上查看更多的内容。
现在我们已经知道在Phaser中游戏循环的实现。让我们在update()函数内通过减少y轴坐标的来实现对子弹精灵的垂直移动。
update: function () {
this.sea.tilePosition.y += 0.2;
this.bullet.y -= 1;
},
正如上面提到的,Phaser引擎将定时的调用update()函数,以每秒60个像素的速度向上移动。
通过这种方式,大多数的游戏引擎或框架实现了精灵的移动。但是在Phaser中,我们通过物理引擎为我们做了大部分的工作。
在我们讨论如何使用物理引擎之前,我们先解释一下没有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个像素。
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;
.......
}