【PIXI】生成随机迷宫太丑?不如试试普里姆算法

1,375 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

前言

开始和结局都是注定好的,

只是不知道过程会是怎么样的扑朔迷离。

介绍

我们如果制作h5游戏尤其是rpg游戏的时候难免会要求生成随机迷宫。众所周知,一般的迷宫是由一个二维数组构成。分别表示行列关系,里面元素代表地块特征。如果单纯使用random随机生成这些信息的话,会显得杂乱无章,而且很难出现一条通路。因为图论中可以认为迷宫属于一种连通图。这就要我们今天的主角登场了——普里姆算法。

先来看效果:

VID_20210926_101112.gif

普里姆算法生成迷宫比较自然随机,正是我们想要的结果。本期我们就用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->墙壁)

    微信截图_20210926103151.png

  • 我们期望通过RandomMap类创建随机地图信息,并通过drawMap方法绘制出来,这里要注意row和col传入的行列数其实是初始地面的数量,实际会行列数是2N+1。

2.普里姆算法

我们这里先介绍一下该算法如果在这里实现的:

  1. 我们先让这个地图完全封闭,将空地块0(0表示地面,1表示墙壁)均匀排布的其中,

    如图所示:

    微信截图_20210926103947.png

  2. 我们随机访问其中一个空地块,访问他的上下左右四个方向,如果对应方向有值并且没访问过,然后要把该地块之间的墙壁打通1->0,最后把这个地块作为当前地块,同时也要就把这个地块放进访问列表。

    如图所示:

    微信截图_20210926104443.png

  3. 如果周围没有可访问的地块了,那么再随机选择一个地块重新循环,直至可用空地块数用光(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;

根据第二部的描述分析实现的源代码就是这样,值得注意的是,我们不仅要做访问判断还要做边界判断。


总体来说还是很简单的,不来实现一下试试么,在线演示

思考

关于连通图算法有很多,我们都可以尝试一下,生成迷宫,还是很有一件挺有意思的事情。