项目参考教程:贪吃蛇案例
1.项目搭建
1.1 项目结构搭建
创建一个名为xxx的项目
项目初始化
npm init -y
安装后面需要用到的依赖,在package.json中查看
项目根目录创建名为tsconfig.json的文件并更改内容如下:
{"compilerOptions": {"module": "ES2015","target": "ES2015","strict": true,"noEmitOnError": true}
}
项目根目录创建名为twebpack.config.js的文件并更改内容如下:
//引入一个包
const path = require("path");
//引入html插件
const HTMLWebpackPlugin = require("html-webpack-plugin");
//引入clean插件
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const {options} = require("less");
//webpack中的所有配置信息都应该写在module.exports中
module.exports = {mode: "development",//指定入口文件entry: "./src/index.ts",//指定打包文件所在目录output: {//打包后文件的目录path: path.resolve(__dirname, "dist"),filename: "bundle.js",//告诉我们webpack不使用箭头函数environment: {arrowFunction: false,},},//指定webpack打包时要使用的模块module: {//指定要再加载的规则rules: [{//test规则生效的文件test: /\.ts$/,use: [//配置babel{//指定加载器loader: "babel-loader",//设置babeloptions: {//设置预定义的环境presets: [[//指定环境的插件"@babel/preset-env",//配置信息{//要兼容的目标浏览器targets: {chrome: "88",ie: "11",},//指定corejs的版本corejs: "3",//按需使用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"],},
};
创建如下目录
编写index.html文件内容
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"/><title>贪吃蛇</title>
</head>
<body>
<!--主容器-->
<div id="main"><!--游戏舞台--><div id="stage"><!--蛇--><div id="snake"><!--蛇的内部div,表示蛇的各个部分--><div></div></div><!--食物--><div id="food"><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>
编写index.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: 15px;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;flex-flow: row wrap;justify-content: space-between;align-content: space-between;transform: rotate(45deg);& > div {width: 4px;height: 4px;background-color: black;}}}//积分牌#score-panel {width: 300px;display: flex;justify-content: space-between;}
}
编写index.ts文件内容
import './style/index.less';
最终搭建项目如图所示(项目名称不做严格要求)
1.2 项目启动
在终端运行npm run build
编译项目
运行npm start
启动项目,如下为项目启动成功、
效果如图
2.编写Food类
index.ts添加如下内容
//定义食物Food
class Food {//定义一个属性表示食物对应的元素element: HTMLElement;constructor() {//获取页面中的food元素并赋值给elementthis.element = document.getElementById('food')!;}//获取食物x轴坐标的方法get x() {return this.element.offsetLeft;}//获取食物y轴坐标的方法get y() {return this.element.offsetTop;}//修改食物位置change() {//生成随机位置//食物的随机位置最小是0,最大是290//蛇移动一次就是一格,一格就是10,所以食物的坐标必须是整10let top = Math.round(Math.random() * 30) * 10;let left = Math.round(Math.random() * 30) * 10;this.element.style.left = top + 'px';this.element.style.top = left + 'px'}
}
3.编写ScorePanel类
在index.ts添加代码
class ScorePanel {//score和level用来记录分数和等级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 + '';}}
}const scorePanel = new ScorePanel(100, 2);
for (let i = 0; i < 200; i++) {scorePanel.addScore();
}
运行代码效果如图
4.模块化
在src下创建名为modules的文件夹
创建Food.ts文件
将index.ts的Food类代码复制到Food.ts文件中
在Food.ts添加代码将模块暴露出去
export default Food;
ScorePanel类进行同样操作
在index.ts中引入模块并调用
5.编写snake类
class Snake {//蛇头的元素head: HTMLElement;bodies: HTMLCollection;//获取蛇的容器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) {this.head.style.left = value + 'px';}set Y(value) {this.head.style.top = value + 'px';}//设置蛇增加身体的方法addBody() {this.element.insertAdjacentHTML("beforeend","<div></div>")}
}
6.GameControoller实现
src下创建GamaController类,用来管理Food类、scorePanel类和Snake类,并将其暴露,并在index.ts中引入该模块
import Snake from "./snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";//游戏控制器 控制其他所有类
class GameController {//定义三个属性snake: Snake;food: Food;scorePanel: ScorePanel;//创建一个属性来存储移动方向(按键的方向)dirdction: 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) {//需要检查event.key的值是否合法(是否按了四个方向键)//修改direction属性this.dirdction = event.key;}
}export default GameController;
6.1 GameController实现蛇的移动
在GameController中添加代码实现跑
import Snake from "./snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";//游戏控制器 控制其他所有类
class GameController {//定义三个属性snake: Snake;food: Food;scorePanel: ScorePanel;//创建一个属性来存储移动方向(按键的方向)dirdction: 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) {//需要检查event.key的值是否合法(是否按了四个方向键)//修改direction属性this.dirdction = event.key;}//控制蛇移动run() {/** 根据方向(this.diretion)来使蛇的位置改变* 向上 top 减少* 向上 top 增加* 向左 left 减少* 向右 left 增加* *///获取蛇现在坐标let X = this.snake.X;let Y = this.snake.Y;//根据按键方向修改X值和Y值switch (this.dirdction) {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;}//修改蛇的x和Ythis.snake.X = X;this.snake.Y = Y;//开启定时调用this.isLive && setTimeout(this.run.bind(this), 300 * (this.scorePanel.level - 1) * 30);//等级越高速度越快}
}export default GameController;
6.2 蛇撞墙和吃食的检测
snake.ts做如下修改
snake.ts
class Snake {//蛇头的元素head: HTMLElement;bodies: HTMLCollection;//获取蛇的容器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;}//X的合法值为0~290之间if (value < 0 || value > 290) {//进入判断说明蛇撞墙了throw new Error('蛇撞墙了!')}this.head.style.left = value + 'px';}set Y(value) {//如果新值和旧值相等,则直接返回不修改if (this.Y === value) {return;}//Y的合法值为0~290之间if (value < 0 || value > 290) {//进入判断说明蛇撞墙了throw new Error('蛇撞墙了!')}this.head.style.top = value + 'px';}//设置蛇增加身体的方法addBody() {this.element.insertAdjacentHTML("beforeend", "<div></div>")}
}export default Snake;
GameController.ts做如下修改
import Snake from "./snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";//游戏控制器 控制其他所有类
class GameController {//定义三个属性snake: Snake;food: Food;scorePanel: ScorePanel;//创建一个属性来存储移动方向(按键的方向)dirdction: 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) {//需要检查event.key的值是否合法(是否按了四个方向键)//修改direction属性this.dirdction = event.key;}//控制蛇移动run() {/** 根据方向(this.diretion)来使蛇的位置改变* 向上 top 减少* 向上 top 增加* 向左 left 减少* 向右 left 增加* *///获取蛇现在坐标let X = this.snake.X;let Y = this.snake.Y;//根据按键方向修改X值和Y值switch (this.dirdction) {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);//修改蛇的x和Ytry {this.snake.X = X;this.snake.Y = Y;} catch (e) {//进入catch,说明出现异常,游戏结束,弹出提示信息alert((e as Error).message + 'GAME OVER!');//将isLive设置为falsethis.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 GameController;
6.2 设置蛇吃食后增加身体长度
在snake.ts中修改
class Snake {//蛇头的元素head: HTMLElement;bodies: HTMLCollection;//获取蛇的容器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;}//X的合法值为0~290之间if (value < 0 || value > 290) {//进入判断说明蛇撞墙了throw new Error("蛇撞墙了!");}//移动身体this.moveBody();this.head.style.left = value + "px";}set Y(value) {//如果新值和旧值相等,则直接返回不修改if (this.Y === value) {return;}//Y的合法值为0~290之间if (value < 0 || value > 290) {//进入判断说明蛇撞墙了throw new Error("蛇撞墙了!");} //移动身体this.moveBody();this.head.style.top = value + "px";}//设置蛇增加身体的方法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';}}
}export default Snake;
6.3 优化
蛇在向右走走时,不能按←键,反之亦然
6.3.1 移动方向的优化
snake.ts代码修改
class Snake {//蛇头的元素head: HTMLElement;bodies: HTMLCollection;//获取蛇的容器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;}//X的合法值为0~290之间if (value < 0 || value > 290) {//进入判断说明蛇撞墙了throw new Error("蛇撞墙了!");}//修改X时,在修改水平坐标,蛇在左右移动,Y轴同理,反之亦然if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {// console.log('水平方向掉头')//如果发生掉头,继续向反方向掉头if (value > this.X) {//如果新值大于旧值。则说明蛇在向右走。此时发生掉头,继续向左走value = this.X - 10;} else {value = this.X + 10;}}//移动身体this.moveBody();this.head.style.left = value + "px";}set Y(value) {//如果新值和旧值相等,则直接返回不修改if (this.Y === value) {return;}//Y的合法值为0~290之间if (value < 0 || value > 290) {//进入判断说明蛇撞墙了throw new Error("蛇撞墙了!");}//修改Y时,在修改水平坐标,蛇在上下移动,X轴同理,反之亦然if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {// console.log('水平方向掉头')//如果发生掉头,继续向反方向掉头if (value > this.Y) {//如果新值大于旧值。则说明蛇在向右走。此时发生掉头,继续向左走value = this.Y - 10;} else {value = this.Y + 10;}}//移动身体this.moveBody();this.head.style.top = value + "px";}//设置蛇增加身体的方法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';}}
}export default Snake;
6.3.2 穿越身体的优化
穿越身体撞到自己游戏结束
snake.ts修改
class Snake {//蛇头的元素head: HTMLElement;bodies: HTMLCollection;//获取蛇的容器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;}//X的合法值为0~290之间if (value < 0 || value > 290) {//进入判断说明蛇撞墙了throw new Error("蛇撞墙了!");}//修改X时,在修改水平坐标,蛇在左右移动,Y轴同理,反之亦然if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {// console.log('水平方向掉头')//如果发生掉头,继续向反方向掉头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) {//如果新值和旧值相等,则直接返回不修改if (this.Y === value) {return;}//Y的合法值为0~290之间if (value < 0 || value > 290) {//进入判断说明蛇撞墙了throw new Error("蛇撞墙了!");}//修改Y时,在修改水平坐标,蛇在上下移动,X轴同理,反之亦然if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {// console.log('水平方向掉头')//如果发生掉头,继续向反方向掉头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;
7 得分测试
为了便于测试,加快升级速度,修改GameController代码
至此,完结撒花!!!