SpringBoot项目——vue 实现游戏页面
回顾:
文章目录
vue 实现前端页面——Web
呈现效果如下:
一、导航栏功能+PK地图的实现
- 导航栏组件及所路由跳转各组件代码如下:
// NavBar.vue
<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>
<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="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 active" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Lijiao
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><router-link class="dropdown-item" :to="{name: 'user_bot_index'}">我的Bot</router-link></li>
<li><a class="dropdown-item" href="#">退出</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
// 为了判断当前url在哪个页面
import {
useRoute } from 'vue-router'
import {
computed } from 'vue'
export default {
name: 'NavBar',
setup() {
const route = useRoute();
let route_name = computed(() => route.name);
return{
route_name,
}
},
}
</script>
<style scoped>
</style>
路由跳转到的组件PKIndexView
、RecordIndexView
、RanklistIndexView
、UserBotIndexView
、NotFound
。
// PKIndexView.vue
<template>
<PKPlayGround/>
</template>
<script>
import PKPlayGround from '@/components/PKPlayGround.vue'
export default {
name: 'PKIndexView',
components: {
PKPlayGround,
},
setup() {
},
}
</script>
<style scoped>
</style>
路由操作:
// router/index.js
import {
createRouter, createWebHistory } from 'vue-router'
import PKIndexView from '../views/pk/PKIndexView.vue'
import RecordIndexView from '@/views/record/RecordIndexView'
import RanklistIndexView from '@/views/ranklist/RanklistIndexView'
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(.*)",
name: "404",
component: NotFound,
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
二、js 控制 html、css 实现 PK 地图
-
AcGameObject.js
作为总父类 js 控制 每秒60次 执行GameMap.js
、Wall.js
的 updata;注意:只要new了某个js类,就会执行其所在的整个js文件
-
js 控制
GameMap.vue
组件中地图canvas
随PKPlayGround动态改变大小; -
js 控制
GameMap.vue
组件中墙canvas
的构建。
GameMap 组件代码
// GameMap.vue
<template>
<div ref="parent" class="gamemap">
<canvas ref="canvas">
</canvas>
</div>
</template>
<script>
// 引入自己写的 js
import {
GameMap} from "@/assets/scripts/GameMap.js"
import {
ref} from 'vue'
// 组件挂载完以后需要执行的操作
import {
onMounted} from 'vue'
export default {
setup:() => {
let parent = ref(null);
let canvas = ref(null);
// 组件挂载完以后需要执行的操作:这里是js控制canvas
onMounted(() => {
// 通过 js 控制 html 显示
// 传入画布和父组件的值
// getContext('2d') 获取这个元素的 context,由 CanvasRenderingContext2D 接口完成实际的绘制。
// 用 js 求出动态长方形 PKPlayGround(w * L)的 最大网格(row*col)正方形,可求内部小正方形的边长
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.js
// 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 = 15;
// 画墙
this.walls = [];
}
// 检查左下和右上是否联通
check_connectivity(g, sx, sy, ex, ey){
if (sx == ex && sy == ey) 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, ex, ey)) return true;
}
return false;
}
create_walls() {
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; i++){
for (let j = 0; j<1000 ; j++){
// js 如何产生随机数
// Math.random: 产生[0,1)之间随机浮点数
// 即产生0-行数之间的随机浮点值后取整。
let r = parseInt(Math.random() * this.rows);
let c = parseInt(Math.random() * this.cols);
if (g[r][c] || g[c][r] ) continue;
if (c == 1 && r == this.rows-2 || c == this.cols-2 && r == 1) continue;
g[r][c] = g[c][r] = true;
break;
}
}
// js 实现深度复制一个对象
// 先转化成JSON, 再把JSON解析出来
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] == true) {
new Wall(r, c, this);
}
}
}
// 如果联通,返回true
return true;
}
start() {
for (let i = 0; i < 1000; i++){
if (this.create_walls()){
break;
}
}
}
updata_size() {
// 用 js 求出动态长方形 PKPlayGround(w * L)的 最大网格(row*col)正方形,可求内部小正方形的边长
// clientWidth/clientHeight: API,求div的长宽
// why parseInt()? 这里算得为浮点型,而canvas渲染为整型,渲染会有空隙
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;
}
updata() {
// 每帧都更新下边长
this.updata_size();
this.render();
}
render() {
// 画基数偶数格颜色分离
// #424242
// fillStyle 属性赋值颜色。fillRect(坐标,宽,高)
const color_even = "#D8D8D8", color_odd = "#E7E7E7";
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_odd;
} else {
this.ctx.fillStyle = color_even;
}
// canvas 往右是x正方向,往下是y正方向
// 因此第 r 行 c 列坐标是(c,r)
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
}
}
}
}
Wall.js
import {
AcGameObject } from "./AcGameObject";
export class Wall extends AcGameObject {
// r,c 为画墙的起始坐标,gamemap 为canvas画布
constructor(r, c, gamemap) {
super();
this.r = r;
this.c = c;
this.gamemap = gamemap;
this.color = "#424242"
}
updata() {
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);
}
}
效果如下:
三、js 控制 html、css 实现蛇各项操作
GameMap.js 实现如下:
import {
AcGameObject } from "./AcGameObject";
import {
Wall } from "./Wall";
import {
Snake } from './Snake';
export class GameMap extends AcGameObject {
// 构造函数传入画布和画布的父元素
constructor(ctx, parent) {
super();
this.ctx = ctx;
this.parent = parent;
// 存画布格子的绝对距离
this.L = 0;
// 为避免两蛇可能在同一时刻进入同一个格子对优势者不公平,设置地图列数行数奇偶性不同
// 蛇的走路路程坐标x和y的和总是奇偶交替的,让其初始的和奇偶不同即保证同时刻不可能走入同格子
// 后续需要将加墙坐标改为中心对称
this.rows = 13;
this.cols = 14;
// 存内部障碍物数量
this.inner_walls_count = 30;
// 画墙,存Wall对象
this.walls = [];
// 画蛇
this.snakes = [
new Snake({
id:0, color: "#4876EC", r: this.rows-2, c: 1}, this),
new Snake({
id:1, color: "#F94848", r: 1, c: this.cols-2}, this),
];
}
check_connectivity(g, sx, sy, ex, ey){
if (sx == ex && sy == ey) 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, ex, ey)) return true;
}
return false;
}
create_walls() {
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++){
// js 如何产生随机数
// Math.random: 产生[0,1)之间随机浮点数
// 即产生0-行数之间的随机浮点值后取整。
let r = parseInt(Math.random() * this.rows);
let c = parseInt(Math.random() * this.cols);
if (g[r][c] || g[this.rows-1-r][this.cols-1-c] ) continue;
if (c == 1 && r == this.rows-2 || c == this.cols-2 && r == 1) continue;
g[r][c] = g[this.rows-1-r][this.cols-1-c] = true;
break;
}
}
// js 实现深度复制一个对象
// 先转化成JSON, 再把JSON解析出来
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));
}
}
}
// 如果联通,返回true
return true;
}
// 写键盘监听事件
add_listening_events() {
// 聚焦canvas
this.ctx.canvas.focus();
console.log("聚焦");
const [snake0, snake1] = this.snakes;
// 获取用户事件
this.ctx.canvas.addEventListener("keydown", e => {
if (e.key === 'w') snake0.set_direction(0);
else if (e.key === "d") snake0.set_direction(1);
else if (e.key === "s") snake0.set_direction(2);
else if (e.key === "a") snake0.set_direction(3);
else if (e.key === 'ArrowUp') snake1.set_direction(0);
else if (e.key === "ArrowRight") snake1.set_direction(1);
else if (e.key === 'ArrowDown') snake1.set_direction(2);
else if (e.key === "ArrowLeft") snake1.set_direction(3);
// console.log("键盘呀");
});
}
start() {
for (let i = 0; i < 1000; i++){
if (this.create_walls()){
break;
}
}
this.add_listening_events();
}
updata_size() {
// 用 js 求出动态长方形 PKPlayGround(w * L)的 最大网格(row*col)正方形,可求内部小正方形的边长
// clientWidth/clientHeight: API,求div的长宽
// why parseInt()? 这里算得为浮点型,而canvas渲染为整型,渲染会有空隙
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;
}
// 检查两蛇是否准备好下一步操作
check_ready() {
// 目前未移动且有指令,表示准备好
for (const snake of this.snakes) {
if ( snake.status !== "idle" ) return false;
if ( snake.direction === -1) return false;
}
return true;
}
// 让两条蛇进入下一回合
next_step(){
for (const snake of this.snakes ) {
snake.next_step();
}
}
// 检测加的蛇头是否合法
// 检测目标位置是否合法:没有撞到两条蛇的身体和障碍物
check_valid(cell) {
for (const wall of this.walls) {
if (wall.r === cell.r && wall.c === cell.c)
{
console.log("撞墙");return false;
}
}
for (const snake of this.snakes) {
let k = snake.cells.length;
// 当蛇尾会前进的时候,蛇尾不要判断
if (!snake.check_tail_increasing()) {
k -- ;
}
for (let i = 0; i < k; i ++ ) {
if (snake.cells[i].r === cell.r && snake.cells[i].c === cell.c)
return false;
}
}
return true;
}
updata() {
// 每帧都更新下边长
this.updata_size();
if (this.check_ready()) {
this.next_step();
}
this.render();
}
render() {
// 画基数偶数格颜色分离
// #424242
// fillStyle 属性赋值颜色。fillRect(坐标,宽,高)
const color_even = "#D8D8D8", color_odd = "#E7E7E7";
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_odd;
} else {
this.ctx.fillStyle = color_even;
}
// canvas 往右是x正方向,往下是y正方向
// 因此第 r 行 c 列坐标是(c,r)
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
}
}
}
}
Snake.js 蛇整体实现如下:
import {
AcGameObject } from "./AcGameObject";
import {
Cell } from "./Cell";
export class Snake extends AcGameObject {
// 传入蛇信息以及gamemap
constructor(info, gamemap) {
super();
this.id = info.id;
this.color = info.color;
this.gamemap = gamemap;
// 存放蛇身体,cells[0]存放蛇头。
this.cells = [new Cell(info.r, info.c)];
// 蛇每秒走5个格子
this.speed = 5;
// -1表示没有指令,0、1、2、3表示上右下左
this.direction = -1
// 蛇状态:idle表示静止,move 表示正在移动,die表示死亡
this.status = 'idle';
// 蛇走下一步的目标位置
this.next_cell = null;
// move上右下左的偏移量
this.dr = [-1, 0, 1, 0];
this.dc = [0, 1, 0, -1];
// 存当前蛇的回合数(判断蛇是否要边长)
this.count = 0;
// 允许的误差0.01
this.eps = 1e-2;
// 蛇眼睛
// 1.移动方向
this.eye_direction = 0;
// 左下角的蛇眼睛移动方向初始朝上,右上角的蛇朝下
if (this.id === 1) this.eye_direction = 2;
// 2. 眼睛距离蛇头位置,即圆心不同方向偏移量x
this.eye_dx = [
[-1,1], // 向上
[1,1], // 向右
[1,-1], // 向下
[-1,-1] // 向左
]
// 眼睛距离圆心不同方向偏移量y
this.eye_dy = [
[-1,-1],
[-1,1],
[1,1],
[1,-1]
]
}
start() {
}
// 统一接口用来设置方向,方便后来除键盘外的后端控制方向
set_direction(d) {
this.direction = d;
}
// 真正移动
updata_move() {
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);
// (距离小于0.01表示走完一格了停下的状态:)
if (distance < this.eps) {
// 1.1:蛇头停止态:—→停下时直接按新蛇头坐标渲染新头
this.cells[0] = this.next_cell; // 目标结点做新蛇头(放入新蛇头)
this.next_cell = null; // 目标清空
this.status = 'idle'; // 状态改为停止
// 2.2:尾巴停止态:—→移动停下后删除尾巴
//(蛇没有增加长的时候,即蛇不加尾巴的时候:每次添加新头,连续移动尾巴到前一个位置,停下时直接把尾巴删掉)
if(!this.check_tail_increasing()) {
this.cells.pop();
console.log("删除蛇尾");
}
} else {
// (距离不小于0.01表示移动的状态:)
// 1.1:蛇头移动态:—→当前放入的新蛇头动态移动一个格子
// 每两帧之间走过的距离(目的使得看起来是动态的不是直接移动一顿一顿的)
const move_distance = this.speed * this.timedelta / 1000;
this.cells[0].x += move_distance * dx / distance;
this.cells[0].y += move_distance * dy / distance;
// 2.1:尾巴移动态:—→蛇尾连续动态移动到前一个(使得蛇尾是移动消失而不是凭空消失)
//(蛇没有增加长的时候,即蛇不加尾巴的时候:每次添加新头,连续移动尾巴到前一个位置,停下时直接把尾巴删掉)
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;
// console.log("不 %3 移动蛇尾");
}
}
}
// 判断需要增加蛇尾
check_tail_increasing() {
if (this.count <= 5) return true;
if (this.count % 3 === 1) {
// console.log("判断是否 % 3");
return true;}
return false;
}
// 将蛇的状态变为走下一步
next_step() {
const d = this.direction;
// 下一个头cell的位置,增加新蛇头位置信息
this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]);
// 用完方向以后清空操作
this.direction = -1;
// 蛇眼方向
this.eye_direction = d;
// 状态由静止变为移动
this.status = 'move';
this.count++;
// 以前的cells存储位置均完后退一位,以便于空出cells[0]存新蛇头位置信息
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";
}
}
updata() {
if (this.status === 'move') {
this.updata_move();
}
this.render();
}
// 渲染画蛇
render() {
const L = this.gamemap.L;
const ctx = this.gamemap.ctx;
ctx.fillStyle = this.color;
// 蛇die颜色变色
if(this.status === "die") {
ctx.fillStyle = "white";
}
for (const cell of this.cells) {
// canvas画圆开启路径
ctx.beginPath();
// 画圆圆心x,圆心y,半径,起始角度,终止角度
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, 0.8 * L, 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, 0.8 * L );
}
}
// 画眼睛
ctx.fillStyle = "black";
for(let i = 0; i < 2; i++){
const eye_x = (this.cells[0].x + this.eye_dx[this.eye_direction][i]*0.15)*L;
const eye_y = (this.cells[0].y + this.eye_dy[this.eye_direction][i]*0.15)*L;
ctx.beginPath();
ctx.arc(eye_x, eye_y, 0.05*L, 0, 2 * Math.PI);
ctx.fill();
}
}
}
Cell.js 蛇每一部分子元素如下:
export class Cell {
constructor(r, c){
this.r = r;
this.c = c;
this.x = c + 0.5;
this.y = r + 0.5;
}
}