本文深入解析一段仅20行的JavaScript贪吃蛇代码,探讨其如何利用Canvas进行网格绘制、通过一维数组管理蛇身状态,以及IIFE和运算符优先级的巧妙应用。文章详细拆解了蛇的移动逻辑、食物生成机制及键盘事件处理,适合希望理解JS核心技巧的开发者阅读。
前言与背景
前言
近期在CSDN平台看到有开发者使用仅20行代码便完成了贪吃蛇游戏的开发,其实现方式令人印象深刻。为了深入理解这段代码的精妙之处,我对源码进行了研读,并发现其中涉及了运算符优先级及JavaScript的一些高级技巧。因此,我将整理并分享其中的实现思路,旨在帮助初学者更好地掌握相关知识点。
我对原始代码进行了适度的修改,并补充了详细的注释,以增强代码的可读性和理解度。
代码示例与核心逻辑
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>贪吃蛇重构</title>
<style>
body {
display: flex;
height: 100vh;
margin: 0;
padding: 0;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<canvas id="can" width="400" height="400" style="background-color: black">对不起,您的浏览器不支持canvas</canvas>
<script>
var snake = [41, 40], //snake队列表示蛇身,初始节点存在但不显示
direction = 1, //1表示向右,-1表示向左,20表示向下,-20表示向上
food = 43, //食物的位置
n, //与下次移动的位置有关
box = document.getElementById('can').getContext('2d');
//从0到399表示box里[0~19]*[0~19]的所有节点,每20px一个节点
function draw(seat, color) {
box.fillStyle = color;
box.fillRect(seat % 20 *20 + 1, ~~(seat / 20) * 20 + 1, 18, 18);
//用color填充一个矩形,以前两个参数为x,y坐标,后两个参数为宽和高。
}
document.onkeydown = function(evt) {
//当键盘上下左右键摁下的时候改变direction
direction = snake[1] - snake[0] == (n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction) ? direction : n;
};
!function() {
snake.unshift(n = snake[0] + direction);
//此时的n为下次蛇头出现的位置,n进入队列
if(snake.indexOf(n, 1) > 0 || n < 0 || n > 399 || direction == 1 && n % 20 == 0 || direction == -1 && n % 20 == 19) {
//if语句判断贪吃蛇是否撞到自己或者墙壁,碰到时返回,结束程序
return alert("GAME OVER!");
}
draw(n, "lime"); //画出蛇头下次出现的位置
if(n == food) { //如果吃到食物时,产生一个蛇身以外的随机的点,不会去掉蛇尾
while (snake.indexOf(food = ~~(Math.random() * 400)) > 0);
draw(food, "yellow");
} else { //没有吃到食物时正常移动,蛇尾出队列
draw(snake.pop(),"black");
}
setTimeout(arguments.callee, 150);
//每隔0.15秒执行函数一次,可以调节蛇的速度
}();
</script>
</body>
</html>
开发贪吃蛇游戏的核心在于构建蛇的活动区域以及实现蛇的移动逻辑。
首先分析游戏区域的构建方式:
<!-- html -->
<canvas id="can" width="400" height="400" style="background-color: black">
对不起,您的浏览器不支持canvas
</canvas>
<!-- js -->
box = document.getElementById('can').getContext('2d');
这是一个400px*400px的canvas,思路是以20px*20px为一个方格,组成20行20列的方阵,总共400格,然后绿色填充的格子表示蛇身,用黄色表示食物。这400个格子和数字0~399一一对应,对应的方式就是以20作为基数,n / 20再取整表示第几行,n % 20表示第几列。行数和列数都用0~19表示。
蛇的数据结构采用一维数组表示,数组中的每个元素对应上述400个网格中的一个索引。使用var snake = [41, 40];初始化蛇的初始状态,其中索引0代表蛇头。变量food记录食物的位置,direction记录蛇头下一步的移动方向。蛇的移动通过数组元素的增删来实现:每次绘制时,先添加新的蛇头节点,再移除蛇尾节点,通过循环执行这一过程实现蛇的持续运动。
函数执行与IIFE解析
接下来从函数执行的起始位置(代码第39行)开始分析:
!function() {}();
这里的语法结构实际上是立即执行函数(IIFE)的一种变体写法。关于IIFE的更多细节,可参考相关技术文章。继续向下分析,代码为蛇头添加一个新节点n,其值为当前蛇头索引值加上direction的值,这解释了为何向下移动用20表示,向上移动用-20表示。随后的if语句用于判断新蛇头是否碰撞到蛇身或超出边界,其中需注意&&运算符的优先级高于||。若未发生碰撞(即未死亡),则绘制新的蛇头,具体绘制逻辑如下:
function draw(seat, color) {
box.fillStyle = color;
box.fillRect(seat % 20 *20 + 1, ~~(seat / 20) * 20 + 1, 18, 18);
}
绘制函数填充18×18像素的矩形,并预留1像素的边框。.fillRect()方法的第一个参数计算矩形的x坐标,公式为seat % 20 * 20 + 1。该公式先确定矩形在网格中的列位置(第seat % 20列),再转换为像素坐标。其中~~(seat / 20)用于获取行号,这里的~~运算符作用类似于Math.floor(),即对浮点数进行两次按位非运算,从而得到去掉小数部分的整数。
回到代码第47行,此处有一个判断逻辑:检查新蛇头的位置是否与当前食物位置重合。若重合,则生成新的随机食物位置,但需确保新食物不生成在蛇身上,随后绘制食物。若未吃到食物(即蛇正常移动),则通过弹出蛇尾元素并调用绘制函数将其渲染为黑色背景色,从而实现蛇身移动的视觉效果。
最后,使用setTimeout递归调用当前函数,通过设置执行间隔来控制蛇的移动速度。
键盘交互与方向控制
此时蛇已具备基本移动能力,接下来通过添加键盘交互功能完善游戏:
document.onkeydown = function(evt) {
direction = snake[1] - snake[0] == (n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction) ? direction : n;
};
将方向控制逻辑绑定到键盘事件上。其中evt || event的写法旨在兼容旧版IE浏览器,确保事件对象获取的兼容性。
三目运算符?前的判断逻辑可拆分为两个部分进行分析:
snake[1] - snake[0]的值理论上应等于-direction,即当前运动方向的反向。若直接写为-direction,虽然逻辑上看似等效,但实际开发中避免这样做的原因是:防止玩家在一个函数执行周期内多次改变direction的值,导致direction与实际运动方向不一致,进而引发游戏逻辑错误。- 在==运算符之后,
[-1, -20, 1, 20][(evt || event).keyCode - 37]部分的前半部分是一个数组,后半部分通过索引取值。左上右下四个方向键的keyCode分别为37、38、39、40,减去37后索引分别为0、1、2、3,从而将按键映射到direction的取值[-1, -20, 1, 20]中。此设计的巧妙之处在于:若按下的不是方向键,数组索引将返回undefined。此时,由于后续||运算符的作用,n将取direction的原有值,从而忽略无效按键。
最后通过三目运算符进行判断:若按键方向不是当前运动方向的反向,则更新direction的值,从而改变蛇的移动方向。
以上即为该贪吃蛇游戏的核心实现内容。尽管涉及的多为基础知识,但通过精简代码实现完整功能仍颇具趣味。若文中存在理解偏差,欢迎指正,以期共同进步。
总结
总结
本文完整介绍了该贪吃蛇游戏的实现细节,希望能为读者的学习或工作提供参考。如有任何疑问,欢迎留言交流,感谢支持。