TypeScript入门实战小游戏-----贪吃蛇的面向对象开发总结

前言:花了近一个星期左右的时间去学习了一下TypeScript的基本语法和开发环境的配置,跟着教程做了个简单的贪吃蛇的游戏,所有的功能基本实现,在开发初期通过安装parcel并在package.json中进行配置解决了直接在html中引入ts文件的交互问题,写篇博客做一下总结。所有源码包括html,less,ts文件都在博客中给出,并就用到的一些知识点进行总结归纳。

一、功能介绍以及实现结果展示

贪吃蛇小游戏不少人应该都玩过,废话不多说,直接上图展示最终的实现效果:通过下面视频的展示,我们实现了以下的主要功能点:

1、游戏分数面板和游戏等级的展示,游戏等级会随着设定每一级分数自动增加,且可以通过外部传入数据设定每个游戏等级的分数和最高游戏等级。

2、蛇运动面板的控制,蛇吃到食物后分数增加且食物的位置在游戏面板区域内随机变化,每次吃到食物后蛇的身体长度增加一节,蛇在不同游戏等级中运动速度的动态变化,游戏等级越高蛇的运动速度越快,蛇在运动中的撞墙检测以及蛇头和蛇身发生碰撞的检测,蛇在运动过程中阻止其反向掉头运动。

展示动画区域:

贪吃蛇运行功能效果展示:

二、开发前的一些小问题

2.1 如何在html文件中直接引入ts文件并编译成功?

         开始做的时候并不知道ts文件直接引入到html文件中没有作用,在ts文件中使用document.getElementById()方法无法从html中获取到元素时才意识到存在这个问题,初学者碰到问题总是难免的,及时总结归纳才能提高自己。经过自己查询到了解决方案,可以使用parcel并在package.json中进行配置即可实现自动将ts文件编译成js文件,而我们在index.html中直接以src引入ts文件即可。安装配置代码如下:

npm install -d parcel@next

在package.json中进行如下配置:

 "start": "parcel ./src/index.html"

通过上述安装配置后,直接使用npm start进行运行,可以得到如下结果(默认开启端口号为1234):

 2.2、index.html中引入ts文件的位置问题。

       需要注意的是,我们以script的形式引入ts文件,其位置应该放在body闭合标签后面,才能实现dom元素的获取。

2.3、项目中相关tsconfig.json文件的配置

       由于本项目比较简单,没有进行过多的配置项选择,需要注意的是后面两个noEmitOnError和removeComments默认值为false,我们将其设置为false的目的分别是如果存在错误则不进行编译、移出编译前中ts文件中的注释以达到缩小编译后文件的体积的目的。

{
    "compilerOptions": {
        "module": "es2015",
        "target": "es2015",
        "strict": true,
        "noEmitOnError":true,
        "removeComments":true,
    }
}

三、项目中的html结构代码和css样式代码

         本项目中的整体结构比较简单,布局样式主要采用了主流的flex弹性盒子布局方式,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>document</title>
</head>
<!-- less样式文件的引入 -->
<link rel="stylesheet" href="./style/index.less">

<body>
  <!-- 游戏主体区域 -->
  <div id="main">
      <!-- 游戏面板区域 -->
     <div id="game-panel">
         <!-- 蛇 -->
        <div id="snake">
             <div></div>
        </div>
        <div id="food">
          <!-- 组成食物的四个小方块 -->
          <div></div>
          <div></div>
          <div></div>
          <div></div>
        </div>
     </div>
     <!-- 分数等级面板区域 -->
     <div id="title">
         <div >
           SCOURCE:<span id="score">0</span>
         </div>
         <div >
           LEVEL:<span id="level">1</span>
         </div>
     </div>
  </div> 
</body>
<!-- 外部ts文件的引入 -->
<script src="./index.ts" type="text/typescript" ></script>
</html>

