前端迷宫绘制完成

This commit is contained in:
flykhan 2023-02-11 18:51:00 +08:00
parent 4eb9cdfae7
commit faa10babe6
No known key found for this signature in database
10 changed files with 371 additions and 6 deletions

View File

@ -17,7 +17,8 @@ export default {
</script> </script>
<style> <style>
body { body {
background-image: url("@/assets/background.png"); /* 页面背景图片 */
background-image: url("@/assets/images/background.png");
background-size: cover; background-size: cover;
} }
</style> </style>

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -0,0 +1,83 @@
// 该类作为基类使用,用于刷新绘制
// 定义绘制对象数组,存放每一帧绘制的对象
const AC_GAME_OBJECTS = [];
// 导出类
export class AcGameObject {
// 构造函数
constructor(){
// push(this) 是将当前对象存下来的意思
// 每创建一个,就 push 一个,先创建先 push,后创建后 push
// 先创建的先执行 update ,后创建的会把先创建的给覆盖掉
AC_GAME_OBJECTS.push(this);
// 帧与帧执行的时间间隔
this.timedelta=0;
// 是否执行过 start 函数
this.has_called_start = false;
}
// start 函数只执行一次
start(){
}
// 除第一帧之外,每一帧执行一遍
update(){
}
// 删除之前执行
on_destroy(){
}
// 删除
destroy(){
// 删除之前调用 on_destroy 函数
this.on_destroy();
// 在 js 里,使用 of 遍历的是数组里的值;使用 in 遍历的是数组的下标。
for(let i in AC_GAME_OBJECTS){
const obj = AC_GAME_OBJECTS[i];
// 如果 obj 等于当前对象,则删除该对象
if(obj === this){
// 使用 splice 删除数组里的对象
AC_GAME_OBJECTS.splice(i);
break;
}
}
}
}
// 上一帧执行的时刻
let last_timestamp;
// step 函数需要传入当前帧执行的时刻 timestamp
const step = (timestamp) => {
// 遍历所有的物品
// 在 js 里,使用 of 遍历的是数组里的值;使用 in 遍历的是数组的下标。
for(let obj of AC_GAME_OBJECTS){
// 如果当前物品没有执行 start 函数,则该物品执行一次 start 函数
if(!obj.has_called_start){
// 将该物品的 has_called_start 赋值为 true,表示其已经执行过了
obj.has_called_start = true;
obj.start();
}
// 如果执行过 start ,则接下来应该执行 update 函数
else{
// 当前帧与上一帧的时间间隔:当前帧执行时刻减去上一帧执行时刻
obj.timedelta = timestamp - last_timestamp;
obj.update();
}
}
// 更新 last_timestamp ,作为下一次更新的“上一帧执行的时刻”
last_timestamp = timestamp;
// 递归调用
requestAnimationFrame(step)
}
// 定义需要的刷新次数,传入的函数step会在下一帧浏览器渲染之前执行一遍。
requestAnimationFrame(step)

View File

@ -0,0 +1,170 @@
// 在 AcGameObject.js 里使用的是 export class ,因此这里需要使用 {} 括起来引用;如果是 export default 则不需要用括号括起来
import { AcGameObject } from "./AcGameObject";
// 导入墙组件
import { Wall } from "./Wall";
// 导出定义的 GameMap 游戏地图类
export class GameMap extends AcGameObject{
// 构造函数参数: ctx 画布; parent 画布的父元素,用来动态修改画布的长宽
constructor(ctx,parent){
// super() 用于先执行基类的构造函数
super();
// 存下 ctx 和 parent
this.ctx = ctx;
this.parent = parent;
// 存下每个格子的绝对距离
this.L = 0;
// 定义棋盘格的行数和列数
this.rows = 13;
this.cols = 13;
// 绘制棋盘内部区域的障碍物(墙)的数量
this.inner_walls_count = 50;
// 存储所有的墙
// 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制
this.walls = [];
}
// 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标
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];
// 判断是否撞墙,如果没有撞墙,且可以搜到终点的话,返回 true ,否则返回 false
if(!g[x][y] == true && this.check_connectivity(g, x, y, tx, ty))
return true;
}
// 搜不到终点,返回 false
return false;
}
// 创建墙函数
create_wall(){
// 创建一个墙格进行测试
// new Wall(0,0,this);
// 开一个布尔数组,有墙为 true
// 一开始先将所有墙初始化为 false
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;
}
// 创建内部随机障碍物
// 因为每次计算都会生成两个障碍物,因此这里的循环次数 this.inner_walls_count 需要处以 2
for(let i = 0; i < this.inner_walls_count / 2; i ++){
// 避免位置重复:重复 1000 次,只要找到了就禁止随机
for(let j = 0; j < 1000; j ++){
let r = parseInt(Math.random()*this.rows);
let c = parseInt(Math.random()*this.cols);
// 主对角线对称 g[r][c] 和 g[c][r] 完成两种联合判断
// 当此位置已经有障碍物了,则重新计算下一个位置
if(g[r][c] || g[c][r]) continue;
// 将计算求得的随机障碍物的位置置为 true ,以对该位置进行绘制
// g[r][c] 和 g[c][r] 的坐标在对角线位置会重合,会被绘制为一个障碍物
g[r][c] = g[c][r] = true;
// 1000 次中,规定数量的内部障碍物已经够了之后就 break 掉
break;
}
}
// 避免内部障碍物覆盖掉左下角和右上角的角色出发点
g[this.rows-2][1] = g[1][this.cols-2] = false;
// 保证两个对角角色的运动区域是联通的
// 检测联通需要把 g[][] 传过去给 check_connectivity() 函数进行判断,传过去之前需要把当前 g[][] 状态复制一份,避免当前数据被修改掉
// 深度复制方法:先转换数据为 JSON ,再把 JSON 解析出来
const copy_g = JSON.parse(JSON.stringify(g));
// 检测到不连通,则直接在生成对象之前 return false 退出函数
if(!this.check_connectivity(copy_g, this.rows-2, 1, 1, this.cols-2)) return false;
// 枚举数组,将 g[r][c] == true 的部分绘制出来
// 如果上一步连通性检测失败,则退出 this.create_wall() 函数,本步骤不再执行生成新对象的操作
for(let r = 0; r < this.rows; r ++){
for(let c = 0; c < this.cols; c ++){
if(g[r][c]){
// 将每个新生成的 Wall 对象 push 存入 walls 数组中
this.walls.push(new Wall(r,c,this));
}
}
}
// 绘制成功则 return turn
return true;
}
start(){
// 开始时调用一次创建墙的函数
// 循环 1000 次,如果成功创建则 break ,否则继续循环创建
for(let i = 0; i < 1000; i ++)
if(this.create_wall())
break;
}
// 每一帧都更新一下小正方格的边长
update_size(){
// 计算当前帧每个格子的宽度, parseInt 取整是为了避免渲染出的格子之间出现小空隙
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(){
// b47226 棕色 aad751 浅绿 a2d048 深绿
// this.ctx.fillStyle = 'green';
// this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
// 定义偶数格even、奇数格odd的颜色
const color_even = "#aad751", color_odd = "#a2d048";
for(let r = 0; r < this.rows; r++){
for(let c = 0; c < this.cols; c++){
// 当列标加行标: r + c 是偶数时,选取偶数颜色,否则选取奇数颜色。
if((r + c) % 2 == 0){
this.ctx.fillStyle = color_even;
}else{
this.ctx.fillStyle = color_odd;
}
// 绘制小方格:起始坐标x,起始坐标y,水平边长,竖直边长
this.ctx.fillRect(c*this.L, r*this.L, this.L, this.L);
}
}
}
}

