JS TypeScript贪吃蛇
前端小白在前进 人气:0项目背景及简介
typescript系列到这篇文章正式进入尾声了,我们通过以上学习ts的知识,想要熟悉的掌握必须要写一个小demo综合运用所学的知识,这个项目的目的就是综合ts所学知识,实现面向对象的实际开发!
项目地址 : https://gitee.com/liuze_quan/ts-greedy-snake
多模块需求分析
场景模块需求
- 具有长和宽的容器,容器内分成蛇移动和记分牌两个板块
- 蛇移动的场景设置边界线,边界线一旦触碰直接结束游戏
- 记分牌记录蛇吃到食物的分数以及等级
- 蛇吃到食物分数涨一分,每涨一定分数等级提高一级
- 等级设有上限
- 等级和蛇移动速度有关
食物类模块需求
- 在游戏开始时候食物生成在随机位置
- 当蛇吃掉食物后食物会再次随机出现(出现的位置不能与蛇身重合)
记分牌模块需求
- 设置限制等级
- 可以提升等级
- 可以增加获取的分数
蛇类模块需求
- 游戏开始的时候只有一个方块(蛇头),随后每吃掉一个食物则增加一节身体
- 当游戏进行过程中蛇头碰到身体则结束游戏
- 蛇的前进是持续的,不能停顿下来,只能去改变方向
控制模块需求
- 按下方向键开始游戏
- 只能通过四个方向键改变蛇前进的方向
- 判断游戏是否结束,蛇是否吃到食物
- 控制分数和等级是否相应增长,食物是否刷新
项目搭建
ts转译为js代码
我们需要创建tsconfig.json
文件,文件代码如下:
{ "compilerOptions": { "module": "es6", "target": "es6", "strict": true, "noEmitOnError": true } }
package.json包配置文件
- 在这个小项目中我们需要webpack打包工具,所以我们要对
package.json
文件进行一些配置。 - 选择该项目在集成终端中打开并输入代码
npm init -y
进行项目初始化,这个时候会在你的项目中生成一个初步的package.json文件,然后我们进一步完善 - 在集成终端中输入指令
npm i -D webpack webpack-cli typescript ts-loader
用来下载相关依赖(如果可以看见package.json的depDependencies中更新了你下载的依赖表示下载成功)。i表示install下载的意思,-D意思是下载的作为依赖使用 - 继续输入指令npm i -D css-loader 等依赖,这些后面都有用
- 请注意上述代码中scripts中的"build": "webpack"键值对,这个设置说明我们可以用npm run build的代码来启用webpack打包工具
{ "name": "part2", "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.18.9", "@babel/preset-env": "^7.18.9", "babel-loader": "^8.2.5", "clean-webpack-plugin": "^4.0.0", "core-js": "^3.24.0", "css-loader": "^6.7.1", "html-webpack-plugin": "^5.5.0", "less": "^4.1.3", "less-loader": "^11.0.0", "postcss": "^8.4.14", "postcss-loader": "^7.0.1", "postcss-preset-env": "^7.7.2", "style-loader": "^3.3.1", "ts-loader": "^9.3.1", "typescript": "^4.7.4", "webpack": "^5.74.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.3" } }
webpack.config.js打包工具配置
webpack打包文件配置中,代码注释非常清楚,代码如下:
//引入一个包 const path = require('path'); //引入html插件 const HTMLWebpackPlugin = require('html-webpack-plugin') const { CleanWebpackPlugin } = require("clean-webpack-plugin"); // webpack 中所有的配置信息都写道吗module.exports中 module.exports = { //指定入口文件 entry: './src/index.ts', //指定打包文件所在的目录 output: { //指定打包文件的目录 path: path.resolve(__dirname,'dist'), //打包后文件的名字 filename: "bundle.js", //告诉webpack不使用箭头函数 environment: { arrowFunction: false } }, mode: 'development', //指定webpack打包时要使用的模块 module: { //指定要加载的规则 rules: [ { //test指定规则生成的文件 test: /\.ts$/, //要使用的loader use : [ //配置babel { // 指定加载器 loader: "babel-loader", //设置babel options: { //设置预定义的环境 presets:[ [ //指定环境的插件 "@babel/preset-env", //配置信息 { //要兼容的目标浏览器 targets : { "chrome" : "101" }, //指定core.js的版本 "corejs":"3", //使用core.js的方式 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({ // title: "自定义的title" template: "./src/index.html" }), ], //用来设置引用模块 resolve: { extensions: ['.ts', '.js'] } }
到这里,我们要配置的文件都已经配置结束,接下来正式进入项目的开发
项目结构搭建
首先要进行我们的html和css样式搭建,搭建出来项目的页面!
html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <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>
css文件(这里使用的是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 black; // 设置圆角 border-radius: 40px; // 开启弹性盒模型 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: #000; border: 1px solid @bg-color; // 开启绝对定位 position: absolute; } } // 设置食物 #food{ width: 10px; height: 10px; position: absolute; left: 40px; top: 100px; // 开启弹性盒 display: flex; // 设置横轴为主轴,wrap表示会自动换行 flex-flow: row wrap; // 设置主轴和侧轴的空白空间分配到元素之间 justify-content: space-between; align-content: space-between; &>div{ width: 4px; height: 4px; background-color: black; // 使四个div旋转45度 transform: rotate(45deg); } } } // 记分牌 #score-panel{ width: 300px; display: flex; // 设置主轴的对齐方式 justify-content: space-between; } }
项目页面
多模块搭建
在项目开发中我们不可能把所有的代码写到一个文件中,所以项目开发必须会灵活运用模块化开发思想,把实现的功能细化成一个个模块。
完成Food(食物)类
//定义食物类 class Food { element : HTMLElement; constructor() { //获取页面中的food元素并赋给element this.element = document.getElementById('food')!; } //获取食物x轴坐标的方法 get X() { return this.element.offsetLeft; } //获取食物y轴坐标的方法 get Y() { return this.element.offsetTop; } //修改食物位置的方法 change() { //生成随机位置 //食物的最小位置是0 最大是290 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'; } } export default Food
代码分析:
由于在配置typescript时我们设置了strict(严格)模式,因此
this.element = document.getElementById('food')!
中如果我们不加!
会让编译器不确定我们是否会获取到food的dom元素而发生报错- 准备了
get()
方法可以在控制模块中随时获取food的具体定位 change()
方法为随机刷新一次food的位置export default Food
代码加在最后。为的是把food成为全局模块暴露出去,这样的话其他的模块可以调用这个food模块
完成ScorePanel(记分牌)类
//定义表示记分牌的类 class ScorePanel { score : number = 0; level : number = 1; scoreSpan :HTMLElement; levelEle : HTMLElement; //设置变量限制等级 maxLevel : number; //设置一个变量多少分升级 upScore : number; constructor(maxLevel : number = 10,Score : number = 10) { this.scoreSpan = document.getElementById('score')!; this.levelEle = document.getElementById('level')!; this.maxLevel = maxLevel this.upScore = Score } //设置加分的方法 AddScore() { this.score++; this.scoreSpan.innerHTML = this.score + '' if (this.score % this.upScore === 0 ) { this.AddLevel() } } //提升等级 AddLevel() { if (this.level < this.maxLevel) { this.levelEle.innerHTML = ++this.level +'' } } } export default ScorePanel
代码分析:
在记分牌模块主要是两种方法AddScore()
和AddLevel()
,分别用来控制分数增加和等级提升,重点也有设置一个变量来限制等级和设置变量来判断多少分上升一个等级
完成Snake(蛇)类
class Snake { //表示蛇头的元素 head : HTMLElement; bodies : HTMLCollectionOf<HTMLElement>; //获取蛇的容器 element : HTMLElement; constructor() { this.element = document.getElementById('snake')! this.head = document.querySelector('#snake>div') as HTMLElement; this.bodies = this.element.getElementsByTagName('div') } //获取蛇的坐标 get X() { return this.head.offsetLeft; } get Y() { return this.head.offsetTop; } set X(value) { 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大于旧值X,则说明蛇在向右走,此时应该发生掉头,应该使蛇继续向左走 value = this.X - 10 } else { value = this.X + 10 } } this.moveBody() this.head.style.left = value +'px' this.checkHeadBody() } set Y(value) { 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大于旧值X,则说明蛇在向右走,此时应该发生掉头,应该使蛇继续向左走 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++) { if(this.X === this.bodies[i].offsetLeft && this.Y === this.bodies[i].offsetTop) { //进入判断说明蛇头撞到了身体,游戏结束 throw new Error('撞到自己了') } } } } export default Snake
代码分析:
首先它自身只添加了三个功能函数addbody,movebody和checkHeadBody
movebody
的实现逻辑非常的巧妙,根据从后往前的顺序来确定位置,根据前一节的位置,从而让后边的位置替换到前一节的位置上,从而实现蛇可以移动的逻辑。
为什么get,set,判断蛇是否死亡机制以及之后的蛇移动的代码一定要写在constructor()函数中而不是写在外面?
在后面还有一个控制模块中
首先利用get()方法获得蛇头坐标,当蛇头移动一次以后,立刻刷新后的蛇头坐标反馈给蛇对象
蛇这个对象更新以后constructor代码就会执行一遍,执行过程中首先蛇头的坐标用set()函数重新设置,然后蛇的movebody
函数就会执行一次。最后对蛇进行判断死没死。
这样一次代码就执行完成啦。此时整条蛇都前进了一次。然后我们通过定时器定个时间不断让蛇移动就可以了。
完成GameControl(控制)类
import Food from "./food"; import Snake from "./Snake"; import ScorePanel from "./ScorePanel"; //游戏控制器,控制其他所有类 class GameControl { snake : Snake; food : Food; scorePanel : ScorePanel direction : string = ''; //创建一个变量来判断游戏是否结束 isLive : boolean = true; constructor() { this.snake = new Snake() this.food = new Food() this.scorePanel = new ScorePanel() this.init() } //游戏的初始化,调用后游戏将开始 init() { document.addEventListener('keydown',this.keydownHandler.bind(this)) //调用run this.run() } /*ArrowUp ArrowDown ArrowLeft ArrowRight */ //创建一个键盘按下的响应函数 keydownHandler(event: KeyboardEvent) { console.log(event.key) this.direction = event.key } //创建一个控制蛇移动的方法 /* * 根据方向(this.direction)来使蛇位置发生改变 * * */ 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 { //修改X和Y的值 this.snake.X = X; this.snake.Y = Y; }catch (e) { //进入到catch出现异常 alert((e as any).message + '游戏结束了,老表!'); this.isLive = false; } this.isLive && setTimeout(this.run.bind(this),300 - (this.scorePanel.level-1)*30) } //定义方法检查蛇是否吃到食物 checkEat(X:number,Y:number) { if (X === this.food.X && Y === this.food.Y) { //食物的位置要进行重置 this.food.change() //分数增加 this.scorePanel.AddScore() //蛇要增加一节 this.snake.addBody() } } } export default GameControl
代码分析:
我们设置控制类主要目的在于整合之前的三个类,从而在这个类中调用之前声明的类,在该类中重点在于初始化游戏、控制蛇的移动、检查蛇是否吃到食物,这个类相当于一个总开关。
这里还有一个重点在于this指向问题,这里使用了bind()
函数,bind最直接的定义就是将this指向到当前的对象。
完成index类(启动项目)
import './style/index.less' import GameControl from './modules/GameControl' new GameControl()
代码分析:
大家都非常清楚,想要让对象执行,我们必须要进行实例化,这里只用new
一下进行调用即可,项目就可以执行了
项目启动
最后我们打开终端输入npm start
或者npm run build
,项目就跑起来了,它可以自动打开浏览器进行执行
总结
学习完了typescript,其实最主要的在于运用它实现面向对象的开发,我们在日常开发中基本不会用到面向对象,就算es6中涉及到类、接口等等,但是在实际中很少人去使用,面向对象的开发中其实使得项目变得更加的严谨和合理化,我们在书写代码的时候会更加的规范,ts的类型严格更加的使它方便大型项目开发!
加载全部内容