上一篇文章写了星星生成的逻辑,详情请看Cocos Creator开发游戏消灭星星——星星生成
写在消除星星之前
星星消除是发生在用户点击之后,所以需要处理用户触摸操作。在上一篇制作星星预制时有提及,在脚本组件starCtr.js的start函数里监听触摸。
start () {
this.node.on(cc.Node.EventType.TOUCH_START, function (event) {
//TODO:触摸处理
}, this);
},
消除星星是消除上下左右相连的星星,所以需要根据用户点击的星星找到其他相连的星星。在Utils中增加方法needRemoveList:
//Utils.js
//检测数组array中是否坐标p
function indexOfV2 (array, p) {
return array.some(function (elem, index, arr) {
return elem.x == p.x && elem.y == p.y
});
};
//根据矩阵数据查找消除的星星
function needRemoveList (data, p) {
var list = [];
var travelList = [];
travelList.push(p);
var tag = data[p.x][p.y];
do {
var any = travelList.pop();
//左
if (any.y - 1 >= 0 && tag == data[any.x][any.y-1]) {
var tp = cc.v2(any.x, any.y-1);
if (!this.indexOfV2(list, tp) && !this.indexOfV2(travelList, tp)) {
travelList.push(tp);
}
}
//右
if (any.y + 1 < Config.matrixCol && tag == data[any.x][any.y+1]) {
var tp = cc.v2(any.x, any.y+1);
if (!this.indexOfV2(list, tp) && !this.indexOfV2(travelList, tp)) {
travelList.push(tp);
}
}
//下
if (any.x - 1 >= 0 && tag == data[any.x-1][any.y]) {
var tp = cc.v2(any.x-1, any.y);
if (!this.indexOfV2(list, tp) && !this.indexOfV2(travelList, tp)) {
travelList.push(tp);
}
}
//上
if (any.x + 1 < Config.matrixRow && tag == data[any.x+1][any.y]) {
var tp = cc.v2(any.x+1, any.y);
if (!this.indexOfV2(list, tp) && !this.indexOfV2(travelList, tp)) {
travelList.push(tp);
}
}
list.push(any);
} while (travelList.length > 0);
return list;
};
消除星星
现在来完成触摸处理逻辑:
//starCtr.js
//触摸处理
var list = Utils.needRemoveList(GameData.starMatrix, cc.v2(this._gx, this._gy));
if (list.length >= 2) {
var event = new cc.Event.EventCustom("delete_stars", true);
event.detail = list;
this.node.dispatchEvent(event);
}
通过用户点击的星星坐标找到与其相连的星星们,然后发射delete_stars事件,通知地图消除星星。关于监听和发射时间参考官方文档监听和发射事件。
在matrixCtr.js的onLoad方法中添加事件监听
//matrixCtr.js
onLoad () {
this.node.on("delete_stars", this.deleteSprites, this);
//其他无关代码这里省略
},
onDestroy () {
this._starPool.clear();
this.node.off("delete_stars", this.deleteSprites, this); //移除监听
},
先添加几个属性来记录消除数据
//matrixCtr.js
properties: {
//其他无关属性这里省略
_totalCounts: 0, //总的消除星星个数
_currCount: 0, //已经消除的星星个数
_bombList: [], //存储待消除的星星的坐标
_tamping: false, //夯实数组的标记,就是消除星星后的地图处理(星星下落动画、整列星星左移动画等)
},
在回调函数中处理消除逻辑
//matrixCtr.js
// 监听回调函数
deleteSprites (event) {
if (this._tamping) {
return;
}
var bombList = event.detail;
//防止重复消除
if (Utils.indexOfV2(this._bombList, bombList[0])) {
return;
}
this._totalCounts += bombList.length;
this._bombList = this._bombList.concat(bombList);
this.showComboEffect(bombList.length);
GameData.cleanStarData(bombList);
this.bomb(bombList, bombList.length);
}
播放combo特效
上一篇说过,动画和特效主要放在节点ActionRoot中处理。如图,combo特效就在combNode节点中播放。
特效是用骨骼动画制作的,所以在combNode上添加dragonBones的渲染组件,同时,再添加脚本组件dragonBonesCtr来控制逻辑。
combNode节点需要播放不同的动画,所以组件中没有指定资源,这个在脚本中控制。看一下dragonBonesCtr.js的属性:
//dragonBonesCtr.js
properties: {
asset: {
default: [],
type: dragonBones.DragonBonesAsset,
},
atlasAsset: {
default: [],
type: dragonBones.DragonBonesAtlasAsset,
},
combName: {
default: [],
type: cc.String,
},
_anim: dragonBones.ArmatureDisplay,
},
asset、atlasAsset分别存储骨骼动画资源,combName中存储骨骼动画的名字,和资源数组一一对应,_anim是dragonBones组件。
//dragonBonesCtr.js
onLoad () {
this._anim = this.node.getComponent(dragonBones.ArmatureDisplay);
},
playComb (type) {
var i = this.combName.indexOf(type);
if (i >= 0) {
this._anim.dragonAsset = this.asset[I];
this._anim.dragonAtlasAsset = this.atlasAsset[I];
this._anim.armatureName = "armatureName";
this._anim.playAnimation("Animation1");
}
},
playComb即是播放特效的方法。
//matrixCtr.js
showComboEffect (count) {
if (count == 5) {
this.combCtr.playComb("GOOD");
}
else if (count >= 6 && count <= 7) {
this.combCtr.playComb("NICE");
}
else if (count >= 8 && count <= 9) {
this.combCtr.playComb("EXCELLENT");
}
else if (count >= 10) {
this.combCtr.playComb("UNBELIEVABLE");
}
},
combCtr是脚本组件matrixCtr中的属性,即是场景中ActionRoot节点的脚本组件。
//matrixCtr.js
properties: {
//省略其他属性
combCtr: cc.Node,
},
onLoad () {
//其他无关代码这里省略
this.combCtr = this.combCtr.getComponent("dragonBonesCtr");
},
数据处理
将需要消除的星星对应的坐标清空(赋值-1)
//gamedata.js
//清除星星
function cleanStarData (list) {
list.forEach(function (elem, index, arr) {
this.starMatrix[elem.x][elem.y] = -1;
}, this);
};
消除星星
按规则星星是一个一个消除的,所以bomb会递归调用,直到所有星星都消除。在消除星星的同时,有分数计算和动画逻辑。
//matrixCtr.js
bomb (list, count) {
if (list.length > 0) {
var gridPos = list.shift();
var index = Utils.indexValue(gridPos.x, gridPos.y);
this.bombStar(GameData.starSprite[index]);
GameData.starSprite[index] = null;
++this._currCount;
//单个方块的分数动画
var wp = this.convertGridPositionToWorldSpaceAR(gridPos);
var starScore = Utils.getOneScore(count-list.length);
this.actCtr.playSingleScoreAction(starScore, wp);
this.scheduleOnce(function () {
this.bomb(list, count);
}, 0.1);
if (list.length == 0) {
//消除总得分动画
var wp = this.convertGridPositionToWorldSpaceAR(gridPos);
this.actCtr.showScoreAction(Utils.getScore(count), wp);
this.uiCtr.updateScoreSchedule(starScore); //当前总分滚动累计效果
}
else {
this.uiCtr.updateScore(starScore);
}
this.checkIsSuccessed(); //检测是否达到目标分
}
else {
//TODO: 星星消除完的逻辑处理
}
},
星星的移除是在方法bombStar中处理的,在创建星星的时候使用了对象池,所以移除时把它重新放入对象池。
//matrixCtr.js
bombStar (node) {
if (node) {
var p = node.getPosition();
var type = node.getComponent("starCtr")._starType;
this._starPool.put(node); //移除星星,把它放入对象池中
// 星星爆炸动画
var particle = cc.instantiate(this.starParticle);
particle.setPosition(p);
this.node.addChild(particle);
particle.getComponent("particleCtr").init(type);
}
},
在移除星星的同时,伴随有星星爆炸的特效。starParticle是一个预制,层级很简单,在一个空节点中,添加Particle System组件和脚本组件particleCtr。
starSpriteFrames中存储了粒子系统使用的纹理资源,对应每一种星星。
//particleCtr.js
properties: {
particle: cc.ParticleSystem,
starSpriteFrames: {
default: [],
type: cc.SpriteFrame,
},
},
init (type) {
this.particle.spriteFrame = this.starSpriteFrames[type];
this.particle.resetSystem();
},
Particle System组件设置自动移除,在属性检查器中勾选Auto Remove On Finish选项。
分数计算
我们知道一次消除星星方块越多,得分越高。
//Utils.js
//消除得分计算
function getScore (count) {
var score = 0;
for(var i = 0; i < count; i++) {
score += this.getOneScore(i);
}
return score;
};
//消除第i个方块的分数
function getOneScore (i) {
return 10 + (i-1) * 5;
};
分数动画有几种:
- 单个方块的分数动画
- 消除总得分动画
- 当前总分有滚动累计的效果
//matrixCtr.js
//格子的世界坐标
convertGridPositionToWorldSpaceAR (gp) {
var p = Utils.grid2Pos(gp.x, gp.y);
var wp = this.node.convertToWorldSpaceAR(p);
return wp;
},
动画在actionCtr.js中处理:
//actionCtr.js
//单个方块的分数动画
playSingleScoreAction (score, wp) {
var label = null;
if (this._pScorePool.size() > 0) {
label = this._pScorePool.get();
}
else {
label = cc.instantiate(this.partScore);
}
label.getComponent("partScore").setScore(score);
this.node.addChild(label);
label.setPosition(this.convertPosition(wp));
var action = cc.spawn(cc.scaleTo(0.5, 0.4), cc.moveTo(0.5, this.refrencePoint.getPosition()));
label.runAction(cc.sequence(action, cc.delayTime(0.2), cc.callFunc(function(){
this._pScorePool.put(label);
}, this)));
},
// 世界坐标转成当前节点中的坐标
convertPosition (wp) {
return this.node.convertToNodeSpaceAR(wp);
},
因为分数也会被频繁的创建和移除,所以也使用了对象池,分数的预制制作后面介绍。
//actionCtr.js
properties: {
// 省略其他无关属性
totalScore: cc.Prefab,
_tScorePool: null,
partScore: cc.Prefab,
_pScorePool: null,
poolCapacity: 30,
},
onLoad () {
this._pScorePool = new cc.NodePool();
for (var i = 0; i < this.poolCapacity; ++i) {
var partscore = cc.instantiate(this.partScore);
this._pScorePool.put(partscore);
}
this._tScorePool = new cc.NodePool();
for (var i = 0; i < 3; ++i) {
var totalscore = cc.instantiate(this.totalScore);
this._tScorePool.put(totalscore);
}
},
与单个方块的分数动画一样,消除总得分动画:
//actionCtr.js
//消除总得分动画
showScoreAction (score, wp) {
var node = null;
if (this._tScorePool.size() > 0) {
node = this._tScorePool.get();
}
else {
node = cc.instantiate(this.totalScore);
}
this.node.addChild(node);
node.getComponent("totalScore").setScore(score);
var lw = node.getComponent("totalScore").scoreLabel.node.width;
var ddd = lw * 0.5 * 1.2;
var eddd = ddd - cc.winSize.width*0.5;
var p = this.convertPosition(wp);
if (p.x < eddd) {
p.x = eddd;
}
if (p.x > cc.winSize.width*0.5 - ddd) {
p.x = cc.winSize.width*0.5 - ddd;
}
node.setPosition(p);
},
分数预制
层级结构很简单,都是空节点下加一个Label节点。父节点上都有一个脚本组件partScore、totalScore。
//partScore.js
properties: {
scoreLabel: cc.Label,
},
setScore (score) {
this.scoreLabel.string = score;
},
脚本也很简单,setScore方法给Label赋值。
//totalScore.js
properties: {
scoreLabel: cc.Label,
},
setScore (score) {
this.scoreLabel.string = "+"+score;
this.playAnim();
},
playAnim () {
var anim = this.scoreLabel.getComponent(cc.Animation);
anim.play();
},
与单个分数不同的,总得分的Label动画使用Creator的Animation编辑器制作。所以,预制中需要在节点label中添加Animation组件,在这里我们在添加一个脚本组件totalScoreLabel,这个脚本主要处理Animation动画的事件回调方法。