今天跟大家分享一个热门游戏的核心玩法实现,那就是曾经很火的超级解压馆。超级解压馆的实现思路跟纸牌接龙有点像,为什么这么说,发牌这个逻辑基本差不多,给N个位置发牌,牌也可以挪动,但都有条件规则限制。然后是合成,颜色对了,数量满足条件就消除。而纸牌接龙则是花色一条龙即可完成目标。要说这个游戏的主要爽点在于音效和振动以及卡牌的收发动效,真是戳到了玩家的爽点。
本期就来深度拆解超级解压馆实现逻辑。还是老规矩,按模块分解游戏玩法。那么实现引擎还是Cocos Creator。这个游戏的核心有以下几个组成部分:
-
槽位
-
卡牌
-
发牌和合并卡牌
1、槽位
一共12个槽位,三行四列,当然这个你可以自己决定多少。这种规则布局本能反应就是要用Cocos Creator的Layout组件的Grid布局来实现。那么槽位当然是要抽象为预制体Prefab,方便复用逻辑。每个槽位都有自己的坐标,那么发牌就是给槽位添加卡牌,发到对应的槽位就行,那重点是槽位预制体的实现。如下图:卡槽有四个子节点,lock锁的标志,videoAd视频广告标志,video是否可以合并标志,cards存放卡牌容器。slot本身负责根据状态切换纹理:stateFrame。
槽位属性状态
-
已解锁,还可以细分已满,未满。
-
未解锁,未解锁还包含看广告解锁,远期可解锁,每合并升级一个等级解锁一个槽位。
超级解压馆设计每个槽位能放10个卡牌,我也照抄。这里给槽位一个cards属性,卡牌的设计是先进后出,天然适合用栈实现。每个卡片Y坐标错开一定的像素才能有堆叠效果,取决于卡牌的大小,这里设定为18像素。为了方便坐标统一方向,这里把槽位的Y方向锚点设置为1,子元素从顶部开始放置。
槽位的几个方法
点击选中
选中槽位卡牌,遍历栈,遇到颜色不一致的卡牌就停止。设置当前选中的卡牌状态动画。标记槽位为选中状态。
// 选择卡片
selectCards(){
let tempCards = []
let lastCardNum = null
for(let i = this.cards.length - 1; i >= 0; i--){
let card = this.cards[i]
// 卡牌颜色类型相等,或者有空槽的情况
if(lastCardNum === null || lastCardNum === card.getComponent(Card).getNum()){
tempCards.push(card)
lastCardNum = card.getComponent(Card).getNum()
}else{
break; // 遇到不一样的,中断循环
}
}
tempCards.forEach(node => {
node._originY = node.y; // 临时存储,因为选择后可能又不选择,要还原位置
node.y += 10;
this.setSelectAnimation(node); // 上下浮动的动画
})
this.tempSelectCards = tempCards;
}
看视频效果:
https://github.com/iamaddy/minigame-developer/assets/3387191/466ccdbe-dbe6-4166-9467-1be05bf66dbb
入栈
往cards中push卡牌,并且新增卡牌的Y坐标新增18像素。
addCards(position, cardType, config, finishCallback){
let card = cc.instantiate(this.cardPrefab) // 卡牌预制体
let cardScript = card.getComponent(Card)
cardScript.setNum(cardType); // 设置卡牌类型
card.parent = this.cardsNode; // 入槽
cardScript._originzIndex = card.zIndex;
card.zIndex = 1000;// 暂时提高层级,可能存在遮挡卡牌
let targetPosition = cc.v2({ x: 0, y: config.targetY })
// 很关键,坐标转换
let localPos = this.cardsNode.convertToNodeSpaceAR(position);
card.setPosition(localPos);
let moveDuration = .5
let moveto = cc.moveTo(moveDuration, targetPosition);
let delayAction = cc.delayTime(config.delay);
// 创建一个回调动作,当动作执行完毕后调用指定的回调函数
let callbackAction = cc.callFunc(() => {
this.cards.push(card)
card.zIndex = cardScript._originzIndex;
finishCallback && finishCallback();
}, this);
// 将两个动作按顺序执行
let sequenceAction = cc.sequence(delayAction, moveto , callbackAction);
card.runAction(sequenceAction);
}
比较核心的一点就是移动的动画,涉及到坐标转化。因为这里card已经指定parent为cardsNode,那card起始坐标是从发牌按钮开始。那么可以先获取发牌按钮的世界坐标,再将这个世界坐标转到cardsNode的本地坐标,即可完成从发牌点到卡槽位置的移动。
let position = this.addNewCardsButton.parent.convertToWorldSpaceAR(this.addNewCardsButton.position)
看视频效果:
https://github.com/iamaddy/minigame-developer/assets/3387191/b1978411-7616-4ace-8075-3ca2e4ceed2a
出栈
cards数组pop出来,从槽位移出,放入新的槽位。这里的逻辑比较简单,就是临时选中的卡牌依次出栈就行。
卡牌移动
最复杂的逻辑是卡牌的移动,如果已经选中其他槽位,并且点击的目标槽位顶部卡牌颜色匹配则可以移动,否则不可移动。移动的逻辑如下,旧的槽位选中卡牌出栈,并且在新的槽位入栈。这里可以加一个动画,每个卡牌延迟一定时间移动,设置一个移动的Action,目标位置可以通过转换成世界坐标计算得到。
checkCanMove(){
// 遍历槽位,找出可以合并的
let allSlotNodes = this.node.parent.children
for(let i = 0; i < allSlotNodes.length; i++){
if(this.node === allSlotNodes[i]){
continue
}
let slotCom = allSlotNodes[i].getComponent(Slot);
// 卡槽是否选中
if(slotCom.isSelect){
let lastCardNum = this.getLastCardNum()
// 颜色要一样
if(slotCom.getLastCardNum() === lastCardNum ||
lastCardNum === -1){
// 已经满了,也不能移动
if(this.cards.length >= this.totalCardCount){
return false
}
// 真正的移动逻辑
this.moveCardsToNewSlot(allSlotNodes[i])
return true
}else{
// shake error
}
}
}
return false
}
moveCardsToNewSlot(originSlotNode){
// originSlotNode是待移动的槽位
let slot = originSlotNode.getComponent(Slot);
let cardLen = this.cards.length;
let targetY = cardLen ? this.cards[cardLen - 1].position.y : this.startY;
let index = 0;
let delay = 0.05
let len = slot.tempSelectCards.length
// 待移动的槽位层级要最高,否则会出现遮挡
slot._originzIndex = originSlotNode.zIndex;
originSlotNode.zIndex = 1000;
// 能移动的卡牌的数量
let canMoveCardCount = this.totalCardCount - cardLen;
// 加起来的数量不能超出总数
while(slot.tempSelectCards.length &&
index < canMoveCardCount){
// 出栈
let card = slot.tempSelectCards.shift();
card.stopAllActions() // 停止之前的上下浮动动画
let y = targetY - (len - index) * this.offsetY; // 目标位置
let that = this;
(function(index){
// 开始移动
that.moveAction(card, new cc.Vec2(0, y), delay * index, () => {
// 全部移除后还原槽位的zIndex
if(index + 1 === len || index + 1 === canMoveCardCount){
originSlotNode.zIndex = slot._originzIndex;
}
})
})(index);
index++;
// 删除卡牌
slot.cards.splice(slot.cards.indexOf(card), 1)
}
slot.isSelect = false;
// 如果tempSelectCards有剩余,也恢复状态
slot.tempSelectCards.forEach(node => {
node.stopAllActions()
node.y = node._originY;
})
// 清空tempSelectCards
slot.tempSelectCards = [];
}
https://github.com/iamaddy/minigame-developer/assets/3387191/bfb029f6-21c7-4ff0-adab-c7f00758191e
2、卡牌预制体
卡牌相对简单一点,有10种类型,1-10,用不同的颜色表示即可,提供一个setNum和getNum的方法。
getNum(){
return this.num
}
setNum(num){
this.num = num
// 同时改变纹理
this.numSprite.spriteFrame = this.numSpriteFrames[num - 1]
this.node.getComponent(cc.Sprite).spriteFrame = this.cardSpriteFrames[num - 1]
}
3、卡牌发放/合并逻辑
遍历已经解锁的槽位,判断当前槽位的空间长度,根据剩余可用长度随机派发1-2个卡牌。这里也用到了卡牌预制体,实例化后将从发放按钮位置移动到具体的卡槽位置。起点是按钮的世界作为转为槽位的本地坐标,终点是槽位顶部的位置加上偏移量,动画完成后完成入栈,这样就完成了发牌逻辑。
合并逻辑就更简单,选中槽位,如果满足10个颜色一致,就可以进行合并,销毁已有的卡牌,然后合并成2个下一等级的卡牌。加上一个动画效果,每个卡牌延迟0.1秒消除,配合音效和震动,给用户增加解压爽点。看视频效果
https://github.com/iamaddy/minigame-developer/assets/3387191/88ee042f-e0e0-46b1-90b9-1c4be08cccca
mergeCards(){
let children = this.cardsNode.children;
// 不满足
if(children.length < 10){
return
}
// 颜色存在不一样的
let firstNum = children[0].getComponent(Card).getNum();
for(let i = 1; i < children.length; i++){
if(children[i].getComponent(Card).getNum() !== firstNum){
return
}
}
// 播放音频
cc.audioEngine.playEffect(this.mergeAudioClip, false);
// 隔一定时间销毁一个子节点
let delay = 0.05 * children.length
let moveTime = 0.01
for(let i = 0; i < children.length; i++){
let move = cc.scaleTo(moveTime, 0);
let delayAction = cc.delayTime(delay - 0.05 * i);
let child = children[i]
let callbackAction = cc.callFunc(() => {
child.destroy();
}, this);
let sequenceAction = cc.sequence(delayAction, move, callbackAction);
child.runAction(sequenceAction);
}
// 还原状态
setTimeout(() => {
this.cardsNode.removeAllChildren()
this.cards = []
this.tempSelectCards = []
this.isSelect = false;
}, (delay + moveTime * children.length) * 1000)
}
核心玩法到这里就差不多了,一共400行代码可以完成,可以扫码体验下,也可以点击链接。剩下的就是解锁更多槽位的辅助逻辑,有兴趣的可以自己去探索。
欢迎关注我的公众号,获取更多游戏开发知识和游戏源码,手把手教你做游戏。