一、项目搭建
package.json
-
{ "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 chrome.exe" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.12.9", "@babel/preset-env": "^7.12.7", "babel-loader": "^8.2.2", "clean-webpack-plugin": "^3.0.0", "core-js": "^3.8.0", "css-loader": "^5.0.1", "html-webpack-plugin": "^4.5.0", "less": "^3.12.2", "less-loader": "^7.1.0", "postcss": "^8.1.13", "postcss-loader": "^4.1.0", "postcss-preset-env": "^6.7.0", "style-loader": "^2.0.0", "ts-loader": "^8.0.11", "typescript": "^4.1.2", "webpack": "^5.6.0", "webpack-cli": "^4.2.0", "webpack-dev-server": "^3.11.0" } } - 配置好这些文件之后,npm i安装依赖
tsconfig.json
-
{ "compilerOptions": { "module": "ES2015", "target": "ES2015", "strict": true, "noEmitOnError": true } }
webpack.config.js
-
// 引入一个包 const path = require('path'); // 引入html插件 const HTMLWebpackPlugin = require('html-webpack-plugin'); // 引入clean插件 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不使用箭头,不使用const environment:{ arrowFunction: false, const: false } }, // 指定webpack打包时要使用模块 module: { // 指定要加载的规则 rules: [ { // test指定的是规则生效的文件 test: /\.ts$/, // 要使用的loader use: [ // 配置babel { // 指定加载器 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/ }, // 设置less文件的处理, // 1、前提安装好六个包(如果package.json已经写好了,按老规矩) // npm i -D less less-loader css-loader style-loader // postcss postcss-loader postcss-loader // 2、ess-loader加载器,帮我们把webpack和less做整合, // 把这两个工具连结在一起; // 3、css-loader,处理css代码; // 4、style-loader,把css引进我们的项目里; // 5、postcss,核心工具; // 6、postcss-loader加载器,帮我们把webpack和postcss做整合; // 7、postcss-preset-env,postcss提供浏览器预制环境; // (和babel很像吧!postcss让低版本浏览器兼容新的css) // 8、去webpack.config.js里添加配置,顺序不可乱,如下所示 { 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'] } };
Food.ts
-
// 定义食物类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 // 蛇移动一次就是一格,一格的大小就是10,所以就要求食物的坐标必须是整10 let top = Math.round(Math.random() * 29) * 10; let left = Math.round(Math.random() * 29) * 10; this.element.style.left = left + 'px'; this.element.style.top = top + 'px'; } } // 测试代码 // const food = new Food(); // console.log(food.X, food.Y); // food.change(); // console.log(food.X, food.Y); export default Food;
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; } // 获取蛇的Y轴坐标 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时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然 if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value){ // console.log('水平方向发生了掉头'); // 如果发生了掉头,让蛇向反方向继续移动 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; } // Y的值的合法范围0-290之间 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 = this.Y - 10; }else{ value = this.Y + 10; } } // 移动身体 this.moveBody(); this.head.style.top = value + 'px'; // 检查有没有撞到自己 this.checkHeadBody(); } // 蛇增加身体的方法 addBody(){ // 向element中添加一个div this.element.insertAdjacentHTML("beforeend", "<div></div>"); } // 添加一个蛇身体移动的方法 moveBody(){ /* * 将后边的身体设置为前边身体的位置 * 举例子: * 第4节 = 第3节的位置 * 第3节 = 第2节的位置 * 第2节 = 蛇头的位置 * */ // 遍历获取所有的身体 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;
GameControl.ts
-
// 引入其他的类 import Snake from "./Snake"; import Food from "./Food"; import ScorePanel from "./ScorePanel"; // 游戏控制器,控制其他的所有类 class GameControl { //定义三个属性 // 蛇 snake: Snake; // 食物 food: Food; // 记分牌 scorePanel: ScorePanel; // 创建一个属性来存储蛇的移动方向(也就是按键的方向) direction: string = ''; // 创建一个属性用来记录游戏是否结束 isLive = true; constructor() { this.snake = new Snake(); this.food = new Food(); this.scorePanel = new ScorePanel(10,2); this.init(); } // 游戏的初始化方法,调用后游戏即开始 init() { // 绑定键盘按键按下的事件 document.addEventListener('keydown', this.keydownHandler.bind(this)); // 调用run方法,使蛇移动 this.run(); } /* * ArrowUp Up ArrowDown Down ArrowLeft Left ArrowRight Right * */ // 创建一个键盘按下的响应函数 keydownHandler(event: KeyboardEvent) { // 需要检查event.key的值是否合法(用户是否按了正确的按键) // 修改direction属性 this.direction = event.key; } // 创建一个控制蛇移动的方法 run() { /* * 根据方向(this.direction)来使蛇的位置改变 * 向上 top 减少 * 向下 top 增加 * 向左 left 减少 * 向右 left 增加 * */ // 获取蛇现在坐标 let X = this.snake.X; let Y = this.snake.Y; // 根据按键方向来修改X值和Y值 switch (this.direction) { case "ArrowUp": case "Up": // 向上移动 top 减少 Y -= 10; break; case "ArrowDown": case "Down": // 向下移动 top 增加 Y += 10; break; case "ArrowLeft": case "Left": // 向左移动 left 减少 X -= 10; break; case "ArrowRight": case "Right": // 向右移动 left 增加 X += 10; break; } // 检查蛇是否吃到了食物 this.checkEat(X, Y); //修改蛇的X和Y值 try{ this.snake.X = X; this.snake.Y = Y; }catch (e){ // 进入到catch,说明出现了异常,游戏结束,弹出一个提示信息 alert(e.message+' GAME OVER!'); // 将isLive设置为false 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;
ScorePanel.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 + ''; } } } export default ScorePanel; // 测试代码 // const scorePanel = new ScorePanel(100, 2); // for(let i=0; i<200; i++){ // scorePanel.addScore(); // }
index.ts
-
// 引入样式 import './style/index.less'; // import Food from './moduls/Food'; // const food = new Food(); // console.log(food.X, food.Y); // food.change(); // console.log(food.X, food.Y); import GameControl from "./moduls/GameControl"; const gameControl = new GameControl(); // setInterval(()=>{ // console.log(gameControl.direction); // }, 1000);
index.html
-
<!DOCTYPE html> <html lang="zh"> <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>
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: 40px; // 开启弹性盒模型,IE9浏览器不支持,有需要要另外找办法比如用浮动定位, //本例没有给出这一点的解决方案,自己想。。。 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; } }