HTML结构样式源代码

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
@bg-color: #b7d4a8;
body {
  background-color: grey;
}
body {
  font-size: 18px;
  font-family: 'cursive';
  font-weight: bold;
}
#main {
  width: 360px;
  height: 420px;
  background-color: #b7d4a8;
  border: 10px solid black;
  border-radius: 10px;
  margin: 120px auto;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  align-items: center;
  position: relative;
}
#game-panel {
  width: 304px;
  height: 304px;
  border: 6px solid black;
  position: relative;
  #snake {
    & > div {
      height: 10px;
      width: 10px;
      background-color: black;
      position: absolute;
      //保证多节蛇体之间存在间隙
      border: 1px solid @bg-color;
    }
  }
  #food {
    width: 10px;
    height: 10px;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: space-between;
    position: absolute;
    left: 40px;
    top: 70px;
    & > div {
      width: 4px;
      height: 4px;
      background-color: black;
      transform: rotate(45deg);
    }
  }
}

#title {
  width: 302px;
  display: flex;
  justify-content: space-between;
}

less源代码(msdn编辑器中貌似没有less代码模块选项,故以css代码选择上传)

        补充说明:上述的less代码中使用到了变量声明,将背景颜色以变量的形式声明,后面各模块调用此颜色时可以直接通过变量名进行调用。跟多有关less的内容知识可以自行查阅相关文档;

四、项目中的ts文件代码

          我在写代码的过程中,ts部分没有考虑使用模块化的方法将不同的类进行分文件编写,而是统一写在了index.ts文件中,这是一种不太好的做法。大家在进行开发的时候引入模块化会更好,后期的可维护性也会比较好。在每写完一个类的时候,写一些测试代码去测试类中的属性和方法是否正常是一个良好的习惯。

          根据贪吃蛇游戏功能分析,分别将游戏积分和等级面板、食物、蛇头和蛇体、游戏控制划分为四个类,并分别分析其具备的属性和方法。下面分别对四个类的代码进行简单的介绍:

class Food {
  //声明一个属性接收表示食物的div元素,要操作dom元素,则必须先声明属性接收。
  element: HTMLElement
  constructor() {
    //从结构中获取食物的元素进行赋值,后面的感叹号的作用是强调一定能获取到指定id的dom元素。
    this.element = document.getElementById('food')!;
  }
  //经过分析确认
  //我们需要获取食物的位置信息,也就是相对于游戏面板的位置信息,使用类中封装好的访问器属性获取其坐标位置。
  get x() {
    return this.element.offsetLeft
  }
  get y() {
    return this.element.offsetTop
  }

  //在进行游戏的时候,食物的位置是在一定区域内随机变动的,因此具有位置变化的函数。
  changePosition() {
    //定义变量接收面版范围内任意生成的坐标。此生成的任意值范围和less中设置游戏面板的尺寸相关。
    const x = Math.round(Math.random() * 29) * 10
    const y = Math.round(Math.random() * 29) * 10
    this.element.style.left = x + 'px'
    this.element.style.top = y + 'px'
  }
}
//测试代码,检查类中的方法是否有效。
let food = new Food()
console.log(food.x, food.y)  

food类源码

      food类中需要关注的有几个要点:

1、使用!表示一定能获取到dom元素,避免在类型检查时发生报错。

2、使用get类提供的访问器属性在创建的对象中简便获取食物的坐标位置,为food.x,food.y;

3、食物的位置如何在游戏面板区域内随机生成,所使用到的Math.round()和Math.random()方法要十分熟悉。

class ScorePanel {
  //定义和初始化两个变量用于改变分数和等级
  score: number = 0
  level: number = 1
  //初始化两个html元素用于接收结构中的元素
  scoreElm: HTMLElement
  levelElm: HTMLElement

  //声明变量接收游戏等级上限和每个等级的分数上限
  maxLevel: number
  levelScore: number

  constructor(levelScore: number = 3, maxLevel: number = 10) {
    //赋值时类型检查如果无法获取元素则会提示报错,后面加上感叹号的目的是为了说明一定能获取到元素;
    this.scoreElm = document.getElementById('score')!;
    this.levelElm = document.getElementById('level')!;

    this.maxLevel = maxLevel
    this.levelScore = levelScore
  }

