贪吃蛇
- 一、游戏介绍
-
- 1、Map地图类
- 2、Food食物类
- 3、Shake蛇类
- 4、Game 游戏类
- 5、实现效果
- 二、页面结构框架
- 三、Map地图类
- 四、Food食物类
- 五、Snake蛇类
- 六、Game游戏控制类
- 七、主函数调用
- 八、总结
一、游戏介绍
1、Map地图类
- 1. -clearData清空数据(不管是舍得位置还是食物的位置都是通过数据来控制)
- 2. -setData设置数据
- 3.-include 该坐标是否包含数据
- 4.-render将数据渲染在地图元素上
2、Food食物类
3、Shake蛇类
- 1. -move移动蛇
- 2. -eatFood吃食物
4、Game 游戏类
- -start 开始游戏
- -stop 暂停游戏
- -over 结束游戏
- -isEat 是否吃到食物
- -changeGrade 改变分数
- -isOver 判断是否结束
- -control 游戏控制器
5、实现效果
二、页面结构框架
<div id="map"></div>
<h1 id="grade"></h1>
<button id="start">开始游戏</button>
<button id="stop">暂停游戏</button>
- 页面非常简单,只包含四个元素,这里不做过多的样式追求
三、Map地图类
- map可以看做是一个二维的数组,把它分为若干份,每一份就是一个格子,我们可以控制每一格的数据,我们可以控制格子的颜色来表示蛇,食物也可以通过格子的下标来控制。
- 把地图想象成一个直角坐标系的话,就会非常简单
- map只负责把数据提供地图,并把渲染到地图上
class Map {
constructor(el,rect){
this.el = el;
this.rect = rect;
this.data = [];
this.rows = Math.ceil(Map.getStyle(el,"height")/rect);
this.columns = Math.ceil(Map.getStyle(el,"width")/rect);
Map.setStyle(el,"height",this.rows*rect);
Map.setStyle(el,"width",this.columns*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 `${
item.x*this.rect}px;top:${
item.y*this.rect}px;width:${
this.rect}px;height:${
this.rect}px;background:${
item.color};">`
}).join("");
}
}
四、Food食物类
- 继承上文中的map类
- 就一个方法,随机生成食物坐标:
- 食物坐标的x就是不超过列数columns的一个随机数
- 食物坐标的y就是不超过行数rows的一个随机数
- 随机数方法用Math.random()来实现
- 注意,这里要进行是否包含判断,即食物必须出现在蛇以外的地方,利用map类里已经写好的include方法
class Food {
constructor(map,colors = ["red","blue","yellow","pink"]){
this.map = map;
this.colors = colors;
this.data = null;
this.create();
}
create(){
let x = Math.floor(Math.random()*this.map.columns);
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)
}
}
五、Snake蛇类
- 此类中有两个主要功能:
- 蛇的移动
- 吃食物
- 蛇类需要继承map类和food类,因为蛇需要在地图上移动,操作地图,和吃食物
- 移动分为两个部分,蛇头移动和蛇神移动
- 蛇身移动就是格子移到上一个位置;
- 蛇头移动就是格子上下左右加减变化;
- 吃食物功能:
相对来说比较简单,就是在data里的末尾加一项
class Snake{
constructor(map,food){
this.data = [
{
x:6 ,y:2, color:"green"},
{
x:5 ,y:2, color:"white"},
{
x:4 ,y:2, color:"white"},
{
x:3 ,y:2, color:"white"},
{
x:2 ,y:2, color:"white"}
];
this.map = map;
this.food = food;
this.direction = "right"
this.map.setData(this.data);
}
move(){
let length = this.data.length
this.lastData = {
x:this.data[length-1].x,
y:this.data[length-1].y,
color:"white"
}
for(let i = this.data.length-1; 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 "up":
this.data[0].y--;
break;
case "down":
this.data[0].y++;
break;
}
}
changeDirection(dir){
if(this.direction === "left"||this.direction === "right"){
if(dir === "left"||dir === "right"){
return false
}
} else {
if(dir === "up"||dir === "down"){
return false
}
}
this.direction = dir;
return true;
}
eatFood(){
this.data.push(this.lastData);
}
}
六、Game游戏控制类
class Game {
constructor(el,rect,toGrade=null,toOver=null){
this.map = new Map(el,rect);
this.food = new Food(this.map);
this.snake = new Snake(this.map,this.food);
this.map.render();
this.timer = 0;
this.interval = 200;
this.keyDown = this.keyDown.bind(this);
this.grade = 0;
this.toGrade = toGrade;
this.toOver = toOver;
this.control();
}
start(){
this.move();
}
stop(){
clearInterval(this.timer);
}
move(){
this.stop();
this.timer = setInterval(()=>{
this.snake.move();
this.map.clearData();
if(this.isEat()){
this.grade++
this.snake.eatFood();
this.food.create();
this.changeGrade(this.grade);
this.interval *= .9;
this.stop();
this.start();
}
if(this.isOver()){
this.over()
}
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;
}
isOver(){
if(this.snake.data[0].x < 0
|| this.snake.data[0].x >= this.map.columns
|| 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;
}
over(){
this.toOver&&this.toOver();
this.stop();
}
changeGrade(grade){
this.toGrade && this.toGrade(grade);
}
keyDown({
keyCode}){
let isDir;
switch(keyCode){
case 37:
isDir = this.snake.changeDirection("left");
break;
case 38:
isDir = this.snake.changeDirection("up");
break;
case 39:
isDir = this.snake.changeDirection("right");
break;
case 40:
isDir = this.snake.changeDirection("down");
break;
}
}
control(){
window.addEventListener("keydown",this.keyDown);
}
}
七、主函数调用
{
let map = document.querySelector("#map");
let game = new Game(map,10);
let gradeEl = document.querySelector("#grade")
game.toGrade = function(grade){
gradeEl.innerHTML = grade;
}
game.toOver = function(){
alert("游戏结束!")
}
document.querySelector("#start").onclick = (()=>{
console.log("开始游戏");
game.start();
})
document.querySelector("#stop").onclick = (()=>{
console.log("暂停游戏");
game.stop();
})
}
八、总结
- 上述代码功能虽然都完成了,但其实不是很符合面向对象的写法规范,因为有些类里面的功能完全可以不需要,上述代码类与类之间的依赖很严重
- 为了解决上述问题,我已按照模块化思想和面向对象的思想对代码进行了优化,代码在github上,有需要可以自行获取 https://github.com/CCoisini/Snake
- 巩固自己的同时也希望可以帮到大家~