目录
PS:整篇文章全是实现前端的工作,如果大家不了解vue3,建议补一下前置知识~~

实现导航栏的组件我们可以在 bootstrap 中获得
Bootstrap v5 中文文档 · Bootstrap 是全球最受欢迎的 HTML、CSS 和 JS 前端工具库。 | Bootstrap 中文网 (bootcss.com)
在componets 中创建组件 NavBar.vue
在template标签中导入模板
- <nav class="navbar navbar-expand-lg navbar-light bg-light">
- <div class="container-fluid">
- <a class="navbar-brand" href="#">Navbar w/ texta>
- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
- <span class="navbar-toggler-icon">span>
- button>
- <div class="collapse navbar-collapse" id="navbarText">
- <ul class="navbar-nav me-auto mb-2 mb-lg-0">
- <li class="nav-item">
- <a class="nav-link active" aria-current="page" href="#">Homea>
- li>
- <li class="nav-item">
- <a class="nav-link" href="#">Featuresa>
- li>
- <li class="nav-item">
- <a class="nav-link" href="#">Pricinga>
- li>
- ul>
- <span class="navbar-text">
- Navbar text with an inline element
- span>
- div>
- div>
- nav>
App.vue中添加 NavBar 组件(App.vue 就是我们的首页页面)
- <template>
- <NavBar>NavBar>
- <router-view>router-view>
- template>
-
- <script>
-
- import NavBar from "@/components/NavBar.vue";
- import "bootstrap/dist/css/bootstrap.min.css";
- import "bootstrap/dist/js/bootstrap"
-
- export default {
- components:{
- NavBar
- }
- }
- script>
-
- <style>
- body {
- background-image: url("./assets/images/background.png");
- background-size: cover;
- }
- style>

安装依赖

成功之后的导航栏~

由于我们每个页面可能设计很多组件,因此我们每个页面建一个文件夹比较好~
需要用到 5 个界面
pk + record + ranklist + userbots + 404

这里我启动vue时发生了错误 :

解决方法:删掉node_modules,重新cnpm install一下就好了
每个页面的模板如下,不同的页面修改div里面的字体就可。
对战的修改对战、对局列表就修改为对局列表等。
- <template>
- <div>对战div>
- template>
-
- <script>
-
- script>
-
- <style scoped>
- style>
如何把地址和页面产生关联呢?
在router/index.js 中定义~~
- import PkIndexView from '../views/pk/PkIndexView'
- import RanklistIndexView from '../views/ranklist/RanklistIndexView'
- import RecordIndexView from '../views/record/RecordIndexView'
- import UserBotIndexView from '../views/user/bot/UserBotIndexView'
- import NotFound from '../views/error/NotFound'
-
- const routes = [
- {
- path: "/",
- name: "home",
- redirect: "/pk/"
- },
- {
- path: "/pk/",
- name: "pk_index",
- component: PkIndexView,
- },
- {
- path: "/record/",
- name: "record_index",
- component: RecordIndexView,
- },
- {
- path: "/ranklist/",
- name: "ranklist_index",
- component: RanklistIndexView,
- },
- {
- path: "/user/bot",
- name: "user_bot_index",
- component: UserBotIndexView,
- },
- {
- path: "/404/",
- name: "404",
- component: NotFound,
- },
- {
- path: "/:catchAll(.*)",
- redirect: "/404/",
- }
- ]
这里有个小细节:如果输入了根路径的网址,我们把它重定向到PK页面~~ 如果是一个不存在的网址,我们跳转到404(从上到下匹配,最后的全是404)
还需要在 NavBar 中更改跳转的路径:

如何实现 点击不同页面,但不刷新呢?
把 换成 <router-link> ,按照以下格式
class="nav-link" :to="{name: 'pk_index'}">对战

每个页面有一部分都是公共的,因此我们把这一部分抽取出来作为一个组件
在Commponents中新建ContentField.vue 组件
- <div class="container content-field">
- <div class="card">
- <div class="card-body">
- <slot>slot>
- div>
- div>
- div>
-
- <script>
-
- script>
-
- <style scoped>
- div.content-field{
- margin-top: 20px;
- }
- style>
-
在不同界面引入组件,比如 PK 界面:
- <ContentField>
- 对战
- ContentField>
-
-
- <script>
- import ContentField from '../../components/ContentField'
-
- export default {
- components: {
- ContentField
- }
- }
- script>
-
- <style scoped>
- style>
如何实现我们在哪个界面,那个界面上边的标题高亮呢?

我们需要添加三元运算符来判断 是否添加 active 属性:
- <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
- <div class="container">
- <router-link class="navbar-brand" :to="{name: 'home'}">King of Botsrouter-link>
- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
- <span class="navbar-toggler-icon">span>
- button>
- <div class="collapse navbar-collapse" id="navbarText">
- <ul class="navbar-nav me-auto mb-2 mb-lg-0">
- <li class="nav-item">
- <router-link :class="route_name == 'pk_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'pk_index'}">对战router-link>
- li>
- <li class="nav-item">
- <router-link :class="router_name == 'record_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'record_index'}">对局列表router-link>
- li>
- <li class="nav-item">
- <router-link :class="router_name == 'ranklist_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'ranklist_index'}">排行榜router-link>
- li>
- ul>
-
- <ul class="navbar-nav">
- <li class="nav-item dropdown">
- <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
- RedFlower
- a>
- <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
- <li>
- <router-link class="dropdown-item" :to="{name: 'user_bot_index'}">我的Botrouter-link>
- 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>
地图的特征:
1)大小:13*13
2)中心对称,边缘是墙体,左下角和右下角生成两条蛇,并且左下角和右下角联通
3)地图上会随机生成不同的障碍物