  //分数的增加,此函数调用之后分数增加一分
  addScore() {
    this.score++
    this.scoreElm.innerHTML = this.score + ''
    //达到指定的分数之后调用等级增加函数
    if (this.score % this.levelScore === 0) {
      this.addLevel()
    }
  }
  //游戏等级的增加,调用时游戏等级加一
  addLevel() {
    if (this.level < this.maxLevel) {
      this.level++
      this.levelElm.innerHTML = this.level + ''
      //一般而言,游戏是存在等级上限的,因此需要对游戏等级做出限制
    }
  }
}
//测试代码,每个等级10分,一共10个等级,通过循环体调用分数增加函数48次,为第5等级,满足预设条件。

let score = new ScorePanel(10, 10)
for (let i = 0; i < 48; i++) {
  score.addScore()
}
 

游戏面板ScorePanel类源码

       在游戏面板类中,我们需要注意以下内容知识点:

1、声明两个HTMLElement类型元素接收游戏面板中的分数和等级的dom元素,获取dom元素后才能操纵dom元素在条件满足的时候修改分数和游戏等级。

2、如何使得游戏分数增加和游戏等级增加,在类内声明两个number类型的变量score和level,当分数和等级增加的函数调用后,其值分别加1,并通过innerHTML的方式修改dom中的分数和游戏等级。

3、游戏分数和游戏等级之间是存在一定关系的,每个等级有多少分数,也就是在addScore函数中满足了一定的条件时要对addLevel函数进行调用。而游戏等级是存在上限的,只有在游戏等级不超过上限的条件下,游戏等级才会增加,如何实现这些应该认真思考。

4、写完此类后声明一个对象,设计一个循环体去多次调用addScore()函数,检验其分数和游戏等级是否存在正确的关联性。

class Snake {
  //获取蛇的容器,便于给其增加身体
  snakeElm: HTMLElement
  //蛇头,便于控制蛇的运动方向
  head: HTMLElement
  //蛇的身体
  bodies: HTMLCollection
  constructor() {
    this.snakeElm = document.getElementById('snake')! as HTMLElement
    this.head = document.querySelector('#snake>div')!
    this.bodies = this.snakeElm.getElementsByTagName('div')!
  }
  //获取蛇头的坐标
  get x() {
    return this.head.offsetLeft
  }
  get y() {
    return this.head.offsetTop
  }

  //改变蛇头的坐标
  set x(value: number) {
      //如果新值和旧值相同,则没有必要进行再次的修改
      if(this.x===value){
          return ;
      }
      //设置如果传入的value值异常则抛出错误,游戏终止
      if(value<0||value>290){
          throw new Error("撞墙了!");
      }

      //修改x的时候,是在修改水平坐标,不能发生直接掉头的现象。
      if(this.bodies[1]&&(this.bodies[1] as HTMLElement).offsetLeft===value){
          //如果传入的新值value大于旧值x则证明蛇在往右走,此时发生掉头,应该使得蛇继续往左走
          if(value>this.x){
              value=this.x-10;
          }else{
              //向左走,发生掉头则直接向右走
              value=this.x+10;
          }
      }
    this.bodyMove();
    this.head.style.left = value + 'px';
    //在蛇头的位置发生更新后调用函数判断是否发生了碰撞。
    this.checkHeadBody();
    
  }
  set y(value: number) {
      //如果新值和旧值相同,则没有必要进行再次的修改
      if(this.y===value){
        return ;
    }
    //设置如果传入的value值异常则抛出错误,游戏终止
    if(value<0||value>290){
        throw new Error("撞墙了!");
    }
    //此处应该在蛇头的位置更新之前调用蛇身移动的函数。
    this.bodyMove();
    this.head.style.top = value + 'px';
    //在设置完新传入的值后再检查头身是否发生了碰撞;
    this.checkHeadBody();
    
  }

