先给一个线上地址,大家体验一下,鼠标拖动飞机可以进行移动。http://game.webxinxin.com/plane/。
进行简单分析之后,我们发现飞机大战主要有三个场景,开始场景,游戏中场景和结束场景。
开始场景
开始场景其实很简单,一个背景,一个飞机,一个开始按钮和一个版权说明。背景,我们使用tileSprite,因为背景移动的时候,我们要使用它的autoScroll方法。版权说明,我们直接使用image,因为它没有任何动作。飞机我们使用sprite,它有一个帧动画要播放,而按钮,我们使用button,点击的时候,跳转到游戏中场景。
game.States.main = function() { this.create = function() { // 背景 var bg = game.add.tileSprite(0, 0, game.width, game.height, 'background'); // 版权 this.copyright = game.add.image(12, game.height - 16, 'copyright'); // 我的飞机 this.myplane = game.add.sprite(100, 100, 'myplane'); this.myplane.animations.add('fly'); this.myplane.animations.play('fly', 12, true); // 开始按钮 this.startbutton = game.add.button(70, 200, 'startbutton', this.onStartClick, this, 1, 1, 0); }; this.onStartClick = function() { game.state.start('start'); }; };
游戏中场景
游戏中场景是最复杂的一个场景,我们同样需要添加背景,添加飞机,这些和上面是一样的。然后,我们让背景开始滚动,给飞机加上一个tween动画,当动画结束的时候,开始发射子弹,显示分数,敌机也开始进入。同时,还要处理敌机被子弹打中时的逻辑,敌机爆炸的效果,以及我方被子弹打中的逻辑和我方的爆炸效果等等。所以我们一点一点来分析。
首先打开物理引擎,然后把背景加进来,让它自动滚动。然后把我们的飞机加进来,让它执行帧动画,同时设置它的碰撞规则,让它不能够超出游戏区域的边界。
在进入该场景的时候,我们让飞机直接进行一个tween动画,从中间到达底部,然后开始调用onStart。
this.create = function() { // 物理系统 game.physics.startSystem(Phaser.Physics.ARCADE); // 背景 var bg = game.add.tileSprite(0, 0, game.width, game.height, 'background'); bg.autoScroll(0, 20); // 我的飞机 this.myplane = game.add.sprite(100, 100, 'myplane'); this.myplane.animations.add('fly'); this.myplane.animations.play('fly', 12, true); game.physics.arcade.enable(this.myplane); this.myplane.body.collideWorldBounds = true; this.myplane.level = 2; // 动画 var tween = game.add.tween(this.myplane).to({y: game.height - 40}, 1000, Phaser.Easing.Sinusoidal.InOut, true); tween.onComplete.add(this.onStart, this); };
首先需要处理的就是发射子弹和鼠标拖拽。鼠标拖拽在phaser中十分简单,只要设置inputEnabled为true,然后enableDrag就行了。飞机不能拖拽出屏幕,当然,这一点我们已经在之前确保过了。
子弹的处理倒是有一定的技巧。首先要考虑的是,子弹要从飞机的首部发射出来,还得连续发射,超出边界的时候需要释放,如果一直进行子弹对象的创建和销毁,会一直进行内存的创建与回收,这样会浪费很多资源。
对于这种情况,一个比较通用的解决办法是对象池。它的原理是,先初始化一批对象,放到一个池子里,然后需要使用的时候,从池子里拿一个出来,再使用完成后,又放回池子里。这样就减少了不必要的内存创建与回收。this.mybullets.createMultiple(50, 'mybullet');就是一次性创建50个子弹,相当于一个对象池。然后我们设置该组的对象,在超过游戏边界的时候,进行回收。
this.onStart = function() { // 我的子弹 this.mybullets = game.add.group(); this.mybullets.enableBody = true; this.mybullets.createMultiple(50, 'mybullet'); this.mybullets.setAll('outOfBoundsKill', true); this.mybullets.setAll('checkWorldBounds', true); this.myStartFire = true; this.bulletTime = 0; // 我的飞机允许拖拽 this.myplane.inputEnabled = true; this.myplane.input.enableDrag(false); };
敌机的逻辑和子弹其实很像,每种敌机,也都是从一个池子里拿出来,然后,当超过边界或者被子弹消灭之后,会被kill,就相当于放回池子里了。
但是每种敌机的逻辑都是一样的,只是里面有一些参数不太一样,所以这里把敌机抽象出来一个类,类中的一些方法也很简单,包括初始化,开火,被敌机打中,产生等等逻辑。这里简单说一下,被打中后消灭的爆炸效果。其实爆炸效果也是使用了对象池,因为同一刻可能存在好几个爆炸点,而爆炸结束后同样需要回收动画资源,所以也通过一个池子进行管理。每次发生爆炸的时候,从池子中拿出一个爆炸的动画对象,然后播放动画,完毕后再将爆炸资源回收。
function Enemy(config) { this.init = function() { this.enemys = game.add.group(); this.enemys.enableBody = true; this.enemys.createMultiple(config.selfPool, config.selfPic); this.enemys.setAll('outOfBoundsKill', true); this.enemys.setAll('checkWorldBounds', true); // 敌人的子弹 this.enemyBullets = game.add.group(); this.enemyBullets.enableBody = true; this.enemyBullets.createMultiple(config.bulletsPool, config.bulletPic); this.enemyBullets.setAll('outOfBoundsKill', true); this.enemyBullets.setAll('checkWorldBounds', true); // 敌人的随机位置范围 this.maxWidth = game.width - game.cache.getImage(config.selfPic).width; // 产生敌人的定时器 game.time.events.loop(Phaser.Timer.SECOND * config.selfTimeInterval, this.generateEnemy, this); // 敌人的爆炸效果 this.explosions = game.add.group(); this.explosions.createMultiple(config.explodePool, config.explodePic); this.explosions.forEach(function(explosion) { explosion.animations.add(config.explodePic); }, this); } // 产生敌人 this.generateEnemy = function() { var e = this.enemys.getFirstExists(false); if(e) { e.reset(game.rnd.integerInRange(0, this.maxWidth), -game.cache.getImage(config.selfPic).height); e.life = config.life; e.body.velocity.y = config.velocity; } } // 敌人开火 this.enemyFire = function() { this.enemys.forEachExists(function(enemy) { var bullet = this.enemyBullets.getFirstExists(false); if(bullet) { if(game.time.now > (enemy.bulletTime || 0)) { bullet.reset(enemy.x + config.bulletX, enemy.y + config.bulletY); bullet.body.velocity.y = config.bulletVelocity; enemy.bulletTime = game.time.now + config.bulletTimeInterval; } } }, this); }; // 打中了敌人 this.hitEnemy = function(myBullet, enemy) { try { config.firesound.play(); } catch(e) {} myBullet.kill(); enemy.life--; if(enemy.life <= 0) { try { config.crashsound.play(); } catch(e) {} enemy.kill(); var explosion = this.explosions.getFirstExists(false); explosion.reset(enemy.body.x, enemy.body.y); explosion.play(config.explodePic, 30, false, true); score += config.score; config.game.updateText(); } }; }
碰撞检测,可以使用phaser中的game.physics.arcade.overlap,也是十分的方便,指定两个对象进行碰撞后,需要回调的函数。
游戏中场景是一个非常重要的场景,也是这个游戏的核心,讲到这里已经差不多了,更加具体的逻辑,大家自己看源码进行分析吧。
结束场景
结束场景相对就比较简单了,背景、版权、飞机、分数、两个按钮,将它们排布好就行了。
在这里想介绍一下分享功能的实现。因为一般来说,我们做的游戏会放到微信中进行分享,分享给我们的小伙伴们。
我的需求是这样的,我希望在分享的时候,弹出一个新的场景,包含二维码,别人可以长按二维码进行识别,然后我诱导别人点击左上角菜单进行分享。当别人分享后,能够出现一个带logo和标题的消息,标题中把分数带进去。
实现的时候发现几个坑,首先,微信只有img标签的图片才能进行长按识别,而phaser是在canvas中进行绘制,所以直接在phaser中画图是没有长按识别功能的;第二,想要让分享的链接带一个图标,需要有一个大小大于300x300的图片,微信会找到链接中的第一个大小大于300x300的图片,把它设置为图标;第三,分享的标题其实就是页面的title。
好了,所有的功能都实现完毕了。整个代码大概450行js,50行html,熟练的话,大概一两个小时就能编码完毕,可见用phaser开发游戏,效率之高,速度之快。
只是有一个大多数html5游戏的一个问题,就是在低端安卓机上面性能太差,基本无法正常运行,但是我相信在不久的将来,随着硬件越来越强悍,浏览器内核越来越高级,这个问题一定会被解决。