面向对象-贪吃蛇

初学面向对象比较难,基本每行都有注释,大家要耐心,细心。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>贪吃蛇</title>
    <style>
        .map {
            width: 400px;
            height: 400px;
            background-color: black;
            position: relative;
        }
    </style>
</head>

<body>

    <h2 id="grade">0</h2>  
    <!-- 记录分数 得起到事件监听的作用 -->

    <div class="map"></div>
    
    <script>
        // 根本性的 流程 先有数据准备数据 更新数据 再把数据渲染到视图上(数据驱动地图)
        {

            class Map { //Map地图类 rect 定义一个当前每个小格的宽。
                constructor(el, rect = 10) {  //接收实例化对象gameMap的内容
                    this.el = el;
                    this.rect = rect;
                    this.data = [
                        //     {x:5,y:8,color:"red"},  //x y 只是测试数据(蛇出现的位置)
                        //     {x:6,y:8,color:"#fff"},
                        //    {x:7,y:8,color:"red"},
                    ];

                    // 去找行和列
                    //行列数 = 总宽高 / 行列的大小 获取
                    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"
                }

                //设置数据  newData接收  concat给一个数组添加数据(添加进来的可以是数组) 添加完有返回值 去前面接收
                setData(newData) {       //push 不能添加数组
                    this.data = this.data.concat(newData)
                }

                // 清除数据
                clearData() {
                    this.data.length = 0;
                }
                // 判断指定位置,是否包含数据
                // x y 形参谁调用谁给他传参数
                include({ x, y }) {   //some判断数组里的元素是满足指定条件(就是碰撞检测蛇有没有碰到食物)
                    return this.data.some(item => (item.x == x && item.y == y));
                    //这里写大括号 是代码段函数
                    //    不写或小括号 是返回
                }
                // 通过数据把(蛇)渲染到页面元素
                render() {           
                    
                    
                                //map() 遍历data数组的每一项
                    this.el.innerHTML = this.data.map(item => {          //item*rect  第几行*每个小格的宽/高 = 得出位置信息
                        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("")     //join() 指定拼接  这里数据会处理的更好                                                                             //蛇 宽高各占一小格                    
                }
            }

            // 第二部分食物类
            class Food {         //默认值,也可写外面
                constructor(cells,rows, color = ["red", 'yellow', "pink"]) {    //地图实例方法已经在Food当中 需要直接调用
                    
                    this.cells =cells;//保存行
                    this.rows = rows;
                    this.data = null; // 储存食物
                    this.color = color
                    this.create(); //初始化食物(早不到x 报错)  因为游戏还没开始 刷新应该有初始蛇 初始食物,初始地图三样 
                                    // 游戏开始 再去操控他修改 和删除 这三样东西
                }

                //创建食物
                create() {
                    //create this > 指向实例化
                    //静态 create this 指向方法
                    let x = Math.floor(Math.random() * this.cells);
                    let y = Math.floor(Math.random() * this.rows);
                    //食物颜色                                     // 0-数组长度随机
                    let color = this.color[parseInt(Math.random() * this.color.length)]
                    this.data = { x, y, color } //
                                // 把自身传进去判断 看一下是否和蛇的位置重叠了
                    // if (this.map.include(this.data)) { //include 传进去一个数据 返回true 
                    //     this.create();                          // 如果是true 调用自己方法重新走一遍再判断
                    //     //否则 结束
                    // }
                    // //地图类设置数据  //食物类的
                    // this.map.setData(this.data)
                }

            }

            // 第三部分蛇类
            class Snake {   ///创建一空的蛇类
                constructor() {   //食物和地图添加进来
                    this.data = [
                        { x: 6, y: 4, color: "red" },
                        { x: 5, y: 4, color: "blue" },
                        { x: 4, y: 4, color: "blue" },
                        { x: 3, y: 4, color: "blue" }
                    ];
                    // this.map = map;
                    // this.food = food;
                    this.direction = "right";
                    this.lastData = {}
                }
                //蛇的移动
                move() {  //for循环每一格都往前添加
                    let i = this.data.length - 1
                    this.lastData = {
                        x: this.data[i].x, //吃到的食物 应该天机到最后一位this.data.length - 1
                        y: this.data[i].y,
                        color: this.data[i].color
                    }

                    for (i; i > 0; i--) { //这里 是从后往前倒着
                        //假如i =4   这里[]中就是3  也就是说把后面的数据不断往前添加
                        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; //不能修改       结束的是整段
                        }
                    } else {
                        // 否则当前的方向是上 或者  下
                        // 如果传入的新方向是上 或者 下
                        if (dir === "top" || dir === "bottom") {
                            return false; //不能修改  
                        }
                    }
                    this.direction = dir;
                    return true;
                }

                //吃到了食物,蛇会变大
                eatFood() {
                    this.data.push(this.lastData); // 变大的是this.lastData
                }

            }//蛇类

            // 第四游戏类  总控制
            class Game {
                constructor(el, rect, toControl = null,toGrade =null) {
                    //着三个类原来在外面放在这里隐藏起来 方便游戏类进行控制
                    this.map = new Map(el, rect)  // 地图
                    this.food = new Food(this.map.cells,this.map.rows);// 食物类 直接把地图的列,和地图的行
                    this.snake = new Snake(this.map);  //蛇 
                    this.map.setData(this.snake.data) //蛇放到地图中   
                    this.createFood(); //检测食物
                    this.render(); // 绘制初始地图 就是刷新页面时候 有东西在   蛇已经在地图里就蛇就被带进去了
                    this.timer = 0;  // 声明一下  说明他的存在
                    this.ineterval = 200;
                    this.toControl = toControl  //控制器
                    this.keyDown = this.keyDown.bind(this) ;//保存当前this这个保存具体某个值  前用那个保存方法更全面
                    this.grade = 0; //记录分数
                    this.toGrade = toGrade   //改变分数  事件监听
                    this.control();//  调用控制器 
                }
                // 将食物 和地图产生关系 食物渲染到地图中
                createFood(){
                    this.food.create(); //创建食物
                    // 把自身传进去判断 看一下是否和蛇的位置重叠了
                    if (this.map.include(this.food.data)) { //include 传进去一个数据(把食物传进地图) 返回true 
                        this.createFood();                          // 如果是true 调用自己方法重新走一遍再判断
                        //否则 结束
                    }
                    //地图类设置数据  //食物类的
                    this.map.setData(this.food.data)
                }

                //开始游戏

                start() {
                    this.move(); // 开始移动
                }

                //暂停游戏
                stop() {
                    clearInterval(this.timer)  //清除定时器
                }
                // 给地图渲染数据
                render(){
                    this.map.clearData();               // 每次渲染首先清除数据  为了不让旧数据(已经渲染)新数据冲突,重叠,叠加
                    this.map.setData(this.snake.data); //蛇数据放进来 这里只控制结果 具体过程在具体的蛇类里
                        this.map.setData(this.food.data);  //食物的类
                        // console.log(this.map.data)  // 有个问题 找不到x 输出地图 食物为空没有数据 那就去食物类里找(没有初始食物)
                        this.map.render();  //绘制地图
                }

                //控制移动
                move() {
                    this.timer = setInterval(() => {
                        this.snake.move();//蛇开始移动
                        // this.map.clearData(); // 清除移动后重影的数据
                        if(this.isEat()){   //清除数据后判断是否吃到食物
                            this.grade++   //吃到食物 分数++
                            console.log(this.grade)
                            this.snake.eatFood(); //吃到的话  蛇变大
                            this.createFood();   // 被吃掉之后 新食物出现
                            this.changeGrade(this.grade)  //上面grade值发生改变 传到下面changeGrade函数里 函数接收到 看一下 外面有没有方法
                                                          // r如果有 就去调用把参数传进去  
                            this.ineterval *=0.9  // 吃掉食物后 速度变快
                            this.stop();          // ineterval的原因定时器的运行速度更新了 不再是原来的速度               
                            this.start();         // 需要重新开启 更新速度  不重启的话ineterval虽然改变了 但定时器已经走起来 影响不了定时器 
                            
                            if(this.grade >= 3){ // 分数增长重启定时器 就去判断分数 大于3分
                                this.over(1);   // 传一个状态码 调用游戏状态码 再去判断是胜利 还是 失败结束
                            }
                        
                        }

                        if(this.isOver()){ //每次移动 吃完 调用
                            this.over();  //如果碰到 游戏结束
                            return;     
                        }

                        // this.map.setData(this.snake.data); //蛇数据放进来 这里只控制结果 具体过程在具体的蛇类里
                        // this.map.setData(this.food.data);  //食物的类
                        // console.log(this.map.data)  // 有个问题 找不到x 输出地图 食物为空没有数据 那就去食物类里找(没有初始食物)

                        this.render();  //上面的三行集中写完外面,这里调用了

                        //以上这些都是数据的操作
                    }, this.ineterval) //速度写成属性(变量)  吃一个速度加快一点

                }

                // 判断是否吃到食物  去判断蛇头的x,y 值与食物的x,y值位置 当相等时说明碰撞在一起
                isEat(){
                    return (this.snake.data[0].x == this.food.data.x) &&
                           (this.snake.data[0].y == this.food.data.y)
                }
                // 改变分数  事件监听
                changeGrade(grade){  //grade 传一个参数  给外面留接口 不写死
                    this.toGrade && this.toGrade(grade); //首先看看有没没有用户自身调用这个方法 如果有 就去调用这个方法
                                                        //实参grade 会传到调用他的地方

                }


                //判断是否结束
                isOver() {
                    // 根据蛇头的位置 去判断蛇头 是否出了地图
                    let i = this.snake.data[0];
                     //左          右
                    if(i.x < 0 || i.x >= this.map.cells
                    || i.y < 0 || i.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.toWin && this.toWin();   // 如果方法存在调用
                    }else{
                        this.toOver && this.toOver(); //游戏失败结束
                    }
                    

                    this.stop();
                }
                //键盘事件 37 38 39 40 左 上 右 下
                keyDown({ keyCode }) {
                    let isDir; //输出当前方向,是否修改成功
                    // console.log(keyCode);

                    switch (keyCode) {
                        case 37:
                           isDir = this.snake.changeDir("left"); //蛇里面控制方向的方法 给他传一个新方向
                                            //新方向通过一系列类的方法测试 如果通过测试 才修改当前方向
                                            // 然后会有一个布尔返回值
                            break;
                        case 38:
                            isDir = this.snake.changeDir("top");
                            break;
                        case 39:
                            isDir = this.snake.changeDir("right");
                            break;
                        case 40:
                            isDir = this.snake.changeDir("bottom");
                            break;


                    }
                    console.log(isDir) //是否修改成功 都会有一个返回值 可以拿这个返回值做,一个用户提示
                }

                //控制器  更换键位作用
                control(){
                    //判断用户是否添加了控制器   通过控制器上下左右可以改成 W A S D
                    if(this.toControl){   //如果存在
                        this.toControl(); //调用方法
                        return;           //结束
                    }
                    window.addEventListener("keydown",this.keyDown)  //上  如果没写 给window添加一个键盘按下事件
                }
                addControl(fn){ //添加控制器
                    fn.call(this);  //调用(继承) fn 自己使用
                    window.removeEventListener("keydown",this.keyDown); //下 this发生改变 上面得保存
                }       //这几行代码 当我有新的控制器添加进来的时候 借用一下把 上面的添加进来 把下面的删除掉


            } //游戏类





            {
                let map = document.querySelector(".map")// 获取html元素 
                // let gameMap = new Map(map, 10)  // 地图类获取后传到实例  这里实例化对象是入口
                // 地图类、食物类、蛇类 都放到这是为了 测试 注释了放到游戏类的costructor里
                // let gameFood = new Food(gameMap); //食物类
                // let gameSnake = new Snake(gameMap, gameFood);  //实例化一下 ,需要穿两个参数进去 地图和食物

                let game = new Game(map, 10); //游戏类实例 元素 大小  10后面也可以在传分数的参数
                let gradel = document.querySelector("#grade"); //获取分数
                
                game.toWin  = function(){
                    alert("您胜利了,真棒")
                }

                game.toOver = function(){
                    alert("游戏结束");
                }

                game.toGrade = function(){  // 写个函数调用
                    // console.log(grade)
                    gradel.innerHTML = game.grade; // 设置分数   然后添加事件监听
                }   

                //w: 87 上
                //d: 68 右
                //s: 83 下
                //a: 65 左   //
                game.addControl(function(){   // 这里新定义上下左右键 通过上面代码 现在使用把键位替换wasd
                    window.addEventListener("keydown",({keyCode})=>{
                        switch (keyCode) {
                        case 65:
                           this.snake.changeDir("left"); 
                            break;
                        case 87:
                            this.snake.changeDir("top");
                            break;
                        case 68:
                            this.snake.changeDir("right");
                            break;
                        case 83:
                            this.snake.changeDir("bottom");
                            break;


                    }
                    })
                })

                document.onclick = function(){
                    game.start();   //点击空白处加载游戏
                }

                //
            }
        }
    </script>
</body>

</html>
发布了101 篇原创文章 · 获赞 26 · 访问量 9288

猜你喜欢

转载自blog.csdn.net/weixin_46146313/article/details/104293791