这节课只涉及前端知识

bootsrap 是让程序员实现美工的效果
建议先不跟着敲代码,可以先写笔记
工欲善其事,必先利其器;

启动脚手架 ,powershell 中使用 vue ui 上节课实现前端最终的效果,如下 img

分析页面: 网站导航栏都一样,先实现导航栏组件 NavBar.vue;接在在 bootstrap 找到一个合适的导航栏模板前端代码;粘贴到标签中

导航栏都是一样的,不怎么改变,因此可以把导航栏提取出来,作为一个组件,复用。

创建一个组件 NavBar

在 App.vue 中 import NavBar from ‘./components/NavBar.vue’

利用 bootsrap 来快速实现
https://v5.bootcss.com/docs/components/navbar/

img img 导航栏分析:

对应要写的各个页面,vue 文件,推荐在 views 文件夹创建对应的文件夹。每个页面包含三个标签 img img

router

知识点 1:写完每个页面,需要和地址 url 关联起来 ;会使用到 router 路由组件。
<router-view>会自动根据网址来变。定义的方式在 router/index.js 文件定义;

知识点 2:实现点击每个导航栏,填写<a href>标签就跳转相应页面;
但是页面会重新刷新,前后端分离为了不让页面刷新;在 NavBar.vue 页面使用router-link标签,代替<a href>
<router-link class=”navbar-brand” :to=”{ name: ‘home’ }”>King of Bots</router-link>

bootsrap 卡片

知识点 3:内容页面使用 card,在 bootstrap 框起来-

bootsrap 网址:https://v5.bootcss.com/docs/components/card/
在这里插入图片描述每个页面的内容都要使用 card 框起来。会发现这个框是公共的部分。因此单做一个组件ContentField.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<!-- 这可以当作公共内容,实现一个白框 -->
<div class="container">
<div class="card">
<div class="card-body">
<!-- 框内填充的内容写在slot里面 -->
<slot></slot>

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

<script>
</script>

<style>
div.content-field {

margin-top: 20px;
}
</style>

之后把每个页面使用这个组件就可以。 例如,NotFound 页面先引入组件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<ContentField>
404 Not Found
</ContentField>
</template>

<script>


import ContentField from '../../components/ContentField.vue'

export default {


components: {

ContentField
}
}
</script>

<style>
</style>

另外一个功能就是点击每个导航栏,显示聚焦效果,高亮;
在标签里面加 active;
点击那个页面,使得哪个栏高亮;
方法:
用到userRoute API; 在 NavBar 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {
useRoute } from "vue-router"
// 实时计算
import {
computed } from "vue"

export default {

setup() {

const route = useRoute();
//获得当前页面的名称
let route_name = computed(() => route.name)
return {

route_name
}
}
}

获取之后怎样使用? 需要在里面加:表达式,写法如下; 对战</router-link>

13*13 的地图,周围一圈都是墙,里面有些随机是墙。保证从左下角能够走到右上角。 生成合法地图,每次刷新都可以生成新的地图。

游戏里面怎移动的?
游戏动画的原理;
在这里插入图片描述假设每秒刷新 60 次,每秒画面渲染 60 次,60 帧;每一帧的位置在哪里,放 60 张图片就会显示物体在移动;
需要写一个基类(工具),实现公共的功能;移动的物体。
每秒刷新 60 次;每个物体都需要 60 次;
代码脚本放到 assest 文件、里面
每一帧都要渲染一次

img 移动的基类:

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

export class AcGameObject {

constructor() {

AC_GAME_OBJECTS.push(this);
// 移动的速度涉及到时间间隔,每一帧执行的额时间不一定是均匀的
this.timedelta = 0;
this.has_called_start = false;
}
start() {
//只执行一次

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

}
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)
}
// 这个函数会在下一帧刷新的时候,一般浏览器默认刷新60次,传入一个函数
requestAnimationFrame(step)

墙或者障碍物每一帧都需要渲染一遍; 游戏里面每一个组件都是一个类; 创建地图类:img

游戏的地图随着浏览器界面的变化,会自动发生界面变化;所以距离在写的时候不要使用绝对距离,而要使用相对距离。
整个地图单位是 13x13

在 PK 界面,内容框没有用,直接删区,需要把地图界面显示到这个页面;
可以写一个组件,让游戏地图显示在这个区域;
PlayGroud.vue

playground

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
<template>
<div class="playground">
<GameMap />


</div>
</template>
<script>
import GameMap from "./GameMap.vue";
export default {

components: {

GameMap,
}
}



</script>

<style scoped>
div.playground {

width: 60vw;
height: 70vh;
/* 删掉背景 */
/* background: lightblue; */
/* 距离上下左右四个方向的距离 */
margin: 40px auto;
}
</style>

需要在 PkIndexView.vue 页面把 playground 引入 img

之后对局页面会显示方框,方框会随着浏览器大小自动变化在这里插入图片描述使其变为正方形;

创建一个板子 GameMap.vue 首先需要在 PlayGround.vue 引入 GameMap.vue plyground img GameMap.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
<template>
<div ref="parent" class="gamemap">
<!-- 画布 -->
<canvas ref="cannas"></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>

MDN 前端资料网站

接下来全程写 GameMap.js 文件~

img需要求被这个方框包裹的最大面积的方块; img

数组坐标系 ,往下是行,往右是列 canva 坐标系颠倒了 横着的是行 x 竖着的是列 y

课后一定要抄代,而不是复制代码,一个字母一个字母。

这节课实现结果如下:

img 代码保存一下 打开 kob 这个项目文件夹,打开 git bash

$ git status
$ git add .
$ git commit -m “实现了导航栏+对战页面的地图和障碍物”
$ git push

学会浏览器调错; 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
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
144
145
146
147
148
149
150
151
152
153
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 = 20;
this.walls = [];
}
// flood_fill算法 判断是否连通
check_connectivity(g, sx, sy, tx, ty) {

if (sx == tx && sy == ty) {

return true;
}
g[sx][sy] = true;
let dx = [-1, 0, 1, 0], dy = [0, 1, 0, -1];
for (let i = 0; i < 4; i++) {

let x = sx + dx[i], y = sy + dy[i];
if (!g[x][y] && this.check_connectivity(g, x, y, tx, ty)) {

return true;
}
}

return false;
}
create_walls() {

// new Wall(0, 0, this);
const g = [];
for (let r = 0; r < this.rows; r++) {

g[r] = [];
for (let c = 0; c < this.cols; c++) {

g[r][c] = false;
}
}

// 给四周加上障碍物
for (let r = 0; r < this.rows; r++) {

g[r][0] = g[r][this.cols - 1] = true;
}
// 上下
for (let c = 0; c < this.cols; c++) {

g[0][c] = g[this.rows - 1][c] = true;
}
// 创建随机障碍物
for (let i = 0; i < this.inner_walls_count / 2; i++) {

for (let j = 0; j < 1000; 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] = 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 r = 0; r < this.rows; r++) {

for (let c = 0; c < this.cols; c++) {

if (g[r][c]) {

this.walls.push(new Wall(r, c, 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() {

// this.ctx.fillStyle ='green';
// this.ctx.fillRect(0,0,this.ctx.canvas.width,this.ctx.vanvas.height);
const color_event = "#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_event;
} else {

this.ctx.fillStyle = color_odd;
}
// 画矩形,注意这里与与正常坐标系的坐标是相反的
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L)
}
}


}

}

这节课效果如下,地图和蛇已经完成 img