这个贪吃蛇好像有点不简单,玩了半天20分都没有获得

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

大家好,我是一碗周,一个不想被喝(内卷)的前端。如果写的文章有幸可以得到你的青睐,万分有幸~

写在前面

贪吃蛇这个游戏大家都耳熟能详了,我一次接触这个游戏还是在诺基亚手机上,相信大家都见过这个手机,就是下面这个样子:

200px-Nokia_1110_DG_01.jpg

这篇文章我们就是用前端技术来实现这个小游戏,这个游戏的代码大部分采用TS+Less编写,正好练习一下TS。

游戏演示如下:

demo.gif

在线体验地址ywanzhou.github.io/eating-snak…

HTML结构

首先还是先确定我们的HTML结构,代码如下:

<body>
  <!-- 创建游戏的主窗口 -->
  <div id="main">
    <!-- 主要区域 -->
    <div id="state">
      <!-- 蛇 -->
      <div id="snake">
        <div></div>
      </div>
      <!-- 设置食物 -->
      <div id="food"></div>
    </div>
    <!-- 展示 -->
    <div id="score-panel">
      <div>Score:<span id="score">0</span></div>
      <div>Level: <span id="level">1</span></div>
    </div>
  </div>
</body>
复制代码

HTML结构比较简单,CSS的样式也不复杂,如果需要可以自行去github查看。

食物类

首先我们先编写一下食物类,首先我们需要获取HTML结构中的元素,然后可以获取食物的x轴y轴的坐标,最后定义一个方法可以改变他的坐标,示例代码如下:

// 定义食物类 food
class Food {
  // 定义一个属性表示食物所对应的元素  名称element:类型是 HTMLElement
  element: HTMLElement
  constructor() {
    // ! 表示document.getElementById('food')已经确定可以找到对应的元素不会为空
    this.element = document.getElementById("food")!
  }
  // 获取 x y 轴坐标
  get X() {
    return this.element.offsetLeft
  }
  get Y() {
    return this.element.offsetTop
  }

  change() {
    // 生成一个随机的位置来表示食物  位置最小为0 最大为290  必须是10的倍数
    let top = Math.round(Math.random() * 29) * 10
    let left = Math.round(Math.random() * 29) * 10

    this.element.style.left = left + "px"
    this.element.style.top = top + "px"
  }
}

export default Food

复制代码

蛇类

首先我们定义属性,来表示元素,代码如下:

class Snake {
  // 表示蛇儿头
  head: HTMLElement
  // 表示蛇的身体(包括蛇头)
  bodies: HTMLCollection
  // 蛇的父级元素
  element: HTMLElement
  constructor() {
    // 元素初始化
    this.element = document.getElementById("snake")!
    this.head = document.querySelector("#snake > div") as HTMLElement
    this.bodies = this.element.getElementsByTagName("div")
  }
}

复制代码

然后定义两个方法,一个用于添加蛇的身体,一个用于移动蛇,示例代码如下:

// 蛇增加身体的方法
addBody() {
  // 向element 中添加一个div
  this.element.insertAdjacentHTML("beforeend", "<div></div>")
}

// 添加一个蛇身体移动的方法
moveBody() {
  /* 
    将后一节的位置设置为前一节的位置,比如第二节的位置设置为蛇头的位置

    因为要先获取前一个节的位置所有需要从后往前遍历 i>0
  */

  for (let i = this.bodies.length - 1; i > 0; i--) {
    // 获取前边身体的位置   类型“Element”上不存在属性“offsetLeft”需要来一个类型断言
    let X = (this.bodies[i - 1] as HTMLElement).offsetLeft
    let Y = (this.bodies[i - 1] as HTMLElement).offsetTop

    // 将值设置到当前身体上
    ;(this.bodies[i] as HTMLElement).style.left = X + "px"
    ;(this.bodies[i] as HTMLElement).style.top = Y + "px"
  }
}
复制代码

获取蛇头的坐标,实现比较简单,代码如下:

// 获取蛇的坐标(蛇头坐标)
get X() {
  return this.head.offsetLeft
}
get Y() {
  return this.head.offsetTop
}
复制代码

定义一个方法,用于检测是否撞到了身体,代码如下:

// 检查蛇头是否撞到身体的方法
checkHeadBody() {
  // 获取所有的身体,检查其是否和蛇头的坐标发生重叠
  /* 
    let i = 1; i < this.bodies.length; i++ 
    从1 开始不包含蛇头0 
  */
  for (let i = 1; i < this.bodies.length; i++) {
    let bd = this.bodies[i] as HTMLElement
    if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
      // 进入判断说明蛇头撞到了身体,游戏结束
      throw new Error("不要自己撞自己哦~")
    }
  }
}
复制代码

然后我们去设置蛇头的坐标,在实现过程中需要添加一些逻辑判断,例如不能撞墙,不能向下移动时向上掉头等;

代码如下:

