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

491 阅读4分钟

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

写在前面

  • 最近在整理 JavaScript 面向对象的知识点,不得不说面向对象还是很经典的编程范式
  • 通过对现实世界事物的抽象,将事物的行为与属性进行封装聚合,需要使用时,直接实例化出一个对象,通过对对象的操作来实现我们想要的程序
  • 光说不练假把式,直接使用面向对象的思想实现一个贪吃蛇小游戏
  • 为了体现面向对象的优雅,本文代码均使用原生 JavaScript 编写
  • 下面是效果图: image.png
  • 蛇头部元素默认为绿色,蛇身元素默认为白色,食物颜色随机,出现位置随机

抽象

  • 我们要实现的是:贪吃蛇游戏
  • 提到 贪吃蛇,那肯定是要有 食物 这两个比较显而易见的类
  • 蛇 需要有运动的场地,那么我们可以再抽象一个 地图
  • 我们实现的是一个游戏,那游戏的主流程需要抽象为一个类吗?
    • 按照面向对象提倡的封装思想,我认为是需要的
    • 将游戏主流程封装为一个类,可以让我们的整个程序实现更加清晰,代码也会随之变得优雅
    • 如果未来,我们想要换一中贪吃蛇的玩法,我们只需要替换这个 游戏类 的实现逻辑即可,这也体现了遵循面向对象范式的代码易于维护的特点
  • 总结一下,我们想要实现贪吃蛇游戏,需要实现 4 个类
    • 地图类

    • 食物类

    • 蛇类

    • 游戏类

实现地图类

  • 很显然,地图类需要负责地图上的所有元素的渲染逻辑
        class Map {
            /**
             * el 表示地图的 dom 元素
             * rect 单元格的宽高
             */
            constructor({ el, rect }) {
              this.el = el;
              this.rect = rect || 10;

              // 存放 蛇 与 食物 的位置和颜色数据
              this.data = [];

              this.rows = 0;
              this.columns = 0;

              this.adjustMap();
            }

            // 清空地图上的元素数据
            clear() {
              this.data.length = 0;
            }

            // 判断传入的数据 是否 已经存在于当前地图上了
            check({ x, y }) {
              return !!this.data.find((i) => {
                i.x === x && i.y === y;
              });
            }

            setData(newData) {
              this.data = this.data.concat(newData);
            }

            adjustMap() {
              const { el, rect } = this;

              // 单元格的长和宽
              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 parseInt(getComputedStyle(el)[attr]);
            }

            static setStyle(el, attr, num) {
              el.style[attr] = num + 'px';
            }

            // 渲染地图上的元素
            render() {
              this.el.innerHTML = this.data
                .map((i) => {
                  return `
                            <span
                              style="position: absolute;
                              left: ${i.x * this.rect}px;
                              top: ${i.y * this.rect}px;
                              width: ${this.rect}px;
                              height: ${this.rect}px;
                              background: ${i.color}"
                            >
                            </span>
                      `;
                })
                .join('');
            }
      }

初始化

  • 在上面的地图内中,我们在初始化时,接收两个参数
    • el,表示的是地图的 dom 元素
    • rect,表示地图中每个单元格的宽高,为了方便计算,规定每个单元格都是正方形
      • rect 默认为 10px
  • 最后初始化一个数组 data,用于存放蛇身体的坐标数据,以及食物的坐标数据

反向修正地图宽高

  • 由于我们示例化地图时,可能存在 rect 不能整数倍填满整个地图的情况,为了避免这样的尴尬情况发生,我们需要实现一个反向修正地图宽高的功能,如代码所示的三个函数 adjustMapgetStylesetStyle
  • 根据传入的 el 得到地图真实宽高,然后除以 rect 计算出实际的单元格数量
  • 再根据单元格数量和传入的单元格大小,算出实际地图应有的宽高

渲染地图上的元素

  • 实现方法很简单,通过 data 中数据的坐标信息,生成一个个 span 标签
  • 然后将标签字符串塞到地图 dom 元素的 innerHTML 中

小结

  • 到这里,我们的地图类,实现的差不多了
  • 大家可以看到上面代码中,还有三个方法:clearsetDatacheck,没有提到
  • 我们按需实现,在后面的文章中,会根据实际需求再来介绍这些方法的用处
  • 为了不影响阅读体验,我会将这个 demo 的实现过程分为 4 篇文章来写,下一篇实现食物类

最后

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