前言
很久没更这个系列,其实是我发现在国内如果想要运营发布游戏不是那么简单的事情,需要有公司并且去申请运营资格,如果要有收费内容还需要申请版号。作为一个独立开发者,可能很难做到这些,所以前段时间有些灰心,不太提的起劲做这个项目。
不过最近也想通了,最初这个项目也不是以运营为目的做的,单纯的分享讨论技术也是快乐的。
线上地址:http://cardgame.xiejingyang.com
github:https://github.com/xieisabug/card-game
这次实现的效果代码很多,推荐大家看视频:https://www.bilibili.com/video/av73030461,和视频结合博客一起看会比较直观。
开始
第一个实现的功能是断线重连,这是个实现简单但是作用巨大的功能,尤其是在网页游戏中更甚,防止了网页刷新、电脑断电、浏览器崩溃、网络断线等带来的影响。
断线重连的思路是使用userId去查询当前用户有没有已经存在的对局,如果有,直接加入到当前对局。
在之前的代码handler.js中,有个existUserGameRoomMap变量,这个变量保存了每个用户的userId对应的roomNumber,所以,只要在connect的函数中加上一段判断:
if (existUserGameRoomMap[userId]) { // 如果存在已经加入的对局,则直接进入之前的对战 let roomNumber = existUserGameRoomMap[userId]; let identity = memoryData[roomNumber]["one"].userId === userId ? "one" : "two"; memoryData[roomNumber][identity].socket = socket; memoryData[roomNumber][identity].socket.emit("RECONNECT", { roomNumber: roomNumber, memberId: identity }); sendCards(roomNumber, identity); // 把牌发送给玩家 } else { // ...之前的操作 }
这样就完成了断线重连。
第二个实现的功能是伤害,也就是在攻击的时候,进行伤害和血量的计算,这件事听起来比较简单,但是比断线重连要复杂很多:
- 要处理是否有嘲讽。
- 要处理圣盾,圣盾不扣血,只掉圣盾的状态。
- 要处理事件,比如当攻击时触发的特效。
- 要处理死亡卡牌,从桌面上去除掉。
- 要处理亡语导致的卡牌递归死亡。
首先要在桌面展示两张可攻击的牌,为了方便开发,就在开局初始化卡牌的时候随机给双方派一张牌,在之前的initCard
方法中,为双方初始化手牌的时候,同时初始化桌面牌:
Object.assign(memoryData[roomNumber][first], { tableCards:[ getNextCard(firstRemainingCards), // 记得后面要删除 ], cards: [ getNextCard(firstRemainingCards), getNextCard(firstRemainingCards), ] }); Object.assign(memoryData[roomNumber][second], { tableCards:[ getNextCard(secondRemainingCards), // 记得后面要删除 ], cards: [ getNextCard(secondRemainingCards), ] });
初始化完成桌面卡牌之后,修改sendCards
函数,在发送到客户端的数据中,添加桌面卡牌的数据:
function sendCards(roomNumber, identity) { if (identity) { let otherIdentity = identity === "one" ? "two" : "one"; memoryData[roomNumber][identity].socket.emit("SEND_CARD", { myCard: memoryData[roomNumber][identity]["cards"], myTableCard: memoryData[roomNumber][identity]["tableCards"], // 双方的桌面卡牌也一并发送 otherTableCard: memoryData[roomNumber][otherIdentity]["tableCards"], }) } else { sendCards(roomNumber, "one"); sendCards(roomNumber, "two"); } }
接下来在客户端处理桌面卡牌的显示,在客户端文件GameTable.vue
中,在class为other-card-area的dom里展示对方桌面卡牌,在class为my-card-area的dom里展示我方桌面卡牌:
<div class="table"> <div class="other-card-area"> <Card :key="c.k" :index="index" :data="c" v-for="(c, index) in gameData.otherTableCard" /> </div> <div class="my-card-area"> <Card :key="c.k" :index="index" :data="c" @onAttackStart="onAttackStart" v-for="(c, index) in gameData.myTableCard" /> </div> </div>
完善之前的代码,打开Card.vue
,以前在最外层我们是用index做为dom的dataset,但是现在最好改为k,因为k是整个对局中标记这张卡牌的唯一值,以后我们标记卡牌最好也都使用k。
<div class="card" @mousedown="mouseDown($event)" :data-k="data.k">
同时在mouseDown里,还要将当前index发送给外部,让外部能够获取到对应的卡牌:
mouseDown(e) { this.$emit('onAttackStart', { startX: e.pageX, startY: e.pageY, index: this.index }) }
对应的,在GameTable.vue
中,onAttackStart接收对应的index,并且保存起来:
onAttackStart({startX, startY, index}) { this.showCanvas = true; window.isAttackDrag = true; this.attackStartX = startX; this.attackStartY = startY; this.currentTableCardK = this.gameData.myTableCard[index].k; // 将k保存 },
在到之前onmouseup
事件中,修改之前为了测试方便而写的attackCard
相关代码:
if (x > left && x < (left + width) && y > top && y < (top + height)) { // 边缘检测 k = cd.dataset.k; // 修改之前的index,改为k // this.attackAnimate(0, k); this.attackCard(k); }
attackCard
中的参数也修改:
attackCard(k) { this.socket.emit("COMMAND", { type: "ATTACK_CARD", r: this.roomNumber, myK: this.currentTableCardK, // 改为真实的k attackK: k }) },
然后进行后端基础数据获取,确保拿到两张卡牌,保证后面的逻辑不出错:
let index = memoryData[roomNumber][belong]["tableCards"].findIndex(c => c.k === myK); let attackIndex = memoryData[roomNumber][other]["tableCards"].findIndex(c => c.k === attackK); if (index !== -1 && attackIndex !== -1 && memoryData[roomNumber][other]["tableCards"].length > attackIndex && memoryData[roomNumber][belong]["tableCards"].length > index) { card = memoryData[roomNumber][belong]["tableCards"][index]; attackCard = memoryData[roomNumber][other]["tableCards"][attackIndex]; // 后面从这里继续逻辑 }
接着判断嘲讽,思路是看看桌上的卡牌有没有带嘲讽的,然后看自己攻击的卡牌是不是带嘲讽,如果桌上有嘲讽的卡牌,而攻击的不是带嘲讽的卡牌,那么攻击应该就是无效的,代码很简单:
let hasDedication = memoryData[roomNumber][other]["tableCards"].some(c => c.isDedication); if (attackCard.isDedication || !hasDedication) { // 做我们攻击的其他逻辑 } else { // error 您必须攻击带有奉献的单位 }
处理圣盾代码也比较简单,就是分别判断两张卡牌有没有圣盾,有圣盾的扣除圣盾状态,没有圣盾的扣血:
if (attackCard.isStrong) { // 强壮 attackCard.isStrong = false; } else { attackCard.life -= card.attack; } if (card.isStrong) { // 强壮 card.isStrong = false; } else { card.life -= attackCard.attack; }
然后就是处理事件了,因为之前没做过这样的卡牌,所以这里就只处理攻击和被攻击,然后做一张攻击特效和被攻击特效的卡牌试试。
处理事件其实就是在卡牌上写了回调函数,如果卡牌上有这个回调,就是带有这个事件,代码如下:
if (card.onAttack) { card.onAttack({ myGameData: memoryData[roomNumber][belong], otherGameData: memoryData[roomNumber][other], thisCard: card, beAttackedCard: attackCard, // specialMethod: getSpecialMethod(belong, roomNumber), }) } if (attackCard.onBeAttacked) { attackCard.onBeAttacked({ myGameData: memoryData[roomNumber][other], otherGameData: memoryData[roomNumber][belong], thisCard: attackCard, attackCard: card, // specialMethod: getSpecialMethod(other, roomNumber), }) }
这里传进去了相当多的参数,几乎是整个游戏的参数都放到了回调的方法中,这样能够保证我们在onAttack
这种类似的方法里,能够实现任何天马行空的效果。
大家还注意到了我注释了一行specialMethod
,这个specialMethod
是一系列快捷工具方法的集合,目前还没编写,后面会有的。
处理完事件,接下来要处理卡牌的死亡检查,思路是遍历整个桌面看看是否有血量到0或者以下的卡牌,如果有,则判定为死亡。遍历整个桌面是因为有可能有AOE技能导致大量怪物死亡。
for (let i = memoryData[roomNumber]["one"]["tableCards"].length - 1; i >= 0; i--) { let c = memoryData[roomNumber]["one"]["tableCards"][i]; if (c.life <= 0) { if (c.onEnd) { c.onEnd({ myGameData: memoryData[roomNumber]["one"], otherGameData: memoryData[roomNumber]["two"], thisCard: c, // specialMethod: oneSpecialMethod }); } memoryData[roomNumber]["one"]["tableCards"].splice(i, 1); } } for (let i = memoryData[roomNumber]["two"]["tableCards"].length - 1; i >= 0; i--) { let c = memoryData[roomNumber]["two"]["tableCards"][i]; if (c.life <= 0) { if (c.onEnd) { c.onEnd({ myGameData: memoryData[roomNumber]["two"], otherGameData: memoryData[roomNumber]["one"], thisCard: c, // specialMethod: twoSpecialMethod }); } memoryData[roomNumber]["two"]["tableCards"].splice(i, 1); } }
卡牌死亡需要发送到客户端,因为卡牌死亡在很多地方会用到,所以将这个方法封装到工具方法里比较好,那么就来编写一下刚刚一直出现的specialMethod
吧,封装一个获取对应对战房间和某个玩家的specialMethod
方法,在handler.js
中添加:
function getSpecialMethod(identity, roomNumber) { let otherIdentity = identity === "one" ? "two" : "one"; return { } }
向里面添加卡牌死亡的方法:
function getSpecialMethod(identity, roomNumber) { let otherIdentity = identity === "one" ? "two" : "one"; return { dieCardAnimation(isMine, myKList, otherKList) { memoryData[roomNumber][identity].socket.emit("DIE_CARD", { isMine, myKList, otherKList, myHero: extractHeroInfo(memoryData[roomNumber][identity]), otherHero: extractHeroInfo(memoryData[roomNumber][otherIdentity]) }); memoryData[roomNumber][otherIdentity].socket.emit("DIE_CARD", { isMine: !isMine, myKList: otherKList, otherKList: myKList, myHero: extractHeroInfo(memoryData[roomNumber][identity]), otherHero: extractHeroInfo(memoryData[roomNumber][otherIdentity]) }); }, } }
后续会向这个方法里面添加许多常用的工具方法,这些方法在回调的事件中也可以实现,但是因为非常频繁的使用,所以封装到一个工具方法集合里面,会提高很多以后制作卡牌的速度,之前代码里注释掉的specialMethod也可以去掉注释了。
目前还有问题,也就是刚刚说的第五点,当onEnd亡语触发的时候,有可能还会造成伤害,这个时候还得检查一次,但是死亡的怪物可能又会有亡语伤害,这个时候只能递归进行判断了,所以将这个卡牌死亡封装一个方法,进行递归调用。
/** * 检查卡片是否有死亡 * @param roomNumber 游戏房间 * @param level 递归层级 * @param myKList 我方死亡卡牌k值 * @param otherKList 对方死亡卡牌k值 */ function checkCardDieEvent(roomNumber, level, myKList, otherKList) { if (!level) { level = 1; myKList = []; otherKList = []; } if (memoryData[roomNumber]["one"]["tableCards"].some(c => c.life <= 0) || memoryData[roomNumber]["two"]["tableCards"].some(c => c.life <= 0)) { let oneSpecialMethod = getSpecialMethod("one", roomNumber), twoSpecialMethod = getSpecialMethod("two", roomNumber); for (let i = memoryData[roomNumber]["one"]["tableCards"].length - 1; i >= 0; i--) { let c = memoryData[roomNumber]["one"]["tableCards"][i]; if (c.life <= 0) { if (c.onEnd) { c.onEnd({ myGameData: memoryData[roomNumber]["one"], otherGameData: memoryData[roomNumber]["two"], thisCard: c, specialMethod: oneSpecialMethod }); } memoryData[roomNumber]["one"]["tableCards"].splice(i, 1); myKList.push(c.k); } } for (let i = memoryData[roomNumber]["two"]["tableCards"].length - 1; i >= 0; i--) { let c = memoryData[roomNumber]["two"]["tableCards"][i]; if (c.life <= 0) { if (c.onEnd) { c.onEnd({ myGameData: memoryData[roomNumber]["two"], otherGameData: memoryData[roomNumber]["one"], thisCard: c, specialMethod: twoSpecialMethod }); } memoryData[roomNumber]["two"]["tableCards"].splice(i, 1); otherKList.push(c.k); } } checkCardDieEvent(roomNumber, level + 1, myKList, otherKList); } if (level === 1 && (myKList.length !== 0 || otherKList.length !== 0)) { let oneSpecialMethod = getSpecialMethod("one", roomNumber); oneSpecialMethod.dieCardAnimation(true, myKList, otherKList); } }
这样判断卡牌死亡的方法算是完成了,在攻击之后调用checkCardDieEvent(roomNumber)就能完成进攻了。
不过之前发送给客户端的数据太少,不足以支撑客户端的展示,所以将更多的数据传送到客户端:
memoryData[roomNumber][belong].socket.emit("ATTACK_CARD", { index, attackIndex, attackType: AttackType.ATTACK, animationType: AttackAnimationType.NORMAL, // 为了日后不同的卡片攻击方式 card, attackCard }); memoryData[roomNumber][other].socket.emit("ATTACK_CARD", { index, attackIndex, attackType: AttackType.BE_ATTACKED, animationType: AttackAnimationType.NORMAL, card, attackCard });
接下来在客户端处理卡牌死亡事件,也就是DIE_CARD
:
const { isMine, myKList, otherKList } = param; let myCardList, otherCardList; if (isMine) { myCardList = thiz.gameData.myTableCard; otherCardList = thiz.gameData.otherTableCard; } else { myCardList = thiz.gameData.otherTableCard; otherCardList = thiz.gameData.myTableCard; } setTimeout(() => { myKList.forEach((k) => { let index = myCardList.findIndex(c => c.k === k); myCardList.splice(index, 1); }); otherKList.forEach((k) => { let index = otherCardList.findIndex(c => c.k === k); otherCardList.splice(index, 1); }); }, 920)
完整的代码太长,可以直接check项目下来看,不贴了。
总结
我还会继续往后面做这个游戏,毕竟倾注了很多心血在里面,也许哪天就真的注册公司申请运营了呢。
下次会分享下我最新制作的登录界面,然后做几张有特殊效果的牌,看看卡牌的特殊效果是什么制作的。
0 条评论