  //根据游戏的进度增加蛇的身体
  addBody() {
    const temDiv = document.createElement('div')
    //思考,直接插入字符串的“<div></div>的元素会报错?不是Element元素。”
    this.snakeElm.insertAdjacentElement('beforeend', temDiv);
    console.log('身体增加一节了');
  }

  //控制身体的移动
  bodyMove(){
      //其整体的思路是后一节蛇的位置会跟上前一节蛇刚刚经过的位置,此函数需要在上面两个set函数中进行调用。
      for(let i=this.bodies.length-1;i>0;i--){
          //获取前一节蛇的位置,使用类型断言解决类型声明中的自动报错问题。
          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';
      }
  }

  //检查蛇头和蛇身是否发生碰撞
  checkHeadBody(){
      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("撞到自己了!")
          }
      }
  }
}

//测试代码,获取snake的坐标位置,并尝试修改是否成功
let snake = new Snake();
snake.addBody();
console.log(snake.bodies.length);

蛇Snake类

   Snake类在所有的类中是最为复杂的,因此我们要考虑的关键点也是最多的:

1、首先是如何获取蛇在dom中的容器,控制蛇运动方向的蛇头、蛇的所有身体部位等dom元素,蛇的身体随着游戏的进行会慢慢增加,因此我们使用的是HTMLCollection类型声明去接收。在获取到蛇的容器dom元素的前提下再去获取蛇头和蛇的身体部分很重要。

2、蛇头的坐标需要获取,同时还需要随着时间发生更改,也就是蛇头会运动。我们可以使用类中提供的访问器属性去获取蛇头和修改蛇头的坐标。在修改蛇头的坐标函数中,我们需要考虑游戏面板的大小以进行是否发生撞墙检测的功能。

3、如何实现蛇的身体的增加函数,再调用insertAdjacetElement插入一个div元素时,我们需要使用createElement方法实现字符串和Element元素的转换,才能正确插入dom元素。

4、如何实现蛇的身体随着蛇头进行运动呢?仔细思考一下,其实就是当前一个模块移动后,后面一个模块应该占据其位置,能想到这一点十分的重要。

5、玩过贪吃蛇的都知道,蛇头和蛇的身体发生碰撞的时候,游戏就会结束掉。如何实现这个功能呢?发生碰撞的意思简单的说就是蛇头和蛇的某一节身体的位置相同。在函数体中依次获取每一节蛇身的坐标,并和蛇头的坐标进行对比,如果相同则抛出错误提示游戏结束了。

6、所有有关Snake中的方法声明后,都应该创建对象实例调用检查其是否已经生效。

class GameControls {
  //创建三个属性用于存储引入的三个类
  //游戏面板
  scorepanel: ScorePanel
  //蛇类
  snake: Snake
  //食物类
  food: Food
  //定义一个属性存储蛇的运动方向,也就是按键的方向。
  direction: string = 'ArrowRight';
  //定义一个属性,判断游戏是否已经结束,游戏结束则终止运动。
  isAlive = true;

  constructor() {
    this.scorepanel = new ScorePanel()
    this.snake = new Snake()
    this.food = new Food()
    this.initGame()
  }
  //游戏的初始化方法,调用之后游戏开始
  initGame() {
    //绑定键盘按下的事件
    //疑惑,绑定this有什么用呢?将回调函数以函数名的形式传入代码的维护性会更好。
    //this的指向问题
    document.addEventListener('keydown', this.keyDownHandler.bind(this));
    this.snakeRun();
  
  }
  //处理键盘按下的事件,事件对象要深入理解一下。
  keyDownHandler(event:KeyboardEvent) {
    //console.log(event.key);
    //修改direction属性
    //执行前需要进行检查是否满足设定的条件。
    this.direction = event.key;
    
  }
  //创建使得蛇运动起来的方法
  snakeRun() {
    //首先获取蛇现在的坐标位置
    let X = this.snake.x
    let Y = this.snake.y

    //根据按下不同的方向键决定蛇的运动方向
    switch (this.direction) {
      case 'ArrowUp':
        //向上移动y减小
        Y -= 10;
        break;
      case 'ArrowDown':
        //向下移动y增加
        Y += 10;
        break;
      case 'ArrowLeft':
        //向左移动x减小
        X -= 10;
        break;
      case 'ArrowRight':
        //向右移动x增加
        X += 10;
        break;
    }
    //判断是否吃到食物了
    this.foodEatCheck(X,Y);
    // if(this.foodEatCheck(X,Y)){
    //     console.log("吃到食物了")
    // }

    //将改变后的坐标值赋值给蛇,使得蛇发生运动。
    try {
    this.snake.x = X
    this.snake.y = Y
    
    } catch (e:any) {
       alert(e.message+'GameOver'); 
       //将游戏结束的属性设置为false;
       this.isAlive=false;
    }
        //设置蛇运动的速度,通过定时器
        this.isAlive&&setTimeout(this.snakeRun.bind(this), 300-(this.scorepanel.level-1));
    
  }

