1.整体框架
2. 实现导航栏
2.1 基础导航栏
在components
文件夹创建组件: NavBar.vue
在template
下 导入 bootstrap
模版
举一个栗子:
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar w/ text</a>
<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="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Pricing</a>
</li>
</ul>
<span class="navbar-text">
Navbar text with an inline element
</span>
</div>
</div>
</nav>
创建成功的 NavBar.vue
在App.vue
中添加NavBar
组件:
<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>
如果报错可能需要添加@popperjs/core
依赖 刷新一下
同时修改bootstrap模板中的字,改为自己需要的样式
成功的导航栏:
2.2 网址跳转
aims: 在地址栏输入不同的地址,需要跳到不同的界面
例如 :
http://localhost:8080/pk/
转到pk
界面http://localhost:8080/record/
转到对战记录
界面
2.2.1设计页面
需要用到 5 个界面
pk + record + ranklist + userbots + 404
2.2.2 创建文件夹 和 对应的页面
在views文件夹下创建:
每个页面的模板如下,不同的页面修改div
里面的字体就可。
对战的修改对战、对局列表就修改为对局列表等、
<template>
<div>对战</div>
</template>
<script>
</script>
<style scoped>
</style>
2.2.3 地址和页面产生关联
在 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/",
}
]
同时在NavBar
里修改herf
2.2.4 实现点击、图标不刷新
在NavBar
下修改:把<a>
换成 <router-link>
同时按照下面修改:
以下为完整过程:
2.3 实现每个页面的框
由于这几个页面都需要用到边框,所有我们可以把这个公共的部分作为一个组件
新建一个 ContentField.vue
组件
快捷键: div.container>div.card>div.card-body>
组件代码:
<template>
<div class="container content-field">
<div class="card">
<div class="card-body">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
</script>
<style scoped>
div.content-field{
margin-top: 20px;
}
</style>
组件内容:
创建好组件后、我们可以在不同的界面引入组件,例如在pk
界面:
<template>
<ContentField>
对战
</ContentField>
</template>
<script>
import ContentField from '../../components/ContentField'
export default {
components: {
ContentField
}
}
</script>
<style scoped>
</style>
这样就可以在不同的界面引入组件。
成功的界面:
2.4 页面聚焦
思想:
取得鼠标当前在哪个界面,修改 NavBar.vue
<script>
是取得当前页面,上面的<template>
使用 三元运算符来判断, 如果是就active
。
如下:
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<router-link class="navbar-brand" :to="{name: 'home'}">King of Bots</router-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'}">我的Bot</router-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>
3. 实现地图
3.1 地图特征:
-
大小:
13 * 13
-
地图中心对称,边缘是是墙,左下角和右下角生成两条蛇,且左下角和右下角连通
-
地图上会随机生成不同的障碍物。
3.2 绘制游戏区域
相当于自己造轮子
在 assets
目录下新建文件夹 命名为 scripts
新建一个游戏类 名字为 : AcGameObject.js
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() {
}
}
3.2.1 绘制游戏区域
在 pk
界面创建一个游戏区域,用来显示对战。
在 commponts
写一个组件: PlayGround.vue
<template>
<div class="playground">
</div>
</template>
<script>
</script>
<style scoped>
div.playground {
width: 60vw;
height: 70vh;
background: lightblue;
}
</style>
然后在 pk_index
中引入这个组件:
<template>
<PlayGround/>
</template>
<script>
import PlayGround from '../../components/PlayGround.vue'
export default {
components: {
PlayGround
}
}
</script>
<style scoped>
</style>
因为在 pk
界面可能还包含记分板等不同的东西。
所以开一个新组件存放别的类型的组件 GameMap.vue
<template>
<div class="gamemap"></div>
</template>
<script>
</script>
<style scoped>
div.gamemap {
width: 100%;
height: 100%;
}
</style>
在 PlayGround.vue
中引入 GameMap.vue
<template>
<div class="playground">
<GameMap/>
</div>
</template>
<script>
import GameMap from "./GameMap.vue";
export default {
components: {
GameMap,
}
}
</script>
在 GameMap.vue
中添加 canvas
<template>
<div ref="parent" class="gamemap">
<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%;
}
</style>
初步地图如下:
3.3 动态计算内部面积:
在 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
中添加
<style scoped>
div.gamemap {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>
绘制正方形:
3.4 奇偶显示正方形区域内的小格子:
在 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);
}
}
}
奇偶显示小格子效果如下:
3.5 设计墙
在 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);
}
}
}
3.6 生成地图
修改 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);
}
}
}
3.6.1 使两个块连通
使用 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);
}
}
}
最终效果:
小tips: 如何更换更换图标: 在public替换favicon.icon