贪吃蛇项目笔记

本文最后更新于:1 年前

项目搭建

初始配置文件

在根目录下,添加初始配置文件package.json,tsconfig.json,webpack.config.js
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
{
"name": "part3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack serve --open chrome.exe",
"dev": "webpack --mode development",
"build": "webpack --mode production"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"clean-webpack-plugin": "^4.0.0-alpha.0",
"html-webpack-plugin": "^5.3.1",
"ts-loader": "^9.2.2",
"typescript": "^4.3.2",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
}
}

tsconfig.json

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"strict": true,
"noEmitOnError": true
}
}

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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不使用箭头
// 默认打包后是一个立即执行的箭头函数,在IE 11中也是无法执行的!
// 加上下面的配置,可以在webpack打包时,最外层不再是箭头函数
// webpack新版本已经不想兼容IE了!233
environment: {
arrowFunction: false,
},
},
// 指定webpack打包时要使用模块
module: {
// 指定要加载的规则
rules: [
{
// test指定的是规则生效的文件
test: /\.ts$/,
// 要使用的loader
// Webpack在加载时是"从后向前"加载!
use: [
// 配置babel
{
// 指定加载器
loader: 'babel-loader',
// 设置babel
options: {
// 设置预定义的环境
presets: [
[
// 指定环境的插件
'@babel/preset-env',
// 配置信息
{
// 要兼容的目标浏览器
targets: {
chrome: '58',
ie: '11',
},
// 指定corejs的版本
// package.json中的版本为3.8.1
corejs: '3',
// 使用corejs的方式,"usage" 表示按需加载
useBuiltIns: 'usage',
},
],
],
},
},
'ts-loader',
],
// 要排除的文件
exclude: /node-modules/,
},
],
},

// 配置Webpack插件
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
// title: "这是一个自定义的title"
template: './src/index.html',
}),
],
// 用来设置引用模块
resolve: {
extensions: ['.ts', '.js'],
},
}

配置环境

安装 node_modules 依赖

npm i

更新依赖

npm install -D babel-loader @babel/core @babel/preset-env webpack

在根目录新建 src 文件夹,src 下新建 index.html 文件和 index.ts 文件
index.html

1
2
3
4
5
6
7
8
9
10
11
<!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></body>
</html>

打包运行项目

npm run build

安装 less 依赖

npm i -D less less-loader css-loader style-loader

配置 less 文件

在 webpack.config.js 中设置 less 文件的处理

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
...

// 指定webpack打包时要使用模块
module: {
// 指定要加载的规则
rules: [
{
// test指定的是规则生效的文件
test: /\.ts$/,
// 要使用的loader
// Webpack在加载时是"从后向前"加载!
use: [
// 配置babel
{
// 指定加载器
loader: 'babel-loader',
// 设置babel
options: {
// 设置预定义的环境
presets: [
[
// 指定环境的插件
'@babel/preset-env',
// 配置信息
{
// 要兼容的目标浏览器
targets: {
chrome: '58',
ie: '11',
},
// 指定corejs的版本
// package.json中的版本为3.8.1
corejs: '3',
// 使用corejs的方式,"usage" 表示按需加载
useBuiltIns: 'usage',
},
],
],
},
},
'ts-loader',
],
// 要排除的文件
exclude: /node-modules/,
},
//设置less文件的处理
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
],
},
...

其中关键代码,use 中的文件执行先后顺序是从后往前执行

1
2
3
4
5
//设置less文件的处理
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},

测试 less 文件

在 src 文件夹下新建 style 文件,并在其中新建 index.less 文件
简单设置样式

1
2
3
4
body {
background-color: aquamarine;
}

在 src\index.ts 中引入样式文件

1
2
3
import './style/index.less'
console.log(123)

npm run duild在打包生成的 dist 文件夹中,点击 ndex.html 文件进行运行,网页有样式生效,则说明 less 文件运行成功

安装 css 兼容插件

npm i -D postcss postcss-loader postcss-preset-env

配置 postcss

在 webpack.config.js 添加 postcss 配置

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
			...
use: [
"style-loader",
"css-loader",

// 引入postcss
// 类似于babel,把css语法转换兼容旧版浏览器的语法
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
// 浏览器兼容插件
"postcss-preset-env",
{
// 每个浏览器最新两个版本
browsers: 'last 2 versions'
}
]
]
}
}
},
"less-loader"
]
...

测试 css 兼容

在 src\style\index.less 中添加display: flex;布局

1
2
3
4
5
body {
background-color: aquamarine;
display: flex;
}

npm run duild,在在打包的文件夹 dist 中找到 bundle.js,查看display属性值是否添加了前缀,有则 css 兼容成功

项目界面

每次调试都要进入进入开发环境
npm start

​## 基本页面

在 src\index.html 中

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
<!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>

<!-- 游戏积分牌 -->
<div id="score-panel">
<div>
SCORE:
<span id="score">0</span>
</div>
<div>
level:
<span id="level">1</span>
</div>
</div>
</div>
</body>
</html>

