SpringBoot学习笔记-创建菜单与游戏页面(下)

  1. 1. 地图优化改进
  2. 2. 绘制玩家的起始位置
  3. 3. 实现玩家移动
  4. 4. 优化蛇的身体效果
  5. 5. 碰撞检测实现
  6. 6. 绘制蛇的眼睛

本节内容为实现两名玩家即两条蛇的绘制与人工操作移动功能。

1. 地图优化改进

之前我们设计的地图尺寸是13×13,两名玩家的起点横纵坐标之和均为偶数,因此可能在同一时刻走到同一个格子上,为了避免这种情况,可以将地图改为13×14的大小(即将 GameMap.js 中的 this.cols 改为14),这样两名玩家就不会在同一时刻走到同一个格子上。这样修改完之后就不能用轴对称了,需要改为中心对称:

1
2
3
4
5
6
7
8
9
10
11
// 添加地图内部的随机障碍物,需要有对称性因此枚举一半即可,另一半对称生成
for (let i = 0; i < this.inner_walls_count / 2; i++) {
for (let j = 0; j < 10000; j++) {
let r = parseInt(Math.random() * this.rows);
let c = parseInt(Math.random() * this.cols);
if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue;
if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) continue; // 判断是否覆盖到出生地
g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;
break;
}
}

2. 绘制玩家的起始位置

刚开始玩家占一个格子,我们可以规定一下前七步的每一步将蛇的长度加一,之后改为每三步长度加一。每条蛇其实就是一堆格子的序列,我们可以将一个格子定义成一个 Cell 对象,在 scripts 目录下创建 Cell.js 记录格子的坐标。

我们在每个格子中绘制的是一个圆,若格子的左上角坐标为 (x, y) 则圆的圆心坐标应该是 (x + 0.5, y + 0.5)Cell.js 如下:

1
2
3
4
5
6
7
8
export class Cell {
constructor(r, c) {
this.r = r;
this.c = c;
this.x = c + 0.5;
this.y = r + 0.5;
}
}

此外每条蛇也可以定义成一个对象 Snake.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
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
constructor(info, gamemap) {
super();

this.id = info.id; // 每条蛇有唯一的id进行区分
this.color = info.color; // 颜色
this.gamemap = gamemap;

this.cells = [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头
}

start() {}

update() {
this.render();
}

render() {
const L = this.gamemap.L;
const ctx = this.gamemap.ctx;
ctx.fillStyle = this.color;
for (const cell of this.cells) {
ctx.beginPath();
ctx.arc(cell.x * L, cell.y * L, L / 2, 0, Math.PI * 2); // 半径为半个单元格
ctx.fill();
}
}
}

然后我们在 GameMap.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
import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";

export class GameMap extends AcGameObject {
constructor(ctx, parent) { // ctx表示画布,parent表示画布的父元素
super();

this.ctx = ctx;
this.parent = parent;
this.L = 0; // 一个单位的绝对长度
this.rows = 13; // 地图的行数
this.cols = 14; // 地图的列数

this.inner_walls_count = 20; // 地图内部的随机障碍物数量,需要是偶数
this.walls = []; // 所有的障碍物

this.snakes = [
new Snake({ id: 0, color: "#4876EC", r: this.rows - 2, c: 1 }, this),
new Snake({ id: 1, color: "#F94848", r: 1, c: this.cols - 2 }, this),
];
}

check_connectivity(g, sx, sy, tx, ty) { // 用flood fill算法判断两名玩家是否连通
...
}

create_walls() {
...
}

start() {
...
}

update_size() { // 每一帧更新地图大小
...
}

update() {
...
}

render() {
...
}
}

3. 实现玩家移动

