说在前面
在网上看了许多的贪吃蛇这类游戏,效果基本还可以,功能也实现了,不过看代码大都是冗余或杂乱不堪的,难以维护。
所以花了点时间,对整个游戏重构了一下,也算是站在各位前辈的肩膀上做的优化,希望对大家有帮助。
效果图.gif
功能描述
生成一条蛇,可以上下左右移动,目标只有一个:吃食物。吃到一个食物蛇的身体增加一节,然后生成下一个食物,撞到地图就GG,game over。
设计思路
1. 整体实现采用原生JS,使用ES6的Class类构造,完美的诠释了面向对象编程的编程思想。
2. js 主体文件分成 food.js(食物类),snake.js(蛇类), game.js(游戏入口文件),util.js(工具函数)。讲道理,地图也需要用到一个map.js(地图类),由于这里的地图过分简单,所以不搞也罢。
3. 设计思路图解:
1. 工具类设计 ( util.js )
目标功能点:
- 生成随机坐标
export function getRandom(a, b){ let max = Math.max(a, b); let min = Math.min(a, b); return parseInt(Math.random() * (max - min)) + min; }
2. 食物类设计(food.js)
目标功能点:
- 初始化食物(宽,高,颜色等)
- 在地图上随机生成
- 管理食物(删除)
import { getRandom } from './util.js'; // 食物类 class Food { // 初始化 constructor({x = 0, y = 0, width = 20, height = 20, color = 'green'} = {}){ // 结构赋值 参数默认值 // let options = {x = 0, y = 0, width = 20, height = 20, color = 'green'} || {}; // 存储食物 this.elements = []; // 坐标 this.x = x; this.y = y; this.width = width; this.height = height; this.color = color; } render(map){ this.remove(); // 删除之前创建的食物 // 随机设置x,y的值 this.x = getRandom(0, map.offsetWidth / this.width - 10) * this.width; this.y = getRandom(0, map.offsetHeight / this.height - 1) * this.height; console.log(this.x, this.y); // 创建食物 dom let div = document.createElement('div'); map.appendChild(div); this.elements.push(div); // 设置div的样式 div.style.position = 'absolute'; div.style.left = this.x + 'px'; div.style.top = this.y + 'px'; div.style.width = this.width + 'px'; div.style.height = this.height + 'px'; div.style.backgroundColor = this.color; } remove() { // 从后往前 for(let i = this.elements.length -1; i >= 0; i--){ this.elements[i].parentNode.removeChild(this.elements[i]); // 删除div this.elements.splice(i, 1); // 删除数组中的元素 } } } export default Food;
3. 蛇类(snake.js)
目标功能点:
- 初始化蛇(宽,高,颜色、长度等)
- 在地图上初始定位
- 蛇的移动与管理(吃一个食物生成一个新的蛇对象)
- 判断是否吃到食物(蛇头的坐标与食物坐标重合)
// 蛇类 class Snake { constructor({ width = 20, height = 20, direction = 'right' } = {}){ // 存储蛇 this.elements = []; this.width = width; this.height = height; this.direction = direction; // 蛇的身体 初始三节 this.body = [ {x: 3, y: 2, color: 'red'}, {x: 2, y: 2, color: 'blue'}, {x: 1, y: 2, color: 'blue'}, ]; } render(map){ this.remove(); // 删除之前创建的蛇 for(let i = 0, len = this.body.length; i < len; i++ ){ let object = this.body[i]; let div = document.createElement('div'); map.appendChild(div); this.elements.push(div); // 设置样式 div.style.position = 'absolute'; div.style.width = this.width + 'px'; div.style.height = this.height + 'px'; div.style.left = object.x * this.width + 'px'; div.style.top = object.y * this.height + 'px'; div.style.backgroundColor = object.color; } } move(food, map){ // 控制蛇的移动 (当前蛇节 移动到上一个蛇节) for(let i = this.body.length - 1; i > 0; i--){ this.body[i].x = this.body[i - 1].x; this.body[i].y = this.body[i - 1].y; } // 蛇头 let head = this.body[0]; // 蛇头的行进方向 switch(this.direction) { case 'right': head.x += 1; break; case 'left': head.x -= 1; break; case 'top': head.y -= 1; break; case 'bottom': head.y += 1; break; } // 蛇吃食物 // 判断蛇头的位置是否与食物的位置重合 let headX = head.x * this.width; let headY = head.y * this.height; if(headX === food.x && headY === food.y){ let last = this.body[this.body.length -1 ]; this.body.push({ x: last.x, y: last.y, color: last.color }); // 重新生成一个食物 food.render(map); } } remove() { for (let i = this.elements.length - 1; i >= 0; i--) { // 删除div this.elements[i].parentNode.removeChild(this.elements[i]); // 删除数组中的元素 this.elements.splice(i, 1); } } } export default Snake;
4. 游戏入口文件(game.js)
目标功能点:
- 实例化蛇与食物
- 让蛇动起来
- 绑定按键,控制方向
- 开始游戏
- 当蛇撞到地图边缘GG,显示 game over!
import Food from "./food.js"; import Snake from "./snake.js"; // 游戏的入口文件 class Game { constructor() { // 创建食物和蛇的实例 this.food = new Food(); this.snake = new Snake(); this.map = map; // 定时器 this.timerId = null; } start() { // 食物和蛇 渲染到地图上 this.food.render(this.map); this.snake.render(this.map); this.runSnake(); this.bindKey(); } // 让蛇动起来 runSnake() { this.timerId = setInterval( () => { // 要获取游戏对象中的蛇属性 this.snake.move(this.food, this.map); // 2.2 当蛇遇到边界游戏结束 var maxX = this.map.offsetWidth / this.snake.width; var maxY = this.map.offsetHeight / this.snake.height; var headX = this.snake.body[0].x; var headY = this.snake.body[0].y; if (headX < 0 || headX >= maxX || headY < 0|| headY >= maxY) { console.log('Game Over'); clearInterval(this.timerId); return } this.snake.render(this.map); // 根据body 的数据 重新渲染蛇在页面位置 }, 150); } // 绑定键盘事件 控制蛇的方向 bindKey() { document.addEventListener('keydown', (e) => { switch (e.keyCode) { case 37: this.snake.direction = 'left'; break; case 38: this.snake.direction = 'top'; break; case 39: this.snake.direction = 'right'; break; case 40: this.snake.direction = 'bottom'; break; } }); } } export default Game;
5. 调用(index.html)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="map" style="width:80%;height:400px;border: 1px solid orange;"></div> <script type="module"> import Game from './game.js'; // 全局的地图 map let map = document.getElementById('map'); let game = new Game(map); // 调用开始方法 game.start(); </script> </body> </html>
FAQ:
由于整个项目采用ES6的模块设计,所以需要启动一个本地服务才可以跑,单独点开index.html,是没得用的。