大家好呀!
这几天,“羊了个羊”真的火爆全网,连续好几天了,热度仍然不减。我也不太懂这种游戏有什么好玩的,但是看到微信群里不断发来的链接,我也忍不住玩了一会。可惜呀!实在是过不了呀。正好小组内的技术分享轮到我了,我决定把这个游戏仿写出来在公司内讲讲。
话不多说,先上效果图!!!
怎么样,看起来是不是有那个味了!
不过我的功能框每个都能用10次,这次再也不怕高血压上来了。哈哈哈!!!
闲聊结束,现在开始真正到技术篇了。本游戏使用原生js+canvas开发,开发总共耗时大约15个小时,游戏实现了关卡制(3关),并且每关提供3种道具,每种道具提供10次使用机会(第二种道具暂时没做)。
源码地址放在最后!!!
第一部分:货物绘制与消除
游戏的大致思路是:定义一个全局的货物列表goodList,然后使用轮询的方式,不断绘制goodList,goodList里面包含层次信息,坐标位置,可以精准的绘制每一层的货物。初次之外,开始全局监听canvas的点击事件,每次点击事件发生时,会拿到点击的坐标位置,然后到goodsList里面进行比对,判断是点击了哪一层的哪一个货物,然后返回对应的坐标位置 [i,j] ,之后通过坐标位置,将货物从goodList里面取出,放到货架上,然后执行“三消”程序。
这里所谓的“三消”,其实是一个算法,算法的大致内容是,取出一个列表内3个相同数字的坐标。拿到坐标之后,会从货架上删除。
// 关卡货物的数据结构
{
"goodArray": {
"1": [
{
"name": "chicken", // 对应得图片名
"width": 60,
"height": 66,
"x": 240,
"y": 300, // 坐标
"canClick": false // 是否可点击
}
],
"2": [
{
"name": "cabbage",
"width": 60,
"height": 66,
"x": 120,
"y": 240,
"canClick": true
}
],
"3": [
{
"name": "chicken",
"width": 60,
"height": 66,
"x": 360,
"y": 400,
"canClick": true
},
{
"name": "cabbage",
"width": 60,
"height": 66,
"x": 310,
"y": 300,
"canClick": true
}
]
}
接下来,介绍具体的实现逻辑,大家可以先看看游戏的背景,背景当中的小草在不断抖动,这里使用的是canvas提供的api,这个api叫做二次贝塞尔曲线。这个函数可以弧度好看的小草。看下面是从文档上截取的介绍,这个曲线的绘制,需要三个点,(20,100)相当于一个锚点,为其他两个点绘制一条漂亮的曲线。
绘制代码如下:
// 绘制小草
ctx.beginPath();
// 控制点
ctx.moveTo(this.x[i], this.y[i]);
// 二次贝塞尔曲线,结束点
ctx.quadraticCurveTo(this.x[i] + 2, this.y[i] - (this.alpha + l), this.x[i] + 8, this.y[i]);
ctx.moveTo(this.x[i] + 5, this.y[i]);
ctx.quadraticCurveTo(this.x[i] + 8 + 2, this.y[i] - (this.alpha + l), this.x[i] + 16, this.y[i]);
ctx.moveTo(this.x[i] + 11, this.y[i]);
ctx.quadraticCurveTo(this.x[i] + 11 + 2, this.y[i] - (this.alpha + l * 2 ), this.x[i] + 24, this.y[i]);
ctx.stroke();
接下来绘制货物,通过全局的goodList,canvas不断绘制,通过点击,达到消除的目的。
// 绘制货物
for (var i = 0; i < this.goodsList.length; i++) {
var indexGoods = this.goodsList[i];
for (var j = 0; j < indexGoods.length; j++) {
// ---部分代码先删除,后面放出---
ctx.drawImage(picResourceMap.get("blank"), goods.x, goods.y, goods.width, goods.height);
ctx.drawImage(picResourceMap.get(goods.name), goods.x + 8, goods.y + 8, goods.width * 0.7, goods.height * 0.7);
if (!goods.canClick) {
ctx.drawImage(picResourceMap.get("shadow"), goods.x, goods.y, goods.width, goods.height);
}
}
}
然后就是“三消”的代码
// 这块是一个算法,计算一个队列里有3个相同元素的货物,然后删除掉
// 消除数量超过3个相同的
Box.prototype.clearSame = function () {
var boxArray = {};
this.boxList.forEach((it, index) => {
boxArray[it.name] = (boxArray[it.name] ? boxArray[it.name] : 0) + 1;
});
// 消除目标
var target;
for (var item in boxArray) {
if (boxArray[item] >= 3) {
target = item;
}
}
console.log("消除", target);
if (!target) return;
// 删除列表,用来显示星星
var removeList = [];
// 临时盒子列表,用来替换旧的盒子列表
var tempBoxList = [];
this.boxList.forEach((it, index) => {
if (it.name === target) {
removeList.push(index);
} else {
tempBoxList.push(it);
}
});
this.boxList = tempBoxList;
}
这里需要注意的是,由于货物的摆放是分层的,如果消除了上一层,那么判断这个货物压住的下一层货物是否可以点击,后面介绍“碰撞检测”时会说到。
第二部分:功能按钮
现在介绍功能按钮,第一个时移出货价,如下图,
其实实现原理很简单,需要再定义一个列表,从货架上取出3个货物,放到新的列表里,然后再函数绘制的时候绘画出来就可以了。
撤回一步的按钮目前还没有做,其实也是需要做一个临时变量,临时存储。目前此游戏只剩这一个功能,后期会补上。
最后一个就是洗牌逻辑,洗牌的动画后面一起提到,大家可以猜猜,这是怎么画出来的,其实用到是高中学的数学公式呦!
洗牌的逻辑其实就是定义一个栈,然后遍历goodList里面的所有获取,依次放入栈中,之后在使用pop()方法,依次将货物放入原先的位置,这样就可以实现洗牌的底层逻辑。
Goods.prototype.refreshGoods = function () {
var goodListStack = [];
for (var i = 0; i < this.goodsList.length; i++) {
for (var j = 0; j < this.goodsList[i].length; j++) {
goodListStack.push(this.goodsList[i][j].name);
}
}
for (var i = 0; i < this.goodsList.length; i++) {
for (var j = 0; j < this.goodsList[i].length; j++) {
this.goodsList[i][j].name = goodListStack.pop();
}
}
this.tempGoodsList = JSON.stringify(this.goodsList);
this.refreshSwitch = true;
}
第三部分:关卡设计
一开始已经说了,本游戏是设计了三关的,为了实现这三关,定义了三个json文件,每个json代表每一关的货物排列方式(理论上应该做一个服务器的)。
Level.prototype.draw = function () {
if (this.levelSwitch) {
ctx.drawImage(picResourceMap.get("shadow"), -50, -50, 540, 890);
ctx.drawImage(picResourceMap.get("blackBlock"), 20 + this.offset, 300, 420, 200);
ctx.save();
ctx.font="50px Arial";
ctx.strokeStyle = "#000";
ctx.strokeText("第 " + this.level + " 关",140 + this.offset, 365);
ctx.font="20px Arial";
ctx.strokeStyle = "#fff";
ctx.drawImage(picResourceMap.get("nextLevel"), 160 + this.offset, 390, 150, 60);
ctx.strokeText("开始关卡",200 + this.offset, 425);
ctx.restore();
if (this.offset >= 0) {
this.offset -= 3;
}
}
}
这里levelSwitch默认游戏开始是开启的,就是直接加载关卡动画,关卡动画默认是在画布外的,随着不断的加载,会移动到屏幕的中间位置。点击开始游戏后,会调用init()函数,并且将levelSwitch开关关闭,然后调用json文件,获取关卡信息。
游戏结束之后,会再次打开游戏关卡,然后将游戏等级+1,相应的恢复所有的道具。
至此,此游戏逻辑结束,其实还是比较简单的。
第四部分:碰撞检测与洗牌动画
碰撞检测使用的是一个数学公式,两点的直线距离完成的,如下图
假设两个物体相撞,那么必然如图,其实也就是勾股定理的一个扩展,可以计算两个点之间的距离,如果小于l,则就是碰撞。
var coverGoods = [];
for (var i = 0; i < this.goodsList[nextIndex].length; i++) {
if (goods.calLength(this.goodsList[nextIndex][i].x, this.goodsList[nextIndex][i].y, selectGoods.x, selectGoods.y) < 4356) {
coverGoods.push({"line": nextIndex, "row": i});
}
}
洗牌动画使用的也是高中的数学题,一直圆心坐标(x,y),半径为r,以圆心为顶点,做射线,角度为a,求与圆的交点坐标。
// 圆心坐标
this.dots = {
x: 210,
y: 200
};
// 半径
this.radius = 100;
// 圆角度
this.angle = 0;
// -----------------------------------
// 0.9 是用来减速的
this.angle = this.angle + deltaTime * 0.9;
goods.x = this.dots.x + this.radius * Math.sin(this.angle * 3.14 / 180);
goods.y = this.dots.y + this.radius * Math.cos(this.angle * 3.14 / 180);
源码地址:sheep: “羊了个羊”复写版,使用原生js与canvas开发,实现三消,回退,洗牌功能
马上要国庆了,大家国庆快乐呀!希望疫情早点结束,都能与家人团聚呀!