先来看一个效果吧!
怎么样?和真正的水果忍者有点像吧?再给一个体验地址,自己先体验一下:http://game.webxinxin.com/fruit/。
好了,才实现基本功能,还有很多可以扩展的功能,如果你有兴趣可以在我的这个版本基础上继续开发。下面简单讲解一下代码设计思路,具体的还是自己撸吧。
先看下主菜单页面。
主菜单上面的部分还是比较简单的,主要是几个开场动画,把元素定好位后,做一个带次序的tween动画就可以了。可以用tween的chain来做,也可以在tween结束时onComplete回调启动下一个动画。下面的几个水果和炸弹要想一想,尤其是炸弹。炸弹是会冒火星的,这里我简单用方块来代替了,而且使用了粒子系统。
左边桃子和DOJO的圆圈,可以组成一个group,这两个元素都是旋转,但是他们的旋转方向和速度是不一样的。中间的西瓜和NEWGAME的圆圈也一样,还有右边的炸弹和QUIT圆圈。但是炸弹应该怎么来做呢,其实它本身也是三部分组成,一个炸弹图片,一个烟雾图片,一个火花的粒子发射器。一开始,我也想把它们三组成一个group,但是后来想一想,其实整个炸弹应该是一个sprite。在phaser中,group和sprite都可以去addChild。至于是一个group还是一个sprite,主要看内部元素的关系,如果这些内部元素自己是一个个体,可以有不用的速度,位置,旋转方向等,那么它们应该组成一个group,如果内部元素位置相互固定,速度,旋转都一致,而且本来就是某个物体的组成部分,那么它们就组成一个sprite。组成一个sprite的好处就是,物理属性共享。
sprite = game.add.sprite(env.x || 0, env.y || 0); var bombImage = game.add.sprite(0, 0, 'bomb'); bombImage.anchor.setTo(0.5, 0.5); // 烟雾 var bombSmoke = game.add.sprite(-55, -55, 'smoke'); // 粒子发射器 var bombEmit = game.add.emitter(-30, -30, 20); // 设置粒子,使用我们自定义的粒子 bombEmit.particleClass = FlameParticle; bombEmit.makeParticles(); // 设置属性 bombEmit.setScale(1, 0.8, 1, 0.8, 1500); bombEmit.setAlpha(1, 0.1, 1500); // 发射 bombEmit.start(false, 500, 50); // 什么时候用Group,什么时候用sprite,一个炸弹,是一个sprite,刚体,速度,旋转都一致。group里面的东西可以速度不一致。 sprite.addChild(bombImage); sprite.addChild(bombEmit); sprite.addChild(bombSmoke); // 物理属性 game.physics.enable(sprite, Phaser.Physics.ARCADE); sprite.enableBody = true;
接下来的一个难点就是鼠标划过屏幕时的刀光怎么来做。这块确实想了很久,后来决定用图形来画,graphics,把鼠标划过的点连成折线,往周围延伸,形成一个刀光的模样。这里面涉及到一些数学的计算,具体的逻辑都封装在了mathTool里面。算法最后得到的就是组成刀光轮廓的一系列点,而输入就是鼠标滑过的点。再记录一下鼠标点的时间,设置一个超时,超时后移除,这个刀光效果就完成了。
// 形成刀光点 var res = []; if(points.length <= 0) { return; } else if(points.length == 1) { var oneLength = 6; res.push(new Phaser.Point(points[0].x - oneLength, points[0].y)); res.push(new Phaser.Point(points[0].x, points[0].y - oneLength)); res.push(new Phaser.Point(points[0].x + oneLength, points[0].y)); res.push(new Phaser.Point(points[0].x, points[0].y + oneLength)); } else { var tailLength = 10; var headLength = 20; var tailWidth = 1; var headWidth = 6; res.push(this.calcParallel(points[0], points[1], tailLength)); for(var i=0; i<points.length-1; i++) { res.push(this.calcVertical(points[i+1], points[i], Math.round((headWidth - tailWidth) * i / (points.length - 1) + tailWidth), true)); } res.push(this.calcVertical(points[points.length-2], points[points.length-1], headWidth, false)); res.push(this.calcParallel(points[points.length-1], points[points.length-2], headLength)); res.push(this.calcVertical(points[points.length-2], points[points.length-1], headWidth, true)); for(var i=points.length-1; i>0; i--) { res.push(this.calcVertical(points[i], points[i-1], Math.round((headWidth - tailWidth) * (i - 1) / (points.length - 1) + tailWidth), false)); } } return res;
还有很多人会好奇,一刀把水果切成两半是怎么实现的?其实你看一下图片资源就知道了。图片中每一个水果都配有两个半片的水果,只要计算一下切过去的角度,然后把原来水果消失,把两半的水果贴上去,再把速度和加速度调整一下,就实现了水果切成两半的效果。大概像这样:
// 两半 halfOne = game.add.sprite(sprite.body.x + sprite.width/2, sprite.body.y + sprite.height/2, env.key + '-1'); halfOne.anchor.setTo(0.5, 0.5); halfOne.rotation = deg + 45; game.physics.enable(halfOne, Phaser.Physics.ARCADE); halfOne.body.velocity.x = 100 + sprite.body.velocity.x; halfOne.body.velocity.y = sprite.body.velocity.y; halfOne.body.gravity.y = 2000; halfOne.checkWorldBounds = true; halfOne.outOfBoundsKill = true; halfTwo = game.add.sprite(sprite.body.x + sprite.width/2, sprite.body.y + sprite.height/2, env.key + '-2'); halfTwo.anchor.setTo(0.5, 0.5); halfTwo.rotation = deg + 45; game.physics.enable(halfTwo, Phaser.Physics.ARCADE); halfTwo.body.velocity.x = -100 + sprite.body.velocity.x; halfTwo.body.velocity.y = sprite.body.velocity.y; halfTwo.body.gravity.y = 2000; halfTwo.checkWorldBounds = true; halfTwo.outOfBoundsKill = true; sprite.kill();
其实到这里,都还没有意识到,需要做一点封装了,知道做到下一个场景。
在这个场景中,很多东西其实和主菜单场景类似。比如刀光,水果被切成两半,炸弹。所以这个时候,我开始去封装一些东西,我把水果,炸弹,刀光代码都抽出来,封装成一些类。这样用起来确实方便很多了。
其实在这个场景中,基本动画就不说了,最关键的就是随机产生水果和炸弹,还要注意产生的范围,上抛的速度和方向,这些东西,就要大量用到随机数,熟练了之后也很简单,具体可看代码。
最后一个点就是炸弹爆炸时的动画,这个动画是用tween的chain功能来实现的。先随机一个初始角度,然后每隔45度增加一个光线,先把graphic画出来,但是设置透明度为0,通过tween动画将透明度设为1,就实现了光线一道一道出现的效果。当然光线数组设置好了之后,还要shuffle一下。
var explode = function(onWhite, onComplete) { var lights = []; var startDeg = Math.floor(Math.random() * 360); for(var i=0; i<8; i++) { var light = game.add.graphics(sprite.body.x, sprite.body.y); var points = []; points[0] = new Phaser.Point(0, 0); points[1] = new Phaser.Point(Math.floor(800*mathTool.degCos(startDeg + i*45)), Math.floor(800*mathTool.degSin(startDeg + i*45))); points[2] = new Phaser.Point(Math.floor(800*mathTool.degCos(startDeg + i*45 + 10)), Math.floor(800*mathTool.degSin(startDeg + i*45 + 10))); light.beginFill(0xffffff); light.drawPolygon(points); light.endFill(); light.alpha = 0; lights.push(light); } lights = mathTool.shuffle(lights); var firstTween; var lastTween; for(var i=0; i<8; i++) { var light = lights[i]; var tween = game.add.tween(light).to({alpha: 1}, 100, "Linear", false); if(i == 0) { firstTween = tween; } if(lastTween) { lastTween.chain(tween); } lastTween = tween; if(i == 7) { tween.onComplete.add(function() { var whiteScreen = game.add.graphics(0, 0); whiteScreen.beginFill(0xffffff); whiteScreen.drawRect(0, 0, game.width, game.height); whiteScreen.endFill(); whiteScreen.alpha = 0; var tween = game.add.tween(whiteScreen).to({alpha: 1}, 100, "Linear", true); // 开始和结束的回调 tween.onComplete.add(function() { onWhite(); for(var i=0; i<8; i++) { var light = lights[i]; light.kill(); } var tweenBack = game.add.tween(whiteScreen).to({alpha: 0}, 100, "Linear", true); tweenBack.onComplete.add(function() { onComplete(); }); }); }); } } firstTween.start(); };
最后还有一个坑要注意,在GAMEOVER之后,我们点一下鼠标又能回到主菜单场景,但是这时候并不是开了一个新的对象,而是用的原来内存里的对象,也就是主菜单场景里面的一些属性,还会是跳转到play场景时的值,所以在跳转前,需要reset一下。否则可能会有意想不到的结果。
好了,整个水果忍者游戏大概就介绍到这里,有兴趣的朋友可以去翻看源码。当然,这个游戏和原版的还是有一些区别的,有的功能还不完善,期待你来改吧。