ts实战-贪吃蛇

项目搭建

  准备好之前的几个文件:

    webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // webpack中html插件,用来自动创建html文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin') // clean插件
module.exports = {
  mode: 'none',
  entry: './src/index.ts', // 指定入口文件
  output: {
    path: path.resolve(__dirname, 'dist'), // 指定打包文件的目录
    filename: 'bundle.js', // 打包后文件的名称
    environment: { arrowFunction: false } // 告诉webpack打包后的【立即执行函数】不使用箭头函数(新版的webpack不支持ie11,如果需要打包后的代码支持ie11需要加上该配置)
  },
  // 指定webpack打包时要使用的模块
  module: {
    // 指定loader加载的规则
    rules: [
      {
        test: /\.ts$/, // 指定规则生效的文件:以ts结尾的文件
        // use: 'ts-loader', // 要使用的loader
        // ts先由ts-loader转换成js文件,再由babel中target指定的浏览器版本,将js转成对应的语法。配置了babel后不需要考虑使用es5还是es6的版本了,在target中指定了需要兼容的浏览器版本,babel会自动帮我们转
        use: [
          {
            loader: 'babel-loader', // 指定加载器
            // 设置babel
            options: {
              // 设置预定义的环境
              presets: [
                [
                  '@babel/preset-env', // 指定环境的插件
                  // 配置信息
                  {
                    targets: { chrome: 58, ie: 11 }, // 要兼容的目标浏览器及版本(ie11不支持es6语法,写上 ie: 11 打包时就会编译成支持到ie11)
                    corejs: 3, // 指定corejs的版本(根据package.json中的版本,只写整数)
                    useBuiltIns: 'usage' // 使用corejs的方式,'usage'表示按需加载
                  }
                ]
              ]
            }
          },
          'ts-loader'
        ],
        exclude: /node-modules/ // 要排除的文件
      }
    ]
  },
  // 配置webpack插件
  plugins: [
    new HtmlWebpackPlugin({
      title: '自定义标题', // 自定义title标签内容
      template: './src/index.html' // 以index.html文件作为模板生成dist/index.html(设置了template,title就失效了)
    }),
    new CleanWebpackPlugin()
  ],
  // 设置哪些文件类型可以作为模块被引用
  resolve: {
    extensions: ['.ts', '.js']
  }
}
View Code

    tsconfig.json

{
  "compilerOptions": {
    "module": "es6",
    "target": "es6",
    "strict": true,
    "noEmitOnError": true
  }
}
View Code

    package.json

{
  "name": "greedySnake",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack serve --open chrome.exe"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.15.8",
    "@babel/preset-env": "^7.15.8",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "core-js": "^3.19.0",
    "html-webpack-plugin": "^5.5.0",
    "ts-loader": "^9.2.6",
    "typescript": "^4.4.4",
    "webpack": "^5.60.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.3.1"
  }
}
View Code

    src/index.html

<!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>贪吃蛇</title>
</head>
<body>
  
</body>
</html>
View Code

    src/index.ts

console.log(100)
View Code

  安装依赖:npm i

  打包:npm run build

  打开dist/index.html,控制台打印100就可以了

 

  项目中使用less预处理语言,安装处理css和less的包:npm i -D less less-loader css-loader style-loader

    less  less核心工具包

    less-loader  将less和webpack整合

    css-loader  将css和webpack整合

    style-loader  将css引入到项目中

  根据每个loader的功能,使用时,应该先应用less-loader,再css-loader,再style-loader

  修改webpack配置,在rules中添加(loader的执行是从后往前的)

      // 设置less文件的处理
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      }

  此时在项目中就可以使用less了,src/style/index.less

body {
  background-color: red;
}

  index.ts中引入less文件

import './style/index.less'

console.log(100)

  执行npm run build,打开dist/index.html可以看到less文件已生效

 

  安装postcss来处理css的浏览器兼容问题npm i -D postcss postcss-loader postcss-preset-env

    postcss  postcss核心工具

    postcss-loader  将postcss和webpack整合

    postcss-preset-env  设置浏览器预置环境

  修改webpack.config.js中对less文件的处理

      // 设置less文件的处理
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          // 引入postcss
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  ['postcss-preset-env', { browsers: 'last 2 versions' }]
                ]
              }
            }
          },
          'less-loader'
        ]
      }

  执行npm run build,bundle.js中,对于部分css代码已加上前缀

    

 

项目界面

  src/index.html

