本文阅读时间15分钟,要求你有一定的html5+css3+es6基础。打开编辑器跟着我一起把代码敲一遍,效果更佳。
废话不多说,先贴代码,里面有详细的注释,不想看的可以跳过看下面的讲解:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.game-container {
color: gray;
border: 10px double gray;
width: 40%;
text-align: center
}
p:first-child {
margin-right: 200px;
}
.snake-area {
width: 315px;
height: 525px;
margin: auto;
padding-bottom: 10px
}
.grid {
border-right: 1px solid purple;
border-bottom: 1px solid purple;
width: 20px;
height: 20px;
border-radius: 10px;
float: left;
}
.snake {
background: pink;
}
.food {
background: yellow;
}
p {
display: inline-block;
}
.first-one {
margin-right: 20px;
}
button {
width: 100px;
height: 21px;
line-height: 18px;
color: brown;
font-weight: bold;
font-size: 15px;
}
.statement-wrapper {
background: orange;
display: none;
padding: 15px;
opacity: 0.9;
position: fixed;
top: 40%;
left: 16%;
color: #fff;
}
</style>
</head>
<body>
<div class="game-container">
<h1>SUPER SNAKE</h1>
<p class="first-one">
<span>CURRENT SCORE:</span>
<strong class="score"></strong>
</p>
<p>
<span>HISTORY SCORE:</span>
<strong class="history-score"></strong>
</p>
<div class="statement-wrapper"></div>
<section class="snake-area"></section>
</div>
<script type="text/javascript">
class Snake {
constructor(height, width) {
this.y_edge = height / 20; // 下边缘
this.x_edge = width / 20; // 右边缘
this.body = ['grid-1-1', 'grid-1-2', 'grid-1-3']; // 蛇身的初始位置
this.moveTo = 39; // 默认向右移动,对应的键码为39
this.interval = null; // 用来保存秒表
this.shouldRemove = true; // 用来判断是否要去掉蛇尾的颜色(吃到食物时为假,此时身长增加1)
this.stopGame = false; // 用来判断游戏是否还在进行中
this.eatSelf = false; // 判断是否吃到自己
this.food = new Food(); // 引入食物
this.showScore = new Score(); // 引入比分
this.addColor(); // 给蛇添加背景色
this.move(); // 让蛇动起来
this.controller(); // 绑定键盘事件
}
addColor() { // 循环蛇的身体,给每个格子添加class
this.body.forEach(item => {
document.querySelector(`#${item}`).classList.add('snake');
})
};
removeColor() { // 去掉蛇身体数组的第一个元素,去掉class
const uncolored = this.body.shift()
document.querySelector(`#${uncolored}`).classList.remove('snake');
};
move(to = 'right') {
this.interval = setInterval(() => { // 每隔500ms执行一次:
const headId = this.body.slice(-1)[0], // 取出蛇头
headY = headId.split('-')[1], // 获取蛇头Y坐标
headX = headId.split('-')[2], // 获取蛇头X坐标
directions = {
right: [`${headY}`, +headX + 1, this.x_edge], // 蛇向右移动时,Y坐标保持不变,X坐标+1
left: [`${headY}`, +headX - 1, this.x_edge], // 蛇向左移动时,Y坐标保持不变,X坐标-1
down: [+headY + 1, `${headX}`, this.y_edge], // 蛇向下移动时,Y坐标+1,X坐标保持不变
up: [+headY - 1, `${headX}`, this.y_edge] // 蛇向上移动时,Y坐标-1,X坐标保持不变
};
this.eat(headX, headY); // 取到蛇头的坐标,若该坐标与食物坐标相等,执行吃的方法
for (const item of this.body.slice(0, this.body.length - 1)) { // 取出蛇身体除蛇头的部分
if (headId === item) this.eatSelf = true; // 循环判断每一部分的ID是否与蛇头ID相等,是则吃到了自己
}
this.body.push(`grid-${directions[to][0]}-${directions[to][1]}`); // 给蛇的数组添加新蛇头
this.addColor(); // 给蛇头染上颜色
if (this.shouldRemove) { // 如果这次移动没有迟到食物,去掉蛇尾颜色
this.removeColor();
} else { // 如果吃到了食物,重置shouldRemove为true(在eat方法里如果吃到了会将其设为false)
this.shouldRemove = true;
}
if (this.body.length + 1 === this.x_edge * this.y_edge) { // 当蛇身的长度与格子的数量相差1时,赢
clearInterval(this.interval); // 停止秒钟
this.stopGame = true; // 按键失效
this.showStatement('YOU WIN !');
this.showScore.updateHistory();
}
// 移动到边缘或吃到自己时,输
const last = typeof directions[to][1] === 'number' ? directions[to][1] : directions[to][0];
if (last === directions[to][2] || last === 1 || this.eatSelf) {
clearInterval(this.interval);
this.stopGame = true;
this.showStatement('GAME OVER !');
this.showScore.updateHistory();
};
}, 500);
};
controller() {
document.addEventListener('keyup', e => {
if (this.moveTo !== e.keyCode &&
this.moveTo !== e.keyCode + 2 &&
this.moveTo !== e.keyCode - 2 &&
!this.stopGame) {
this.moveTo = e.keyCode;
clearInterval(this.interval);
switch (e.keyCode) {
case 40:
this.move('down');
break;
case 38:
this.move('up');
break;
case 37:
this.move('left');
break;
case 39:
this.move('right');
break;
default:
break;
}
}
}, false)
};
eat(x, y) {
if (x == this.food.X && y == this.food.Y) { // 如果蛇头坐标与食物坐标相等
document.querySelector(`#grid-${y}-${x}`).classList.remove('food'); // 移除当前食物
this.shouldRemove = false; // 增加蛇长
this.showScore.addScore(); // 加分
this.food = new Food(); // 重新生成食物
}
};
showStatement(content) {
const statement = document.createElement('h4');
statement.innerHTML = content;
const restart = document.createElement('button');
restart.innerHTML = 'RESTART';
const statementWrapper = document.querySelector('.statement-wrapper');
statementWrapper.style.display = 'block';
statementWrapper.appendChild(statement);
statementWrapper.appendChild(restart).addEventListener('click', () => location.reload(), false)
};
}
class Food {
constructor() {
if (!document.querySelector('.food')) {
this.x = Math.floor(13 * Math.random() + 2);
this.y = Math.floor(23 * Math.random() + 2);
document.querySelector(`#grid-${this.y}-${this.x}`).classList.add('food');
return {
X: this.x,
Y: this.y
};
}
}
};
class Score {
constructor() {
// 用try/catch,以便在用户开启浏览器隐私模式或localStorage内存已满时使用
try {
localStorage.getItem('historyScore') ? this.historyScore = localStorage.getItem('historyScore') :
this.historyScore = 0; // 取出历史得分,若没有则设为0,显示在页面
document.querySelector('.history-score').innerHTML = this.historyScore;
} catch {
alert('Your localStorage is not functioning, please open this page in another browser!')
}
this.score = 0; // 本次游戏初始得分为0,显示在页面
document.querySelector('.score').innerHTML = this.score;
};
addScore() {
this.score++;
document.querySelector('.score').innerHTML = this.score;
};
updateHistory() {
if (this.score > this.historyScore) { // 若本次得分高于历史得分,存储新的历史得分
try {
localStorage.setItem('historyScore', this.score);
document.querySelector('.history-score').innerHTML = this.score;
} catch {
alert('Your localStorage is not functioning, please open this page in another browser!')
}
}
}
}
// 初始化页面,显示蛇和得分
const initializeGrids = (height = 500, width = 300) => {
for (let i = 1; i < height / 20 + 1; i++) {
for (let j = 1; j < width / 20 + 1; j++) {
const grid = document.createElement('div');
grid.setAttribute('id', `grid-${i}-${j}`);
grid.classList.add('grid');
const con = document.querySelector('.snake-area').appendChild(grid);
}
}
const showSnake = new Snake(height, width);
const showScore = new Score();
};
initializeGrids();
</script>
</body>
</html>
首先我们要有一个蛇的类,它要移动自己的身体,吃到食物;移动的时候撞了墙,或者咬到了自己的身体,会死;吃到了食物,身长会增加。
所以这个蛇的类,要有一个Move的方法,Move里包含Eat,还有控制Move方向的Controller。
那么怎么让它Move呢?是通过不断往蛇前进的方向添加DOM,并在尾巴处删除DOM?还是通过不断地改变蛇头和蛇尾的背景色来实现移动的效果?我选择了后者。
把蛇的活动区域(300px * 500px)打上格子,每个格子是一个div(20px * 20px)。每个div的ID按照自己所在的位置编号:比如第一排的第一个,编号为”grid-1-1“。
snake area
for (let i = 1; i < height / 20 + 1; i++) {
for (let j = 1; j < width / 20 + 1; j++) {
const grid = document.createElement('div');
grid.setAttribute('id', `grid-${i}-${j}`);
grid.classList.add('grid');
const con = document.querySelector('.snake-area').appendChild(grid);
}
}
这样我们就能通过编号来记录并控制蛇运动的路线了。我们给蛇设置的初始身长为3个格子的长度(背景色为粉色);初始位置是第一排从左边起的前三个(把这三个位置保存在数组里:['grid-1-1', 'grid-1-2', 'grid-1-3']);初始的移动方向是从左到右。
为了实现从左到右的移动,每隔500ms,给蛇头右边的第一个div添加粉色背景,并去掉蛇尾的背景色。同时移除蛇身体数组的第一个元素,并添加新的蛇头位置。
snake
class Snake {
constructor(height, width) {
this.y_edge = height / 20;
this.x_edge = width / 20;
this.body = ['grid-1-1', 'grid-1-2', 'grid-1-3'];
this.interval = null;
this.wrapper = document.querySelector('.statement')
this.addColor();
this.move();
}
addColor() {
this.body.forEach(item => {
document.querySelector(`#${item}`).classList.add('snake');
})
};
removeColor() {
const uncolored = this.body.shift()
document.querySelector(`#${uncolored}`).classList.remove('snake');
};
move(to = 'right') {
this.interval = setInterval(() => {
const headId = this.body.slice(-1)[0],
headY = headId.split('-')[1],
headX = headId.split('-')[2],
directions = {
right: [`${headY}`, +headX + 1, this.x_edge],
left: [`${headY}`, +headX - 1, this.x_edge],
down: [+headY + 1, `${headX}`, this.y_edge],
up: [+headY - 1, `${headX}`, this.y_edge]
};
this.body.push(`grid-${directions[to][0]}-${directions[to][1]}`);
this.removeColor();
this.addColor();
};
}, 500);
};
};
取出蛇头的id,并用headY和headX分别保存蛇头在Y轴和X轴上的位置;y_edge和x_edge分别为蛇活动区域的下边界和有边界;directions主要保存蛇的位移和位置,为了方便后面使用,把y_edge和x_edge也放在了里面。
接下来我们来写controller方法,即通过控制键盘的上下左右键,来实现蛇前进方向的转变:
controller() {
document.addEventListener('keyup', e => {
if (this.moveTo !== e.keyCode &&
this.moveTo !== e.keyCode + 2 &&
this.moveTo !== e.keyCode - 2 &&
!this.stopGame) {
this.moveTo = e.keyCode;
clearInterval(this.interval);
switch (e.keyCode) {
case 40:
this.move('down');
break;
case 38:
this.move('up');
break;
case 37:
this.move('left');
break;
case 39:
this.move('right');
break;
default:
break;
}
}
}, false);
绑定keyup事件。this.moveTo用来保存当前按键的值,避免重复按键或按下方向相反的键。this.stopGame后面会讲到。
keyup事件触发之后,清除当前interval(停止蛇在当前方向上的运动),改变方向后(this.moveTo取得了新的值)继续移动。
蛇的移动到这里告一段落。下面讲吃食物且身体长度增加的部分。
食物的出现位置是随机的,且每次蛇吃完以后需要重新生成。于是我们写一个Food的类:
class Food {
constructor() {
if (!document.querySelector('.food')) {
this.x = Math.floor(13 * Math.random() + 2);
this.y = Math.floor(23 * Math.random() + 2);
document.querySelector(`#grid-${this.y}-${this.x}`).classList.add('food');
return {
X: this.x,
Y: this.y
};
}
}
};
对应的蛇类里的吃的方法:
eat(x, y) {
if (x == this.food.X && y == this.food.Y) {
document.querySelector(`#grid-${y}-${x}`).classList.remove('food');
this.shouldRemove = false;
this.showScore.addScore();
this.food = new Food();
}
};
到这里为止核心的部分就完成了。最后我们再添加一个得分的类:
class Score {
constructor() {
// 用try/catch,以便在用户开启浏览器隐私模式或localStorage内存已满时使用
try {
localStorage.getItem('historyScore') ? this.historyScore = localStorage.getItem('historyScore') :
this.historyScore = 0; // 取出历史得分,若没有则设为0,显示在页面
document.querySelector('.history-score').innerHTML = this.historyScore;
} catch {
alert('Your localStorage is not functioning, please open this page in another browser!')
}
this.score = 0; // 本次游戏初始得分为0,显示在页面
document.querySelector('.score').innerHTML = this.score;
};
addScore() {
this.score++;
document.querySelector('.score').innerHTML = this.score;
};
updateHistory() {
if (this.score > this.historyScore) { // 若本次得分高于历史得分,存储新的历史得分
try {
localStorage.setItem('historyScore', this.score);
document.querySelector('.history-score').innerHTML = this.score;
} catch {
alert('Your localStorage is not functioning, please open this page in another browser!')
}
}
}
}
所有这些都完成之后,我们需要有一个初始化的方法,建立蛇的活动区域,并显示蛇和得分:
const initializeGrids = (height = 500, width = 300) => {
for (let i = 1; i < height / 20 + 1; i++) {
for (let j = 1; j < width / 20 + 1; j++) {
const grid = document.createElement('div');
grid.setAttribute('id', `grid-${i}-${j}`);
grid.classList.add('grid');
const con = document.querySelector('.snake-area').appendChild(grid);
}
}
const showSnake = new Snake(height, width);
const showScore = new Score();
};
initializeGrids();
这样,一个完整的贪食蛇小游戏就完成了,复制最前面的代码,在浏览器打开就可以跑起来了。也可以到这里下载:https://gist.github.com/PengHoliday/65029f78f385b5884c1b9dfd2162c611
欢迎留言与我交流~
网友评论