贪吃蛇项目练习
注意分工明确,遵循单一职责原则,就是一个类只做一件事,把任务分清楚,方便日后维护,分工逐一谁的事谁去做。
1. 项目搭建
该部分可参考前面TypeScript——5. 编译选项、6. 使用webpack打包ts代码部分生成对应的配置文件(可直接使用part3中的配置文件)
-
在之前的基础上在webpack中引入css插件,使得webpack可以对css代码进行处理并增加css的兼容性
-
npm i -D less less-loader css-loader style-loader postcss postcss-loader postcss-preset-env
-
-
修改webpack配置文件,设置less文件的处理
// 引入一个包
const path = require("path");
// 引入插件
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
// webpack所有的配置信息都应该写在module.exports中
module.exports = {
// 指定入口文件
entry: "./src/index.ts",
// 指定打包文件所在目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, "dist"),
// 打包后文件的名字
filename: "bundle.js",
// 告诉webpack不使用箭头函数
environment: {
arrowFunction: false,
const: false
},
},
// 指定webpack打包时要使用的模块
module: {
// 指定要加载的规则
rules: [
{
// 指定规则生效的文件
test: /\.ts$/, //匹配以.ts结尾的文件
// 要使用的loader
use: [
// 配置babel
{
// 指定加载器
loader: "babel-loader",
// 设置babel
options: {
// 设置预定义的环境
presets: [
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 要兼容的目标浏览器
targets: {
chrome: "56",
ie: "8",
},
// 指定corejs版本
corejs: "3",
// 使用corejs的方式
useBuiltIns: "entry", // 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({
// 复制'./src/index.html'文件,并自动引入打包输出的所有资源(JS/CSS)
template: "./src/index.html",
}),
],
// 用来设置引用模块
resolve: {
extensions: [".ts", "js"],
},
mode: 'development',
// 用于开发环境,不要用于生产环境
devServer: {
// 运行代码的目录
contentBase: path.resolve(__dirname, 'dist'),
// 监视contentBase目录下的所有文件,一旦文件变化就会reload
watchContentBase: true,
watchOptions: {
// 忽略文件
ignored: /node_modules/
},
// 端口号
port: 5000,
// 域名
host: 'localhost',
// 自动打开浏览器
open: true,
// 开启HMR功能
hot: true,
}
};
- 编译项目测试是否编译成功:npm run build
- npm run start 运行
2. 项目界面
- src/style/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: 30px;
// 开启弹性盒模型
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旋转45度
transform: rotate(45deg);
& > div {
width: 4px;
height: 4px;
background-color: #000;
}
}
}
// 游戏记分牌
#score-panel {
width: 300px;
display: flex;
// 设置主轴对齐方式
justify-content: space-between;
}
}
- src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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 id="score-panel">
<div>SCORE:<span id="score">0</span></div>
<div>LEVEL:<span id="level">1</span></div>
</div>
</div>
</body>
</html>
- npm run start
3. 完成Food类
src/module/Food.ts
// 引入样式
import "./style/index.less";
// 定义类
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 left = Math.round(Math.random() * 29) * 10; // 四舍五入0到29取整再乘以10
let top = 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);
4. 完成ScorePanel类
src/module/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(10, 50);
// for (let i = 0; i < 200; i++) {
// scorePanel.addScore();
// }
5. 完成Snake类
src/module/Snake.ts
class Snake {
// 表示蛇头的元素
head: HTMLElement;
// 蛇的身体(包括蛇头)
bodies: HTMLCollection; // 集合
// 获取蛇的容器
element: HTMLElement;
constructor() {
this.head = document.querySelector("#snake > div")!;
this.bodies = document.getElementById("#snake")?.getElementsByTagName("div")!;
this.element = document.getElementById("#snake")!;
}
// 获取蛇坐标(蛇头坐标)
get X() {
return this.head.offsetLeft;
}
// 获取蛇的Y轴坐标
get Y() {
return this.head.offsetTop;
}
// 设置蛇坐标
set X(value:number) {
this.head.style.left = value + "px";
}
set Y(value:number) {
this.head.style.top = value + "px";
}
// 蛇增加身体
addBody() {
// 想element添加一个div
this.element.insertAdjacentHTML("beforeend", "<div></div>");
}
}
export default Snake;
6. GameControl键盘事件
src/module/GameControl.ts
// 引入其他类
import Food from "./Food";
import ScorePanel from "./ScorePanel";
import Snake from "./Snake";
// 游戏控制器,控制其他所有类
class GameControl {
// 定义三个属性
// 蛇
snake: Snake;
// 食物
food: Food;
// 记分牌
scorePanel: ScorePanel;
// 创建一个属性来存储蛇的移动方向(也就是按键方向)
direction: string = "";
/**
* ArrowUp Up
* ArrowDown Down
* ArrowLeft Left
* ArrowRight Right
*/
// 记录游戏是否结束
isLive = true;
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel();
this.Init();
}
// 游戏的初始化方法
Init() {
// 调用后游戏即开始
// bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入
// bind()第一个参数的值,例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj
document.addEventListener("keydown", this.keydownHandler.bind(this));
// 调用run方法,使蛇动起来
this.run();
}
// 创建一个键盘按下得响应函数
keydownHandler(event: KeyboardEvent) {
// 检查event.key的值是否合法(用户是否按了正确的按键)
// 修改direction属性
this.direction = event.key;
console.log(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;
}
// 修改蛇的X和Y
this.snake.X = X;
this.snake.Y = Y;
// 开启一个定时调用
this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30);
}
}
export default GameControl;
7. 蛇撞墙和吃食检测
src/module/GameControl.ts
// 引入其他类
import Food from "./Food";
import ScorePanel from "./ScorePanel";
import Snake from "./Snake";
// 游戏控制器,控制其他所有类
class GameControl {
// 定义三个属性
// 蛇
snake: Snake;
// 食物
food: Food;
// 记分牌
scorePanel: ScorePanel;
// 创建一个属性来存储蛇的移动方向(也就是按键方向)
direction: string = "";
/**
* ArrowUp Up
* ArrowDown Down
* ArrowLeft Left
* ArrowRight Right
*/
// 记录游戏是否结束
isLive = true;
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel();
this.Init();
}
// 游戏的初始化方法
Init() {
// 调用后游戏即开始
// bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入
// bind()第一个参数的值,例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj
document.addEventListener("keydown", this.keydownHandler.bind(this));
// 调用run方法,使蛇动起来
this.run();
}
// 创建一个键盘按下得响应函数
keydownHandler(event: KeyboardEvent) {
// 检查event.key的值是否合法(用户是否按了正确的按键)
// 修改direction属性
this.direction = event.key;
console.log(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 (error) {
// 进入catch说明出现了异常,游戏结束
alert(error.message);
// 将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) {
console.log("吃到食物了!!!");
// 食物的位置要进行重置
this.food.change();
// 分数增加
this.scorePanel.addScore();
// 蛇要增加一节
this.snake.addBody();
}
}
}
export default GameControl;
8. 身体的移动
src/module/Snake.ts
class Snake {
// 表示蛇头的元素
head: HTMLElement;
// 蛇的身体(包括蛇头)
bodies: HTMLCollection; // 集合
// 获取蛇的容器
element: HTMLElement;
constructor() {
this.head = document.querySelector("#snake > div")!;
this.bodies = document.getElementById("snake")?.getElementsByTagName("div")!;
this.element = document.getElementById("snake")!;
}
// 获取蛇坐标(蛇头坐标)
get X() {
return this.head.offsetLeft;
}
// 获取蛇的Y轴坐标
get Y() {
return this.head.offsetTop;
}
// 设置蛇坐标
set X(value: number) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.X == value) {
return;
}
// X值的合法范围:0~290
if (value < 0 || value > 290) {
// 进入判断说明蛇撞墙了,抛出异常
throw new Error("蛇撞墙了!GAME OVER");
}
// 修改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: number) {
if (this.Y == value) {
return;
}
// Y值的合法范围:0~290
if (value < 0 || value > 290) {
// 进入判断说明蛇撞墙了
throw new Error("蛇撞墙了!GAME OVER");
}
// 修改Y时,是在修改水平坐标,蛇在左右移动,蛇在向下移动时,不能往上掉头,反之亦然
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
console.log("垂直方向发生了掉头!");
// 如果发生了掉头,让蛇方反方向继续移动
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() {
// 想element添加一个div
console.log(this);
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 = this.bodies.length -1; i > 0; i--) {
let bd = this.bodies[i] as HTMLElement
if(this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
// 进入判断说明蛇头撞到身体,游戏结束
throw new Error("撞到自己了!GAME OVER!");
}
}
}
}
export default Snake;