没错,就是你认为的那个三国杀(#滑稽)
当然,此次还是用vue来写。可能有点标题党,但是代码绝对简练,保证你一看就会
1.透过现象看本质
写代码是需要事先构思的,最开始的时候,我问了自己一个问题:
三国杀的本质是什么?
作为一个杀龄7年的老玩家,这个问题我整整想了一天,最终的答案是:
本质上这是一个牌和体力的游戏
无论你干了什么,发动了什么技能。最终的结果无非都是改变场上的牌和体力罢了
当然,这个游戏本质是牌和体力,但是不止牌和体力,还有一个很重要的元素,它就是游戏中各式各样的阶段 ,它是游戏的助推剂,没有阶段,游戏将根本无法进行
所以这篇文章将分为3节分别是:
1. 体力篇
2. 阶段篇(附带技能实现)
3. 牌篇
为什么牌放最后讲呢?因为牌的结算最为复杂,并且大部分牌都是在出牌阶段使用的,所以放在阶段后面讲。
2.准备工作
其实准备工作很少,就是类似这样,准备若干个玩家就行了
<div> // game.vue
// seat是玩家座位,类似于id来确保玩家唯一性
<player v-for="i in players" :name="i.name" :seat="i.seat" :hp="i.hp" :skills="i.skills">
</div>
<div> // player.vue
{{name}}
{{hp}}
</div>
3.从一张图开始
我们先来看一张伤害流程图(由易到难,杀的结算流程第三篇会讲)
可以发现,一个完整的伤害流程由4个事件构成。
好,发现是发现了,但是怎么用代码实现一个完整的伤害流程?
在vue里面,你可能会想,用$emit
和$on
来实现不就行了吗?
伤害来源通过$emit
一个伤害事件,这个事件目标可以用$on
来接受,这不是很简单吗?类似这样
// player.vue
this.$on('damage',e = >{
//当接受到伤害事件后,我就发动卖血技能,嘿嘿
})
但是仔细一想,假如是下面的场景:
神周瑜发动【业炎】,对司马,郭嘉,曹丕各造成了一点伤害
上面的场景中,需要先询问司马懿是否发动【反馈】,执行【反馈】之后,才能询问郭嘉是否发动【遗计】,并且还要等待郭嘉分牌才能结算曹丕等。
可以发现,通过$on
注册的事件,是不好处理异步函数的。它不能return
一个 Promise
,然后通过then
来继续结算当前事件。你或许想到使用callback
,但是callback
每一次都会不一样,并且会层层嵌套,让事件难以被理清。这种方法反而是费力不讨好。
4.事件池
于是我换了一种思路
改为创建一个事件池
//game.vue
eId:0, //用于区分事件池内事件流程,eId是事件流程整体的id,并且事件流程内部所有的子事件都是这个id
EventPool: [], //闪亮登场,没想到吧,我只是个简简单单的数组
事件池由两部分组成:添加 和 运行。
事件池将会从左到右,依次运行事件。而添加事件之后,不会立即运行,而是等待所有排队的事件运行完才运行。添加 和 运行是独立作业,互不干扰的。
事件池的添加
比如以下这个函数
// player.vue
this.damage(target, num) //this对target造成num点伤害
这个函数不会立即执行结算伤害等等,它只是将一个完整的伤害流程添加进事件池,类似这样:
// player.vue
damage(target, num = 1, cards = []) {
const e = {
source: this.seat, //伤害来源
target, //伤害目标
num, //伤害数量
cards, //造成伤害的牌,默认为空
};
return this.createDamageEvent(e);
},
// player.vue
createDamageEvent(e) {
// 创建伤害事件流程
const progress = [
// 里面的每一项都是子事件的名称
'source.damage', //造成伤害时
'target.wounded', //受到伤害时
'target.woundedContent', //执行扣血的内容函数,不触发任何技能
'source.damageEnd', //造成伤害后
'target.woundedEnd', //受到伤害后
];
this.$parent.pushEventPool(e, progress);
},
// game.vue
// 代码已做适量精简
pushEventPool(e, list) { //list为事件流程,是一个数组
const arr = [];
const id = this.eId++;
const { EventPool } = this;
forEach(list, (i, k) => { //这里只示例第一次循环的结果,注意
const iarr = i.split('.'); //iarr = ['source', 'damage']
const name = iarr[1];
const ev = {//新事件ev融合老事件e,并添加新的必要属性
name, //name = 'damage',代表这是【造成伤害时】这个时机
id, //0
...e, //将老e解构在新ev的内部
finish() {
// 事件取消即移除其(指事件流程)在事件池中的剩余子事件
//调用:ev.finish();
//例如,如果在受到伤害时,并且伤害为1时发动【名士】,则之后的子事件将会被移除
//而整个伤害事件流程将因为没有剩余子事件而直接结束
//例如公孙瓒的【趫猛】('damageEnd')(造成伤害后)就无法发动了
//因为它时机在【名士】之后,由于其和其之后的子事件都被移除了,自然无法触发
remove(EventPool, item => item.id === this.id); //lodash函数
console.log('事件取消');
},
};
if (!ev.player) { //这里的player即子事件的执行者
//假如source和target都有【裸衣】,则只会由source来执行【裸衣】,player就是指定谁来执行的
const player = e[iarr[0]];// player = e['source']
ev.player = player;
}
arr.push(ev);
});
EventPool.splice(0, 0, ...arr); //为什么是splice而不是push?接下来会讲
//并且此语句在事件池为空时,等同于 EventPool.push(...arr)
},
事件池的插入
插入其实也是添加的一部分。只不过事件流程是从事件池的头部被添加进去
同时,事件池会移除已经执行的事件,正在执行的事件也被移除了。所以能保证,头部的事件就是即将执行的事件!
假设一个新技能:
【反噬】:当你受到一次伤害时,你对伤害来源造成等量的伤害。
再来看一个经典案例:
郭嘉,拥有【遗计】
曹操,拥有【反噬】,【奸雄】
郭嘉对曹操使用【杀】造成伤害时,【曹操】发动【反噬】,对郭嘉造成了一点伤害。
之后该怎么结算?老玩家应该都知道,先【遗计】再【奸雄】,这是三国杀的插入结算机制
可是按照事件池从左到右的执行顺序,会先【奸雄】再【遗计】,那怎么办?
这个时候从头部插入的优势就体现了。此时:
郭嘉 对 曹操造成一点伤害,eId
为0
曹操 对 郭嘉造成一点伤害,eId
为1,因为这是一个新的伤害事件流程
再来张图帮助你们理解。不同的事件流程用了不同颜色帮助区分。但是注意,图中的技能并不在事件池里,
事件池的执行
// game.vue
async IterEventPool() {
while (!this.empty) { //当事件池不为空
const ev = this.EventPool.shift(); //执行的时候就已经被移除了
//这里做了一个优化,即this.triggersAll不包含事件名时,则不运行主体函数
//例如,全场没有卖血流时,this.triggersAll自然不会包括'woundEnd'(受到伤害后)这个事件名
//这样可以加快程序运行速度
if (includes(this.triggersAll, ev.name) || ev.name.indexOf('Content') !== -1) {
//获取player组件。ev.player其实是ev.player的seat来代替
const player = this.getPlayer(ev.player);
//这里是检测是否是事件的content,例如伤害事件流程的content就是woundedContent(执行扣血)
if (ev.name.indexOf('Content') !== -1) {
await player[ev.name](ev); //player.woundedContent(ev)
} else {
// 查询此时机是否有其他玩家的技能可以响应
// 如果有,则按当前回合玩家逆时针排序依次结算
// 如果无,则事件执行者直接结算
// findTriggerGlobal函数用于查找所有的global技能,例如【悲歌】【献图】【鸩毒】等
const skills = this.findTriggerGlobal(ev.name);
if (skills) {
// getPlayersBySkill即通过技能来查找玩家seat,返回一个数组
const seats = this.getPlayersBySkill(skills);
// 将事件执行者也push进去,进行排序
seats.push(ev.player);
// 获取排序后的玩家seat列表
const sorted = intersection(this.currenSeats, seats);
const players = this.getPlayers(sorted);
let i = 0;
while (i < players.length) {
ev.player = sorted[i];
// trigger方法用于玩家发动技能,是一个async方法
await players[i].trigger(ev.name, ev); //await是核心
i++;
}
} else {
await player.trigger(ev.name, ev);
}
}
}
}
},
下一篇阶段篇将顺带讲解技能实现哦!想继续看的关注我吧,嘻嘻