View File

@ -0,0 +1,36 @@
// 定义墙组件
import { AcGameObject } from "./AcGameObject";
export class Wall extends AcGameObject {
// 构造函数定义,参数为墙的坐标 r 行, c 列,gamemap 用于绘制
constructor(r,c,gamemap){
// 先执行基类的构造函数
super();
this.r = r;
this.c = c;
this.gamemap = gamemap;
this.color = "#b47226";
}
// 墙的更新
update(){
// 执行渲染
this.render();
}
// 墙的渲染
render(){
// 从 gamemap 对象中拿到小格(墙)的边长
const L = this.gamemap.L;
// 拿到ctx画布
this.ctx = this.gamemap.ctx;
// 设置 ctx 画布填充色
this.ctx.fillStyle = this.color;
// 绘制矩形
this.ctx.fillRect(this.c*L, this.r*L, L, L);
}
}

View File

@ -14,7 +14,7 @@
<script></script> <script></script>
<style> <style scoped>
.container { .container {
/* 到顶部距离 20px */ /* 到顶部距离 20px */
margin-top: 20px; margin-top: 20px;

View File

@ -0,0 +1,47 @@
//
<template>
<!-- ref="parent" 用于将 return 返回的 parent 指向 div -->
<div ref="parent" class="gamemap">
<!-- canvas 画布 -->
<canvas ref="canvas"></canvas>
</div>
</template>
<script>
// GameMap.js
import { GameMap } from "@/assets/scripts/GameMap";
// canvas, onMounted
import { ref, onMounted } from "vue";
export default {
setup() {
// parent canvas, , ref(null)
let parent = ref(null);
let canvas = ref(null);
// ,
onMounted(() => {
new GameMap(canvas.value.getContext("2d"), parent.value);
});
return {
parent,
canvas,
};
},
};
</script>
<style scoped>
.gamemap {
/* 宽度、高度设为100%,目的是与其父元素等长等宽 */
width: 100%;
height: 100%;
/* flex可用于同时实现水平和竖直居中 */
display: flex;
/* 水平居中 */
justify-content: center;
/* 竖直居中 */
align-content: center;
}
</style>

View File

@ -73,4 +73,4 @@ export default {
}; };
</script> </script>
<style></style> <style scoped></style>

View File

@ -0,0 +1,28 @@
//
<template>
<div class="playground">
<GameMap />
</div>
</template>
<script>
import GameMap from "./GameMap.vue";
export default {
components: {
GameMap,
},
};
</script>
<style scoped>
.playground {
/* 60% 浏览器宽度, 70% 浏览器高度 */
width: 60vw;
height: 70vh;
/* 背景色 */
/* background: lightblue; */
/* 距上边距40px,左右居中 */
margin: 40px auto;
}
</style>

View File

@ -1,13 +1,13 @@
<template> <template>
<ContentBase>对战</ContentBase> <PlayGround>对战</PlayGround>
</template> </template>
<script> <script>
import ContentBase from "../../components/ContentBase.vue"; import PlayGround from "../../components/PlayGround.vue";
export default { export default {
components: { components: {
ContentBase, PlayGround,
}, },
}; };
</script> </script>