前端案例:贪吃蛇

307 阅读4分钟

贪吃蛇

  • 重点在于面向对象的学习
  • 不注重css样式渲染

一、游戏介绍

1、Map地图类

  • 1. -clearData清空数据(不管是舍得位置还是食物的位置都是通过数据来控制)
  • 2. -setData设置数据
  • 3.-include 该坐标是否包含数据
  • 4.-render将数据渲染在地图元素上

2、Food食物类

  • 1.-create创建食物

3、Shake蛇类

  • 1. -move移动蛇
  • 2. -eatFood吃食物

4、Game 游戏类

  • -start 开始游戏
  • -stop 暂停游戏
  • -over 结束游戏
  • -isEat 是否吃到食物
  • -changeGrade 改变分数
  • -isOver 判断是否结束
  • -control 游戏控制器

5、实现效果

QQ图片20210312153902.png

二、页面结构框架

<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 `<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("");
    }
}

四、Food食物类

  • 继承上文中的map类
  • 就一个方法,随机生成食物坐标:
    1. 食物坐标的x就是不超过列数columns的一个随机数
    2. 食物坐标的y就是不超过行数rows的一个随机数
    3. 随机数方法用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蛇类

  • 此类中有两个主要功能:
    1. 蛇的移动
    2. 吃食物
  • 蛇类需要继承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上,有需要可以自行获取 github.com/CCoisini/Sn…
  • 巩固自己的同时也希望可以帮到大家~