最近工作量好大,好忙,趁周末练练手,花了近3小时写了一个贪吃蛇。(后续会把自己的一些想法继续更新,目前是1.3版本~更新过4次,文末有更新说明)。
这是实践操作的地址:http://iforj.com:8000/tool/snake/
实现贪吃蛇的功能很简单。
我就分享一下我实现贪吃蛇看起来在界面上移动并且吃食物长大的原理。
我建了一个数组list_arr[]来保存贪吃蛇所在的每个格子的id,并建了2个全局变量x和y,监听贪吃蛇头的位置,当然x和y也是贪吃蛇的起始位置。
那么贪吃蛇移动实际上就是每次从list_arr[]里取出第一个元素(list_arr.shift()方法),取出的第一个元素也可以认为是贪吃蛇的尾巴,然后把这个元素(也就是格子的id)代表的格子的css中的贪吃蛇的样式去掉。然后,把[x, y]这组坐标所代表的格子的id扔进list_arr[]里(list_arr.push()),并给这个格子添加上贪吃蛇的样式。所以实际上每次就操作了2个格子的样式。
如果贪吃蛇吃食物([x, y]与食物所在坐标相同),那么把食物那个格子的css去掉代表食物的样式并加上贪吃蛇的样式,并且不去掉尾巴,也就是不去掉list_arr[]里的第一个元素所在格子的样式。
<!doctype html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <style> body { position: relative; margin: 0; 100vw; height: 100vh; } .wrapper { padding: 40px 0; } .snake-box { margin: 0 auto; border-collapse: collapse; border:solid #ddd; border-1px 0px 0px 1px; } .snake-box td { padding: 10px; border: solid #ddd; border- 0px 1px 1px 0px; } .snake-body { background-color: #8078f3; } .snake-initial-right, .snake-right { position: relative; border-top-right-radius: 12px; border-bottom-right-radius: 12px; background-color: #8078f3; } .snake-right { /*-webkit-animation: moveright .2s linear; -moz-animation: moveright .2s linear;*/ animation: moveright .2s linear; } @keyframes moveright { 0% {transform: translate(-10px, 0);} 100% {transform: translate(0, 0);} } .snake-initial-right:before, .snake-right:before { content: ''; position: absolute; top: 4px; right: 6px; 3px; height: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; background-color: #000; } .snake-initial-right:after, .snake-right:after { content: ''; position: absolute; bottom: 4px; right: 6px; 3px; height: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; background-color: #000; } .snake-left { position: relative; border-top-left-radius: 12px; border-bottom-left-radius: 12px; background-color: #8078f3; animation: moveleft .2s linear; } @keyframes moveleft { 0% {transform: translate(10px, 0);} 100% {transform: translate(0, 0);} } .snake-left:before { content: ''; position: absolute; top: 4px; left: 6px; 3px; height: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; background-color: #000; } .snake-left:after { content: ''; position: absolute; bottom: 4px; left: 6px; 3px; height: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; background-color: #000; } .snake-top { position: relative; border-top-left-radius: 12px; border-top-right-radius: 12px; background-color: #8078f3; animation: movetop .2s linear; } @keyframes movetop { 0% {transform: translate(0, 10px);} 100% {transform: translate(0, 0);} } .snake-top:before { content: ''; position: absolute; top: 6px; left: 4px; 3px; height: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; background-color: #000; } .snake-top:after { content: ''; position: absolute; top: 6px; right: 4px; 3px; height: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; background-color: #000; } .snake-bottom { position: relative; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; background-color: #8078f3; animation: movebottom .2s linear; } @keyframes movebottom { 0% {transform: translate(0, -10px);} 100% {transform: translate(0, 0);} } .snake-bottom:before { content: ''; position: absolute; bottom: 6px; left: 4px; 3px; height: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; background-color: #000; } .snake-bottom:after { content: ''; position: absolute; bottom: 6px; right: 4px; 3px; height: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; background-color: #000; } .food { position: relative; background-color: #008000; } .food:after { content: 'f'; position: absolute; top: 0; left: 0; 100%; height: 100%; text-align: center; line-height: 20px; color: #fff; font-weight: 700; } .food-fast { background-color: #e81616; } .food-convert { background-color: #00f; } .btn-box { text-align: center; margin-bottom: 30px; } </style> </head> <body> <div class="wrapper"> <div class="btn-box"> <p>可以用空格键控制开始、暂停、继续</p> <button id="btn_control">开始</button> </div> <table id="snake_box" class="snake-box"> </table> </div> <script> //Array扩展indexOf Array.prototype.indexOf = function(val) { for (var i = 0; i < this.length; i++) { if (this[i] == val) return i; } return -1; }; (function() { function option() { var option = {}; option.width = 20; //x轴数量 option.height = 20; //y轴数量 option.speed = 600; //setInterval的执行周期 option.fastSpeed = 150; //加速周期 option.fastStep = 20; //加速步长 return option; } var o = new option(); var snakeBox = document.getElementById('snake_box'); snakeBox.innerHTML = null; var fragment = document.createDocumentFragment(); var snakeBoxArray = new Array(); for (var i = 0; i < o.height; i++) { var sTr = document.createElement('tr'); sTr.setAttribute('class', 'row'); var cellFragment = document.createDocumentFragment(); snakeBoxArray[i]= new Array(); for (var j = 0; j < o.width; j++) { var sTd = document.createElement('td'); var sId = j + '_' + i; sTd.setAttribute('id', sId); sTd.setAttribute('class', 'cell'); cellFragment.appendChild(sTd); snakeBoxArray[i][j] = j + '_' + i; } fragment.appendChild(sTr).appendChild(cellFragment); } snakeBox.appendChild(fragment); var x = Math.floor(o.width / 2), y = Math.floor(o.height / 2), list_arr = [], //记录蛇的cell fast_arr = [], //记录蛇加速的格子数 dir_arr = []; //记录蛇身的方向 document.getElementById(snakeBoxArray[x][y]).classList.add('snake-initial-right'); list_arr.push(x + '_' + y); /*实现键盘方向事件, 在非同轴方向移动时,会提前移动一格,以解决快速按下非同轴方向键时,setInterval延迟触发问题, 能实现空格事件控制按钮 */ var lastDir = 39; //记录上一次方向 document.onkeydown = control; function control(e) { var dir, btn = document.getElementById('btn_control'); if (e.keyCode == 32) { if (btn.innerHTML == '开始') { moveEvent = setInterval(move.bind(this, 39), o.speed); btn.innerHTML = '暂停'; } else if (btn.innerHTML == '暂停') { btn.innerHTML = '继续'; clearInterval(moveEvent); } else if (btn.innerHTML == '继续') { btn.innerHTML = '暂停'; moveEvent = setInterval(move.bind(this, lastDir), o.speed); //移动 } return false; //一定要false,否则会触发btn的click监听事件 } if (btn.innerHTML == '开始' || btn.innerHTML == '继续') { return true; //设置为false,会关闭所有键盘触发事件 } switch(e.keyCode) { case 37: //左 if (lastDir == 37 || (list_arr.length > 1 && lastDir == 39)) { return true; } else { document.getElementById(snakeBoxArray[y][x]).setAttribute('class', 'cell snake-body'); x--; snakeHeadStyle = 'snake-left'; lastDir = dir = 37; } break; case 38: //上 if (lastDir == 38 || (list_arr.length > 1 && lastDir == 40)) { return true; } else { document.getElementById(snakeBoxArray[y][x]).setAttribute('class', 'cell snake-body'); y--; snakeHeadStyle = 'snake-top'; lastDir = dir = 38; } break; case 39: //右 if (lastDir == 39 || (list_arr.length > 1 && lastDir == 37)) { return true; } else { document.getElementById(snakeBoxArray[y][x]).setAttribute('class', 'cell snake-body'); x++; snakeHeadStyle = 'snake-right'; lastDir = dir = 39; } break; case 40: //下 if (lastDir == 40 || (list_arr.length > 1 && lastDir == 38)) { return true; } else { document.getElementById(snakeBoxArray[y][x]).setAttribute('class', 'cell snake-body'); y++; snakeHeadStyle = 'snake-bottom'; lastDir = dir = 40; } break; } death(); list_arr.push(x + '_' + y); fast_arr.push(''); dir_arr.push(dir) if (document.getElementById(snakeBoxArray[y][x]).classList.contains('food')) { if (document.getElementById(snakeBoxArray[y][x]).classList.contains('food-fast')) { o.speed = option().fastSpeed; fast_arr.length = 0; //重置加速步长 } document.getElementById(snakeBoxArray[y][x]).setAttribute('class', 'cell'); document.getElementById(snakeBoxArray[y][x]).classList.add(snakeHeadStyle); food(); } else { if (fast_arr.length >= o.fastStep) { o.speed = option().speed; } var tail = list_arr.shift(); dir_arr.shift(); document.getElementById(tail).setAttribute('class', 'cell') document.getElementById(snakeBoxArray[y][x]).classList.add(snakeHeadStyle); } clearInterval(moveEvent); moveEvent = setInterval(move.bind(this, dir), o.speed); //移动 } /*移动函数*/ var moveEvent; function move(direction) { document.getElementById(snakeBoxArray[y][x]).setAttribute('class', 'cell snake-body'); switch(direction) { case 37: //左 x--; snakeHeadStyle = 'snake-left'; convertHeadStyle = 'snake-right'; break; case 38: //上 y--; snakeHeadStyle = 'snake-top'; convertHeadStyle = 'snake-bottom'; break; case 39: //右 x++; snakeHeadStyle = 'snake-right'; convertHeadStyle = 'snake-left'; break; case 40: //下 y++; snakeHeadStyle = 'snake-bottom'; convertHeadStyle = 'snake-top'; break; } death(); list_arr.push(x + '_' + y); dir_arr.push(direction) fast_arr.push(''); if (document.getElementById(snakeBoxArray[y][x]).classList.contains('food')) { if (document.getElementById(snakeBoxArray[y][x]).classList.contains('food-fast')) { o.speed = option().fastSpeed; fast_arr.length = 0; //重置加速步长 clearInterval(moveEvent); moveEvent = setInterval(move.bind(this, direction), o.speed); //移动 } document.getElementById(snakeBoxArray[y][x]).setAttribute('class', 'cell'); document.getElementById(snakeBoxArray[y][x]).classList.add(snakeHeadStyle); food(); } else { if(fast_arr.length >= o.fastStep) { o.speed = option().speed; clearInterval(moveEvent); moveEvent = setInterval(move.bind(this, direction), o.speed); //移动 } var tail = list_arr.shift(); dir_arr.shift(); document.getElementById(tail).setAttribute('class', 'cell') document.getElementById(snakeBoxArray[y][x]).classList.add(snakeHeadStyle); } } /*死亡判定*/ function death() { var flag = list_arr.indexOf(x + '_' + y) if (x < 0 || x >= o.width || y < 0 || y >= o.height || flag != -1) { location.reload(); } } /*食物*/ var foodCell; //食物位置 function food() { var foodX = Math.ceil(Math.random() * o.width) - 1, foodY = Math.ceil(Math.random() * o.height) - 1; foodCell = foodX + '_' + foodY; if (list_arr.indexOf(foodCell) == -1) { n = Math.round(Math.random() * 100); document.getElementById(foodCell).classList.add('food'); if (n >=0 && n < 60) { return true; //普通食物 } else if (n >=60 && n < 85) { document.getElementById(foodCell).classList.add('food-fast'); } else if (n >= 85 && n <= 100) { document.getElementById(foodCell).classList.add('food-convert'); } // document.getElementById(foodCell).classList.add('food-fast'); } else { return food(); } } food(); /*按钮控制*/ document.getElementById('btn_control').addEventListener('click', function(e) { if (e.target.innerHTML == '开始') { moveEvent = setInterval(move.bind(this, 39), o.speed); e.target.innerHTML = '暂停'; } else if (e.target.innerHTML == '暂停') { e.target.innerHTML = '继续'; clearInterval(moveEvent); } else if (e.target.innerHTML == '继续') { e.target.innerHTML = '暂停'; moveEvent = setInterval(move.bind(this, lastDir), o.speed); //移动 } }) })(); </script> </body> </html>
更新说明:
1.0版本:实现最基本的功能,贪吃蛇移动,吃食物和死亡判定;
1.1版本:加入了控制按钮,支持空格控制按钮;解决快速按下非同轴方向键时,setInterval延迟触发问题;在贪吃蛇长度为1时,可以往4个方向移动,当大于2时,不能往移动方向反方向移动;
1.2版本:美化UI(小蛇有蛇头和眼睛了,有兴趣的可以加个蛇尾,原理和蛇头类似);食物加入了颜色和对应效果(目前只有红色的加速效果有效,加速周期和加速步长都在option里配置);o现在是继承option;
其实还写了一个蓝色食物的换向效果:
if (document.getElementById(snakeBoxArray[y][x]).classList.contains('food-convert')) { document.getElementById(snakeBoxArray[y][x]).setAttribute('class', 'cell snake-body'); list_arr.reverse(); dir_arr.reverse(); document.getElementById(list_arr[list_arr.length - 1]).setAttribute('class', 'cell'); document.getElementById(list_arr[list_arr.length - 1]).classList.add(convertHeadStyle); x = list_arr[list_arr.length - 1].split('_')[0]; y = list_arr[list_arr.length - 1].split('_')[1]; clearInterval(moveEvent); var newDir; if (dir_arr[dir_arr.length - 1] == 37) { lastDir = newDir = 39; } else if (dir_arr[dir_arr.length - 1] == 38) { lastDir = newDir = 40; } else if (dir_arr[dir_arr.length - 1] == 39) { lastDir = newDir = 37; } else if (dir_arr[dir_arr.length - 1] == 40) { lastDir = newDir = 38; } moveEvent = setInterval(move.bind(this, newDir, o.speed)); //移动 }
建了一个数组dir_arr来监听蛇身体每个格子的方向(就是蛇头每次进入一个新格子的方向),然后转向也就是把监听小蛇身体位置的list_arr和监听方向的dir_arr均颠倒(reverse()),然后重置蛇头坐标(x,y),list_arr和dir_arr里的最后一个元素就是转向后的小蛇新的头部位置和方向,最后重置setInterval就可以了。原理就是如上。该特殊食物由于效果触发后,转向是往之前蛇头第一次进入当前的蛇尾所在的格子的方向的反方向进行的,所以在小蛇较长或者贴边缘移动时,很容易触发死亡判定,所以最后把这个特殊事务移除了。
1.3版本:美化UI,新增了蛇头进入新格子的动画(如果蛇身也要,原理一样;animation没有对浏览器作适配,需要的自己适配);
2016年11月23日发现一个bug:
在小蛇长度为4时,绕一个2*2的正方形格子转圈为直接触发死亡条件,发现是代码334行先触发死亡判定,而后在350行才将尾巴从小蛇身体内去掉,最简单的改法应该就是下面这样了: