项目搭建 初始配置文件 在根目录下,添加初始配置文件package.json,tsconfig.json,webpack.config.js package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "name" : "part3" , "version" : "1.0.0" , "description" : "" , "main" : "index.js" , "scripts" : { "test" : "echo \"Error: no test specified\" && exit 1" , "start" : "webpack serve --open chrome.exe" , "dev" : "webpack --mode development" , "build" : "webpack --mode production" }, "keywords" : [], "author" : "" , "license" : "ISC" , "devDependencies" : { "clean-webpack-plugin" : "^4.0.0-alpha.0" , "html-webpack-plugin" : "^5.3.1" , "ts-loader" : "^9.2.2" , "typescript" : "^4.3.2" , "webpack" : "^5.38.1" , "webpack-cli" : "^4.7.0" , "webpack-dev-server" : "^3.11.2" } }
tsconfig.json
{ "compilerOptions" : { "target" : "ES2015" , "module" : "ES2015" , "strict" : true , "noEmitOnError" : true } }
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 const path = require('path') const HTMLWebpackPlugin = require('html-webpack-plugin') const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { entry: './src/index.ts', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', environment: { arrowFunction: false , }, }, module: { rules: [ { test: /\.ts$/, use: [ { loader: 'babel-loader', options: { presets: [ [ '@babel/preset-env', { targets: { chrome: '58 ', ie: '11 ', }, corejs: '3 ', useBuiltIns: 'usage', }, ], ], }, }, 'ts-loader', ], exclude: /node-modules/, }, ], }, plugins: [ new CleanWebpackPlugin(), new HTMLWebpackPlugin({ template: './src/index.html', }), ], resolve: { extensions: ['.ts', '.js'], }, }
配置环境 安装 node_modules 依赖 npm i
更新依赖 npm install -D babel-loader @babel/core @babel/preset-env webpack
在根目录新建 src 文件夹,src 下新建 index.html 文件和 index.ts 文件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>
打包运行项目 npm run build
安装 less 依赖 npm i -D less less-loader css-loader style-loader
配置 less 文件 在 webpack.config.js 中设置 less 文件的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 ... module: { rules: [ { test: /\.ts$/, use: [ { loader: 'babel-loader', options: { presets: [ [ '@babel/preset-env', { targets: { chrome: '58 ', ie: '11 ', }, corejs: '3 ', useBuiltIns: 'usage', }, ], ], }, }, 'ts-loader', ], exclude: /node-modules/, }, { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'], }, ], }, ...
其中关键代码,use 中的文件执行先后顺序是从后往前执行
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'], },
测试 less 文件 在 src 文件夹下新建 style 文件,并在其中新建 index.less 文件 简单设置样式
body { background-color: aquamarine; }
在 src\index.ts 中引入样式文件
import './style/index.less' console.log(123 )
先npm run duild
在打包生成的 dist 文件夹中,点击 ndex.html 文件进行运行,网页有样式生效,则说明 less 文件运行成功
安装 css 兼容插件 npm i -D postcss postcss-loader postcss-preset-env
配置 postcss 在 webpack.config.js 添加 postcss 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ... use: [ "style-loader" , "css-loader" , { loader: "postcss-loader" , options: { postcssOptions: { plugins: [ [ "postcss-preset-env" , { browsers: 'last 2 versions' } ] ] } } }, "less-loader" ] ...
测试 css 兼容 在 src\style\index.less 中添加display: flex;
布局
body { background-color: aquamarine; display: flex; }
先npm run duild
,在在打包的文件夹 dist 中找到 bundle.js,查看display
属性值是否添加了前缀,有则 css 兼容成功
项目界面 每次调试都要进入进入开发环境 npm start
## 基本页面
在 src\index.html 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <!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> <!-- 游戏积分牌 --> <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 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 @bg-color: #b7d4a8; * { margin: 0 ; padding: 0 ; box-sizing: border-box; } body { font: bold 20 px 'Courier'; } #main { width: 360 px; height: 420 px; background-color: @bg-color; margin: 100 px auto; border: 10 px solid black; border-radius: 40 px; display: flex; flex-flow: column; align-items: center; justify-content: space-around; #stage { width: 304 px; height: 304 px; border: 2 px solid black; position: relative; position: relative; #snake { & > div { width: 10 px; height: 10 px; background-color: #000 ; border: 1 px solid @bg-color; position: absolute; } } #food { width: 10 px; height: 10 px; position: absolute; display: flex; flex-flow: row wrap; justify-content: space-between; align-content: space-between; left: 40 px; top: 100 px; & > div { width: 4 px; height: 4 px; background-color: black; transform: rotate(45 deg); } } } #score-panel { width: 300 px; display: flex; justify-content: space-between; } }
IE 兼容 IE 自己都已经放弃了!!!
在 webpack.config.js 中,IE10 不兼容箭头函数写法,给environment
添加const: false,
IE9 不兼容flex
布局,IE10 一下可以用其他方式布局,
environment: { arrowFunction: false , const: false , },
逻辑功能 Food 类 在 src\index.ts 中,添加食物类。先获取 food 元素,定义食物的 X、Y 坐标,并生成符合规定的随机坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import './style/index.less' class Food { private element: HTMLElement constructor() { this.element = document.getElementById('food')! } get X() { return this.element.offsetLeft } get Y() { return this.element.offsetTop } change() { let left = Math.round(Math.random() * 29) * 10 let top = Math.round(Math.random() * 29) * 10 this.element.style.left = left + 'px' this.element.style.top = top + 'px' } }
ScorePanel 类 在 src\moduls 中新建 ScorePanel.ts 文件,添加记分牌类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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.levelUp() } } levelUp() { if (this.level < this.maxLevel) { this.levelEle.innerHTML = ++this.level + '' } } } export default ScorePanel
在 src\index.ts 中调用 ScorePanel 类
import './style/index.less' import Food from './moduls/Food' import ScorePanel from './moduls/ScorePanel' const food = new Food() console.log(food.X, food.Y) food.change() console.log(food.X, food.Y) const scorePanel = new ScorePanel(200 , 2 ) for (let i = 0 ; i < 200 ; i++) { scorePanel.addScore() }
Snake 类 初步添加 Snake 类 在 src\moduls 中新建一个 Snake.ts 文件,初步添加 Snake 类,之后再添加其他功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Snake { head: HTMLElement bodies: HTMLCollection element: HTMLElement constructor() { this.head = document.querySelector('#snake > div') as HTMLElement this.element = document.getElementById('snake')! this.bodies = this.element.getElementsByTagName('div') } get X() { return this.head.offsetLeft } get Y() { return this.head.offsetHeight } set X(value: number) { this.head.style.left = value + 'px' } set Y(value: number) { this.head.style.top = value + 'px' } addBody() { this.element.insertAdjacentHTML('beforeend', '<div></div>') } }
GameControl 类 GameControl 类添加键盘事件 在 src\moduls 中添加 GameControl.ts 文件,先引入其他几个类,绑定键盘按键事件,修改方向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import Snake from './Snake' import Food from './Food' import ScorePanel from './ScorePanel' class GameControl { snake: Snake food: Food scorePanel: ScorePanel direction: string = '' constructor() { this.snake = new Snake() this.food = new Food() this.scorePanel = new ScorePanel() this.init() } init() { document.addEventListener('keydown', this.keydownHandler.bind(this)) } keydownHandler(event: KeyboardEvent) { this.direction = event.key } } export default GameControl
在 src\index.ts 中添加游戏控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import './style/index.less' import GameControl from './moduls/GameControl' const gameControl = new GameControl()
GameControl 使蛇移动 在 src\moduls\GameControl.ts 中,创建蛇移动的 run 方法,获取蛇的坐标,并修改蛇的坐标,通过定时器来时时监控
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import Snake from './Snake' import Food from './Food' import ScorePanel from './ScorePanel' 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() 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) { case 'ArrowUp': case 'Up': Y -= 10 break case 'ArrowDown': case 'Down': Y += 10 break case 'ArrowLeft': case 'Left': X -= 10 break case 'ArrowRight': case 'Right': X += 10 break } this.snake.X = X this.snake.Y = Y this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30) } } export default GameControl
蛇撞墙和吃食检测 此时发现一个 bug,在 src\style\index.less 中,设置了食物初始值left: 40px;
但是控制台打印的是 41,设置成其他数值则是正常的
#food { ... left: 40 px; top: 100 px; ... }
, 导致后续检查蛇是否吃到食物无法成功判断 X === this.food.X && Y === this.food.Y
所以将食物初始值 left 设置成left:30px
撞墙检测 在 src\moduls\Snake.ts 中,增加撞墙的判断,并抛出异常throw new Error('蛇撞墙了!')
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class Snake { head: HTMLElement bodies: HTMLCollection element: HTMLElement constructor() { this.head = document.querySelector('#snake > div') as HTMLElement this.element = document.getElementById('snake')! this.bodies = this.element.getElementsByTagName('div') } get X() { return this.head.offsetLeft } get Y() { return this.head.offsetTop } set X(value: number) { if (this.X === value) { return } if (value < 0 || value >= 300 ) { throw new Error('蛇撞墙了!') } this.head.style.left = value + 'px' } set Y(value: number) { if (this.Y === value) { return } if (value < 0 || value >= 300 ) { throw new Error('蛇撞墙了!') } this.head.style.top = value + 'px' } addBody() { this.element.insertAdjacentHTML('beforeend', '<div></div>') } } export default Snake
吃食检测 在 src\moduls\GameControl.ts 中,使用try { } catch (e) { }
处理异常操作,添加checkEat
方法检查是否吃到食物, 在判断是否吃到食物时一个 bug,蛇的 X、Y 坐标超过 50 之后,个位数会加 1 就变成了 51 61 71…,解决办法是四舍五入 Math.round(X / 10) * 10 === Math.round(this.food.X / 10) * 10 && Math.round(Y / 10) * 10 === Math.round(this.food.Y / 10) * 10
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 import Snake from './Snake' import Food from './Food' import ScorePanel from './ScorePanel' 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() 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) { case 'ArrowUp': case 'Up': Y -= 10 break case 'ArrowDown': case 'Down': Y += 10 break case 'ArrowLeft': case 'Left': X -= 10 break case 'ArrowRight': case 'Right': X += 10 break } this.checkEat(X, Y) try { this.snake.X = X this.snake.Y = Y } catch (e) { alert(e.message + ' GAME OVER!') this.isLive = false } this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1 ) * 30 ) } checkEat(X: number, Y: number) { if ( Math.round(X / 10) * 10 === Math.round(this.food.X / 10) * 10 && Math.round(Y / 10) * 10 === Math.round(this.food.Y / 10) * 10 ) { console.log('吃到食物') this.food.change() this.scorePanel.addScore() this.snake.addBody() } } } export default GameControl
小蛇移动 在 src\moduls\Snake.ts 中,添加moveBody
蛇移动的方法,逻辑是将后边的身体设置成前一个身体位置,头不设置。 checkHeadBody
检测是否撞到自己
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 class Snake { head: HTMLElement; bodies: HTMLCollection; element: HTMLElement; constructor() { this.head = document.querySelector('#snake > div') as HTMLElement; this.element = document.getElementById('snake')!; this.bodies = this.element.getElementsByTagName('div'); } get X() { return this.head.offsetLeft; } get Y() { return this.head.offsetTop; } set X(value: number) { if (this.X === value) { return; } if (value < 0 || value >= 300 ) { throw new Error('蛇撞墙了!'); } 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 >= 300 ) { throw new Error('蛇撞墙了!'); } 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(); } addBody() { this.element.insertAdjacentHTML('beforeend', '<div></div>'); } moveBody() { 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('撞到自己了!'); } } } } export default Snake;
最后就大功告成了!