  //检测是否吃到了食物
  foodEatCheck(X:number,Y:number){
      //检测到吃到食物了,进行一些变化
      if(X===this.food.x && Y===this.food.y){
          //游戏积分面上分数增加
          this.scorepanel.addScore();
          //蛇的身体增加一节
          this.snake.addBody();
          //食物的位置发生随机变化;
          this.food.changePosition();

      }
  }
}

//测试代码,检查是否将当前的键盘事件键值传递给了类中存储运动方向的direction
let game=new GameControls();
setInterval(()=>{
    console.log(game.direction)
},1000);

                   游戏对象控制类GameControls源码

    在对游戏面板中与游戏所有相关的元素类进行声明后,我们还需要一个游戏对象控制类来实现最后的游戏功能。在此类中,先声明三个类型元素获取食物、游戏面板和蛇的类的属性和方法。游戏对象控制类中也有一些关键点需要注意:

1、游戏的初始化函数,也就是在检测到键盘的点击后,通过调用snakeRun函数开始蛇的运动。在类的属性中声明两个变量direction和isAlive来判断蛇的运动方向和游戏是否已经结束了。

2、事件监听keydown中绑定的事件回调函数存在this的指向问题,可以通过bind方法来解决。回调函数通过将捕捉到的key传递给类中判断方向的direction属性。

3、在蛇头动起来的函数snakeRun中,先获取蛇头的元素坐标,在通过switch根据不同的direction修改蛇头的坐标实现其运动。修改后的坐标还需要重新赋值给蛇头。

4、如何判断蛇吃到了食物呢?同样的,当蛇头和位置和食物的位置相同时,我们就认为蛇吃到了食物。吃到了食物之后游戏面板上会有一系列的变化,游戏分数的增加、蛇的身体边长和食物的位置发生随机变化,通过调用对应类中的函数实现上述功能。检测是否吃到食物的坐标需要在runSnake中调用传参。

5、在对蛇头的坐标重新赋值时,我们使用try....catch捕捉错误,如果蛇头的碰撞到自身或者撞墙了,则会抛出错误并将isAlive变为false表示游戏结束了。

class Food {
  //声明一个属性接收表示食物的div元素
  element: HTMLElement
  constructor() {
    //从结构中获取食物的元素进行赋值
    this.element = document.getElementById('food')!
  }
  //经过分析确认
  //我们需要获取食物的位置信息,也就是相对于游戏面板的位置信息)
  get x() {
    return this.element.offsetLeft
  }
  get y() {
    return this.element.offsetTop
  }

  //在进行游戏的时候,食物的位置是在一定区域内随机变动的,因此具有位置变化的函数。
  changePosition() {
    //定义变量接收面版范围内任意生成的坐标。
    const x = Math.round(Math.random() * 29) * 10
    const y = Math.round(Math.random() * 29) * 10
    this.element.style.left = x + 'px'
    this.element.style.top = y + 'px'
  }
}
let food = new Food()
console.log(food.x, food.y)
class ScorePanel {
  //定义和初始化两个变量用于改变分数和等级
  score: number = 0
  level: number = 1
  //初始化两个html元素用于接收结构中的元素
  scoreElm: HTMLElement
  levelElm: HTMLElement