绘制游戏区域
在assets 目录下新建文件夹 scripts
新建 AcGameObjects.js (我们创建游戏的经典步骤 hh)
- const AC_GAME_OBJECTS = [];
-
- export class AcGameObject {
- contructor() {
- AcGame_Object.push(this);
- this.timedelta = 0; // 时间间隔每一帧
- this.has_called_start = false;
- }
-
- start() { // 只执行一次
-
- }
-
- update() { //每一帧执行一次
-
- }
-
- on_destroy() { //删除之前执行
-
- }
-
- distory() {
- 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)
实现地图类:GameMap.js
- 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 界面创建一个游戏区域,用来显示对战。
在 commponts 写一个组件: PlayGround.vue
- <div class="playground">
-
- div>
-
- <script>
-
-
- script>
-
- <style scoped>
- div.playground {
- width: 60vw;
- height: 70vh;
- background: lightblue;
- }
- style>
然后在 pk_index 中引入这个组件:
- <PlayGround/>
-
-
- <script>
- import PlayGround from '../../components/PlayGround.vue'
-
- export default {
- components: {
- PlayGround
- }
- }
- script>
-
- <style scoped>
- style>
因为在 pk 界面可能还包含记分板等不同的东西。
所以开一个新组件存放别的类型的组件 GameMap.vue
- <div class="gamemap">div>
-
- <script>
-
- script>
-
- <style scoped>
-
- div.gamemap {
- width: 100%;
- height: 100%;
- }
- style>
在 PlayGround.vue 中引入 GameMap.vue
- <div class="playground">
- <GameMap/>
- div>
-
- <script>
- import GameMap from "./GameMap.vue";
-
- export default {
- components: {
- GameMap,
- }
- }
- script>
在 GameMap.vue 中添加 canvas
- <div ref="parent" class="gamemap">
- <canvas ref="canvas">
- canvas>
- div>
-
- <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%;
- }
- style>
地图展示:

我们需要在不同大小的界面时保持我们的地图维持一个正方形,因此需要动态计算内部面积

在 GameMap.js 中修改:
- 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() {
- //画图
- this.ctx.fillStyle = 'green';
- this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
- }
- }
如何让我们的地图在区域居中?
在GameMap.vue中添加:
-
- div.gamemap {
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- }
绘制正方形作为我们的地图大致轮廓:

如何让我们的地图的每个小格子有辨识度呢?不同的格子有区分?
我们可以奇偶显示正方形区域内的小格子,用不同颜色即可:
在 GameMap.js 中修改, 完整代码如下 :
- 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);
- }
- }
- }
奇偶显示效果:

在 scripts 新建一个 wall.js
- 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);
- }
- }
修改 GameMap.js , 引入 Wall
- 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.wall = [];
- }
-
- creat_walls() {
- // 墙 true 无 false
- const g = [];
- for (let r = 0; r < this.cols; 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 r = 0; r < this.rows; r ++ ) {
- for (let c = 0; c < this.cols; c ++ ) {
- if (g[r][c]) {
- this.wall.push(new Wall(r, c, this));
- }
- }
- }
- }
-
- start() {
- this.creat_walls();
- }
-
- 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);
- }
- }
- }

修改 GameMap.js ,随机生成障碍物,同时禁止在左下角和右上角生成障碍物。
- 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.wall = [];
- }
-
- creat_walls() {
- // 墙 true 无 false
- const g = [];
- for (let r = 0; r < this.cols; 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;
- }
- }
-
- for (let r = 0; r < this.rows; r ++ ) {
- for (let c = 0; c < this.cols; c ++ ) {
- if (g[r][c]) {
- this.wall.push(new Wall(r, c, this));
- }
- }
- }
- }
-
- start() {
- this.creat_walls();
- }
-
- 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);
- }
- }
- }
为了使我们的游戏拥有可玩性,两条蛇存在互动,因此两条蛇必须是“可接触的”,这里我们采用flood fill 算法:
- 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 = 50;
- this.wall = [];
- }
-
- // flood fill算法
- // 参数 ,图 ,起点的x,y 重点的x, y
- 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;
- }
-
- creat_walls() {
- // 墙 true 无 false
- const g = [];
- for (let r = 0; r < this.cols; 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)); // 复制到JSON再转换回来
- 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.wall.push(new Wall(r, c, this));
- }
- }
- }
-
- return true;
- }
-
- start() {
-
- for (let i = 0; i < 1000; i ++ )
- if (this.creat_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);
- }
- }
- }

大功告成!
记得git push 维护~~
