200行代码实现羊了个羊
羊了个羊 前端 JavaScript
前文
前段时间,小游戏“羊了个羊”真是火遍了全网,在收获了巨额的广告费的同时也折磨着每一个不幸点进去的玩家。博主当时接触到这款小游戏的契机是一个群友为了求张通关截图在妹子面前装13,大手一挥悬赏200块寻找第一个通关的群友,然后接下来的几天群里简直是一片哀嚎,“就差3张了阿!!!我他&&%&**@的**&...”,最后在不下30次的尝试后博主果断选择了放弃...
前几天冲浪的时候再次刷到了这款游戏,躺在床上试着思考这款游戏的实现逻辑,感觉也就那么回事,无非就几点:
- 图块被遮挡时颜色变暗且无法被点击
- 点击大盒子中的图块会将其从大盒子转移至小盒子中
- 大盒子空了游戏通关
- 三个同样的图块会消掉
- 小盒子没有3种同样的图块且满了则游戏结束
而以上几点用目前掌握的前端知识完全可以实现,说肝就肝,于是博主立马下床开肝,在5个小时后用200行js代码成功实现了这款小游戏
看着应该还可以吧?页面是比较小清新的,在深思熟虑后博主给它取名“水了个果”(严肃),下面是编写过程中一些关键的步骤
HTML
<body>
<div class="container"></div><!--大盒子-->
<div class="box">
<div class="sideBox"></div><!--小盒子-->
<div class="remake"></div><!--重开按钮-->
</div>
</body>
页面的结构很简单
JS
const arr = [//图块
{name: '香蕉', url: 'xxx'},
{name: '榴莲', url: 'xxx'},
{name: '苹果', url: 'xxx'},
{name: '橙子', url: 'xxx'},
{name: '桃子', url: 'xxx'},
{name: '葡萄', url: 'xxx'},
]
数组中存放的是将会出现在页面中的图块,每个数组元素包括图块的名字和图片的链接
打乱图块
let a = b = c = d = e = f = 0//计数
const array = []//存放打乱后的图块
for(let i = 0; i < 6; i ++) {//打乱图块
for(let j = 0; j < 9; j ++) {
let x = parseInt(Math.random()*6)
switch(x) {
case 0: if(a!==9) {array.push(arr[x]);a++;}break;
case 1: if(b!==9) {array.push(arr[x]);b++;}break;
case 2: if(c!==9) {array.push(arr[x]);c++;}break;
case 3: if(d!==9) {array.push(arr[x]);d++;}break;
case 4: if(e!==9) {array.push(arr[x]);e++;}break;
case 5: if(f!==9) {array.push(arr[x]);f++;}break;
}
}
}
if(a !== 9) {let i = 9 - a; while(i !== 0) {array.push(arr[0]); i--} }//判断是否有图块没达到3组(3*3),如没达到则对应补满
if(b !== 9) {let i = 9 - b; while(i !== 0) {array.push(arr[1]); i--} }
if(c !== 9) {let i = 9 - c; while(i !== 0) {array.push(arr[2]); i--} }
if(d !== 9) {let i = 9 - d; while(i !== 0) {array.push(arr[3]); i--} }
if(e !== 9) {let i = 9 - e; while(i !== 0) {array.push(arr[4]); i--} }
if(f !== 9) {let i = 9 - f; while(i !== 0) {array.push(arr[5]); i--} }
这里图块的数量是6*9,也就是6种水果每种都有3组,每组3个,因为博主设置每个图块的position都为absolute,也就是说如果两个图块有重叠,后一个图块会遮挡住前一个图块,如果不人为打乱直接6*9循环遍历arr数组,就会造成相同的水果绝大多数都处在同一层,或者前后一层,这样不太科学(游戏难度太低了)
因此这里博主采用“暴力打乱法”,使用parseInt(Math.random()*6)产生的54个随机数随机将水果放入array中,再放入之前使用abcdef六个变量进行计数,例如a代表苹果,当摇到了a且a<9时就说明苹果数量没满,就可以将其放入,反之则直接跳过。这样的存放方式大概率是放不满54个水果的,所以在退出循环后我们还要人为判断abcdef是否都为9,如不为9则需要对应补满。
打乱后的效果还是较为理想的,当然博主这里的方法太乱来了,肯定还有更好的方法😥
随机坐标
在游戏中,每次刷新后图块的位置都会发生改变,这也是游戏特色之一,那么这个需求要怎么实现呢?其实也很简单,还是熟悉的Math.random()
let left = parseInt(Math.random()*x)//通过控制left、top来控制密集程度
let top = parseInt(Math.random()*y)
大盒子的position同样是absolute,我们可以通过随机的left和top来打乱每个图块的位置,而其中x、y的取值范围是根据图块、大盒子长宽来决定的,博主这里图块是30px*30px,大盒子是350px*350px,所以x、y取值范围就是0~320px,同时游戏难度和x、y的取值也有关,数值越小图块也就越密集...
element.style.cssText = "left:"+`${left}`+"px; top:"+`${top}`+"px;"
document.querySelector('.container').appendChild(element)
遍历打乱后的数组创建dom元素,设置好left、top值后将其插入大盒子container中即可
判断遮挡
这个环节是博主认为比较难的,刚开始的思路是获取每个图块的坐标等数据进行计算,但一想到这个工作量还是退缩了,在查看了相关问题的大佬回答后决定试试这个方法
原回答链接:https://www.zhihu.com/question/420268420(这是我所找到的最早的回答,如有更早的可以提醒下)
const io = new IntersectionObserver((data)=>{//*判断是否被遮挡
if (data[0].isVisible===false) {
element.classList.add('mask')
}
else {
element.classList.remove('mask')
element.classList.add('visible')
io.unobserve(element)
}
},
{
threshold: [1,0],
delay: 500,
trackVisibility: true,
})
io.observe(element)
IntersectionObserver()这个API通常用于检测元素交互,也可以用于检测元素的可视状态,具体使用方法以及相关属性参数啥的建议看看相关文档,博主也没有仔细研究...这里为每个图块绑定一个监视任务,当图块的可视状态改变后做出相应处理,图块上方无其他元素则为可视,相关返回属性isVisible的值为true,反之就是被遮挡isVisible的值为false。另外delay是决定多少毫秒后做出相应动作的参数,这个根据个人需要设置就好
/*蒙层*/
.mask::after {
position: fixed;
content: '';
display: block;
width: 30px;
height: 30px;
background-color: rgba(0, 0, 0, .5);
border-radius: 5px;
}
当图块被遮挡时我们为其加上mask类名,当图块不被遮挡时则去除掉mask类名并加上visible类名。mask类名的作用是视觉效果和阻止点击,使用伪元素为带有mask类名的图块加上蒙层。在图块的点击事件中会首先检测被点击的元素是否带有mask类名,如带有则会阻止点击
点击去除图块
这个环节分为如下几步:
- 获取被点击图块身上的水果属性名
- 获取图块节点
- 去除被点击图块身上的点击事件
- 将其从大盒子中移出
- 将其加入小盒子中
在之前遍历数组创建图块元素时我们会为其加上名为name值为对应水果名字符串的属性,这个属性名在判断小盒子中是否存在3个相同水果环节中有重要的作用
element.setAttribute('name', `${array[length].name}`)//对应设置标签值
获取被点击图块节点使用的是:
document.elementFromPoint(data.clientX, data.clientY)
去除掉被点击盒子身上的点击事件是为了防止玩家错误点击了小盒子中的图块而产生不必要的影响
ele.removeEventListener('click', click)//去除图块身上的点击事件
将被点击图块移出大盒子:
document.querySelector('.container').removeChild(ele)
将被点击图块加入小盒子:
document.querySelector('.sideBox').appendChild(ele)//将点击的图块加入边栏中
判断小盒子是否存在3个相同水果
switch(attribute) {
case '香蕉':
if(a!==2) {
a++
moveBox.push(attribute)
}
else {
for (let i = 0; i < 2; i ++) {
for (let j = 0; j < moveBox.length; j ++) {
if (moveBox[j] === attribute) {
moveBox.splice(j, 1)
}
}
}
setTimeout(()=>{traverse(attribute)}, 300); a=0
}
break;
case 'xxx':
......
}
这里博主用的是switch判断被点击盒子的name属性值来增加对应计数器的值,进而判断小盒子中是否存在3个相同水果,上文提到的name属性的作用此时就体现出来了。moveBox数组存放的是小盒子中存在的水果名,且最大容量为6,也就是说当moveBox的长度到达了6且没有相同水果数量是3的话游戏就结束了。首先是第一个if判断语句,如果小盒子中a对应水果的数量小于2,就将其加入moveBox;若a为2,就说明如果把手上这个水果加入小盒子中,这种水果的数量就达到了3,即满足消去条件,因此我们需要遍历moveBox数组将这种水果去除,然后再使用上文提到的document.querySelector('.xxx').removeChild(ele)来去除小盒子中的对应水果就好,这里我单独写了一个函数traverse()来执行这个操作,效果是一样的,另外在合适的地方使用定时器来执行操作可以使视觉效果更合理、美观
判断游戏胜利、失败
if(document.querySelector('.container').childNodes.length === 0) {//判断游戏是否结束
alert('恭喜你,成功通关!')
}
else if (moveBox.length === 6) {//小盒子中水果数量
alert('很遗憾,游戏结束!')
}
通过获取大盒子中的子元素列表并检测其长度是否为0来判断是否满足通关条件,至于失败情况上面已经提到了,若小盒子长度为6且没有任何水果被消去则游戏失败,当然这个判断语句必须放在上一环节之后,因为存在虽然格子满了但是有3个相同水果这种情况。做出通关或失败判断后弹出对应提醒窗口,如还需要实现窗口弹出后不能点击任何图块效果则获取图块元素并去除其身上的点击事件即可
重开按钮
创建按钮并为其绑定好点击事件,事件内容如下,主要是利用刷新页面重新渲染来实现
location.reload()
结尾
“水了个果”的制作至此就告一段落了,总体来说难度不算太大 ,但作为一个前端练手小项目还是很合适的。才疏学浅、技艺不精,如有讲解不对的地方还望指出。祝看到这篇文章的猿友学业事业一路通关!
更多推荐
所有评论(0)