为了实现蛇移动的连续性,我们不对每个格子进行更新,只更新头部和尾部,头部创建一个新的点往前动,尾部直接往前动。首先在 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
40
41
42
43
44
45
46
47
48
49
50
51
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
constructor(info, gamemap) {
super();

this.id = info.id; // 每条蛇有唯一的id进行区分
this.color = info.color; // 颜色
this.gamemap = gamemap;

this.cells = [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头

this.speed = 2; // 蛇每秒走2个格子
this.direction = -1; // 下一步移动的指令,-1表示没有指令,0、1、2、3分别表示上、右、下、左
this.status = "idle"; // 蛇的状态,idle表示静止,move表示正在移动,die表示死亡
this.next_cell = null; // 下一步的目标位置
this.step = 0; // 回合数

this.dr = [-1, 0, 1, 0];
this.dc = [0, 1, 0, -1];
}

start() {}

next_step() { // 将蛇的状态变为走下一步
const d = this.direction;
this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]);
this.direction = -1; // 复原
this.status = "move";
this.step++;
}

set_direction(d) { // 由于未来不一定只会从键盘获取输入,因此实现一个接口修改direction
this.direction = d;
}

update_move() {
}

update() {
if (this.status === "move") {
this.update_move();
}
this.render();
}

render() {
...
}
}

由于游戏是回合制的,因此移动的判定条件应该是获取到了两名玩家的指示后才能移动一次,且该指令既可以由键盘输入也可以由 AI 代码输入,判定两条蛇是否准备好执行下一步不能各自判断,需要由上层也就是 GameMap 判定,判定条件是两条蛇都处于静止状态且都已经获取到了下一步指令:

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
import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";

export class GameMap extends AcGameObject {
constructor(ctx, parent) { // ctx表示画布,parent表示画布的父元素
...
}

check_connectivity(g, sx, sy, tx, ty) { // 用flood fill算法判断两名玩家是否连通
...
}

create_walls() {
...
}

start() {
...
}

update_size() { // 每一帧更新地图大小
...
}

check_ready() { // 判断两条蛇是否都准备好下一回合了
for (const snake of this.snakes) {
if (snake.status !== 'idle' || snake.direction === -1) return false;
}
return true;
}

next_step() { // 让两条蛇进入下一回合
for (const snake of this.snakes) {
snake.next_step();
}
}

update() {
this.update_size();

if (this.check_ready()) {
this.next_step();
}

this.render();
}

render() {
...
}
}

现在我们只能从前端获得用户的操作,即获取用户的键盘输入。为了能够让 Canvas 获取键盘输入,需要添加一个 tabindex 属性,在 GameMap.vue 中进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div ref="parent" class="gamemap">
<canvas ref="canvas" tabindex="0"></canvas>
</div>
</template>

<script>
...
</script>

<style scoped>
...
</style>

这样我们就能够在 GameMap.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
import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";

export class GameMap extends AcGameObject {
...

add_listening_events() {
this.ctx.canvas.focus(); // 使Canvas聚焦

const [snake0, snake1] = this.snakes;
this.ctx.canvas.addEventListener("keydown", e => {
if (e.key === "w") snake0.set_direction(0);
else if (e.key === "d") snake0.set_direction(1);
else if (e.key === "s") snake0.set_direction(2);
else if (e.key === "a") snake0.set_direction(3);
else if (e.key === "ArrowUp") snake1.set_direction(0);
else if (e.key === "ArrowRight") snake1.set_direction(1);
else if (e.key === "ArrowDown") snake1.set_direction(2);
else if (e.key === "ArrowLeft") snake1.set_direction(3);
});
}

start() {
for (let i = 0; i < 10000; i++) { // 暴力枚举直至生成合法的地图
if (this.create_walls())
break;
}
this.add_listening_events();
}

...
}

现在我们即可在 Snake.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
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
constructor(info, gamemap) {
super();

this.id = info.id; // 每条蛇有唯一的id进行区分
this.color = info.color; // 颜色
this.gamemap = gamemap;

this.cells = [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头

this.speed = 2; // 蛇每秒走2个格子
this.direction = -1; // 下一步移动的指令,-1表示没有指令,0、1、2、3分别表示上、右、下、左
this.status = "idle"; // 蛇的状态,idle表示静止,move表示正在移动,die表示死亡
this.next_cell = null; // 下一步的目标位置
this.step = 0; // 回合数

this.dr = [-1, 0, 1, 0];
this.dc = [0, 1, 0, -1];

this.eps = 0.01; // 误差
}

start() {}

next_step() { // 将蛇的状态变为走下一步
const d = this.direction;
this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]);
this.direction = -1; // 复原
this.status = "move";
this.step++;