// 设置蛇头的坐标
set X(value: number) {
  // 如果新值和旧值相同,则直接返回不再修改
  if (this.X === value) {
    return
  }
  // 判断蛇有没有在规定的范围内移动
  if (value < 0 || value > 290) {
    throw Error("你的小蛇撞墙了~")
  }

  // 修改x时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然
  /*  
    this.bodies[1]  表示先检查有没有第二节身体
    (this.bodies[1] as HTMLElement).offsetLeft === value 表示第二节身体的X值与蛇头的X值相同
  */
  if (
    this.bodies[1] &&
    (this.bodies[1] as HTMLElement).offsetLeft === value
  ) {
    if (value > this.X) {
      value = this.X - 10
    } else {
      value = this.X + 10
    }
  }
  // 移动身体
  this.moveBody()
  this.head.style.left = value + "px"

  // 检查有没有撞到自己
  this.checkHeadBody()
}

set Y(value: number) {
  // 如果新值和旧值相同,则直接返回不再修改
  if (this.Y === value) {
    return
  }
  // 判断蛇有没有在规定的范围内移动

  if (value < 0 || value > 290) {
    throw Error("你的小蛇撞墙了~")
  }

  // 修改Y时,是在修改垂直坐标,蛇在左右移动,蛇在向上移动时,不能向下掉头,反之亦然
  if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
    if (value > this.Y) {
      value = this.Y - 10
    } else {
      value = this.Y + 10
    }
  }

  // 移动身体
  this.moveBody()
  this.head.style.top = value + "px"
  // 检查有没有撞到自己
  this.checkHeadBody()
}
复制代码

最后导出这个类。

export default Snake
复制代码

分数等级类

现在我们定义一个方法,实现分数和等级的记录,代码如下:

class ScorePanel {
  // 分别记录分数和等级
  score = 0;
  level = 1;

  // 分数和等级所在的元素,在构造函数中进行初始化
  ScoreEle: HTMLElement;
  LevelEle: HTMLElement;
  // 设置一个变量限制等级
  maxLevel: number;
  upScore: number;

  constructor(maxLevel: number = 10, upScore: number = 10) {
    this.ScoreEle = document.getElementById("score")!;
    this.LevelEle = document.getElementById("level")!;
    this.maxLevel = maxLevel;
    this.upScore = upScore;
  }

  // 设置一个加分的方法
  addScore() {
    // 使分数自增
    this.ScoreEle.innerHTML = ++this.score + "";
    if (this.score % this.upScore === 0) {
      this.UpLevel();
    }
  }
  UpLevel() {
    // 使等级自增
    if (this.level < this.maxLevel) {
      this.LevelEle.innerHTML = ++this.level + "";
    }
  }
}

export default ScorePanel;

复制代码

游戏控制器

我们将前面的定义都已经实现,现在我们将前面的3个类进行应用,实现代码如下:

import ScorePanel from "./ScorePanel";
import Snake from "./snake";
import Food from "./Food";

// 游戏控制器,控制其他的所有类
class GameControl {
  // 定义三个属性
  snake: Snake;
  food: Food;
  // 记分牌
  scorePanel: ScorePanel;
  // 创建一个属性用来存储蛇的移动方向(就是键盘的方向键)
  direction: string = "";
  // 创建一个属性用来记录游戏是否结束
  isLive = true;

  constructor() {
    this.snake = new Snake();
    this.food = new Food();
    this.scorePanel = new ScorePanel(10, 1);

    // 游戏初始化
    this.init();
  }
  // 初始化游戏的方法,调用后游戏开始
  init() {
    document.addEventListener("keydown", this.keydownHandler.bind(this));
    this.run();
  }
  // 创建一个键盘按下的响应函数
  keydownHandler(event: KeyboardEvent) {
    // 当用户按下键盘的键之后,需要判断用户按下的键是否符合目标的键

    //  当用户按下键盘时存储键
    this.direction = event.key;
  }

  // 定义一个蛇移动起来的方法
  run() {
    // 获取蛇的坐标
    let X = this.snake.X;
    let Y = this.snake.Y;

    // 根据按键方向来修改蛇的坐标值
    switch (this.direction) {
      // 上移 top减少
      case "ArrowUp":
        Y -= 10;
        break;
      case "ArrowDown":
        Y += 10;
        break;
      case "ArrowLeft":
        X -= 10;
        break;
      case "ArrowRight":
        X += 10;
        break;
      default:
        break;
    }

    // 检查蛇是否吃到了食物
    this.checkEat(X, Y);

    // 修改蛇的X和Y值
    try {
      this.snake.X = X;
      this.snake.Y = Y;
    } catch (e: any) {
      // 捕获异常弹出消息提示
      alert(e.message);
      // isLive改为false 表示游戏结束
      this.isLive = false;
    }
    // 开启一个定时器run()一直执行
    this.isLive &&
      setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30);
  }

  // 定义一个方法,用来检查蛇是否吃到食物
  checkEat(X: number, Y: number) {
    if (X === this.food.X && Y === this.food.Y) {
      // 食物位置刷新
      this.food.change();
      // 加一分
      this.scorePanel.addScore();
      // 蛇的身体增加一格
      this.snake.addBody();
    }
  }
}

export default GameControl;

复制代码

到这为止这个游戏的大部分代码已经完成了。

写在最后

全部代码已经放在了GitHub上,代码不复杂,仅适用于学习。

PS:如果您是大佬,或者觉着这个文章垃圾,请忽略这篇文章,谢谢。

猜你喜欢

转载自juejin.im/post/7079437726658854920