我们在构建贪吃蛇时,先整理好思路,把贪吃蛇模块划分为以下几类,一次完成代码
Map 地图类
- clearData 清空数据
- setData 设置数据
- include 该坐标是否包含数据
- render 将数据渲染在地图元素上
代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
padding: 0px;
margin: 0px;
}
#map{
width: 400px;
height: 400px;
background-color: #000;
position: relative;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
//地图类
class Map{
constructor(el,rect=10){
this.el = el;
this.rect = rect;
this.data = [];
//行列数 = 总宽高 / 行列的大小
this.rows = Math.ceil(Map.getStyle(el,"height") / rect);
this.cells = Math.ceil(Map.getStyle(el,"width") / rect);
//总宽高 = 行列数 * 行列的大小
Map.setStyle(el,"height",this.rows * rect);
Map.setStyle(el,"width",this.cells * rect);
}
//获取到宽高
static getStyle(el,attr){
return parseFloat(getComputedStyle(el)[attr])
}
//根据行列的大小重新赋值总宽高
static setStyle(el,attr,val){
el.style[attr] = val + "px";
}
//设置数据
setData(newData){
this.data = this.data.concat(newData);
}
//清除数据
clearData(){
this.data.length = 0;
}
//判断指定位置,是否包含数据
include({x,y}){
return this.data.some(item => (item.x == x && item.y == y));
}
//通过数据渲染页面
render(){
this.el.innerHTML = this.data.map(item=>{
return `<span style="position:absolute; left:${item.x *
this.rect}px; top:${item.y * this.rect}px;
width:${this.rect}px; height:${this.rect}px;
background:${item.color};"></span>`
}).join("");
}
}
let map = document.getElementById("map");
let gameMap = new Map(map,10);
gameMap.setData([
{x:5,y:3,color:"red"},
{x:4,y:3,color:"#fff"},
{x:3,y:3,color:"#fff"},
{x:2,y:3,color:"#fff"}
])
gameMap.render();
gameMap.clearData();
gameMap.setData([
{x:5,y:6,color:"red"},
{x:4,y:6,color:"#fff"},
{x:3,y:6,color:"#fff"},
{x:2,y:6,color:"#fff"}
])
gameMap.render();
</script>
</body>
</html>
Food 食物类
- create 创建食物
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
padding: 0px;
margin: 0px;
}
#map{
width: 400px;
height: 400px;
background-color: #000;
position: relative;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
{
//地图类
class Map{
constructor(el,rect=10){
this.el = el;
this.rect = rect;
this.data = [];
//行列数 = 总宽高 / 行列的大小
this.rows = Math.ceil(Map.getStyle(el,"height") / rect);
this.cells = Math.ceil(Map.getStyle(el,"width") / rect);
//总宽高 = 行列数 * 行列的大小
Map.setStyle(el,"height",this.rows * rect);
Map.setStyle(el,"width",this.cells * rect);
}
//获取到宽高
static getStyle(el,attr){
return parseFloat(getComputedStyle(el)[attr])
}
//根据行列的大小重新赋值总宽高
static setStyle(el,attr,val){
el.style[attr] = val + "px";
}
//设置数据
setData(newData){
this.data = this.data.concat(newData);
}
//清除数据
clearData(){
this.data.length = 0;
}
//判断指定位置,是否包含数据
include({x,y}){
return this.data.some(item => (item.x == x && item.y == y))
}
//通过数据渲染页面
render(){
this.el.innerHTML = this.data.map(item=>{
return `<span style="position:absolute; left:${item.x *
this.rect}px; top:${item.y * this.rect}px;
width:${this.rect}px; height:${this.rect}px;
background:${item.color};"></span>`
}).join("");
}
}
//食物类
class Food{
constructor(map,colors = ["yellow","orange","blue","pink"]){
this.map = map;
this.data = null;
this.colors = colors;
}
//创建食物
create(){
let x = Math.floor(Math.random() * this.map.cells);
let y = Math.floor(Math.random() * this.map.rows);
let color = this.colors[
parseInt(Math.random() *
this.colors.length)
];
this.data = {x,y,color};
if(this.map.include(this.data)){
this.create();
}
this.map.setData(this.data);
}
}
let map = document.getElementById("map");
let gameMap = new Map(map,10);
let gameFood = new Food(gameMap);
gameMap.setData([
{x:5,y:3,color:"red"},
{x:4,y:3,color:"#fff"},
{x:3,y:3,color:"#fff"},
{x:2,y:3,color:"#fff"}
])
setInterval(() => {
gameFood.create();
gameMap.render();
}, 200);
}
</script>
</body>
</html>
Snake 蛇类
- move 移动蛇
- eatFood 吃食物
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
padding: 0px;
margin: 0px;
}
#map{
width: 400px;
height: 400px;
background-color: #000;
position: relative;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
{
//地图类
class Map{
constructor(el,rect=10){
this.el = el;
this.rect = rect;
this.data = [];
//行列数 = 总宽高 / 行列的大小
this.rows = Math.ceil(Map.getStyle(el,"height") / rect);
this.cells = Math.ceil(Map.getStyle(el,"width") / rect);
//总宽高 = 行列数 * 行列的大小
Map.setStyle(el,"height",this.rows * rect);
Map.setStyle(el,"width",this.cells * rect);
}
//获取到宽高
static getStyle(el,attr){
return parseFloat(getComputedStyle(el)[attr])
}
//根据行列的大小重新赋值总宽高
static setStyle(el,attr,val){
el.style[attr] = val + "px";
}
//设置数据
setData(newData){
this.data = this.data.concat(newData);
}
//清除数据
clearData(){
this.data.length = 0;
}
//判断指定位置,是否包含数据
include({x,y}){
return this.data.some(item => (item.x == x && item.y == y))
}
//通过数据渲染页面
render(){
this.el.innerHTML = this.data.map(item=>{
return `<span style="position:absolute; left:${item.x *
this.rect}px; top:${item.y * this.rect}px;
width:${this.rect}px; height:${this.rect}px;
background:${item.color};"></span>`
}).join("");
}
}
//食物类
class Food{
constructor(map,colors = ["yellow","orange","blue","pink"]){
this.map = map;
this.data = null;
this.colors = colors;
}
//创建食物
create(){
let x = Math.floor(Math.random() * this.map.cells);
let y = Math.floor(Math.random() * this.map.rows);
let color = this.colors[
parseInt(Math.random() *
this.colors.length)
];
this.data = {x,y,color};
if(this.map.include(this.data)){
this.create();
}
this.map.setData(this.data);
}
}
//蛇类
class Snake{
constructor(map,food){ //先传入地图类和食物类,继承里边的方法
this.data = [
{x:6, y:4, color:"red"},
{x:6, y:3, color:"blue"},
{x:6, y:2, color:"blue"},
{x:6, y:1, color:"blue"}
];
this.map = map;
this.food = food;
this.direction = "right"; //默认的方向
this.map.setData(this.data); //重新设置数据
}
//移动
move(){
//声明一个变量,是他的值为this.data这个数组的长度
let i = this.data.length - 1;
//存储蛇尾巴的位置与颜色
this.lastData = {
x: this.data[i].x,
y: this.data[i].y,
color: this.data[i].color
}
//让后边每一格走到前一格的位置上
for(i; i > 0; i--){
this.data[i].x = this.data[i - 1].x;
this.data[i].y = this.data[i - 1].y;
}
//根据方向,移动蛇头
switch(this.direction){
case "left":
this.data[0].x--;
break;
case "right":
this.data[0].x++;
break;
case "top":
this.data[0].y--;
break;
case "bottom":
this.data[0].y++;
break;
}
}
//安全判断
changeDir(dir){
//当方向是左或者右时
if(this.direction === "left" || this.direction === "right"){
//且 传入的方向为左或者右
if(dir === "left" || dir === "right"){
//不能修改方向
return false;
}
}
//当方向是上或者下时
if(this.direction === "top" || this.direction === "bottom"){
//且 传入的方向为上或者下
if(dir === "top" || dir === "bottom"){
//不能修改方向
return false;
}
}
//经过安全判断后,把新方向赋值给当前方向
this.direction = dir;
return true;
}
//吃到食物后,蛇应该变大一格
eatFood(){
this.data.push(this.lastData)
}
}
let map = document.getElementById("map");
let gameMap = new Map(map,10);
let gameFood = new Food(gameMap);
let gameSnake = new Snake(gameMap,gameFood);
gameMap.render();
gameFood.create();
gameMap.setData(gameFood.data);
setInterval(() => {
gameSnake.move();
gameMap.clearData();
gameMap.setData(gameSnake.data);
gameMap.render();
}, 200);
setTimeout(() => {
gameSnake.direction = "bottom";
gameSnake.eatFood();
}, 2000);
setTimeout(() => {
gameSnake.direction = "left";
gameSnake.eatFood();
}, 4000);
setTimeout(() => {
gameSnake.direction = "top";
gameSnake.eatFood();
}, 6000);
}
</script>
</body>
</html>
Game 游戏类 - 控制器
- start 开始游戏
- stop 暂停游戏
- isOver 判断是否结束
- control 游戏控制器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
padding: 0px;
margin: 0px;
}
#map {
width: 400px;
height: 400px;
background-color: #000;
position: relative;
}
</style>
</head>
<body>
<h2 id="grade">0</h2>
<div id="map"></div>
<script>
{
//地图类
class Map {
constructor(el, rect = 10) {
this.el = el;
this.rect = rect;
this.data = [];
//行列数 = 总宽高 / 行列的大小
this.rows = Math.ceil(Map.getStyle(el, "height") / rect);
this.cells = Math.ceil(Map.getStyle(el, "width") / rect);
//总宽高 = 行列数 * 行列的大小
Map.setStyle(el, "height", this.rows * rect);
Map.setStyle(el, "width", this.cells * rect);
}
//获取到宽高
static getStyle(el, attr) {
return parseFloat(getComputedStyle(el)[attr])
}
//根据行列的大小重新赋值总宽高
static setStyle(el, attr, val) {
el.style[attr] = val + "px";
}
//设置数据
setData(newData) {
this.data = this.data.concat(newData);
}
//清除数据
clearData() {
this.data.length = 0;
}
//判断指定位置,是否包含数据
include({ x, y }) {
return this.data.some(item => (item.x == x && item.y == y))
}
//通过数据渲染页面
render() {
this.el.innerHTML = this.data.map(item => {
return `<span style="position:absolute; left:${item.x * this.rect}px; top:${item.y * this.rect}px;
width:${this.rect}px; height:${this.rect}px; background:${item.color};"></span>`
}).join("");
}
}
//食物类
class Food {
constructor(map, colors = ["yellow", "orange", "blue", "pink"]) {
this.map = map;
this.data = null;
this.colors = colors;
this.create();
}
//创建食物
create() {
let x = Math.floor(Math.random() * this.map.cells);
let y = Math.floor(Math.random() * this.map.rows);
let color = this.colors[parseInt(Math.random() * this.colors.length)];
this.data = { x, y, color };
if (this.map.include(this.data)) {
this.create();
}
this.map.setData(this.data);
}
}
//蛇类
class Snake {
constructor(map, food) { //先传入地图类和食物类,继承里边的方法
this.data = [
{ x: 6, y: 4, color: "red" },
{ x: 6, y: 3, color: "blue" },
{ x: 6, y: 2, color: "blue" },
{ x: 6, y: 1, color: "blue" }
];
this.map = map;
this.food = food;
this.direction = "right"; //默认的方向
this.map.setData(this.data); //重新设置数据
}
//移动
move() {
let i = this.data.length - 1; //声明一个变量,是他的值为this.data这个数组的长度
//存储蛇尾巴的位置与颜色
this.lastData = {
x: this.data[i].x,
y: this.data[i].y,
color: this.data[i].color
}
//让后边每一格走到前一格的位置上
for (i; i > 0; i--) {
this.data[i].x = this.data[i - 1].x;
this.data[i].y = this.data[i - 1].y;
}
//根据方向,移动蛇头
switch (this.direction) {
case "left":
this.data[0].x--;
break;
case "right":
this.data[0].x++;
break;
case "top":
this.data[0].y--;
break;
case "bottom":
this.data[0].y++;
break;
}
}
//安全判断
changeDir(dir) {
//当方向是左或者右时
if (this.direction === "left" || this.direction === "right") {
//且 传入的方向为左或者右
if (dir === "left" || dir === "right") {
//不能修改方向
return false;
}
}
//当方向是上或者下时
if (this.direction === "top" || this.direction === "bottom") {
//且 传入的方向为上或者下
if (dir === "top" || dir === "bottom") {
//不能修改方向
return false;
}
}
//经过安全判断后,把新方向赋值给当前方向
this.direction = dir;
return true;
}
//吃到食物后,蛇应该变大一格
eatFood() {
this.data.push(this.lastData)
}
}
//游戏类
class Game {
constructor(el, rect, toControl = null,toGrade = null) {
this.map = new Map(el, rect);
this.food = new Food(this.map);
this.snake = new Snake(this.map); //以上三个可以理解为继承到之前的方法
this.map.render(); //绘制初始化地图
this.timer = null; //定时器
this.interval = 200; //定时器时间设置为一个变量,可以改变速率
this.toControl = toControl; //控制器
this.keyDown = this.keyDown.bind(this); //保存this
this.grade = 0; //保存分数
this.toGrade = toGrade; //存修改分数的函数
this.control(); //调用控制器(可以没有)
}
//开始游戏
start() {
this.move();//调用移动
}
//暂停游戏
stop() {
clearInterval(this.timer);//清除定时器,已达到暂停的效果
}
//控制移动
move() {
this.timer = setInterval(() => {
this.snake.move(); //让蛇开始移动
this.map.clearData(); //清除残影
if(this.isEat()){
this.grade++; //每吃到一次,分数自加1
this.snake.eatFood(); //每吃到一次,变大一格
this.food.create(); //每吃到一次,创建新食物
this.changeGrade(this.grade); //每吃到一次,改变分数
this.interval *= 0.9; //每吃到一次,改变速率
this.stop();
this.start(); //这两步为————每吃到一次,就暂停再开始一下,实现速率的刷新
if(this.grade >= 3){ //当分数为3时调用this.over并且传一个参数1进去调用方法
this.over(1);
}
}
if(this.isOver()){ //如果结束了,那下边的方法都不执行,暂停程序运行
this.over();
return;
}
this.map.setData(this.snake.data); //设置蛇身数据
this.map.setData(this.food.data); //设置食物数据
this.map.render(); //绘制
}, this.interval);
}
//判断是否吃到食物
isEat(){
return (this.snake.data[0].x === this.food.data.x) && (this.snake.data[0].y === this.food.data.y);
}
//改变分数
changeGrade(grade){
this.toGrade && this.toGrade(grade);
}
//判断是否结束
isOver() {
if(this.snake.data[0].x < 0 || this.snake.data[0].x >= this.map.cells || this.snake.data[0].y < 0 || this.snake.data[0].y >= this.map.rows){
return true;
}
for(let i = 1; i < this.snake.data.length; i++){
if(this.snake.data[0].x == this.snake.data[i].x && this.snake.data[0].y == this.snake.data[i].y){
return true;
}
}
return false;
}
//游戏结束
/*
overState 0 中间停止,玩挂了
1 胜利了,游戏结束
*/
over(overState = 0) {
if(overState){
this.toOver && this.toWin();
}else{
this.toOver && this.toOver();
}
this.stop();
}
//键盘按下————通过键盘编码来改变方向
keyDown({keyCode}) {
switch (keyCode) {
case 37:
this.snake.changeDir("left");
break;
case 38:
this.snake.changeDir("top");
break;
case 39:
this.snake.changeDir("right");
break;
case 40:
this.snake.changeDir("bottom");
break;
}
}
//控制器
control(){
//判断用户是否需要改键,如果是的话,就调用自身
if(this.toControl){
this.toControl();
return;
}
//给window添加事件监听,按下后调用keyDown()
window.addEventListener("keydown",this.keyDown);
}
addControl(fn){
fn.call(this);
window.removeEventListener("keydown",this.keyDown);
}
}
//=================================================================================================================
let map = document.getElementById("map");
let game = new Game(map, 30);
let gradeEl = document.getElementById("grade");
game.toWin = function(){
alert("GG")
}
game.toOver = function(){
alert("游戏结束")
}
game.toGrade = function(grade){
gradeEl.innerHTML = grade;
}
game.addControl(function(){
window.addEventListener("keydown",({keyCode})=>{
switch (keyCode) {
case 87:
this.snake.changeDir("top");
break;
case 68:
this.snake.changeDir("right");
break;
case 83:
this.snake.changeDir("bottom");
break;
case 65:
this.snake.changeDir("left");
break;
}
})
});
document.onclick = function(){
game.start();
}
}
</script>
</body>
</html>
总结
- 单一原则
- 低耦合、高内聚