作为一个游戏引擎,不能仅仅实现简单的游戏,这次挑战了水果忍者。水果忍者,游戏玩起来简单,实现起来有一定难度,因为很多效果,很多逻辑,很多元素,很多代码,不用面向对象的方法还真不行。
先来看一个效果吧!
怎么样?和真正的水果忍者有点像吧?再给一个体验地址,自己先体验一下: http://game.webxinxin测试数据/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; i0; 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 一下。否则可能会有意想不到的结果。
好了,整个水果忍者游戏大概就介绍到这里,有兴趣的朋友可以去翻看源码。当然,这个游戏和原版的还是有一些区别的,有的功能还不完善,期待你来改吧。
转载请注明出处:http://HdhCmsTestphaser-china测试数据/tutorial-detail-5.html