const k = this.cells.length;
for (let i = k; i > 0; i--) { // 将所有节点向后移动一位,因为要在头节点前面插入新的头节点
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); // 注意要深层复制一份,还有一个细节是JS的数组越界不会出错
}
}

set_direction(d) { // 由于未来不一定只会从键盘获取输入,因此实现一个接口修改direction
this.direction = d;
}

update_move() { // 将头节点cells[0]向目标节点next_cell移动
const dx = this.next_cell.x - this.cells[0].x; // 在x方向上与目的地的偏移量
const dy = this.next_cell.y - this.cells[0].y; // 在y方向上与目的地的偏移量
const distance = Math.sqrt(dx * dx + dy * dy); // 与目的地的距离
if (distance < this.eps) { // 已经走到目标点
this.status = "idle"; // 状态变为静止
this.cells[0] = this.next_cell; // 将头部更新为目标点
this.next_cell = null;
} else {
const move_length = this.speed * this.timedelta / 1000; // 每一帧移动的距离
const cos_theta = dx / distance; // cos值
const sin_theta = dy / distance; // sin值
this.cells[0].x += move_length * cos_theta;
this.cells[0].y += move_length * sin_theta;
}
}

update() {
if (this.status === "move") { // 只有移动状态才执行update_move函数
this.update_move();
}
this.render();
}

render() {
...
}
}

接着我们还需要实现蛇尾的移动,如果蛇的长度增加了一个单位,那么尾部不用动即可,否则尾部需要向前一个节点移动,且当移动完成后需要将尾部节点对象删去:

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
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
...

check_tail_increasing() { // 检测当前回合蛇的长度是否增加
if (this.step <= 7) return true; // 前7回合每一回合长度都增加
if (this.step % 3 === 1) return true; // 之后每3回合增加一次长度
return false;
}

update_move() { // 将头节点cells[0]向目标节点next_cell移动
const dx = this.next_cell.x - this.cells[0].x; // 在x方向上与目的地的偏移量
const dy = this.next_cell.y - this.cells[0].y; // 在y方向上与目的地的偏移量
const distance = Math.sqrt(dx * dx + dy * dy); // 与目的地的距离
if (distance < this.eps) { // 已经走到目标点
this.status = "idle"; // 状态变为静止
this.cells[0] = this.next_cell; // 将头部更新为目标点
this.next_cell = null;

if (!this.check_tail_increasing()) { // 尾部没有变长则移动完成后要删去尾部
this.cells.pop();
}
} else {
const move_length = this.speed * this.timedelta / 1000; // 每一帧移动的距离
const cos_theta = dx / distance; // cos值
const sin_theta = dy / distance; // sin值
this.cells[0].x += move_length * cos_theta;
this.cells[0].y += move_length * sin_theta;

if (!this.check_tail_increasing()) {
const k = this.cells.length;
const tail = this.cells[k - 1], tail_target = this.cells[k - 2];
const tail_dx = tail_target.x - tail.x;
const tail_dy = tail_target.y - tail.y;
tail.x += move_length * tail_dx / distance; // 此处就不分开计算cos和sin了
tail.y += move_length * tail_dy / distance;
}
}
}

update() {
if (this.status === "move") { // 只有移动状态才执行update_move函数
this.update_move();
}
this.render();
}

...
}

4. 优化蛇的身体效果

现在我们蛇的身体还是分开的若干个圆球,没有连续感。我们可以在两个相邻的圆球中间绘制一个矩形覆盖一遍即可。然后我们这边再做个小优化,将蛇的半径缩小一点,不然贴在一起时就会融合在一起不好看:

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
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
constructor(info, gamemap) {
...

this.radius = 0.4; // 蛇中每个节点的半径

...
}

...