<!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>贪吃蛇</title>
  </head>
  <body>
    <div id="main">
      <!-- 游戏舞台 -->
      <div id="stage">
        <!-- 设置蛇 -->
        <div id="snake">
          <!-- snake内部的div 表示蛇的各部分 -->
          <div></div>
        </div>
        <!-- 设置食物 -->
        <div id="food">
          <div></div>
          <div></div>
          <div></div>
          <div></div>
        </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>
View Code

  src/style/index.less

// 设置变量
@bg-color: #b7d4a8;
// 清除默认样式
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font: bold 20px 'Courier';
}
#main {
  width: 360px;
  height: 420px;
  background-color: @bg-color;
  margin: 100px auto;
  border: 10px solid #000;
  border-radius: 40px;
  display: flex;
  flex-flow: column;
  align-items: center;
  justify-content: space-around;
  #stage {
    width: 304px;
    height: 304px;
    border: 2px solid #000;
    position: relative;
    #snake {
      & > div {
        width: 10px;
        height: 10px;
        background-color: #000;
        border: 1px solid @bg-color;
        position: absolute;
      }
    }
    #food {
      width: 10px;
      height: 10px;
      position: absolute;
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      align-content: space-between;
      left: 40px;
      top: 100px;
      & > div {
        width: 4px;
        height: 4px;
        background-color: red;
        transform: rotate(45deg);
      }
    }
  }
  #score-panel {
    width: 300px;
    display: flex;
    justify-content: space-between;
  }
}
View Code

  效果:

    

 

定义Food类

  Food类为定义事物的类

  主要实现

    获取事物的坐标

    修改食物的位置(随机生成)

// 食物类
class Food {
  // 定义的一个属性表示食物所对应的元素
  element: HTMLElement
  constructor() {
    this.element = document.getElementById('food')! // 获取页面中的food元素并将其赋值给element 后面加 ! 表示该值一定不为空
    this.change()
  }
  // 获取食物x轴坐标
  get X() {
    return this.element.offsetLeft
  }
  // 获取食物y轴坐标
  get Y() {
    return this.element.offsetTop
  }
  // 修改食物位置 [0, 290]
  change() {
    const top = Math.round(Math.random() * 29) * 10 // Math.floor(Math.random() * 30)  -->  [0, 30)    Math.round(Math.random() * 29)  -->  [0, 29]
    const left = Math.round(Math.random() * 29) * 10
    this.element.style.top = top + 'px'
    this.element.style.left = left + 'px'
  }
}

export default Food
View Code

 

定义ScorePanel类

  ScorePanel类为定义记分牌的类

  主要实现

    记录分数和速度等级

    实现加分功能

    实现升级功能

// 定义表示记分牌的类
class ScorePenel {
  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.levelUp()
  }
  // 提升等级
  levelUp() {
    if (this.level < this.maxLevel) this.levelEle.innerHTML = ++this.level + ''
  }
}
const s = new ScorePenel()
for (let i = 0; i < 1; i++) {
  s.addScore()
}

export default ScorePenel
View Code

 

定义Snake类

  Snake类为定义蛇的类

  主要实现:

    获取和设置蛇头的坐标

    蛇身体变长

    蛇身体移动

    蛇不能掉头

 

    检查蛇头是否撞到身体