  //声明变量接收游戏等级上限和每个等级的分数上限
  maxLevel: number
  levelScore: number

  constructor(levelScore: number = 3, maxLevel: number = 10) {
    //赋值时类型检查如果无法获取元素则会提示报错,后面加上感叹号的目的是为了说明一定能获取到元素;
    this.scoreElm = document.getElementById('score')!
    this.levelElm = document.getElementById('level')!

    this.maxLevel = maxLevel
    this.levelScore = levelScore
  }

  //分数的增加,此函数调用之后分数增加一分
  addScore() {
    this.score++
    this.scoreElm.innerHTML = this.score + ''
    //达到指定的分数之后调用等级增加函数
    if (this.score % this.levelScore === 0) {
      this.addLevel()
    }
  }
  //游戏等级的增加,调用时游戏等级加一
  addLevel() {
    if (this.level < this.maxLevel) {
      this.level++
      this.levelElm.innerHTML = this.level + ''
      //一般而言,游戏是存在等级上限的,因此需要对游戏等级做出限制
    }
  }
}
//测试代码,每个等级16分,一共10个等级,传入24分,为第二等级,满足预设条件。

let score = new ScorePanel(10, 10)
for (let i = 0; i < 48; i++) {
  score.addScore()
}
 
class Snake {
  //获取蛇的容器,便于给其增加身体
  snakeElm: HTMLElement

  //蛇头,便于控制蛇的运动方向
  head: HTMLElement

  //蛇的身体
  bodies: HTMLCollection

  constructor() {
    this.snakeElm = document.getElementById('snake')! as HTMLElement
    this.head = document.querySelector('#snake>div')!
    this.bodies = this.snakeElm.getElementsByTagName('div')!
  }

  //获取蛇头的坐标
  get x() {
    return this.head.offsetLeft
  }
  get y() {
    return this.head.offsetTop
  }

  //改变蛇头的坐标
  set x(value: number) {
      //如果新值和旧值相同,则没有必要进行再次的修改
      if(this.x===value){
          return ;
      }
      //设置如果传入的value值异常则抛出错误,游戏终止
      if(value<0||value>290){
          throw new Error("撞墙了!");
      }

      //修改x的时候,是在修改水平坐标,不能发生直接掉头的现象。
      if(this.bodies[1]&&(this.bodies[1] as HTMLElement).offsetLeft===value){
          //如果传入的新值value大于旧值x则证明蛇在往右走,此时发生掉头,应该使得蛇继续往左走
          if(value>this.x){
              value=this.x-10;
          }else{
              //向左走,发生掉头则直接向右走
              value=this.x+10;
          }
      }
    this.bodyMove();
    this.head.style.left = value + 'px';
    //在蛇头的位置发生更新后调用函数判断是否发生了碰撞。
    this.checkHeadBody();
    
  }
  set y(value: number) {
      //如果新值和旧值相同,则没有必要进行再次的修改
      if(this.y===value){
        return ;
    }
    //设置如果传入的value值异常则抛出错误,游戏终止
    if(value<0||value>290){
        throw new Error("撞墙了!");
    }
    //此处应该在蛇头的位置更新之前调用蛇身移动的函数。
    this.bodyMove();
    this.head.style.top = value + 'px';
    //在设置完新传入的值后再检查头身是否发生了碰撞;
    this.checkHeadBody();
    
  }

  //根据游戏的进度增加蛇的身体
  addBody() {
    const temDiv = document.createElement('div')
    //思考,直接插入字符串的“<div></div>的元素会报错?不是Element元素。”
    this.snakeElm.insertAdjacentElement('beforeend', temDiv);
    console.log('身体增加一节了');
  }

  //控制身体的移动
  bodyMove(){
      //其整体的思路是后一节蛇的位置会跟上前一节蛇刚刚经过的位置,此函数需要在上面两个set函数中进行调用。
      for(let i=this.bodies.length-1;i>0;i--){
          //获取前一节蛇的位置,使用类型断言解决类型声明中的自动报错问题。
          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';
      }
  }