基本样式

在 src\style\index.less 中

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
87
88
89
90
91
// 设置变量
@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;
// 开启相对定位
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;
// 开启弹性盒模型
display: flex;
// 设置横轴位主轴,wrap表示会自动换行
flex-flow: row wrap;
// 设置主轴和侧轴的空白空间分配到元素之间
justify-content: space-between;
align-content: space-between;

left: 40px;
top: 100px;
// transform: rotate(45deg);

& > div {
width: 4px;
height: 4px;
background-color: black;
//使div旋转45度
transform: rotate(45deg);
}
}
}
// 记分牌
#score-panel {
width: 300px;
display: flex;
//设置主轴对齐方式
justify-content: space-between;
}
}

IE 兼容

IE 自己都已经放弃了!!!

在 webpack.config.js 中,IE10 不兼容箭头函数写法,给environment添加const: false,
IE9 不兼容flex布局,IE10 一下可以用其他方式布局,

1
2
3
4
environment: {
arrowFunction: false,
const: false,
},

逻辑功能

Food 类

在 src\index.ts 中,添加食物类。先获取 food 元素,定义食物的 X、Y 坐标,并生成符合规定的随机坐标

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
import './style/index.less'

//定义食物类
class Food {
// 定义一个属性表示食物所对应的元素
private element: HTMLElement

constructor() {
// 获取页面中的food元素并将其赋值给element
// 末尾加上叹号,表示id为food的元素必定存在(非空)
this.element = document.getElementById('food')!
}

// 定义一个获取食物X轴坐标的方法
get X() {
return this.element.offsetLeft
}

// 定义一个获取食物Y轴坐标的方法
get Y() {
return this.element.offsetTop
}

// 修改食物的位置
change() {
// 生成一个随机的位置
// 食物的位置最小,最大290,并且是整数10
// 蛇移动一次就是一格,一格的大小就是10,所以就要求食物的坐标必须是整10
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'
}
}

// 测试代码
// const food = new Food();
// console.log(food.X, food.Y);
// food.change();
// console.log(food.X, food.Y);

ScorePanel 类

在 src\moduls 中新建 ScorePanel.ts 文件,添加记分牌类

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
// 定义记分牌的类
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(200, 2)
// for (let i = 0; i < 200; i++) {
// scorePanel.addScore()
// }

export default ScorePanel

在 src\index.ts 中调用 ScorePanel 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import './style/index.less'

import Food from './moduls/Food'
import ScorePanel from './moduls/ScorePanel'

const food = new Food()
console.log(food.X, food.Y)
food.change()
console.log(food.X, food.Y)

const scorePanel = new ScorePanel(200, 2)
for (let i = 0; i < 200; i++) {
scorePanel.addScore()
}

Snake 类

初步添加 Snake 类

在 src\moduls 中新建一个 Snake.ts 文件,初步添加 Snake 类,之后再添加其他功能

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
class Snake {
//表示蛇头的元素
head: HTMLElement

// 蛇的身体(包括蛇头)
bodies: HTMLCollection
//获取蛇的容器
element: HTMLElement

constructor() {
this.head = document.querySelector('#snake > div') as HTMLElement
this.element = document.getElementById('snake')!
this.bodies = this.element.getElementsByTagName('div')
}
//获取蛇的坐标(蛇头坐标)
get X() {
return this.head.offsetLeft
}

// 获取蛇的Y轴坐标
get Y() {
return this.head.offsetHeight
}

// 设置蛇头的坐标
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>')
}
}

GameControl 类

GameControl 类添加键盘事件

在 src\moduls 中添加 GameControl.ts 文件,先引入其他几个类,绑定键盘按键事件,修改方向

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
import Snake from './Snake'
import Food from './Food'
import ScorePanel from './ScorePanel'

//游戏控制器,控制其他所有类
class GameControl {
//定义三个属性
snake: Snake
food: Food
scorePanel: ScorePanel

//创建一个属性来存储蛇的移动方向(也就是按键的方向)
direction: 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.direction = event.key
}
}

export default GameControl

在 src\index.ts 中添加游戏控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import './style/index.less'

// import Food from './moduls/Food'
// import ScorePanel from './moduls/ScorePanel'

// const food = new Food()
// console.log(food.X, food.Y)
// food.change()
// console.log(food.X, food.Y)

// const scorePanel = new ScorePanel(200, 2)
// for (let i = 0; i < 200; i++) {
// scorePanel.addScore()
// }

import GameControl from './moduls/GameControl'

const gameControl = new GameControl()

// setInterval(() => {
// console.log(gameControl.direction)
// }, 1000)

GameControl 使蛇移动

在 src\moduls\GameControl.ts 中,创建蛇移动的 run 方法,获取蛇的坐标,并修改蛇的坐标,通过定时器来时时监控

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
87
88
89
90
91
92
93
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()

this.init()
}

