bootstrap地址
Bootstrap v5 中文文档 · Bootstrap 是全球最受欢迎的 HTML、CSS 和 JS 前端工具库。 | Bootstrap 中文网 (bootcss.com)
创建导航栏组件
web--src--components--NavBar.vue
<!-- html --> <template> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <!-- 刷新 --> <!-- <a class="navbar-brand" href="/">King Of Bots</a> --> <!-- 点击页面不刷新用router-link --> <router-link class="navbar-brand" :to="{ name: 'pk_index' }">King Of Bots</router-link> <div class="collapse navbar-collapse" id="navbarText"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li class="nav-item"> <!-- active高亮 --> <!-- <router-link class="nav-link active " :to="{ name: 'pk_index' }">对战</router-link> --> <!-- 选中的高亮 --> <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="route_name == 'record_index' ? 'nav-link active' : 'nav-link'" :to="{ name: 'record_index' }">对局列表</router-link> </li> <li class="nav-item"> <router-link :class="route_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="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> USERNAME </a> <ul class="dropdown-menu"> <li> <router-link class="dropdown-item" :to="{ name: 'user_bot_index' }">my bot</router-link> </li> <li><a class="dropdown-item" href="#">exit</a></li> </ul> </li> </ul> </div> </div> </nav> </template> <!-- js --> <script > // 实现选中的页面高亮 import { useRoute } from 'vue-router'; import { computed } from 'vue'; export default { setup() { const route = useRoute(); let route_name = computed(() => route.name) return { route_name } } } </script> <!-- css --> <!-- scoped 作用:写的css会加上一个随机字符串,使得样式不会影响组件以外的部分 --> <style scoped></style>
App.vue
<template> <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/background.png"); background-size: 100% 900%; } </style>
创建各页面组件
<template> <div>404</div> </template> <script> </script> <style scoped></style>
<template> <ContentField> 对战 </ContentField> </template> <script> import ContentField from "../../components/ContentField.vue" export default { components: { ContentField } } </script> <style scoped></style>
<template> <ContentField> RankList排行榜 </ContentField> </template> <script> import ContentField from "../../components/ContentField.vue" export default { components: { ContentField } } </script> <style scoped></style>
<template> <ContentField> 对局列表 </ContentField> </template> <script> import ContentField from "../../components/ContentField.vue" export default { components: { ContentField } } </script> <style scoped></style>
<template> <ContentField> 我的Bot </ContentField> </template> <script> import ContentField from "../../../components/ContentField.vue" export default { components: { ContentField } } </script> <style scoped></style>
router---组件
import { createRouter, createWebHistory } from 'vue-router' import NotFound from "../views/error/NotFound" 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" const routes = [ { path: "/pk/", name: "pk_index", component: PkIndexView }, { path: "/error/", name: "404", component: NotFound }, { path: "/record/", name: "record_index", component: RecordIndexView }, { path: "/ranklist/", name: "ranklist_index", component: RanklistIndexView }, { path: "/user/bot/", name: "user_bot_index", component: UserBotIndexView }, // 重定向到404 { path: "/:catchAll(.*)", redirect: "/error/" } ] const router = createRouter({ history: createWebHistory(), routes }) export default router
白板--组件
各页面白板构成一个组件components->ContentField.vue
<template> <div class="container content-field"> <div class="card"> <div class="card-body"> <!-- 渲染的东西放到 slot 中 --> <slot></slot> </div> </div> </div> </template> <script> </script> <style scoped> /* 20px的上边距 */ div.content-field { margin-top: 20px; } </style>
游戏对象类
scrips---->ACGameObject.js
const AC_GAME_OBJECTS = []; //存储所有运动对象 export class AcGameObject { constructor() { AC_GAME_OBJECTS.push(this); // 走的距离=速度*时间间隔 //当前帧执行的时刻距离上一帧执行时刻的间隔,浏览器每一帧时间间隔可能有误差不一样因此需要记录 this.timedelta = 0; // 是否执行过start函数 this.has_called_start = false; } start() { //创建时,只执行一次 } update() { // 除了第一帧外每一帧执行一次 } // 删除之前用到的回调函数 on_destroyed() { } destroyed() { this.on_destroyed(); 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; //上一次执行的时刻 // requestAnimationFrame(函数) 函数在浏览器下一次渲染之前执行一遍 // 迭代执行step // in是下标 of是值 const step = timestamp => { // 每次调用会传入当前时刻 for (let obj of AC_GAME_OBJECTS) { // 如果没执行start() if (!obj.has_called_start) { obj.has_called_start = true; obj.start(); } else { obj.timedelta = obj.timestamp - last_timestamp; obj.update(); } } last_timestamp = timestamp; // 下一帧执行step requestAnimationFrame(step) } requestAnimationFrame(step)
地图
动起来:60帧/s
地图对象类
// export import {} export default import // 游戏地图的对象 import { AcGameObject } from "./AcGameObject"; import { Wall } from "./Wall"; export class GameMap extends AcGameObject { // 构造函数的两个参数, 画布,画布的父元素用于动态修改画布的长宽 constructor(ctx, parent) { // ctx表示画布,parent表示画布的父元素 // 先执行AcGameObject的构造函数 super(); this.ctx = ctx; this.parent = parent; this.L = 0; // 一个单位的绝对长度 this.rows = 13; // 地图的行数 this.cols = 13; // 地图的列数 this.inner_walls_count = 6; this.wall = []; } check_connectivity(sx, sy, tx, ty, g) { 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(x, y, tx, ty, g)) { return true; } } return false; } create_walls() { //new Wall(0, 0, this); // 墙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; } // 创建随机障碍物,数量为inner_walls_count for (let i = 0; i < this.inner_walls_count; 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; } } // 转换成JSON再转换回来 const copy_g = JSON.parse(JSON.stringify(g)); if (!this.check_connectivity(this.rows - 2, 1, 1, this.cols - 2, copy_g)) { 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.create_walls()) break; } } 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_even = "#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_even; } else { this.ctx.fillStyle = color_odd; } // 左上角左边,明确canvas坐标系 this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L); } } } }
游戏区域
components--->PlayGround.vue
<!-- 游戏区域 --> <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; margin: 40px auto; background-color: rgb(247, 248, 248); } </style>
地图组件
GameMap.vue
<template> <!-- ref映射关联 --> <div ref="parent" class="gamemap"> <!-- 画布 --> <canvas ref="canvas"></canvas> </div> </template> <script> import { ref, onMounted } from "vue"; import { GameMap } from "@/assets/scripts/GameMap"; export default { setup() { let parent = ref(null); let canvas = ref(null); //组件挂载完之后,创建GameMap对象 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>
实体墙
scrips----->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); } }
参考
项目实战——创建菜单与游戏页面(上)_游戏页面是如何实现的-CSDN博客
构建Cell.js对象
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 对象
注意需要从键盘输入的部分
import { AcGameObject } from "./AcGameObject"; import { Cell } from "./Cell"; export class Snake extends AcGameObject { constructor(info, gamemap) { // 继承AcGameObject的方法 super(); this.id = info.id; this.color = info.color; this.gamemap = gamemap; // 存放蛇的身体,cells[0]存放蛇头 this.cells = [new Cell(info.r, info.c)]; // 下一步的目标位置 this.next_cell = null; // 蛇每秒走5个格子 this.speed = 5; // -1表示没有指令,0,1,2,3表示上下左右 this.direction = -1; // idle表示静止,move表示正在移动,die表示死亡 this.status = "idle"; this.dr = [-1, 0, 1, 0]; this.dc = [0, 1, 0, -1]; this.step = 0; //表示回合数 //允许的误差 this.eps = 1e-2; //初始的状态 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] ] } start() { } set_direction(d) { //辅助函数用于控制方向 this.direction = d; } check_tail_increasing() { //检测当前回合蛇的的长度是否增加,蛇尾不动,蛇变长,蛇尾动蛇尾不变长 if (this.step <= 10) return true; if (this.step % 3 === 1) return true; return false; } next_step() { //将蛇的下一步变为走下一步 const d = this.direction; this.eye_direction = d; 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])); } if (!this.gamemap.check_valid(this.next_cell)) { //下一步操作撞了,蛇瞬间去世 this.status = "die"; } } update_move() { // 向右移动五格,蛇头的横坐标加上每帧移动的距离 // this.cells[0].x += this.speed * this.timedelta / 1000; //向上移动五格 // this.cells[0].y -= this.speed * this.timedelta / 1000; // 组合起来向斜右方移动 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'; //走完了,停下来 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; } } } 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; if (this.status === "die") { ctx.fillStyle = "white"; } 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); } } ctx.fillStyle = "black"; for (let i = 0; i < 2; i++) { const eye_dx = (this.cells[0].x + this.eye_dx[this.eye_direction][i] * 0.15) * L; const eye_dy = (this.cells[0].y + this.eye_dy[this.eye_direction][i] * 0.15) * L; ctx.beginPath(); ctx.arc(eye_dx, eye_dy, L * 0.05, 0, Math.PI * 2); ctx.fill(); } } }
优化
参考
构建过程
项目实战——创建菜单与游戏页面(下)_ctx.arc(eye_x,eye_y, l * 0.1,0,math.pi * 2);-CSDN博客