整体框架

逻辑框架图

实现蛇的头部

如何画蛇?

  • 本质上蛇可以看做一堆各自组成的序列。

新建Cell.js,用于表示一个蛇的方块。

1
2
3
4
5
6
7
8
9
export class Cell {
constructor(r, c) {
this.r = r;
this.c = c;
// 转换为 canvas 的坐标
this.x = c + 0.5;
this.y = r + 0.5;
}
}

新建Snake.js对象

cells数组就用于存放蛇的“身体”。

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


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

// 取出基本的id
this.id = info.id;
this.color = info.color;
this.gamemap = gamemap; // 方便调用函数和参数

//存放蛇的身体;
this.cells = [new Cell(info.r, info.c)];

}

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
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),
}

实现蛇的移动

蛇什么时候可以动?

  • 同时获取两个人/两个机器的合法操作后才能动。

如何移动?

  • 除了头和尾,中间不懂。在蛇的长度增加时,尾也不用动,只需移动头。

GameMap.js中,对蛇是否可以进行移动进行判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
check_ready() { // 判断两条蛇是否准备下一回合了
for (const snake of this.snakes) {
if (snake.status !== "idle") return false;
if (snake.direction === -1) return false;
}
return true;
}

next_step() {
for (const snake of this.snake) {
snake.next_step();
}
}

update() {
this.update_size();
if (this.check_ready()) {
this.next_step();
}
this.render();
}

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

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

this.id = info.id;
this.color = info.color;
this.gamemap = gamemap;

this.cells = [new Cell(info.r, info.c)]; // 存放蛇的身体,cells[0]存放蛇头
this.next_cell = null; // 下一步的目标位置

this.speed = 5; // 蛇每秒走5个格子
this.direction = -1; // -1表示没有指令,0、1、2、3表示上右下左
this.status = "idle"; // idle表示静止,move表示正在移动,die表示死亡

this.dr = [-1, 0, 1, 0]; // 4个方向行的偏移量
this.dc = [0, 1, 0, -1]; // 4个方向列的偏移量

this.step = 0; // 表示回合数
this.eps = 1e-2; // 允许的误差

}


start() {

}

set_direction(d) {
this.direction = d;
}

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]));
}

}

update_move() {
const dx = this.next_cell.x - this.cells[0].x;
const dy = this.next_cell.y - this.cells[0].y;
const distance = Math.sqrt(dx * dx + dy * dy);



if (distance < this.eps) { // 走到目标点了
this.cells[0] = this.next_cell; // 添加一个新蛇头
this.next_cell = null;
this.status = "idle"; // 走完了,停下来

} else {
const move_distance = this.speed * this.timedelta / 1000;
this.cells[0].x += move_distance * dx / distance;
this.cells[0].y += move_distance * dy / distance;
}
}

update() { // 每一帧执行一次
if (this.status === '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 / 2, 0, Math.PI * 2);
ctx.fill();
}
}
}

利用键盘控制移动

首先修改GameMap.vue,令canvas能够获取键盘事件

1
<canvas ref="canvas" tabindex="0"></canvas>

Snake.js中加入一个辅助函数,用于修改蛇的前进方向。

1
2
3
set_direction(d) {
this.direction = d;
}

GameMap.js中添加监听事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add_listening_events() {
this.ctx.canvas.focus();

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);
});
}

蛇尾状态更新

需要一个函数判断蛇的长度是否增加。如果蛇的长度增加,则蛇尾不用更新。

1
2
3
4
5
check_tail_increasing() {
if (step <= 10) return true;
if (step % 3 === 1) return true;
return false;
}

怎么更新蛇尾呢?跟蛇头几乎一模一样。不过在蛇完成当前移动后,更新蛇尾多出了一步,那就是删除掉当前的蛇尾。

这块先别管,首先pop是需要的,因为长度不会增加,蛇头是新增的,所以肯定需要删除一个。但是下面更新蛇尾的操作或许是鸡肋的,并未发现任何逻辑意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (distance < this.eps) {  // 走到目标点了
this.cells[0] = this.next_cell; // 添加一个新蛇头
this.next_cell = null;
this.status = "idle"; // 走完了,停下来

if (!this.check_tail_increasing()) { // 蛇不变长。
this.cells.pop();
}

} else {
const move_distance = this.speed * this.timedelta / 1000;
this.cells[0].x += move_distance * dx / distance;
this.cells[0].y += move_distance * dy / distance;

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_distance * tail_dx / distance;
tail.y += move_distance * tail_dy / distance;
}
}

美化蛇

蛇的身体

此时的蛇是一堆圆连在一起,很难看。我们可以适当缩小圆的大小,并在圆和圆之间上覆盖一个矩形,这样就能使蛇组成一个类似于操场的形状,相对比要好看多了。

修改Snake.jsrender函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.8, 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) {
ctx.fillRect((a.x - 0.4) * L, Math.min(a.y, b.y) * L, L * 0.8, Math.abs(a.y - b.y) * L);
} else {
ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - 0.4) * L, Math.abs(a.x - b.x) * L, L * 0.8);
}
}
}

蛇的眼睛

利用canvas给蛇头上画两个黑色的小圆充当眼睛。眼睛根据蛇移动的方向,和蛇头的偏移量均不同,需要提前打个表。

修改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
this.eye_direction = 0;
if (this.id === 1) this.eye_direction = 2;

this.eye_dx = [
[-1, 1];
[1, 1];
[1, -1];
[-1, -1];
];
this.eye_dy = [
[-1, -1];
[-1, 1];
[1, 1];
[1, -1];
];


next_step() {
this.eye_direction = d;

}

render() {
ctx.fillStyle = "black";
for (let i = 0; i < 2; i++) {
const eye_x = (this.cells[0].x + this.eye_dx[this.eye_direction][i] * 0.15) * L;
const eye_y = (this.cells[0].y + this.eye_dy[this.eye_direction][i] * 0.15) * L;

ctx.beginPath();
ctx.arc(eye_x, eye_y, L * 0.05, 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
check_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.js中修改

1
2
3
4
5
6
7
8
9
10
11
next_step() {
if (!this.gamemap.check_valid(this.next_cell)) {
this.status = "die";
}
}

render() {
if (this.status === "die") {
ctx.fillStyle = "white";
}
}

最终效果

image-20221220225338275