//游戏得初始化方法,调用后游戏即开始
init() {
// 绑定键盘按键按下的事件
document.addEventListener('keydown', this.keydownHandler.bind(this))

// 调用run方法,使蛇移动
this.run()
}
//创建一个键盘按下的响应函数
keydownHandler(event: KeyboardEvent) {
//需要检查event.key的值是否合法(用户是否按了正确的按键)

// 修改direction 属性
this.direction = event.key
}

/*
Chrome IE
ArrowUp Up
ArrowDown Down
ArrowLeft Left
ArrowRight Right
*/
// 创建一个控制蛇移动的方法
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':
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和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

蛇撞墙和吃食检测

此时发现一个 bug,在 src\style\index.less 中,设置了食物初始值left: 40px;但是控制台打印的是 41,设置成其他数值则是正常的

1
2
3
4
5
6
7
8
// 设置食物
#food {
...
// 初始化一个位置
left: 40px;
top: 100px;
...
}

image.png
导致后续检查蛇是否吃到食物无法成功判断 X === this.food.X && Y === this.food.Y
所以将食物初始值 left 设置成left:30px

撞墙检测

在 src\moduls\Snake.ts 中,增加撞墙的判断,并抛出异常throw new Error('蛇撞墙了!')

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
class Snake {
//表示蛇头的元素
head: HTMLElement

// 蛇的身体(包括蛇头)
bodies: HTMLCollection
//获取蛇的容器
element: HTMLElement

constructor() {
this.head = document.querySelector('#snake > div') as HTMLElement
this.element = document.getElementById('snake')!
this.bodies = this.element.getElementsByTagName('div')
}
//获取蛇的坐标(蛇头坐标)
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 >= 300) {
//进入判断说明蛇撞墙了
throw new Error('蛇撞墙了!')
}
this.head.style.left = value + 'px'
}
set Y(value: number) {
//如果新值和旧值相同,则不修改
if (this.Y === value) {
return
}
//Y的值的合法范围0-290
if (value < 0 || value >= 300) {
//进入判断说明蛇撞墙了
throw new Error('蛇撞墙了!')
}
this.head.style.top = value + 'px'
}

// 蛇增加身体的方法
addBody() {
//向element中添加一个div
this.element.insertAdjacentHTML('beforeend', '<div></div>')
}
}
export default Snake

吃食检测

在 src\moduls\GameControl.ts 中,使用try { } catch (e) { }处理异常操作,添加checkEat方法检查是否吃到食物,
在判断是否吃到食物时一个 bug,蛇的 X、Y 坐标超过 50 之后,个位数会加 1 就变成了 51 61 71…,解决办法是四舍五入 Math.round(X / 10) * 10 === Math.round(this.food.X / 10) * 10 && Math.round(Y / 10) * 10 === Math.round(this.food.Y / 10) * 10

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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()

this.init()
}

//游戏得初始化方法,调用后游戏即开始
init() {
// 绑定键盘按键按下的事件
document.addEventListener('keydown', this.keydownHandler.bind(this))

// 调用run方法,使蛇移动
this.run()
}
//创建一个键盘按下的响应函数
keydownHandler(event: KeyboardEvent) {
//需要检查event.key的值是否合法(用户是否按了正确的按键)

// 修改direction 属性
this.direction = event.key
}

/*
Chrome IE
ArrowUp Up
ArrowDown Down
ArrowLeft Left
ArrowRight Right
*/
// 创建一个控制蛇移动的方法
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':
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和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 (
//此处有个bug,蛇的X、Y坐标超过50之后,个位数会加1 就变成了51 61 71...,解决办法是四舍五入
Math.round(X / 10) * 10 === Math.round(this.food.X / 10) * 10 &&
Math.round(Y / 10) * 10 === Math.round(this.food.Y / 10) * 10
) {
console.log('吃到食物')
// 食物的位置进行重置
this.food.change()
//分数增加
this.scorePanel.addScore()
//蛇要增加一节
this.snake.addBody()
}
}
}

export default GameControl

小蛇移动

在 src\moduls\Snake.ts 中,添加moveBody蛇移动的方法,逻辑是将后边的身体设置成前一个身体位置,头不设置。 checkHeadBody检测是否撞到自己

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class Snake {
//表示蛇头的元素
head: HTMLElement;

// 蛇的身体(包括蛇头)
bodies: HTMLCollection;
//获取蛇的容器
element: HTMLElement;

constructor() {
this.head = document.querySelector('#snake > div') as HTMLElement;
this.element = document.getElementById('snake')!;
this.bodies = this.element.getElementsByTagName('div');
}
//获取蛇的坐标(蛇头坐标)
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 >= 300) {
//进入判断说明蛇撞墙了
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: number) {
//如果新值和旧值相同,则不修改
if (this.Y === value) {
return;
}
//Y的值的合法范围0-290
if (value < 0 || value >= 300) {
//进入判断说明蛇撞墙了
throw new Error('蛇撞墙了!');
}

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() {
//向element中添加一个div
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;

最后就大功告成了!