从 0 开始写一个贪吃蛇小游戏(四)

987 阅读2分钟

「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」。

前情回顾

  • 在前面三篇文章,我们实现了地图类、食物类以及蛇类相关的一些逻辑封装
  • 今天我们来实现游戏控制的主流程类 Game

实现游戏主流程控制类

  • 游戏类主要负责的是游戏相关的一些逻辑的封装,也就是按照游戏需求,将前面实现的三个类串联在一起
  • 我们先来看一下代码
class Game {
    /**
     *  el: 为地图 dom 元素
     *  rect: 为单元格的宽高
     *  onControl: 为控制逻辑函数,默认为 null, 可由外部用户传入
     */
    constructor({ el, rect, onControl = null, toGrade, toWin, toOver }) {
      this.map = new Map({ el, rect });
      this.food = new Food({ rows: this.map.rows, columns: this.map.columns });
      this.snake = new Snake();

      // 蛇 移动的时间间隔,默认 200ms
      this.timer = null;
      this.interval = 200;

      // 控制器
      this.onControl = onControl;
      this.keyDown = this.keyDown.bind(this);
      this.controlFn();

      // 分数
      this.grade = 0;
      this.toGrade = toGrade;

      // 成功或者失败时的回调
      this.toOver = toOver;
      this.toWin = toWin;

      // init
      this.initGame();
    }

    // 初始化游戏
    initGame() {
      this.map.setData(this.snake.data);
      this.createFood();
      this.render();
    }

    // 产生食物
    // 如果当前的地图数据中已经包含了当前产生的食物的坐标,就重新生成一个
    // 防止食物出现在 蛇 的身体上
    createFood() {
      const food = this.food.create();
      if (this.map.check(food)) {
        this.createFood();
      }
    }

    // 在地图中渲染数据
    render() {
      this.map.clear();
      this.map.setData(this.snake.data);
      this.map.setData(this.food.data);
      this.map.render();
    }

    // 专门处理 游戏开始 的相关逻辑
    start() {
      this.move();
    }

    // 暂停游戏
    stop() {
      clearInterval(this.timer);
    }

    // 专门处理 蛇 移动的逻辑
    move() {
      this.stop();
      this.timer = setInterval(() => {
        this.snake.moveFn();

        if (this.isEat()) {
          console.log('是否吃到食物', true);
          this.grade++;
          this.snake.eatFn();
          this.food.create();
          this.changeGrade();

          // 加快速度
          this.interval *= 0.9;
          this.stop();
          this.start();

          if (this.grade >= 5) {
            this.over(1);
            // 待会直接完成 over 和 isOver 函数即可
          }
        }

        if (this.isOver()) {
          this.over(0);
          return;
        }

        this.render();
      }, this.interval);
    }

    keyDown({ keyCode }) {
      // console.log(keyCode, this);
      let setDirectionFlag;
      switch (keyCode) {
        case 37:
          setDirectionFlag = this.snake.turnFn('left');
          break;
        case 38:
          setDirectionFlag = this.snake.turnFn('top');
          break;
        case 39:
          setDirectionFlag = this.snake.turnFn('right');
          break;
        case 40:
          setDirectionFlag = this.snake.turnFn('bottom');
          break;
        default:
          break;
      }
      // console.log("是否改变方向", setDirectionFlag);
    }

    // 控制器
    controlFn() {
      if (this.onControl) {
        this.onControl.call(this);
      } else {
        window.addEventListener('keydown', this.keyDown);
      }
    }

    // 判断是否吃到东西
    isEat() {
      let snake = this.snake.data[0];
      let food = this.food.data;
      return snake.x === food.x && snake.y === food.y;
    }

    // 处理分数改变的相关逻辑
    changeGrade() {
      this.toGrade && this.toGrade(this.grade);
    }

    // 处理 游戏结束 的相关逻辑
    /**
     * overState
     * 0 中间停止, 完挂了
     * 1 胜利了, 游戏结束
     */
    over(state) {
      if (state === 1) {
        if (this.toWin) {
          this.toWin();
        } else {
          console.log('通关了');
        }
      } else if (state === 0) {
        if (this.toOver) {
          this.toOver();
        } else {
          console.log('game over!!!');
        }
      }
      this.stop();
    }

    // 判断游戏是否结束
    isOver() {
      const { x, y } = this.snake.data[0];
      const { columns, rows } = this.map;
      let out = x < 0 || x >= columns || y < 0 || y >= rows;
      if (out) {
        return true;
      }

      for (let i = 1; i < this.snake.data.length; i++) {
        const body = this.snake.data[i];
        if (body.x === x && body.y === y) {
          return true;
        }
      }

      return false;
    }
    }
  • 上面代码中,最开始我们需要初始化地图对象,食物对象和游戏的主角 蛇对象
  • 然后初始化一个定时器 timer,以及蛇移动的时间间隔 interval
  • 初始化控制器,分数改变的回调,游戏通关的回调,游戏结束的回调
  • 最后初始化游戏数据
    • 将蛇初始的默认位置数据添加到地图对象中
    • 创建一个食物对象,若食物的坐标数据,已经存在于地图对象中,则再次产生一个食物对象,直至食物对象可用,然后将食物放入地图中
    • 根据地图对象中蛇与食物的信息,渲染相应的页面
  • move 是用来处理蛇移动过程中的一些逻辑,包括吃食物,越过地图边界结束游戏等,蛇的移动需要用到定时器,所以每次 move 都先清空上次的定时器,重新生成一个定时器
  • 如何判断蛇吃到食物?
    • 只用判断蛇头的坐标是否与食物的坐标重叠即可
    • 如果吃到食物,则分数加一,调用初始化时的分数改变回调函数。然后创建新的食物,并添加到地图中
  • 如何判断游戏是否结束?
    • 有两种情况,一种是蛇头部元素撞到地图边界,一种是蛇头部撞到自己的身体
    • 越界情况很好判断,每次移动时,根据地图边界坐标,判断一下是否有与蛇头部坐标相等的,若有则可判断为越界
    • 撞到蛇身体,则需要循环蛇身每个元素,一次判断,蛇头部元素的坐标是否与蛇身元素有相等的,若有则可判断为蛇头撞到身体了

最后

  • 当游戏所有类的逻辑都封装完成后,即可开始编写,游戏开始的引导程序了
let map = document.querySelector('#map');

let onControl = function () {
  window.addEventListener('keydown', ({ keyCode }) => {
    switch (keyCode) {
      case 65:
        setDirectionFlag = this.snake.turnFn('left');
        break;
      case 87:
        setDirectionFlag = this.snake.turnFn('top');
        break;
      case 68:
        setDirectionFlag = this.snake.turnFn('right');
        break;
      case 83:
        setDirectionFlag = this.snake.turnFn('bottom');
        break;
      default:
        break;
    }
  });
};

let gradeEle = document.querySelector('.grade');
let toGrade = (grade) => {
  gradeEle.textContent = grade;
};

let game = new Game({ el: map, rect: 12, toGrade, onControl });
game.toOver = () => {
  alert('游戏结束');
};

game.toWin = () => {
  alert('太棒了,你通关了');
};

game.start();
  • onControl 为外部实现的控制器,监听按键按下事件,来改变蛇头移动的方向
  • 给 game 对象设置相应的游戏通关回调和游戏结束回调
  • game.start() 即可开始游戏了

最后

  • 今天的分享就到这里,欢迎大家在评论区留言讨论
  • 如果觉得文章写的还不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力 🥰