ts基础学习(二) 准备用ts写一个小项目,贪吃蛇。
一、项目搭建 1.1 项目环境配置 生成对应配置文件(里面信息配置因项目需求而异):
tsconfig.json 文件
在文件中添加如下一个字段,避免大括号报错问题。
1 "$schema": "http://json-schema.org/draft-04/schema#",
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 25 26 27 { "name": "snake", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "start": "webpack serve --open" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.24.5", "@babel/preset-env": "^7.24.5", "babel-loader": "^9.1.3", "clean-webpack-plugin": "^4.0.0", "core-js": "^3.37.0", "html-webpack-plugin": "^5.6.0", "ts-loader": "^9.5.1", "typescript": "^5.4.5", "webpack": "^5.91.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" } }
webpack.config.js文件
手动新建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 const path = require("path"); // 引入插件 const HTMLWebpackPlugin = require("html-webpack-plugin"); 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 // } }, module: { rules: [ { test: /\.ts$/, use: [ { loader: "babel-loader", // 设置babel options: { // 设置预定义的环境 presets: [ [ // 指定环境的插件 "@babel/preset-env", // 配置信息 { // 要兼容的目标浏览器 targets: { "chrome": "58", "ie": "11" }, // 指定corejs的版本 "corejs": "3", // 使用corejs的方式:usage表示按需加载 "useBuiltIns": "usage" } ] ] } }, "ts-loader" ], exclude: /node_modules/, } ] }, plugins: [ new HTMLWebpackPlugin({ template: "./src/index.html" }), new CleanWebpackPlugin(), ], // 用来设置引用模块 resolve: { extensions: [".ts", ".js"] } }
执行npm i安装所需依赖。
其他依赖
前面文件里面包含的是常规依赖,此项目需要用到less相关包。
1 npm i -D less less-loader css-loader style-loader
在webpack.config.js进行相关配置。
1 2 3 4 5 6 7 8 9 // 设置less文件的处理 { test: /\.less$/, use: [ "style-loader", "css-loader", "less-loader" ] }
这是需要兼容性处理,使用的是postcss。
1 npm i -D postcss postcss-loader postcss-preset-env
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 // 设置less文件的处理 { test: /\.less$/, use: [ "style-loader", "css-loader", // 引入postcss { loader: "postcss-loader", options: { postcssOptions: { plugins: [ [ "postcss-preset-env", { browsers: "last 2 versions" } ] ] } } }, "less-loader" ] }
1.2 项目界面编写 (可以使用npm start实时查看编写情况)
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 <body > <div id ="main" > <div id ="stage" > <div id ="snake" > <div > </div > </div > <div id ="food" > <div > </div > <div > </div > <div > </div > <div > </div > </div > </div > <div id ="info" > <div > 分数:<span id ="score" > 0</span > </div > <div > 长度:<span id ="level" > 1</span > </div > </div > </div > </body >
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 *{ margin : 0 ; padding : 0 ; box-sizing : border-box; } @bg-color: #b7d4a8 ;body { font : bold 20px "Courier" ; } #main { width : 360px ; height : 420px ; background-color : @bg-color ; margin : 100px auto; border : 10px solid black; border-radius : 30px ; display : flex; flex-flow : column; align-items : center; justify-content : space-around; position : relative; #stage { width : 300px ; height : 300px ; border : 2px solid black; #snake { & >div { width : 10px ; height : 10px ; background-color : black; border : 1px solid @bg-color ; position : absolute; } } #food { width : 10px ; height : 10px ; position : absolute; left : 40px ; display : flex; flex-flow : row wrap; justify-content : space-between; align-content : space-between; & >div { width : 4px ; height : 4px ; background-color : black; transform : rotate (45deg ); } } } #info { width : 300px ; display : flex; justify-content : space-between; } }
二、功能实现 整个项目主要有三个主体:食物、蛇、计分板,在此基础上需要对这三类进行操作的control类。
2.1 Food 食物(food)其实核心是随机出现,也就是能随机修改food位置。
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 class Food { elem : HTMLElement ; constructor ( ){ this .elem = document .getElementById ("food" )!; } get X (){ return this .elem .offsetLeft ; } get Y (){ return this .elem .offsetTop ; } change ( ){ let top = Math .round (Math .random () * 29 ) * 10 ; let left = Math .round (Math .random () * 29 ) * 10 ; this .elem .style .top = top + "px" ; this .elem .style .left = left + "px" ; } }
2.2 Snake 蛇(snake)的构造是最复杂的,首先要明确蛇的基础属性:蛇头、蛇全身、包含蛇的容器。
蛇头作用
判断是否吃到食物(蛇头位置和食物位置重叠);
判断是否撞墙(蛇头位置不能超过游戏区域);
前进的操控(通过监控按键,来让坐标改变);
判断蛇头与蛇身体是否接触;
1 2 3 4 5 6 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 ("撞到蛇身!" ); } }
蛇全身作用
身体移动(循环遍历身体,将后面身体位置设置为前面身体位置);
1 2 3 4 5 6 7 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" ; }
是否掉头(身体的第二个元素位置与最新蛇头位置是否重叠);
蛇容器作用
吃到食物,身体增加(向容器尾部添加div)
1 this .elem .insertAdjacentHTML ("beforeend" , "<div></div>" );
整体代码如下:
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 class Snake { head : HTMLElement ; bodies : HTMLCollection ; elem : HTMLElement ; constructor ( ){ this .elem = document .getElementById ("snake" )! this .head = document .querySelector ("#snake > div" ) as HTMLElement ; this .bodies = this .elem .getElementsByTagName ("div" ); console .log (this .head .offsetLeft ) } get X (){ return this .head .offsetLeft ; } get Y (){ return this .head .offsetTop ; } set X (val: number ){ if (this .X === val) return ; if (val < 0 || val > 290 ){ throw new Error ("蛇撞墙了!" ) } if (this .bodies [1 ] && (this .bodies [1 ] as HTMLElement ).offsetLeft === val){ if (val > this .X ){ val = this .X - 10 ; }else { val = this .X + 10 ; } } this .moveBody (); this .head .style .left = val + "px" ; this .checkCollision (); } set Y (val: number ){ if (this .Y === val) return ; if (val < 0 || val > 290 ){ throw new Error ("蛇撞墙了!" ) } if (this .bodies [1 ] && (this .bodies [1 ] as HTMLElement ).offsetTop === val){ if (val > this .Y ){ val = this .Y - 10 ; }else { val = this .Y + 10 ; } } this .moveBody (); this .head .style .top = val + "px" ; this .checkCollision (); } addBody ( ){ this .elem .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" ; } } checkCollision ( ){ 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 ("撞到蛇身!" ); } } } }
2.3 SocrePanel 计分板(ScorePanel)构造分为两部分:分数(score)、速度(level)。
分数的增加非常简单,就是简单自增。速度增加要设置最大速度上限(MaxLevel)与提升速度一级的分数要求(upScore),我这里默认值都是10,也就是速度等级为[0, 9],分数每过10分提升一级。
1 2 3 4 5 6 7 8 addScore ( ){ this .score ++; this .scoreElem .innerHTML = this .score + "" ; if (this .score % this .upScore === 0 ){ this .levelUp (); } }
1 2 3 4 5 6 7 levelUp ( ){ if (this .level < this .MaxLevel ){ this .level ++; this .levelElem .innerHTML = this .level + "" ; } }
整体代码如下:
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 class ScorePanel { score = 0 ; level = 1 ; scoreElem : HTMLElement ; levelElem : HTMLElement ; MaxLevel : number ; upScore : number ; constructor (MaxLevel: number = 10 , upScore: number = 10 ){ this .scoreElem = document .getElementById ("score" )!; this .levelElem = document .getElementById ("level" )!; this .MaxLevel = MaxLevel ; this .upScore = upScore; } addScore ( ){ this .score ++; this .scoreElem .innerHTML = this .score + "" ; if (this .score % this .upScore === 0 ){ this .levelUp (); } } levelUp ( ){ if (this .level < this .MaxLevel ){ this .level ++; this .levelElem .innerHTML = this .level + "" ; } } }
2.4 GameControl 在引入前三个主体后,第一步进行游戏初始化,监听键盘按下的事件。
1 2 3 4 5 6 7 8 9 10 init ( ){ document .addEventListener ("keydown" , this .keydownHandler .bind (this )); } keydownHandler (event: KeyboardEvent ){ this .direction = event.key ; }
第二步控制蛇移动,这里要考虑兼容性问题,chrome与IE的方向键是不一样的。同时,根据是否吃到食物,去调整计分板分数与蛇的速度等级。
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 move ( ){ 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){ this .isOver = true ; alert ("GAEM OVER!" ); } !this .isOver && setTimeout (this .move .bind (this ), 300 - (this .scorePanel .level - 1 ) * 20 ); }
1 2 3 4 5 6 7 8 checkEat (X: number , Y: number ){ if (X === this .food .X && Y === this .food .Y ){ this .food .change (); this .scorePanel .addScore (); this .snake .addBody (); } }
整体代码如下:
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 import Food from "./Food" ;import Snake from "./Snake" ;import ScorePanel from "./ScorePanel" ;class GameControl { food : Food ; snake : Snake ; scorePanel : ScorePanel ; direction : string = "" ; isOver : boolean = false ; constructor ( ){ this .food = new Food (); this .snake = new Snake (); this .scorePanel = new ScorePanel (10 , 6 ); this .init (); this .move (); } init ( ){ document .addEventListener ("keydown" , this .keydownHandler .bind (this )); } keydownHandler (event: KeyboardEvent ){ this .direction = event.key ; } move ( ){ 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){ this .isOver = true ; alert ("GAEM OVER!" ); } !this .isOver && setTimeout (this .move .bind (this ), 300 - (this .scorePanel .level - 1 ) * 20 ); } checkEat (X: number , Y: number ){ if (X === this .food .X && Y === this .food .Y ){ this .food .change (); this .scorePanel .addScore (); this .snake .addBody (); } } }
三、效果展示 npm start进行效果展示。