# ts-snake **Repository Path**: baymax668/ts-snake ## Basic Information - **Project Name**: ts-snake - **Description**: 基于Typescript+HTML实现的贪吃蛇游戏 - **Primary Language**: TypeScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2023-04-02 - **Last Updated**: 2024-11-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 介绍 基于Typescript实现的贪吃蛇游戏 **技术栈**:Typescript、HTML、CSS、Webpack ![image-20230402173235407](readme.assets/image-20230402173235407.png) **运行**: ```shell npm install npm run serve ``` **游戏介绍**: - 通过↑↓←→方向键控制蛇移动 - 吃到食物增加1分,每1分增加1节蛇的长度 - 每5分增加1个level,level越高移动速度越快 - 蛇可以穿墙,从对侧墙出现 - 吃到自己就Game Over # 文件结构 ```shell |-- dist # 编译输出后的文件 | |-- bundle.js | |-- index.html |-- package.json |-- readme.md |-- src | |-- index.html # 界面 | |-- index.ts # 程序入口 | |-- models # 模型/游戏对象 | | |-- food.ts # 食物模型 | | |-- game-control.ts # 游戏主控制器(主逻辑) | | |-- score-panel.ts # 计分板 | | |-- snake.ts # 蛇模型 | |-- style # 全局样式 | |-- index.css |-- tsconfig.json # ts配置 |-- webpack.config.js # webpack配置 ``` # 难点/思路 ## 类抽象 利用TS编写,根据面向对象思想对整个游戏的各个模块和逻辑进行抽象封装 **数据对象**:根据游戏情况,可以抽象出`蛇(Snake)`、`食物(Food)`、`计分板(ScorePanel)`3个数据对象。主要用于记录不同对象的数据和基本逻辑 **程序逻辑**:抽象出游戏控制类(GameControl)用于控制所有对象的交互逻辑。使数据对象的功能更纯粹而不需要包含和其他类的交互逻辑,更好实现高内聚、低耦合 ## this问题 当一个独立的函数被作为回调函数使用时,其被执行时内部的this指向会被修改。那么就需要在传递函数时通过`bind()`绑定this,防止this被修改 ```javascript init() { // ... // 按键输入响应 // 注意:使用bind绑定this,防止传递回调函数时this被修改 document.addEventListener('keydown', this.keydowmHandle.bind(this)) // ... } keydowmHandle(event: KeyboardEvent) { this.direction = event.key } ``` ## 自动移动 玩家按下某个方向键后,蛇会往某个方向自动移动。 **思路**:在`run()`函数使用`setTimeout`调用自己,实现一个循环调用的逻辑。而且使用setTimeout定时下一次调用可以动态设置定时时间 ```typescript import Snake from "./snake" import Food from "./food" import ScorePanel from "./score-panel" // 游戏主控制类,所有游戏逻辑都在此处 class GameControl { // 所有游戏操作对象(精灵) snake: Snake food: Food scorePanel: ScorePanel direction: string = '' //... // 游戏初始化,并且运行游戏 init() { // 按键输入响应 // 注意:使用bind绑定this,防止传递回调函数时this被修改 document.addEventListener('keydown', this.keydowmHandle.bind(this)) // 蛇自动移动 this.run() } keydowmHandle(event: KeyboardEvent) { this.direction = event.key } // 蛇自动移动 run() { // 根据this.direction改变蛇的位置 // 获取蛇的坐标 let [x, y] = [this.snake.x, this.snake.y] //... // 蛇移动坐标计算 switch (this.direction) { // 上 case "ArrowUp": y -= 10 break // 下 case "ArrowDown": y += 10 break // 左 case "ArrowLeft": x -= 10 break // 右 case "ArrowRight": x += 10 break } //... //修改蛇位置 this.snake.x = x this.snake.y = y //自己调用自己,实现循环调用 let speed = 300 - (this.scorePanel.level - 1) * 20 //速度。最大减少时间是9 * 20 = 180 setTimeout(this.run.bind(this), speed) } } ``` ## 蛇身跟随 游戏默认会有一个蛇头,蛇头后面会有一节一节的蛇身。蛇身节点的跟随应该在蛇头移动后根据上一个节点的原先位置进行设置。 **思路1**:在蛇身移动时可以从蛇头的后一个节点开始往后遍历蛇身,让蛇身节点依次设置为上一个节点的原先位置。 > 缺点:但这种方式需要多使用一个临时变量记录当前节点修改前的位置,以供下一个节点使用 **思路2**⭐:从最后一个蛇身节点往前遍历,依次修改为上一个节点的位置。这样可以减少一个临时变量,逻辑跟简单 `sanke.ts` ```typescript class Snake { head: HTMLElement bodies: HTMLCollection // ... // 蛇身移动跟随 moveBody(): void { // 从后往前遍历,后一个身体节点设为上一个身体节点的位置 for (let i = this.bodies.length - 1; i > 0; i--) { const beforeBody = this.bodies[i - 1] as HTMLElement const body = this.bodies[i] as HTMLElement body.style.top = beforeBody.offsetTop + 'px' body.style.left = beforeBody.offsetLeft + 'px' } } } ``` ## 撞墙处理 300×300的地图,蛇节点为10×10,那么蛇的x,y坐标应该为0-290之间 **处理方式一 - 撞墙死亡**:当坐标小于0或大于290时,直接Game Over **处理方式二 - 穿墙**:当坐标小于0时,设为290;大于290时,设为0 **穿墙处理**: `game-control.ts` ```typescript class GameControl{ // 蛇自动移动 run() { // ... //撞墙判断(撞墙后可以穿墙,即位移到另一端出现) if (x < 0) { x = 290 } else if (x > 290) { x = 0 } if (y < 0) { y = 290 } else if (y > 290) { y = 0 } } } ``` ## 吃到食物判断 **思路**:Food类对象保存了生成的坐标,每次蛇头移动时在GameControl类的`run方法`中判断蛇头坐标是否等于食物坐标,相等即为吃到食物,并且计方、随机改变食物的坐标 `game-control.ts` ```typescript class GameControl{ // 蛇自动移动 run() { // 获取蛇的坐标 let [x, y] = [this.snake.x, this.snake.y] // 蛇移动坐标计算 switch (this.direction) { // 上 case "ArrowUp": y -= 10 break // 下 case "ArrowDown": y += 10 break // 左 case "ArrowLeft": x -= 10 break // 右 case "ArrowRight": x += 10 break } //... //吃到食物判断 if (x === this.food.x && y == this.food.y) { console.log('吃到食物') //计分 this.scorePanel.addScore() //修改食物位置 this.food.change() //增加身体 this.snake.addBody() } //... } } ``` ## 防止反方向移动 除蛇头以外,当有一节身体时要禁止直接掉头移动。 **思路**:在`run()`方法中判断当前移动方向和上一次移动的方向,如果两个方向相反则掰正当前方向,从而禁止反方向移动 ```typescript class GameControl{ lastDirection: string = '' direction: string = '' // 蛇自动移动 run() { // 获取蛇的坐标 let [x, y] = [this.snake.x, this.snake.y] //防止掉头 const direction: Map = new Map([ ['ArrowUp', 'ArrowDown'], ['ArrowDown', 'ArrowUp'], ['ArrowLeft', 'ArrowRight'], ['ArrowRight', 'ArrowLeft'], ]) if (this.snake.bodies.length >= 2 && direction.has(this.lastDirection) && direction.get(this.lastDirection) == this.direction) { // 发生掉头 // 掰正方向 this.direction = direction.get(this.direction) as string } } } ``` ## 蛇身碰撞判断 蛇头撞到蛇身则Game Over **思路1**:每次蛇头移动时,遍历蛇身判断蛇头是否撞到蛇身 ```typescript class Snake{ checkEatBody(): boolean { for (let i = 1; i < this.bodies.length; i++) { const body = this.bodies[i] as HTMLElement if (this.x == body.offsetLeft && this.y == body.offsetTop) { // 吃到身体 return true } } return false } } ``` > 缺点:蛇身越多,要遍历的节点越多,而且很多都是无效遍历,效率就会越来越低 **思路2**:牺牲内存提高查找效率。根据300×300的棋盘大小设置一个30×30的二维数组graph,蛇身移动时标记graph\[x\]\[y\];当蛇头移动时判断graph\[x\]\[y\] # 项目搭建过程 ## Node初始化 ```shell npm init -y ``` ## Webpack初始化 **安装Webpack包**: ```shell npm i -D webpack webpack-cli ``` **安装**`html-webpack-plugin`: 安装插件,自动生成html文件,并将代码载入到文件中 ```shell npm i -D html-webpack-plugin ``` **安装**`webpack-dev-server`: 安装插件,使用热更新 ```shell npm i -D webpack-dev-server ``` **安装**`clean-webpack-plugin`: 安装插件,重新编译后自动删除dist文件夹 ```shell npm i -D clean-webpack-plugin ``` **安装**`Babel`: ```shell npm install --save-dev @babel/core @babel/cli @babel/preset-env babel-loader ``` ```shell npm i -D core-js@2.6.12 ``` **安装CSS Loader**: ```shell npm i -D style-loader css-loader ``` **创建配置文件**: `webpack.config.js` ```js const path = require('path') const HTMLWebpackPlugin = require('html-webpack-plugin') const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { mode: "development", // 入口文件 entry: './src/index.ts', // 出口文件 output: { // 指定打包后的目录 path: path.resolve(__dirname, "dist"), // 打包后的文件名 filename: "bundle.js", // webpack编译后不使用箭头函数,用于兼容低版本浏览器 environment: { arrowFunction: false } }, // 设置打包是使用的模块 module: { // 设置各种文件的打包规则和loader选择 rules: [ { test: /\.ts$/, use: [ // 配置的babel { loader: "babel-loader", options: { // 配置兼容版本 presets: [ [ "@babel/preset-env", { targets: { edge: "17", firefox: "60", chrome: "67", safari: "11.1", }, useBuiltIns: "usage", corejs: "2.6.12", }, ], ] } }, 'ts-loader' ], exclude: /node_modules/ } ] }, plugins: [ new HTMLWebpackPlugin({ title: "Snake Game", template: "./src/index.html" // 设置参照模板 }), new CleanWebpackPlugin() ], // 设置能被引用的模块 resolve: { extensions: ['.ts', '.js'] } } ``` **设置配置脚本**: `packge.json` ```json { "name": "ts-snake", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "serve": "webpack serve" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "html-webpack-plugin": "^5.5.0", "ts-loader": "^9.4.2", "typescript": "^5.0.2", "webpack": "^5.76.3", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.13.1" } } ``` ## TS初始化 ### 安装TS ```shell npm i -D typescript ts-loader ``` ### 配置文件 创建`tsconfig.json`文件 ```shell tsc --init ``` `tsconfig.json` ```json { "compilerOptions": { "target": "ES2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "module": "ES2015", /* Specify what module code is generated. */ "sourceMap": true, /* Create source map files for emitted JavaScript files. */ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ "skipLibCheck": true, /* Skip type checking all .d.ts files. */ "noEmitOnError": true /* 代码有错误时不编译 */ } } ``` ## 运行脚本 `package.json` ```json { "name": "ts-snake", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "ts-loader": "^9.4.2", "typescript": "^5.0.2", "webpack": "^5.76.3", "webpack-cli": "^5.0.1" } } ``` # 参考 https://www.bilibili.com/video/BV1Xy4y1v7S2