思维导图

项前端框架

实现导航栏

老生常谈了,没啥好说的。随便去Bootstrap找个类改改构成基础部分

之后依据<router-link>v-bind就能够实现基本的跳转。

组件NavBar.vue

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
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<routerLink :class="route_name == 'home' ? 'nav-link active' : 'nav-link'" :to="{name: 'home'}">SnakeGPT</routerLink>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<routerLink :class="route_name == 'pk_index' ? 'nav-link active' : 'nav-link'" aria-current="page" :to="{name: 'pk_index'}">对战</routerLink>
</li>
<li class="nav-item">
<routerLink :class="route_name == 'record_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'record_index'}">对局列表</routerLink>
</li>
<li class="nav-item">
<routerLink :class="route_name == 'rank_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'rank_index'}">排行榜</routerLink>
</li>
</ul>

<ul class="navbar-nav ">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
沃斯尼蝶
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><routerLink class="dropdown-item" :to="{name: 'user_bot_index'}">我的蛇</routerLink></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="#">退出</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>

<script>
import { useRoute } from "vue-router";
import { computed } from "@vue/reactivity";

export default {
setup() {
const route = useRoute();
let route_name = computed(() => route.name);
return {
route_name
}
}
}

</script>

<style scoped>

</style>

router插件

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 { createRouter, createWebHistory } from 'vue-router'
import NotFoundView from '../views/error/NotFoundView.vue'
import PkIndexView from '../views/pk/PkIndexView.vue'
import RanklistIndexView from '../views/ranklist/RanklistIndexView.vue'
import RecordIndexView from '../views/record/RecordIndexView.vue'
import UserBotIndexView from '../views/user/bot/UserBotIndexView.vue'

const routes = [
{
path: "/",
name: "home",

},
{
path: "/pk/",
name: "pk_index",
component: PkIndexView,
},
{
path: "/ranklist/",
name: "rank_index",
component: RanklistIndexView,
},
{
path: "/record/",
name: "record_index",
component: RecordIndexView,
},
{
path: "/user/bot/",
name: "user_bot_index",
component: UserBotIndexView,
},
{
path: "/404/",
name: "404_index",
component: NotFoundView,
},
{
path: "/:catchAll(.*)",
redirect: "/404/",
}


]

const router = createRouter({
history: createWebHistory(),
routes
})

export default router

修改根

APP.vue中引入RouterNavBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<NavBar></NavBar>
<RouterView></RouterView>
</template>

<script>
// import $ from 'jquery';
// import { ref } from 'vue';
import NavBar from './components/NavBar.vue';
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap";

export default {
components: {
NavBar
}
}
</script>

实现地图

游戏引擎

写一个游戏引擎,每秒刷新60次。所有游戏中的物品都必须继承自该类。

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
const AC_GAME_OBJECTS = [];

export class AcGameObject {
constructor() {
AC_GAME_OBJECTS.push(this);
this.timedelta = 0;
this.has_called_start = false;
}

start() { // 只执行一次
}

update() { // 每一帧执行一次,除了第一帧之外

}

on_destroy() { // 删除之前执行

}

destroy() {
this.on_destroy();

for (let i in AC_GAME_OBJECTS) {
const obj = AC_GAME_OBJECTS[i];
if (obj === this) {
AC_GAME_OBJECTS.splice(i);
break;
}
}
}
}

let last_timestamp; // 上一次执行的时刻
const step = timestamp => {
for (let obj of AC_GAME_OBJECTS) {
if (!obj.has_called_start) {
obj.has_called_start = true;
obj.start();
} else {
obj.timedelta = timestamp - last_timestamp;
obj.update();
}
}

last_timestamp = timestamp;
requestAnimationFrame(step)
}

requestAnimationFrame(step)

实现地图类

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";

export class GameMap extends AcGameObject {
constructor(ctx, parent) {
super();

this.ctx = ctx;
this.parent = parent;
this.L = 0;

}

start() {

}

update() {
this.rander();
}

//渲染函数
render() {

}
}

绘制游戏区域

pk界面中创建一个游戏区域,用来显示对战。

在components中写一个组件:PlayGround.vue,并在PkIndexView引入该组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="playground">

</div>
</template>

<script>


</script>

<style scoped>
div.playground {
width: 60vw;
height: 70vh;
background: lightblue;
}
</style>

再写一个GameMap.vue组件,来渲染游戏界面,并在PlayGround.vue中引入。

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

<div class="gamemap" ref="parent">
<canvas ref="canvas">

</canvas>
</div>
</template>

<script>
import { GameMap} from '../assets/scripts/GameMap'
import { ref, onMounted } from 'vue'


export default {
setup() {
let parent = ref(null);
let canvas = ref(null);

onMounted(() => {
new GameMap(canvas.value.getContext('2d'), parent.value);
})
return {
parent,
canvas,
}
}
}
</script>