class Snake {
  head: HTMLElement // 蛇头
  bodies: HTMLCollection // 蛇的身体(包括蛇头)
  element: HTMLElement // 获取蛇的容器
  constructor() {
    this.element = document.getElementById('snake')!
    // this.head = document.getElementById('#snake > div') as HTMLElement
    this.head = <HTMLElement>document.querySelector('#snake > div')
    this.bodies = this.element.getElementsByTagName('div')
  }
  // 获取蛇头的x轴坐标
  get X() {
    return this.head.offsetLeft
  }
  // 获取蛇头的y轴坐标
  get Y() {
    return this.head.offsetTop
  }
  // 设置蛇头的x轴坐标
  set X(value: number) {
    if (this.X === value) return // 如果新值和旧值相同,则直接返回不再修改属性
    if (value < 0 || value > 290) throw new Error('蛇撞左右墙了!')
    // 修改X时,只能修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然
    if (
      this.bodies[1] && // 有第二节身体
      (this.bodies[1] as HTMLElement).offsetLeft === value // 如果蛇头和第二节身体位置一样
    ) {
      if (value > this.X) {
        value = this.X - 10 // 如果新值value大于旧值Y,则说明蛇向右走,此时发生掉头,应该继续向右走
      } else {
        value = this.X + 10 // 向左走
      }
    }
    this.moveBody() // 移动身体
    this.head.style.left = value + 'px'
    this.checkHeadBody() // 检查有没有撞自己
  }
  // 设置蛇头的y轴坐标
  set Y(value: number) {
    if (this.Y === value) return // 如果新值和旧值相同,则直接返回不再修改属性
    if (value < 0 || value > 290) throw new Error('蛇撞上下墙了!')
    // 修改Y时,只能修改垂直坐标,蛇在上下移动,蛇在向上移动时,不能向下掉头,反之亦然
    if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
      if (value > this.Y) {
        value = this.Y - 10 // 如果新值value大于旧值Y,则说明蛇向下走,此时发生掉头,应该继续向下走
      } else {
        value = this.Y + 10 // 向上走
      }
    }
    this.moveBody() // 移动身体
    this.head.style.top = value + 'px'
    this.checkHeadBody() // 检查有没有撞自己
  }
  // 蛇增加身体长度
  addBody() {
    this.element.insertAdjacentHTML('beforeend', '<div></div>') // 向element中添加一个div
  }
  // 蛇身体移动
  moveBody() {
    /*
      从最后一个身体元素开始,将当前身体的位置设置为前一个身体的位置
        第 4 节 = 第 3 节
        第 3 节 = 第 2 节
        第 2 节 = 第 1 节(蛇头)
    */
    // 遍历获取所有的身体,不包括蛇头
    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++) {
      const bd = this.bodies[i] as HTMLElement
      if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) throw new Error()
    }
  }
}

export default Snake
View Code

 

定义GameControl类

  GameControl类为游戏控制器,控制其他所有的类

  主要实现:

    键盘事件

    使蛇移动

    蛇撞墙

    吃食物检测

import Food from './Food'
import ScorePanel from './ScorePanel'
import Snake from './Snake'
// 游戏控制器,控制其他的所有类
class GameControl {
  snake: Snake //
  food: Food // 食物
  scorePenel: ScorePanel // 记分牌
  direction: string = '' // 创建一个属性来记录蛇的移动方向(按键的方向)
  isLive = true // 创建一个属性来记录游戏是否结束
  constructor() {
    this.snake = new Snake()
    this.food = new Food()
    this.scorePenel = new ScorePanel(10, 10)
    this.init()
  }
  // 初始化
  init() {
    document.addEventListener('keydown', this.keydownHandle.bind(this)) // 绑定键盘按下的事件
    this.run() // 调用run,是蛇移动
  }
  keydownHandle(event: KeyboardEvent) {
    this.direction = event.key
  }
  /*
    谷歌        ie
    ArrowUp     Up
    ArrowDown   Down
    ArrowRight  Right
    ArrowLeft   Left
  */
  run() {
    // 获取蛇当前坐标
    let X = this.snake.X
    let Y = this.snake.Y
    // 根据方向(this.direction)来使蛇的位置改变
    switch (this.direction) {
      case 'ArrowUp':
      case 'Up':
        Y -= 10 // 向上移动 top 减少
        break
      case 'ArrowDown':
      case 'Down':
        Y += 10 // 向下移动 top 增加
        break
      case 'ArrowLeft':
      case 'Left':
        X -= 10 // 向左移动 left 减少
        break
      case 'ArrowRight':
      case 'Right':
        X += 10 // 向右移动 left 增加
        break
    }
    this.checkEat(X, Y) // 坚持蛇是否吃到了食物
    // 修改蛇的X和Y值
    try {
      this.snake.X = X
      this.snake.Y = Y
    } catch (e: any) {
      alert(e.message + 'GAME OVER!')
      this.isLive = false
    }
    // 开启一个定时调用
    clearTimeout()
    this.isLive &&
      setTimeout(this.run.bind(this), 300 - (this.scorePenel.level - 1) * 30)
  }
  // 检查蛇是否吃到食物
  checkEat(X: number, Y: number) {
    if (X === this.food.X && Y === this.food.Y) {
      console.log('吃到食物了')
      this.food.change() // 食物的位置重置
      this.scorePenel.addScore() // 分数增加
      this.snake.addBody() // 蛇要增加一节
    }
  }
}

export default GameControl
View Code

 

src/index.ts引入游戏控制器

import './style/index.less'

import GameControl from './modules/GameControl'

new GameControl()

 

  

 

posted @ 2021-10-29 10:25  吴小明-  阅读(159)  评论(0编辑  收藏  举报