render() {
const L = this.gamemap.L;
const ctx = this.gamemap.ctx;
ctx.fillStyle = this.color;
for (const cell of this.cells) {
ctx.beginPath();
ctx.arc(cell.x * L, cell.y * L, L * this.radius, 0, Math.PI * 2);
ctx.fill();
}

// 将相邻的两个球连在一起
for (let i = 1; i < this.cells.length; i++) {
const a = this.cells[i - 1], b = this.cells[i];
if (Math.abs(a.x - b.x) < this.eps && Math.abs(a.y - b.y) < this.eps)
continue;
if (Math.abs(a.x - b.x) < this.eps) { // 上下排列,即x相同,左上角的点的y值为两者的最小值,因为越往上y越小
ctx.fillRect((a.x - this.radius) * L, Math.min(a.y, b.y) * L, 2 * this.radius * L, Math.abs(a.y - b.y) * L);
} else { // 左右排列,画法同理
ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - this.radius) * L, Math.abs(a.x - b.x) * L, 2 * this.radius * L);
}
}
}
}

5. 碰撞检测实现

我们只要在每一轮判断一下玩家下一步移动的目标格子是不是合法的即可,如果不是两条蛇的身体或障碍物说明是合法的,同理需要在 GameMap 中判断而不能由玩家自己判断,这边需要注意如果在追蛇尾的话需要判断蛇尾是否有移动,如果没有移动则不合法:

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
import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";

export class GameMap extends AcGameObject {
...

check_next_valid(cell) { // 检测目标格子是否合法
for (const wall of this.walls) { // 枚举障碍物
if (wall.r == cell.r && wall.c == cell.c)
return false;
}

for (const snake of this.snakes) { // 枚举蛇的身体
let k = snake.cells.length;
if (!snake.check_tail_increasing()) { // 蛇尾会前进时不检测碰撞蛇尾
k--;
}
for (let i = 0; i < k; i++) {
if (snake.cells[i].r === cell.r && snake.cells[i].c === cell.c)
return false;
}
}

return true;
}

...
}

然后在 Snake 中每次进入下一步时进行合法性判断,如果不合法则将状态更新为 die,且颜色变白:

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
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
...

next_step() { // 将蛇的状态变为走下一步
const d = this.direction;
this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]);
this.direction = -1; // 复原
this.status = "move";
this.step++;

const k = this.cells.length;
for (let i = k; i > 0; i--) { // 将所有节点向后移动一位,因为要在头节点前面插入新的头节点
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); // 注意要深层复制一份,还有一个细节是JS的数组越界不会出错
}

if (!this.gamemap.check_next_valid(this.next_cell)) { // 下一步不合法
this.status = "die";
this.color = "white";
}
}

...
}

6. 绘制蛇的眼睛

绘制眼睛时需要考虑蛇头的朝向,即上一步移动的方向,对于不同方向的眼睛偏移量打个表记录即可,完善后 Snake.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
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
constructor(info, gamemap) {
super();

this.id = info.id; // 每条蛇有唯一的id进行区分
this.color = info.color; // 颜色
this.gamemap = gamemap;

this.cells = [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头
this.radius = 0.4; // 蛇中每个节点的半径

this.speed = 2; // 蛇每秒走2个格子
this.direction = -1; // 下一步移动的指令,-1表示没有指令,0、1、2、3分别表示上、右、下、左
this.status = "idle"; // 蛇的状态,idle表示静止,move表示正在移动,die表示死亡
this.next_cell = null; // 下一步的目标位置
this.step = 0; // 回合数

this.dr = [-1, 0, 1, 0];
this.dc = [0, 1, 0, -1];

this.eps = 0.01; // 误差

this.eye_color = "black";
this.eye_radius = 0.05;
this.eye_direction = 0; // 蛇头朝向的方向,默认左下角的蛇朝上,右上角的蛇朝下
if (this.id === 1) this.eye_direction = 2;
this.eye_dx = [ // 四个方向蛇眼睛的偏移量,x是横轴
[-1, 1],
[1, 1],
[1, -1],
[-1, -1],
];
this.eye_dy = [
[-1, -1],
[-1, 1],
[1, 1],
[1, -1],
];
}

start() {}

next_step() { // 将蛇的状态变为走下一步
const d = this.direction;
this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]);
this.eye_direction = d; // 更新蛇头朝向
this.direction = -1; // 复原
this.status = "move";
this.step++;

const k = this.cells.length;
for (let i = k; i > 0; i--) { // 将所有节点向后移动一位,因为要在头节点前面插入新的头节点
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); // 注意要深层复制一份,还有一个细节是JS的数组越界不会出错
}