  //检查蛇头和蛇身是否发生碰撞
  checkHeadBody(){
      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("撞到自己了!")
          }
      }
  }
}

//测试代码,获取snake的坐标位置,并尝试修改是否成功
let snake = new Snake();
snake.addBody();
console.log(snake.bodies.length);


//以上均通过测试,代码没有问题

class GameControls {
  //创建三个属性用于存储引入的三个类
  //游戏面板
  scorepanel: ScorePanel
  //蛇类
  snake: Snake
  //食物类
  food: Food

  //定义一个属性存储蛇的运动方向,也就是按键的方向。
  direction: string = 'ArrowRight';
  //定义一个属性,判断游戏是否已经结束,游戏结束则终止运动。
  isAlive = true;

  constructor() {
    this.scorepanel = new ScorePanel()
    this.snake = new Snake()
    this.food = new Food()
    this.initGame()
  }

  //游戏的初始化方法,调用之后游戏开始
  initGame() {
    //绑定键盘按下的事件
    //疑惑,绑定this有什么用呢?将回调函数以函数名的形式传入代码的维护性会更好。
    //this的指向问题
    document.addEventListener('keydown', this.keyDownHandler.bind(this));
    this.snakeRun();
  
  }
  //处理键盘按下的事件,事件对象要深入理解一下。
  keyDownHandler(event:KeyboardEvent) {
    //console.log(event.key);
    //修改direction属性
    //执行前需要进行检查是否满足设定的条件。
    this.direction = event.key;
    
  }

  //创建使得蛇运动起来的方法
  snakeRun() {
    //首先获取蛇现在的坐标位置
    let X = this.snake.x
    let Y = this.snake.y

    //根据按下不同的方向键决定蛇的运动方向
    switch (this.direction) {
      case 'ArrowUp':
        //向上移动y减小
        Y -= 10;
        break;
      case 'ArrowDown':
        //向下移动y增加
        Y += 10;
        break;
      case 'ArrowLeft':
        //向左移动x减小
        X -= 10;
        break;
      case 'ArrowRight':
        //向右移动x增加
        X += 10;
        break;
    }
    //判断是否吃到食物了
    this.foodEatCheck(X,Y);
    // if(this.foodEatCheck(X,Y)){
    //     console.log("吃到食物了")
    // }

    //将改变后的坐标值赋值给蛇,使得蛇发生运动。
    try {
    this.snake.x = X
    this.snake.y = Y
    
    } catch (e:any) {
       alert(e.message+'GameOver'); 
       //将游戏结束的属性设置为false;
       this.isAlive=false;
    }
        //设置蛇运动的速度,通过定时器
        this.isAlive&&setTimeout(this.snakeRun.bind(this), 300-(this.scorepanel.level-1));
    
  }

  //检测是否吃到了食物
  foodEatCheck(X:number,Y:number){
      //检测到吃到食物了,进行一些变化
      if(X===this.food.x && Y===this.food.y){
          //游戏积分面上分数增加
          this.scorepanel.addScore();
          //蛇的身体增加一节
          this.snake.addBody();
          //食物的位置发生随机变化;
          this.food.changePosition();

      }
  }

}
//测试代码,检查是否将当前的键盘事件键值传递给了类中存储运动方向的direction
let game=new GameControls();
setInterval(()=>{
    console.log(game.direction)
},1000);

index.ts中的所有代码

五、总结归纳

       两天的时间把这个贪吃蛇的小demo制作了出来,并达到了预定功能。通过这次小练习,对面向对象的编程思想有了初步的理解,未来还需要通过项目进一步提高编程的思想,上述贪吃蛇的游戏只是实现了最基础的功能,还有待进一步的开发。TypeScript的类型声明和语法检查开始的时候是真的不太习惯,TS适用更复杂的应用程序开发注定了其未来发展前景的广阔,学习难的东西才能让自己有所提高,迎难而上永不言弃才能有所收获。

我是未名同学,因为热爱,所以记录。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值