小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
前言
开始和结局都是注定好的,
只是不知道过程会是怎么样的扑朔迷离。
介绍
我们如果制作h5游戏尤其是rpg游戏的时候难免会要求生成随机迷宫。众所周知,一般的迷宫是由一个二维数组构成。分别表示行列关系,里面元素代表地块特征。如果单纯使用random随机生成这些信息的话,会显得杂乱无章,而且很难出现一条通路。因为图论中可以认为迷宫属于一种连通图。这就要我们今天的主角登场了——普里姆算法。
先来看效果:
普里姆算法生成迷宫比较自然随机,正是我们想要的结果。本期我们就用pixi.js轻量级游戏引擎去做模拟实现他,准备好了么,出发~
正文
1.基础结构
我们本次使用vite搭建,module模式后面方面模块导入
<div id="app"></div>
<script type="module" src="./app.js"></script>
/*app.js*/
import * as PIXI from "pixi.js";
import RandomMap from "./js/RandomMap"
const imgs = {
"land": "assets/map0.png",
"wall": "assets/map1.png",
}
class Application {
constructor() {
this.app = null;
this.stage = null;
this.textures = null;
this.row =8;
this.col =6;
this.w = this.row * 100;
this.h = this.col * 100;
this.mapData = [];
this.init();
}
init() {
let el = document.getElementById("app")
this.app = new PIXI.Application({
width: this.w,
height: this.h
});
this.stage = this.app.stage;
this.map = new PIXI.Container();
this.map.interactive = true;
this.stage.addChild(this.map)
this.stage.sortableChildren = true;
el.appendChild(this.app.view);
this.reset();
this.loadTexture(imgs).then(data => {
this.textures = data;
this.map.on('pointerdown', this._pointerDown.bind(this));
this.render();
})
}
reset() {
console.clear()
this.map.removeChildren();
this.createMap();
}
_pointerDown() {
this.reset();
this.render();
}
loadTexture(imgs) {
const textures = {};
return new Promise((reslove, reject) => {
let loader = this.app.loader;
for (const key in imgs) {
loader.add(key, imgs[key])
}
loader.load((info, resources) => {
for (const key in resources) {
let texture = PIXI.Texture.from(resources[key].data);
textures[key] = texture;
}
reslove(textures)
});
})
}
render() {
this.drawMap();
}
createMap() {
const {row, col} = this
this.mapData = new RandomMap({
row,
col
})
}
drawMap() {
const {w, h, row, col, mapData, textures, map} = this;
let sw = w / (row * 2 + 1),
sh = h / (col * 2 + 1);
for (let i = 0; i < mapData.length; i++) {
for (let j = 0; j < mapData[i].length; j++) {
let sprite = PIXI.Sprite.from(textures[["land", "wall"][mapData[i][j]]])
sprite.width = sw;
sprite.height = sh;
sprite.x = j * sw;
sprite.y = i * sh;
map.addChild(sprite)
}
}
}
}
window.onload = new Application();
RandomMap类存在普里姆算法逻辑下面会讲到。
我们先看看主逻辑,主要完成这几件事:
-
定义了行列数目,计算得到画布的宽高
-
加载两张图片资源,作为视图地块(0->地面,1->墙壁)
-
我们期望通过RandomMap类创建随机地图信息,并通过drawMap方法绘制出来,这里要注意row和col传入的行列数其实是初始地面的数量,实际会行列数是2N+1。
2.普里姆算法
我们这里先介绍一下该算法如果在这里实现的:
-
我们先让这个地图完全封闭,将空地块0(0表示地面,1表示墙壁)均匀排布的其中,
如图所示:
-
我们随机访问其中一个空地块,访问他的上下左右四个方向,如果对应方向有值并且没访问过,然后要把该地块之间的墙壁打通1->0,最后把这个地块作为当前地块,同时也要就把这个地块放进访问列表。
如图所示:
-
如果周围没有可访问的地块了,那么再随机选择一个地块重新循环,直至可用空地块数用光(row*col),迷宫就会自然的形成。
3.迷宫的实现
/*RandomMap.js*/
class RandomMap {
constructor(options) {
this.row = 5;
this.col = 5;
Object.assign(this, options)
this.data = [];
this.init();
this.create();
return this.data;
}
init() {
const {row, col, data} = this;
let _row = row * 2 + 1;
let _col = col * 2 + 1;
for (let i = 0; i < _col; i++) {
let _arr = new Array(_row).fill(1);
data.push(_arr)
}
for (let i = 1; i < _col; i += 2) {
for (let j = 1; j < data[i].length; j += 2) {
data[i][j] = 0;
}
}
}
create() {
const {data, row, col} = this;
let num = row * col; // 通路数
let acc = []; // 已访问过
let no_acc = [...Array(num).fill(0)]; // 未访问过
let offRow = [-1, 1, 0, 0],
offCol = [0, 0, -1, 1],
offs = [-row, row, -1, 1];
let index = ~~(Math.random() * num);
no_acc[index] = 1;
acc.push(index);
while (acc.length < num) {
let ls = -1,
offPos = -1;
let n = 0,
offetIndex = 0;
// 随机寻找周围四个关联单元
while (++n < 5) {
offetIndex = ~~(Math.random() * 5);
ls = offs[offetIndex] + index;
let tpr = (index / row | 0) + offCol[offetIndex];
let tpc = (index % row) + offCol[offetIndex];
if (no_acc[ls] == 0 && tpr >= 0 && tpc >= 0 && tpr <= col - 1 && tpc <= row - 1) {
offPos = offetIndex;
break;
}
}
if (offPos < 0) {
// 如果没有找到符合的位置
// 重新在访问列表种随机选取一个位置
index = acc[~~(Math.random() * acc.length)]
} else {
// 否则,找到这个位置的连接点打通
let pos = this.indexToPos(index, row);
let x = pos[0] + offRow[offPos];
let y = pos[1] + offCol[offPos];
data[x][y] = 0;
index = ls;
no_acc[index] = 1;
acc.push(index)
}
}
}
indexToPos(index, c) {
let pos = [...Array(4).fill(0)];
pos[0] = (index / c | 0) * 2 + 1;
pos[1] = (index % c) * 2 + 1;
return pos;
}
}
export default RandomMap;
根据第二部的描述分析实现的源代码就是这样,值得注意的是,我们不仅要做访问判断还要做边界判断。
总体来说还是很简单的,不来实现一下试试么,在线演示
思考
关于连通图算法有很多,我们都可以尝试一下,生成迷宫,还是很有一件挺有意思的事情。