if (!this.gamemap.check_next_valid(this.next_cell)) { // 下一步不合法
this.status = "die";
this.color = "white";
}
}

set_direction(d) { // 由于未来不一定只会从键盘获取输入,因此实现一个接口修改direction
this.direction = d;
}

check_tail_increasing() { // 检测当前回合蛇的长度是否增加
if (this.step <= 7) return true; // 前7回合每一回合长度都增加
if (this.step % 3 === 1) return true; // 之后每3回合增加一次长度
return false;
}

update_move() { // 将头节点cells[0]向目标节点next_cell移动
const dx = this.next_cell.x - this.cells[0].x; // 在x方向上与目的地的偏移量
const dy = this.next_cell.y - this.cells[0].y; // 在y方向上与目的地的偏移量
const distance = Math.sqrt(dx * dx + dy * dy); // 与目的地的距离
if (distance < this.eps) { // 已经走到目标点
this.status = "idle"; // 状态变为静止
this.cells[0] = this.next_cell; // 将头部更新为目标点
this.next_cell = null;

if (!this.check_tail_increasing()) { // 尾部没有变长则移动完成后要删去尾部
this.cells.pop();
}
} else {
const move_length = this.speed * this.timedelta / 1000; // 每一帧移动的距离
const cos_theta = dx / distance; // cos值
const sin_theta = dy / distance; // sin值
this.cells[0].x += move_length * cos_theta;
this.cells[0].y += move_length * sin_theta;

if (!this.check_tail_increasing()) {
const k = this.cells.length;
const tail = this.cells[k - 1], tail_target = this.cells[k - 2];
const tail_dx = tail_target.x - tail.x;
const tail_dy = tail_target.y - tail.y;
tail.x += move_length * tail_dx / distance; // 此处就不分开计算cos和sin了
tail.y += move_length * tail_dy / distance;
}
}
}

update() {
if (this.status === "move") { // 只有移动状态才执行update_move函数
this.update_move();
}
this.render();
}

render() {
const L = this.gamemap.L;
const ctx = this.gamemap.ctx;
ctx.fillStyle = this.color;
for (const cell of this.cells) {
ctx.beginPath();
ctx.arc(cell.x * L, cell.y * L, L * this.radius, 0, Math.PI * 2);
ctx.fill();
}

// 将相邻的两个球连在一起
for (let i = 1; i < this.cells.length; i++) {
const a = this.cells[i - 1], b = this.cells[i];
if (Math.abs(a.x - b.x) < this.eps && Math.abs(a.y - b.y) < this.eps)
continue;
if (Math.abs(a.x - b.x) < this.eps) { // 上下排列,即x相同,左上角的点的y值为两者的最小值,因为越往上y越小
ctx.fillRect((a.x - this.radius) * L, Math.min(a.y, b.y) * L, 2 * this.radius * L, Math.abs(a.y - b.y) * L);
} else { // 左右排列,画法同理
ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - this.radius) * L, Math.abs(a.x - b.x) * L, 2 * this.radius * L);
}
}

ctx.fillStyle = this.eye_color;
for (let i = 0; i < 2; i++) { // 绘制两个眼睛
const eye_x = this.cells[0].x + this.eye_dx[this.eye_direction][i] * this.radius * 0.4; // 偏移距离为半径长度的0.4倍
const eye_y = this.cells[0].y + this.eye_dy[this.eye_direction][i] * this.radius * 0.4;
ctx.beginPath();
ctx.arc(eye_x * L, eye_y * L, this.eye_radius * L, 0, Math.PI * 2);
ctx.fill();
}
}
}

我们可以将 next_step() 中的 this.direction = -1; 还有 update_move() 中的 this.status = "idle"; 注释掉,并在 update_move() 中的 if (distance < this.eps) 判断中添加一行 this.next_step();,这样就可以从回合制改为连续移动:

1
2
3
4
5
6
7
8
9
10
if (distance < this.eps) {  // 已经走到目标点
// this.status = "idle"; // 状态变为静止
this.cells[0] = this.next_cell; // 将头部更新为目标点
this.next_cell = null;
this.next_step();

if (!this.check_tail_increasing()) { // 尾部没有变长则移动完成后要删去尾部
this.cells.pop();
}
}