在前面完成了基本的各种功能逻辑编写后,剩下的就主要是游戏得分的相关控制,以及新加了切水果时的刀划过的效果,炸弹爆炸时的光线、白屏效果等等。另外在文章最后面,放上了游戏所有代码以及下载链接。
首先这是整体水果切开的效果gif图。(mac下用腾讯的jietu来制作的gif,感觉还是蛮舒适好用的。)
主要实现也比较简单,就是在切开的同时,判断一下划过的角度,再放上图片,让其旋转就可以了。水果被切开时的相关代码如下:
// start.js
onTouchMove(event){
//...
// 刀光以及相关audio
let lenV = this.curent_pos.sub(this.prev_pos).mag();
let roateV = 0;
let falsh_angle = this.curent_pos.sub(this.prev_pos).signAngle(cc.v2(1,0)) / Math.PI * 180;
if(lenV > this.knife_height){
let tempVec = cc.v2(0,10)
roateV = this.curent_pos.sub(this.prev_pos).signAngle(tempVec) / Math.PI * 180
//
let end_pooledKnife = this.getPooledKnife();
if(end_pooledKnife != null){
this.node.addChild(end_pooledKnife);
end_pooledKnife.height = lenV;
end_pooledKnife.setPosition((this.sx+this.ex)/2-this.node_w/2,(this.sy+this.ey)/2-this.node_h/2);
end_pooledKnife.angle = -roateV;
}
this.prev_pos = this.curent_pos;
this.sx = this.ex;
this.sy = this.ey;
if(!this.throw_audio_id || this.throw_audio_id && cc.audioEngine.getState(this.throw_audio_id)!=1)
this.throw_audio_id = cc.audioEngine.playEffect(this.throw_audio,false);
}
//...
// 对水果和炸弹被切开时的处理
this.pooled_fruits.forEach(e => {
if(e && !e.isAvailable
&& e.fruit_all!=null
&& (( e.fruit_all.getComponent('cc.PolygonCollider') && cc.Intersection.pointInPolygon(this.curent_pos, e.fruit_all.getComponent('cc.PolygonCollider').world.points) )
|| ( e.fruit_all.getComponent('cc.CircleCollider') && cc.Intersection.polygonCircle([this.curent_pos], e.fruit_all.getComponent('cc.CircleCollider').world) ))){
// cc.log("hit");
if(e.fruit_type != "boom"){
cc.audioEngine.playEffect(this.splatter_audio,false);
this.game_score++;
this.game_start_need[this.game_start_need_name.indexOf('score_label')].getComponent('cc.Label').string = this.game_score;
var flash = cc.instantiate(this.flash_prefab);
flash.position = e.fruit_all.position;
flash.rotation = falsh_angle;
this.node.addChild(flash);
setTimeout(function(){
this.node.removeChild(flash);
}.bind(this),100);
e.apart();
}
else{
e.apart();
this.boomIsBoom(e,e.fruit_all.x + this.node_w/2,e.fruit_all.y + this.node_h/2);
}
}
});
//...
}
首先是先计算下切开时刀划过的角度(方向),同之前所说的刀光制作的原理一样,通过位置向量相减的方式,再求出与水平向量的夹角角度即可。
接着是处理切开碰撞,这里面之所以有两个判断碰撞的条件,是因为我对sandia和banana水果是加了多边形的碰撞组件,其它的是用圆形碰撞组件,所以写了两个,不过我看cocos源码中的圆形碰撞组件的碰撞处理其实也是通过pointInPolygon函数来处理的,所以也可以自己整理写一个。还有就是cocos中的angle和rotation好像是相反的,写的时候要注意一下。
然后就是炸弹爆炸时的特效了,也就是上面代码中的this.boomIsBoom(),做出来大概长这样:
主要实现是通过setTimeout函数,先生成炸弹的光线,然后再生成一片透明度逐渐降低的白色遮罩。相关代码如下:
//start.js
//cc Class 类里面
cc.Class({
// onload update onTouchMove ...
// ...
//boom light 用于下面的第一种方法
createLight(x,y,every_angle,index){
let light_len = 1074;
let light_left_angle = Math.PI * (index * every_angle - 2.5) / 180;
let light_right_angle = Math.PI * (index * every_angle + 2.5) / 180;
let light_left_x = x + light_len * Math.cos(light_left_angle);
let light_left_y = y + light_len * Math.sin(light_left_angle);
let light_right_x = x + light_len * Math.cos(light_right_angle);
let light_right_y = y + light_len * Math.sin(light_right_angle);
// cc.log(x,y);
let paint = cc.instantiate(this.paint)
this.node.addChild(paint);
let g = paint.getComponent(cc.Graphics);
cc.log(index);
// cc.log(g);
// cc.log(light_left_x,light_left_y);
g.moveTo(x,y);
g.lineTo(light_left_x,light_left_y);
g.lineTo(light_right_x,light_right_y);
g.close();
g.stroke();
g.fillColor = new cc.Color().fromHEX("ffffff");
g.fill();
},
boomIsBoom(fruit,x,y){
this.node.off(cc.Node.EventType.TOUCH_START, this.onTouchStart, this, true);
this.node.off(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this, true);
this.unscheduleAllCallbacks();
let num_light = 10;
let light_array = [];
for(var i=0;i
在这里面,首先是建立一个数组通过随机排序存放这10条光线的生成顺序,接着通过定时器setTimeout来逐条画出来。这里涉及到一些javascript的知识,像是bind的使用,给回调函数传参,变量作用域等等,这里我给了两种方法参考(上面代码注释部分)。
第一种方法是将setTimeout函数写进一个新的函数set_time_func()中,根据你要使用到的变量给这个新的函数的设置形参,同时要在外面设置start_this 存放外面的this对象(脚本start.js的this,类似于在furit类里面存放start.js的引用),接着用bind方法来指定。
// 第一种方法:
let start_this = this;
function set_time_func(x,y,light_array,num){
setTimeout(function(){
cc.log(x,y)
this.createLight(x,y,360/num_light,light_array[num]);
}.bind(start_this),time+=100);
}
while(num--){
// 第一种方法:
set_time_func(x,y,light_array,num);
}
之所以要将setTimeout函数写进另一个函数中,不仅仅是为了参数的传递,同时涉及到一些变量作用域的问题,比如如果像下面这样写:
// wrong
while(num--){
setTimeout(function(){
this.createLight(x,y,360/num_light,light_array[ num ]);
}.bind(this),time+=110);
}
那么实际上,真正传进this.createLight里的num实际上都等于-1,因为此时的num对setTimout里的函数来讲,相当于是全局变量,由于while肯定会先一步全部跑完,这时候再传进this.createLight里面的num就只会是跑完后的-1了。
因此也有第二种处理方法,将num作为this,来绑定到setTimeout的回调函数中。这样,每个num值(从10到0)都能被正确传进去。
// 第二种方法
let paint = cc.instantiate(this.paint);
this.node.addChild(paint);
while(num--){
cc.log(num);
setTimeout(function(){
createLight(x,y,360/num_light,light_array[ this ],paint);
}.bind(num),time+=110);
}
注意这里面的createLight前面没有this.,是因为它是在cc class类外面写的,因为bind了num的缘故,只能在外面作为全局函数来做。
这两种方法的creatLight方法实现逻辑是完全一样的,都是先计算每条光线边的位置、角度,再通过Graphics组件来完成绘制。
绘制光线之后,再处理整个屏幕的特效,在update中逐步降低其透明度来实现。
//start.js
update(dt){
// ...
// 当切到炸弹时的特效
if(this.boom_is_broken && this.blank_paint && this.blank_opacity>0){
var g = this.blank_paint.getComponent(cc.Graphics);
g.clear();
g.fillColor = cc.Color.WHITE.setA(this.blank_opacity-=255 * (dt/3));
g.fillRect(0,0,1000,1000);
}
//...
}
最后,就是游戏得分控制了,这里设置当水果漏掉3个或切到炸弹时就game over,因此,只需在start.js中设置一个变量存放当前漏掉的水果个数,以及设置变量代表是否切到炸弹即可,同时添加一个label组件用来存放当前得分。逻辑比较简单,这里不再介绍,结合代码看就行了。
那么整个水果忍者小游戏的制作流程就介绍完毕了,剩下的就是构建发布,以及一些细节优化等等。如果要在手机上调试运行的话,可能需要解决这些问题:
另外,mac上用safari调试或者运行的话,相关声音好像是出不来的,用chrome的话,要滑动一下才会开始有声音。
最后,附上游戏代码的github地址和下载链接:
github:https://github.com/1998y12/FruitNinja.git
下载地址:https://download.csdn.net/download/qq_41508508/12454969