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'] } }
tsconfig.json
{ "compilerOptions": { "module": "es6", "target": "es6", "strict": true, "noEmitOnError": true } }
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" } }
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>
src/index.ts
console.log(100)
安装依赖: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>
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; } }
效果:
定义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
定义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
定义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
定义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
src/index.ts引入游戏控制器
import './style/index.less' import GameControl from './modules/GameControl' new GameControl()