项目准备
项目展示图
建立新文件夹,新建出images CSS JS 三个文件夹,并在根目录下创建出index.html将下列素材图片转到images文件中
接下来可以开始着手操作了
HTML结构
游戏内容区域 (content)游戏开始按钮 (btn startBtn)游戏暂停按钮 (btn pauseBtn)游戏进行区域 (snakeWrap)可以构造出如下的结构
<div class="content"><div class="btn startBtn"><button></button></div><div class="btn pauseBtn"><button></button></div><div id="snakeWrap"></div></div>
在head标签内引入CSS文件夹内的index.css
<link rel="stylesheet" href="CSS/index.css">
在body标签的最末尾,添加script,引入JS文件夹内的index.js
<script src="JS/index.js"></script>
CSS样式
首先,要让游戏内容区域 居中显示
.content {width: 640px;height: 640px;margin: 100px auto;position: relative;}
左右margin为 auto 使其自适应居中显示
给两个按钮以及其蒙层添加属性
.btn {width: 100%;height: 100%;position: absolute;left: 0;top: 0;background-color: rgba(0, 0, 0, 0.3);z-index: 2;}.btn button {background: none;border: none;background-size: 100% 100%;cursor: pointer;outline: none;position: absolute;left: 50%;top: 50%;}
btn 是两个按钮的蒙层属性,完全覆盖了游戏内容区域,而btn 中 的 button 给它设置了一些相应的属性,例如水平垂直居中(在下面获取具体宽高的时候,给予负 的 margin)background-size 平铺背景图片 等属性
.startBtn button {width: 200px;height: 200px;sdabackground-image: url(../images/startGame.png);margin-left: -100px;margin-top: -100px;}.pauseBtn {display: none;}.pauseBtn button {width: 70px;height: 70px;background-image: url(../images/pauseGame.png);margin-top: -35px;margin-left: -35px;}
给予开始按钮 以及 暂停按钮宽高,并设置背景图片,让其居中,并让一开始,暂停按钮不显示
给游戏进行区域添加样式
#snakeWrap {width: 600px;height: 600px;background-color: #225675;border: 20px solid #7dd9ff;position: relative;}
游戏内容区域宽高为640px 游戏进行区域的宽高为600px 加上边框border 刚好就是640px,并且给予其相对定位
预定义蛇的样式
首先,我们的蛇的身体,头部,包括我们的食物苹果,其实都是一个一个的小方块构成,因此,我们可以先预定义 游戏内的div小方块
#snakeWrap div {width: 20px;height: 20px;position: absolute;}
使得其宽高20,游戏进行区域是宽高600,其实相当于最多存放30 * 30 个 小方块
蛇头样式
.snakeHead {background-image: url(../images/snakeHead.png);background-size: cover;border-top-right-radius: 30%;border-bottom-right-radius: 30%;}
引入蛇头图片,让其图片平铺方块,再将其初始的蛇头方向圆角修饰,让其蛇头朝右
.snakeBody {background-color: #9ddbb1;border-radius: 50%;}.food {background-image: url(../images/food.png);background-size: cover;}
蛇的身体其实是一个小圆点,直接设置背景颜色和 border-radius 即可
而食物则是将背景图片引入即可
JS逻辑 && 代码
游戏逻辑
蛇逻辑
食物逻辑
在开始代码之前,先在全局定义几个变量
var sw = 20, // 方块宽度sh = 20, // 方块高度tr = 30, // 行数td = 30; // 列数var snake = null, //蛇的实例food = null, //食物的实例game = null; //游戏的实例
接下来开始着手主要的核心代码
上面说到 蛇 和 食物 可以看成是小方块,所以我们可以先写小方块的构造函数
小方块
function Square(x, y, classname) {this.x = x * sw;// x坐标 * 小方块宽度this.y = y * sh;// y坐标 * 小方块高度this.classname = classname;// 赋值classnamethis.viewContent = document.createElement('div');this.viewContent.className = classname;this.parent = document.getElementById('snakeWrap');// 取出父级游戏进行区域}
接下来,我们在Square的原型链上添加两个方法,创建和移除
Square.prototype.create = function () {this.viewContent.style.left = this.x + 'px';this.viewContent.style.top = this.y + 'px';// 定位this.parent.appendChild(this.viewContent); // 将小方块添加到页面当中}Square.prototype.remove = function () {this.parent.removeChild(this.viewContent);// 移除小方块};
蛇
首先,先写出蛇的构造函数
function Snake() {this.head = null;// 蛇头this.tail = null;// 蛇尾this.pos = [];// 蛇在游戏区域中存在的坐标this.directionNum = {left: {x: -1,y: 0,rotate: 180},right: {x: 1,y: 0,rotate: 0},up: {x: 0,y: -1,rotate: -90},down: {x: 0,y: 1,rotate: 90}};// 设置蛇的方向num,方便后序控制操作}
其中呢,rotate是为了蛇头转向时候,蛇头的朝向设置的
接下来是 蛇的 初始换函数 (最开始的两个蛇身 以及 蛇头)
Snake.prototype.init = function() {var snakeHead = new Square(2, 0, 'snakeHead');snakeHead.create(); // 在(2,0)处创建蛇头this.head = snakeHead;// 并给让这个创建的蛇头 赋值给 this.headthis.pos.push([2, 0]);// 将蛇头所在的位置,用数组pos存储起来var snakeBody1 = new Square(1, 0, 'snakeBody');snakeBody1.create();this.pos.push([1, 0]);var snakeBody2 = new Square(0, 0, 'snakeBody');snakeBody2.create();this.pos.push([0, 0]);this.tail = snakeBody2;// 创建链表关系snakeHead.last = null;// 蛇头的上一个是nullsnakeHead.next = snakeBody1; // 蛇头的下一个是snakeBody1snakeBody1.last = snakeHead; // snakeBody1的上一个是蛇头snakeBody1.next = snakeBody2; // snakeBody1的下一个是蛇尾snakeBody2.last = snakeBody1; // snakeBody2的上一个是snakeBody1snakeBody2.next = null; // snakeBody2的下一个是nullthis.direction = this.directionNum.right; // 设置默认的方向为右};
注释写的挺清楚了 ,也就不做过多的解释,创建链表关系是为了更方便我们写蛇移动的函数
接下来是蛇的下一个点的判断,也就是四块逻辑,撞墙,撞自己,撞食物,未碰撞
Snake.prototype.getNextPos = function () {var nextPos = [this.head.x / sw + this.direction.x,this.head.y / sw + this.direction.y];// 下一个点是自己,代表撞到了自己,游戏结束var selfCollied = false; //是否撞到自己this.pos.forEach(function (value) {// 数组,对象,直接比较,还要比较引用值if (value[0] == nextPos[0] && value[1] == nextPos[1]) {selfCollied = true; // 表示撞到了自己}});if (selfCollied) {// console.log('撞到自己的了');this.strategies.die.call(this);return;}// 下一个点是墙,代表撞到了围墙,游戏结束if (nextPos[0] < 0 || nextPos[1] < 0 || nextPos[0] > td - 1 || nextPos[1] > tr - 1) {// console.log('撞到墙上了');this.strategies.die.call(this);return;}//说明蛇头要走的下一个点是食物的那个点if (food && food.pos[0] == nextPos[0] && food.pos[1] == nextPos[1]) {// console.log('eat food');this.strategies.eat.call(this);return;}// 下一个点是空,继续走this.strategies.move.call(this);}
书写四种逻辑,每种逻辑内有三种处理情况 游戏结束(die) 蛇吃食物(eat) 蛇移动(move)
接下来就是书写着三种处理情况的逻辑
Snake.prototype.strategies = {move: function () {},die: function() {},eat: function() {}}
首先是 move 函数
蛇移动的思路是如下
蛇朝着对应的方向移动的,首先先在蛇头处创建一个蛇身再在蛇头下一个移动的坐标上创建蛇头最后根据是否有吃食物,来进行最后一个蛇身是否消失
move: function (format) {// 传递的参数来表示蛇是否吃了食物,吃了食物传true// 在蛇头位置创建新的蛇身体var newBody = new Square(this.head.x / sw, this.head.y / sh, 'snakeBody');// 更新链表关系newBody.last = null;newBody.next = this.head.next;newBody.next.last = newBody;this.head.remove;newBody.create();// 在蛇的下一个方向,创建一个新的蛇头var newHead = new Square(this.head.x / sw + this.direction.x, this.head.y / sh + this.direction.y, 'snakeBody');// 更新链表关系newBody.last = newHead;newHead.next = newBody;newHead.last = null;newHead.create();// 改变蛇头的朝向newHead.viewContent.style.transform = 'rotate(' + this.direction.rotate + 'deg)'; // 更新位置信息this.pos.unshift([this.head.x / sw + this.direction.x, this.head.y / sh + this.direction.y]);this.head = newHead;//如果format 的值为false 表示需要删除(除了吃之外的操作)if (!format) {this.tail.remove();// 更新链表this.tail = this.tail.last;this.tail.next = null;// 更新蛇身坐标数组this.pos.pop();}},
die 这块逻辑,我们用游戏结束来代替,通过game对象的 over 来结束游戏,并打印分数等
die: function() {game.over();},
eat 这块逻辑,首先,吃了食物,蛇身体变长,但其实,蛇的身体是变长可以通过给move函数传递参数true来实现。蛇吃完了食物,就要创建新的食物,这个食物不和蛇的身体重叠,这块逻辑,就交给食物对象来处理。吃了食物之后,游戏的分数会增加,通过game对象来处理
eat: function () {this.strategies.move.call(this, true);createFood();game.score++;}
到此,蛇的逻辑基本完成了
食物
function createFood() {var x = null,y = null;var include = true;// include 用来表示食物是否在蛇的身体内while (include) {x = Math.round(Math.random() * (td - 1))// 随机生成一个 0~29的 x 坐标y = Math.round(Math.random() * (tr - 1)) // 随机生成一个 0~29的 y 坐标snake.pos.forEach(function (value) {// 通过forEach遍历蛇位置数组,来判断食物和蛇是否重叠if (value[0] != x && value[1] != y) {include = false;// 蛇和食物未重叠}});}food = new Square(x, y, 'food');food.pos = [x, y];// 如果是开局条件,直接创建一个食物对象// 如果开局之后再吃的食物,那么直接改变食物的 left 和 top 值即可var foodDom = document.querySelector('.food');if (foodDom) {foodDom.style.left = x * sw + 'px';foodDom.style.top = y * sh + 'px';} else {food.create();}}
game 对象
首先,是Game 的构造函数
function Game() {this.timer = null;// 游戏进行通过计时器进行,所以需要创建个时间戳this.score = 0;// 记录游戏的分数}
然后是game 的初始化函数
Game.prototype.init = function () {snake.init();createFood();// 通过键盘控制蛇的方向document.onkeydown = function (ev) {if (ev.which == 37 && snake.direction != snake.directionNum.right) {//用户摁下左键时候,这条蛇不能是正在往右走snake.direction = snake.directionNum.left;} else if (ev.which == 38 && snake.direction != snake.directionNum.down) {snake.direction = snake.directionNum.up;} else if (ev.which == 39 && snake.direction != snake.directionNum.left) {snake.direction = snake.directionNum.right;} else if (ev.which == 40 && snake.direction != snake.directionNum.up) {snake.direction = snake.directionNum.down;}}this.start();}
开始游戏
Game.prototype.start = function () {// 开始游戏this.timer = setInterval(function () {snake.getNextPos();}, 200)}
暂停游戏
Game.prototype.pause = function () {clearInterval(this.timer);}
游戏结束
Game.prototype.over = function () {clearInterval(this.timer);alert('你的得分为:' + this.score + '分');// 游戏回到最初始的状态var snakeWrap = document.getElementById('snakeWrap');snakeWrap.innerHTML = '';snake = new Snake();game = new Game();var startBtnWrap = document.querySelector('.startBtn');startBtnWrap.style.display = 'block';}
game逻辑内的 两个按钮 (开始游戏,暂停游戏,继续游戏)
var startBtn = document.getElementsByClassName('startBtn')[0];startBtn.onclick = function () {// 开始游戏点击startBtn.style.display = 'none';game.init();}var snakeWrap = document.getElementById('snakeWrap');var pauseBtn = document.querySelector('.pauseBtn button');snakeWrap.onclick = function () {// 点击屏幕,让游戏暂停game.pause();pauseBtn.parentNode.style.display = 'block'}pauseBtn.onclick = function () {// 点击游戏继续game.start();pauseBtn.parentNode.style.display = 'none';}
完整代码
html
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>贪吃蛇</title><link rel="stylesheet" href="CSS/index.css"></head><body><div class="content"><div class="btn startBtn"><button></button></div><div class="btn pauseBtn"><button></button></div><div id="snakeWrap"></div></div><script src="JS/index.js"></script></body></html>
css
.content {width: 640px;height: 640px;margin: 100px auto;/* background-color: #123; */position: relative;}.btn {width: 100%;height: 100%;position: absolute;left: 0;top: 0;background-color: rgba(0, 0, 0, 0.3);z-index: 2;}.btn button {background: none;border: none;background-size: 100% 100%;cursor: pointer;outline: none;position: absolute;left: 50%;top: 50%;}.startBtn button {width: 200px;height: 200px;background-image: url(../images/startGame.png);margin-left: -100px;margin-top: -100px;}.pauseBtn {display: none;}.pauseBtn button {width: 70px;height: 70px;background-image: url(../images/pauseGame.png);margin-top: -35px;margin-left: -35px;}/* snakeWrap */#snakeWrap {width: 600px;height: 600px;background-color: #225675;border: 20px solid #7dd9ff;position: relative;}#snakeWrap div {width: 20px;height: 20px;position: absolute;}.snakeHead {background-image: url(../images/snakeHead.png);background-size: cover;border-top-right-radius: 30%;border-bottom-right-radius: 30%;}.snakeBody {background-color: #9ddbb1;border-radius: 50%;}.food {background-image: url(../images/food.png);background-size: cover;}
JavaScript
var sw = 20, // 方块宽度sh = 20, // 方块高度tr = 30, // 行数td = 30; // 列数var snake = null, //蛇的实例food = null, //食物的实例game = null; //游戏的实例function Square(x, y, classname) {this.x = x * sw;this.y = y * sh;this.class = classname;this.viewContent = document.createElement('div');this.viewContent.className = classname;this.parent = document.getElementById('snakeWrap');}Square.prototype.create = function () {this.viewContent.style.left = this.x + 'px';this.viewContent.style.top = this.y + 'px';this.parent.appendChild(this.viewContent); // 将小方块添加到页面当中}Square.prototype.remove = function () {this.parent.removeChild(this.viewContent);};function Snake() {this.head = null;this.tail = null;this.pos = [];this.directionNum = {left: {x: -1,y: 0,rotate: 180},right: {x: 1,y: 0,rotate: 0},up: {x: 0,y: -1,rotate: -90},down: {x: 0,y: 1,rotate: 90}};}Snake.prototype.init = function () {var snakeHead = new Square(2, 0, 'snakeHead');snakeHead.create(); this.head = snakeHead;this.pos.push([2, 0]);var snakeBody1 = new Square(1, 0, 'snakeBody');snakeBody1.create();this.pos.push([1, 0]);var snakeBody2 = new Square(0, 0, 'snakeBody');snakeBody2.create();this.tail = snakeBody2;this.pos.push([0, 0]);snakeHead.last = null;snakeHead.next = snakeBody1;snakeBody1.last = snakeHead;snakeBody1.next = snakeBody2;snakeBody2.last = snakeBody1;snakeBody2.next = null;this.direction = this.directionNum.right;}Snake.prototype.getNextPos = function () {var nextPos = [this.head.x / sw + this.direction.x,this.head.y / sw + this.direction.y];// 下一个点是自己,代表撞到了自己,游戏结束var selfCollied = false; //是否撞到自己this.pos.forEach(function (value) {// 数组,对象,直接比较,还要比较引用值if (value[0] == nextPos[0] && value[1] == nextPos[1]) {selfCollied = true; // 表示撞到了自己}});if (selfCollied) {// console.log('撞到自己的了');this.strategies.die.call(this);return;}// 下一个点是墙,代表撞到了围墙,游戏结束if (nextPos[0] < 0 || nextPos[1] < 0 || nextPos[0] > td - 1 || nextPos[1] > tr - 1) {// console.log('撞到墙上了');this.strategies.die.call(this);return;}//说明蛇头要走的下一个点是食物的那个点if (food && food.pos[0] == nextPos[0] && food.pos[1] == nextPos[1]) {// console.log('eat food');this.strategies.eat.call(this);return;}// 下一个点是空,继续走this.strategies.move.call(this);}Snake.prototype.strategies = {move: function (format) {var newBody = new Square(this.head.x / sw, this.head.y / sh, 'snakeBody'); newBody.last = null;newBody.next = this.head.next;newBody.next.last = newBody;this.head.remove();newBody.create();var newHead = new Square(this.head.x / sw + this.direction.x, this.head.y / sh + this.direction.y, 'snakeHead');newHead.last = null;newHead.next = newBody;newBody.last = newHead;newHead.create();newHead.viewContent.style.transform = 'rotate(' + this.direction.rotate + 'deg)';this.pos.unshift([this.head.x / sw + this.direction.x, this.head.y / sh + this.direction.y]);// this.pos.splice(0, 0, [this.head.x / sw + this.direction.x, this.head.y / sh + this.direction.y]);this.head = newHead;if (!format) {//如果format 的值为false 表示需要删除(除了吃之外的操作)this.tail.remove();// 更新链表this.tail = this.tail.last;this.tail.next = null;// 更新蛇身坐标数组this.pos.pop();}},die: function () {game.over();},eat: function () {this.strategies.move.call(this, true);createFood();game.score++;}}function createFood() {var x = null,y = null;var include = true;// include 用来表示食物是否在蛇的身体内while (include) {x = Math.round(Math.random() * (td - 1))// 随机生成一个 0~29的 x 坐标y = Math.round(Math.random() * (tr - 1)) // 随机生成一个 0~29的 y 坐标snake.pos.forEach(function (value) {// 通过forEach遍历蛇位置数组,来判断食物和蛇是否重叠if (value[0] != x && value[1] != y) {include = false;// 蛇和食物未重叠}});}food = new Square(x, y, 'food');food.pos = [x, y];// 如果是开局条件,直接创建一个食物对象// 如果开局之后再吃的食物,那么直接改变食物的 left 和 top 值即可var foodDom = document.querySelector('.food');if (foodDom) {foodDom.style.left = x * sw + 'px';foodDom.style.top = y * sh + 'px';} else {food.create();}}snake = new Snake();function Game() {this.timer = null;this.score = 0;}Game.prototype.init = function () {snake.init();createFood();document.onkeydown = function (ev) {if (ev.which == 37 && snake.direction != snake.directionNum.right) {//用户摁下左键时候,这条蛇不能是正在往右走snake.direction = snake.directionNum.left;} else if (ev.which == 38 && snake.direction != snake.directionNum.down) {snake.direction = snake.directionNum.up;} else if (ev.which == 39 && snake.direction != snake.directionNum.left) {snake.direction = snake.directionNum.right;} else if (ev.which == 40 && snake.direction != snake.directionNum.up) {snake.direction = snake.directionNum.down;}}this.start();}Game.prototype.start = function () {// 开始游戏this.timer = setInterval(function () {snake.getNextPos();}, 200)}Game.prototype.pause = function () {clearInterval(this.timer);}Game.prototype.over = function () {clearInterval(this.timer);alert('你的得分为:' + this.score + '分');// 游戏回到最初始的状态var snakeWrap = document.getElementById('snakeWrap');snakeWrap.innerHTML = '';snake = new Snake();game = new Game();var startBtnWrap = document.querySelector('.startBtn');startBtnWrap.style.display = 'block';}game = new Game();var startBtn = document.getElementsByClassName('startBtn')[0];startBtn.onclick = function () {// 开始游戏点击startBtn.style.display = 'none';game.init();}var snakeWrap = document.getElementById('snakeWrap');var pauseBtn = document.querySelector('.pauseBtn button');snakeWrap.onclick = function () {// 点击屏幕,让游戏暂停game.pause();pauseBtn.parentNode.style.display = 'block'}pauseBtn.onclick = function () {// 点击游戏继续game.start();pauseBtn.parentNode.style.display = 'none';}