<style scoped>
div.gamemap {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>

绘制方格

  • GameMap分成13*13块方格,块与块之前实现深浅交接。

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

export class GameMap extends AcGameObject {
constructor(ctx, parent) {
super();

this.ctx = ctx;
this.parent = parent;
this.L = 0;

this.rows = 13;
this.cols = 13;
}

start() {

}

update_size() {
// 计算小正方形的边长
this.L = Math.min(this.parent.clientWidth / this.cols, this.parent.clientHeight / this.rows);
this.ctx.canvas.width = this.L * this.cols;
this.ctx.canvas.height = this.L * this.rows;
}

update() {
this.update_size();
this.render();
}

render() {
// 取颜色
const color_eve = "#AAD751", color_odd = "#A2D149";
// 染色
for (let r = 0; r < this.rows; r ++ )
for (let c = 0; c < this.cols; c ++ ) {
if ((r + c) % 2 == 0) {
this.ctx.fillStyle = color_eve;
} else {
this.ctx.fillStyle = color_odd;
}
//左上角左边,明确canvas坐标系
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
}
}
}

绘制墙和障碍物

  • GameMap的四周是墙,和普通的游戏方块区分开来
  • 地图中要生成随机障碍物。随机障碍物要满足以下条件:
    • 不能和起点重合(左下角和右上角)
    • 保证左下角和右上角连通

首先新建Wall.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
import { AcGameObject } from "./AcGameObject";

export class Wall extends AcGameObject {
constructor(r, c, gamemap) {
super();

this.r = r;
this.c = c;
this.gamemap = gamemap;
this.color = "#B37226";
}

update() {
this.render();
}

render() {
const L = this.gamemap.L;
const ctx = this.gamemap.ctx;

ctx.fillStyle = this.color;
ctx.fillRect(this.c * L, this.r * L, L, L);
}
}

利用奇偶性,可以实现块与块颜色不同。

利用n*m的布尔数组,可以把最外围的部分标记出来特殊处理(就是跳过)

利用dfs实现的连通块算法,即可实现判断连通性。

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

export class GameMap extends AcGameObject {
constructor(ctx, parent) {
super();

this.ctx = ctx;
this.parent = parent;
this.L = 0;

this.rows = 13;
this.cols = 13;

this.inner_walls_count = 10; //随机障碍物数量
this.walls = [];

}

check_connectivity(g, sx, sy, ex, ey) {
if (sx == ex && sy == ey) return true; //成功
g[sx][sy] = true;
let dx = [0, 0, 1, -1];
let dy = [1, -1, 0, 0];
for (let i = 0; i < 4; i++) {
let a = sx + dx[i], b = sy + dy[i];
if (!g[a][b] && this.check_connectivity(g, a, b, ex, ey)) {
return true;
}
}
return false;
}

create_walls() {
const g = [];
for (let i = 0; i < this.rows; i++) {
g[i] = [];
for (let j = 0; j < this.cols; j++) {
g[i][j] = false;
}
}

//四周是墙
for (let i = 0; i < this.rows; i++) {
g[i][0] = true;
g[i][this.cols - 1] = true;
}
for (let i = 0; i < this.cols; i++) {
g[0][i] = true;
g[this.rows - 1][i] = true;
}

//生成对称的随机障碍物
for (let i = 0; i < this.inner_walls_count; i++) {
for (let j = 0; j < 100000; j++) {
let r = parseInt(Math.random() * this.rows);
let c = parseInt(Math.random() * this.cols);
if (g[r][c] || g[c][r]) continue; //已经有墙了
if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) continue; //排除起点
g[r][c] = true;
g[c][r] = true;
break;
}
}

const copy_g = JSON.parse(JSON.stringify(g));
if (!this.check_connectivity(copy_g, this.rows - 2, 1, 1, this.cols - 2)) {
return false;
}

for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
if (g[i][j]) {
this.walls.push(new Wall(i, j, this));
}
}
}

return true;
}

start() {
for (let i = 0; i < 1000; i++) {
if (this.create_walls()) {
break;
}
}

}

update_size() {
// 计算小正方形的边长
this.L = parseInt(Math.min(this.parent.clientWidth / this.cols, this.parent.clientHeight / this.rows));
this.ctx.canvas.width = this.L * this.cols;
this.ctx.canvas.height = this.L * this.rows;
}

update() {
this.update_size();
this.render();
}

render() {
// 取颜色
const color_eve = "#AAD751", color_odd = "#A2D149";
// 染色
for (let r = 0; r < this.rows; r++)
for (let c = 0; c < this.cols; c++) {
if ((r + c) % 2 == 0) {
this.ctx.fillStyle = color_eve;
} else {
this.ctx.fillStyle = color_odd;
}
//左上角左边,明确canvas坐标系
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
}
}
}

最终效果

image-20221220145448624