From e1dc2551e2f8c613dd5a6124f41b907015a9c967 Mon Sep 17 00:00:00 2001 From: fantasticbin Date: Wed, 8 Jun 2022 17:08:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 80 +++++++++++++++++ package.json | 34 ++++++++ src/index.html | 34 ++++++++ src/index.ts | 6 ++ src/modules/Food.ts | 53 ++++++++++++ src/modules/GameController.ts | 99 +++++++++++++++++++++ src/modules/ScorePanel.ts | 49 +++++++++++ src/modules/Snake.ts | 157 ++++++++++++++++++++++++++++++++++ src/style/index.less | 74 ++++++++++++++++ tsconfig.json | 8 ++ webpack.config.js | 110 ++++++++++++++++++++++++ 11 files changed, 704 insertions(+) create mode 100644 README.md create mode 100644 package.json create mode 100644 src/index.html create mode 100644 src/index.ts create mode 100644 src/modules/Food.ts create mode 100644 src/modules/GameController.ts create mode 100644 src/modules/ScorePanel.ts create mode 100644 src/modules/Snake.ts create mode 100644 src/style/index.less create mode 100644 tsconfig.json create mode 100644 webpack.config.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..0323f67 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +## **贪吃蛇练习** + +使用TypeScript + Webpack + Less实现贪吃蛇的例子; + +### **项目依赖** + +TypeScript: + +- typescript; +- ts-loader; + +Webpack: + +- webpack; +- webpack-cli; +- webpack-dev-server; +- html-webpack-plugin; +- clean-webpack-plugin; + +Babel: + +- core-js; +- babel-loader; +- @babel/core; +- @babel/preset-env; + +Less & CSS资源: + +- style-loader; +- css-loader; +- less; +- less-loader; +- postcss; +- postcss-loader; +- postcss-preset-env; + +### **项目使用** + +#### **编译运行** + +在确保已经正确安装node和npm的前提下: + +分别执行下面的命令安装依赖并编译项目: + +```bash +# 安装依赖 +npm i +# 编译打包 +npm run build +``` + +编译完成后,使用浏览器打开dist目录下的`index.html`即可游玩; + +根据老师的例子优化部分逻辑,及增加食物刷新时与蛇身重叠规避的逻辑,欢迎提出意见或建议 + +#### **继续开发** + +使用`npm run start`进入开发模式; + +默认使用Chrome浏览器打开,可以修改`package.json`中的值: + +```json +{ + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "webpack", + "start": "webpack serve --open chrome.exe" + } +} +``` + +### **其他** + +视频讲解: + +- [尚硅谷2021版TypeScript教程(李立超老师TS新课)](https://www.bilibili.com/video/BV1Xy4y1v7S2?p=22) + +老师源码: + +- [JasonkayZK/typescript-learn](https://github.com/JasonkayZK/typescript-learn/tree/greedy-snake) \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a24b4c7 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "snake", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "webpack --mode development --config webpack.config.js", + "start": "webpack serve --open chrome.exe" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/core": "^7.18.2", + "@babel/preset-env": "^7.18.2", + "babel-loader": "^8.2.5", + "clean-webpack-plugin": "^4.0.0", + "core-js": "^3.22.8", + "css-loader": "^6.7.1", + "html-webpack-plugin": "^5.5.0", + "less": "^4.1.2", + "less-loader": "^11.0.0", + "postcss": "^8.4.14", + "postcss-loader": "^7.0.0", + "postcss-preset-env": "^7.7.1", + "style-loader": "^3.3.1", + "ts-loader": "^9.3.0", + "typescript": "^4.7.3", + "webpack": "^5.73.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.9.1" + } +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..a5ee393 --- /dev/null +++ b/src/index.html @@ -0,0 +1,34 @@ + + + + + + + 贪食蛇 + + + +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ SCORE:0 +
+
+ LEVEL:1 +
+
+
+ + \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7819f97 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,6 @@ +// 引入样式 +import "./style/index.less"; +import GameController from "./modules/GameController"; + +const game = new GameController(); +game.init(); \ No newline at end of file diff --git a/src/modules/Food.ts b/src/modules/Food.ts new file mode 100644 index 0000000..fbb67b0 --- /dev/null +++ b/src/modules/Food.ts @@ -0,0 +1,53 @@ +/** + * 食物 + */ +class Food +{ + protected element: HTMLElement; + + constructor() + { + this.element = document.getElementById("food")!; + } + + /** + * 返回食物X坐标值 + */ + get X(): number + { + return this.element.offsetLeft; + } + + /** + * 返回食物Y坐标值 + */ + get Y(): number + { + return this.element.offsetTop; + } + + /** + * 食物位置重置 + * @param snakeBodies 蛇身元素集合 + */ + public change(snakeBodies: HTMLCollectionOf): void + { + // 随机生成一个位置,最小是0,最大是290,每次移动间隔大小是10 + let top = Math.round(Math.random() * 29) * 10; + let left = Math.round(Math.random() * 29) * 10; + + // 防止食物重置时位置和蛇身重叠 + for (let snakeBody of snakeBodies) { + if (top === snakeBody.offsetTop && left === snakeBody.offsetLeft) { + // 递归调用,生成新的位置,并中止当前程序继续执行(一定要加上中止,否则该逻辑无效) + this.change(snakeBodies); + return; + } + } + + this.element.style.top = top + "px"; + this.element.style.left = left + "px"; + } +} + +export default Food; \ No newline at end of file diff --git a/src/modules/GameController.ts b/src/modules/GameController.ts new file mode 100644 index 0000000..30d85eb --- /dev/null +++ b/src/modules/GameController.ts @@ -0,0 +1,99 @@ +import Snake from "./Snake"; +import Food from "./Food"; +import ScorePanel from "./ScorePanel"; + +/** + * 游戏控制器 + */ +class GameController +{ + protected snake: Snake; + protected food: Food; + protected scorePanel: ScorePanel; + + // 用于防止调头,记录上一次键盘操作 + protected lastKeyDown = ''; + // 用于标记游戏是否未结束 + protected isLive = true; + + constructor() + { + this.snake = new Snake(); + this.food = new Food(); + this.scorePanel = new ScorePanel(); + } + + /** + * 游戏初始化,运行该方法表示游戏开始 + */ + public init(): void + { + // 绑定键盘事件,当按下 上/下/左/右 时改变蛇的行动方向 + document.addEventListener("keydown", this.keyDownHandle.bind(this)); + this.snakeRun(); + } + + /** + * 键盘事件逻辑 + * @param event 事件属性 + */ + protected keyDownHandle(event: KeyboardEvent): void + { + const horizontal = ["ArrowLeft", "Left", "ArrowRight", "Right"]; + const vertical = ["ArrowUp", "Up", "ArrowDown", "Down"]; + + // 防止水平调头 + if (horizontal.includes(this.lastKeyDown) && horizontal.includes(event.key)) { + return; + } + + // 防止垂直调头 + if (vertical.includes(this.lastKeyDown) && vertical.includes(event.key)) { + return; + } + + // 设置当前蛇的行走方向 + this.snake.nowDirection = event.key; + // 记录此次键盘操作 + this.lastKeyDown = event.key; + } + + /** + * 检查蛇是否吃到食物 + * @param x X坐标 + * @param y Y坐标 + */ + protected checkEat(x: number, y: number): void + { + if (x === this.food.X && y === this.food.Y) { + // 食物位置重置 + this.food.change(this.snake.nowBodies); + // 分数增加 + this.scorePanel.addScore(); + // 蛇身增加一节 + this.snake.addBody(); + } + } + + /** + * 处理蛇走逻辑 + */ + protected snakeRun(): void + { + this.checkEat(this.snake.X, this.snake.Y); + + try { + this.snake.checkRearEnd(); + this.snake.run(); + } catch (e: any) { + // 捕获到错误意味着游戏失败 + alert((e as Error).message + " GAME OVER!"); + this.isLive = false; + } + + // 定时调用自身,并根据当前等级动态改变速度 + this.isLive && setTimeout(this.snakeRun.bind(this), 300 - (this.scorePanel.nowLevel - 1) * 30); + } +} + +export default GameController; \ No newline at end of file diff --git a/src/modules/ScorePanel.ts b/src/modules/ScorePanel.ts new file mode 100644 index 0000000..35ccc09 --- /dev/null +++ b/src/modules/ScorePanel.ts @@ -0,0 +1,49 @@ +/** + * 记分牌 + */ +class ScorePanel +{ + protected score = 0; + protected level = 1; + protected scoreElement: HTMLElement; + protected levelElement: HTMLElement; + + constructor(protected maxLavel: number = 10, protected upScore: number = 10) + { + this.scoreElement = document.getElementById("score")!; + this.levelElement = document.getElementById("level")!; + } + + /** + * 加分 + */ + public addScore(): void + { + this.scoreElement.innerText = (++this.score).toString(); + + // 分数达到设置的数则升级 + if (this.score % this.upScore === 0) { + this.levelUp(); + } + } + + /** + * 升级 + */ + protected levelUp(): void + { + if (this.level <= this.maxLavel) { + this.levelElement.innerText = (++this.level).toString(); + } + } + + /** + * 获取当前等级 + */ + get nowLevel(): number + { + return this.level; + } +} + +export default ScorePanel; \ No newline at end of file diff --git a/src/modules/Snake.ts b/src/modules/Snake.ts new file mode 100644 index 0000000..cd4828a --- /dev/null +++ b/src/modules/Snake.ts @@ -0,0 +1,157 @@ +/** + * 蛇 + */ +class Snake +{ + protected element: HTMLElement; + // 蛇头元素 + protected head: HTMLElement; + // 蛇的身体(包括蛇头) + protected bodies: HTMLCollectionOf; + // 蛇当前的行走方向 + protected direction = ""; + + constructor() + { + this.element = document.getElementById("snake")!; + this.head = document.querySelector("#snake > div")!; + this.bodies = this.element.getElementsByTagName("div"); + } + + /** + * 设置蛇头的X坐标值 + */ + set X(value: number) + { + if (this.X === value) { + return; + } + + if (value < 0 || value > 290) { + throw new Error("蛇撞墙了!"); + } + + this.moveBody(); + + this.head.style.left = value + "px"; + } + + /** + * 返回蛇头的X坐标值 + */ + get X(): number + { + return this.head.offsetLeft; + } + + /** + * 设置蛇头的Y坐标值 + */ + set Y(value: number) + { + if (this.Y === value) { + return; + } + + if (value < 0 || value > 290) { + throw new Error("蛇撞墙了!"); + } + + this.moveBody(); + + this.head.style.top = value + "px"; + } + + /** + * 返回蛇头的Y坐标值 + */ + get Y(): number + { + return this.head.offsetTop; + } + + /** + * 设置蛇的行走方向 + */ + set nowDirection(value: string) + { + this.direction = value; + } + + /** + * 获取蛇的身体 + */ + get nowBodies(): HTMLCollectionOf + { + return this.bodies; + } + + /** + * 吃完食物增加身体 + */ + public addBody(): void + { + const div = document.createElement("div"); + this.element.appendChild(div); + } + + /** + * 多节蛇的移动 + */ + public moveBody(): void + { + for (let i = this.bodies.length - 1; i > 0; i--) { + let x = this.bodies[i - 1].offsetLeft; + let y = this.bodies[i - 1].offsetTop; + + this.bodies[i].style.left = x + "px"; + this.bodies[i].style.top = y + "px"; + } + } + + /** + * 追尾检查 + */ + public checkRearEnd(): void | never + { + // 只要蛇头与其他任何蛇身重叠则为追尾 + for (let i = 1; i < this.bodies.length; i++) { + if (this.X === this.bodies[i].offsetLeft && this.Y === this.bodies[i].offsetTop) { + throw new Error("蛇追尾了!"); + } + } + } + + /** + * 蛇头走 + */ + public run(): void + { + let x = this.X; + let y = this.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.X = x; + this.Y = y; + } +} + +export default Snake; \ No newline at end of file diff --git a/src/style/index.less b/src/style/index.less new file mode 100644 index 0000000..6953f76 --- /dev/null +++ b/src/style/index.less @@ -0,0 +1,74 @@ +// 颜色变量 +@bg-color: #b7d4a8; + +// 清除默认样式 +* { + margin: 0; + padding: 0; + // 改变盒子模型的计算方式 + box-sizing: border-box; +} + +// 页面公共样式 +body { + font: bold 20px "Courier"; + overflow: hidden; +} + +// 主窗口的样式 +#main { + width: 360px; + height: 420px; + background-color: @bg-color; + margin: 100px auto; + border: 10px solid black; + border-radius: 20px; + + // 开启弹性盒模型 + display: flex; + // 设置主轴的方向 + flex-flow: column; + // 设置侧轴的对齐方式 + align-items: center; + // 设置主轴的对齐方式 + justify-content: space-around; +} + +// 游戏舞台 +#stage { + width: 304px; + height: 304px; + border: 2px solid black; + // 开启相对定位(因蛇元素和食物元素需要绝对定位,遵循子绝父相的定律) + position: relative; +} + +// 蛇 +#snake > div { + width: 10px; + height: 10px; + background-color: black; + border: 1px solid @bg-color; + // 开启绝对定位 + position: absolute; +} + +// 食物 +#food { + width: 10px; + height: 10px; + background-color: black; + border: 1px solid @bg-color; + border-radius: 50%; + // 开启绝对定位 + position: absolute; + left: 40px; + top: 100px; +} + +// 记分牌 +#score-panel { + width: 300px; + display: flex; + justify-content: space-between; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e6d66b1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "ES2015", + "target": "ES2015", + "strict": true, + "noEmitOnError": true + } +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..854fffd --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,110 @@ +// 引入一个包 +const path = require("path"); +// 引入HTML插件 +const HTMLWebpackPlugin = require("html-webpack-plugin"); +// 引入Clean插件 +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); + +// webpack中的所有配置信息 +module.exports = { + mode: "development", + // 指定入口文件 + entry: "./src/index.ts", + + // 指定打包文件所在目录 + output: { + // 指定打包文件的目录 + path: path.resolve(__dirname, "dist"), + // 打包后的文件名 + filename: "bundle.js", + + // 不使用箭头函数的方式定义 + environment: { + arrowFunction: false, + const: false + } + }, + + // 指定webpack打包时要使用的模块 + module: { + // 指定要加载的规则 + rules: [ + { + // 指定规则生效的文件 + test: /\.ts$/, + // 要使用的loader + use: [ + // 配置babel + { + // 指定加载器 + loader: "babel-loader", + // 设置babel + options: { + // 设置预定义环境 + presets: [ + [ + // 指定环境的插件 + "@babel/preset-env", + // 配置信息 + { + // 要兼容的目标浏览器 + targets: { + "chrome": "100", + "ie": "11" + }, + // 指定corejs的版本 + "corejs": "3", + // 使用corejs的方式:usage表示按需加载 + "useBuiltIns": "usage" + } + ] + ] + } + }, + "ts-loader" + ], + // 要排除的文件 + exclude: /node_modules/ + }, + + // 设置less文件的处理 + { + test: /\.less$/, + use: [ + "style-loader", + "css-loader", + // 引入postcss + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [ + [ + "postcss-preset-env", + { + browsers: "last 2 versions" + } + ] + ] + } + } + }, + "less-loader" + ] + } + ] + }, + + // 配置webpack插件 + plugins: [ + new CleanWebpackPlugin(), + new HTMLWebpackPlugin({ + template: "./src/index.html" + }) + ], + + // 设置引用模块 + resolve: { + extensions: [".ts", ".js"] + } +}; \ No newline at end of file