问题介绍
Project 2介绍了一个渲染引擎,可以根据传入的TETile[][]数组来在对应的位置生成砖块(TETile对象表示各种不同的砖块类型,包括地板、墙、花等元素),我们的任务是完成一个用于生成可探索世界的引擎,并设计和实现一个基于2D图块的游戏(就像塞尔达传说:织梦岛一样)。
在Phrase 1的部分,我们的目标是编写一个世界生成器,这个生成器有以下这些要求:
- 世界必须是一个2D网络,使用提供的TERenderer.java引擎渲染
- 世界必须是伪随机生成的,随机数种子应该由用户输入
- 生成的世界必须包括房间和走廊(走廊是单行地板,房间是多行地板),以及可能的室外空间
- 至少应该存在一些矩形房间
- 生成的走廊需要包含转弯或者相交的部分
- 房间和走廊的数量应该是随机的
- 房间和走廊的位置应该是随机的
- 房间的宽度和高度应该是随机的
- 走廊的长度应该是随机的
- 房间的走廊和墙壁必须在视觉上与地板不同,墙壁和地板在视觉上应与未使用的空间不同
- 房间和走廊应该相连,即相邻房间或走廊之间的地板不应该有间隙
- 世界每次都应该有很大不同,即不应该具有相同的基本布局和易于预测的功能
针对这些要求,给出了一个合格世界的示例,在该图中,#代表墙壁,点代表地板,一个金色的墙代表一扇锁着的门。所有的未使用空间都留空
Project 2还有一些关于游戏进入界面和游玩方式的要求,因为本文仅谈论地图生成算法,所以先暂时略过(可能会在之后的博客中展示实现)
实现思路
通过对问题的拆解,我发现在这些要求中,最应该首先把握住的是房间和走廊连通的问题,从这个角度出发我思考出了第一版算法,他包含三个部分:
- 初始化TETile[][]数组,将其所有位置填充为`Tileset.NOTHING`
- 随机指定一个开始点,确保其坐标在数组内部,并以这个开始点随机游走,每次随机往上下左右四个方向之一前进一格,直到前进的次数除以总的格子数达到一个随机指定的“空间使用率”,对于每个经过的坐标(x, y),在TETile[x][y]中填入`Tileset.FLOOR`(要限制不能走到地图边缘,因为FLOOR周边应该包裹一层WALL)
- 遍历数组中的每个元素,如果这个元素是`Tileset.FLOOR`,那么检查其周围九宫格范围的八个坐标位置,如果为`Tileset.NOTHING`,那么填充为`Tileset.WALL`
为了实现这个算法,我创造了两个类,分别是Position.java和World.java,前者表示一个坐标,同时可以控制四个方向的移动,后者负责初始化和生成这个世界,其中在World.java中还有一个Nested Class,用于提供第二步和第三步中需要使用的一些方法,代码展示如下
// Position.java
package byog.Core;
public class Position {
private int xPos;
private int yPos;
public Position(int x, int y) {
xPos = x;
yPos = y;
}
public Position(Position p) {
xPos = p.xPos;
yPos = p.yPos;
}
public int getXPos() {
return xPos;
}
public int getYPos() {
return yPos;
}
public Position goUp() {
return new Position(xPos, yPos + 1);
}
public Position goDown() {
return new Position(xPos, yPos - 1);
}
public Position goLeft() {
return new Position(xPos - 1, yPos);
}
public Position goRight() {
return new Position(xPos + 1, yPos);
}
public String toString() {
StringBuilder returnSB = new StringBuilder("(");
returnSB.append(xPos);
returnSB.append(", ");
returnSB.append(yPos);
returnSB.append(")");
return returnSB.toString();
}
}
// World.java
import byog.TileEngine.TETile;
import byog.TileEngine.Tileset;
import java.io.*;
import java.util.Random;
public class World implements Serializable {
private static final long serialVersionUID = 123123123123123L;
private final Random r;
private final int WIDTH;
private final int HEIGHT;
private final TETile[][] world;
/** Initialize a new world */
public World(int w, int h, int seed) {
world = new TETile[w][h];
r = new Random(seed);
WIDTH = w;
HEIGHT = h;
}
/** Load the existing world, or create a new world */
public static World loadWorld(int w, int h, int seed) {
File f = new File("./RandomWorld/world.ser");
if (f.exists()) {
try {
FileInputStream fs = new FileInputStream(f);
ObjectInputStream os = new ObjectInputStream((fs));
World loadWorld = (World) os.readObject();
os.close();
return loadWorld;
} catch (FileNotFoundException e) {
System.out.println("File not found!");
System.exit(0);
} catch (IOException e) {
System.out.println(e);
System.exit(0);
} catch (ClassNotFoundException e) {
System.out.println("Class not found!");
System.exit(0);
}
}
return new World(w, h, seed);
}
/** Save the word instance that have been generalized */
public static void saveWorld(World w) {
File f = new File("./RandomWorld/world.ser");
try {
if (!f.exists()) {
f.createNewFile();
}
FileOutputStream fs = new FileOutputStream(f);
ObjectOutputStream os = new ObjectOutputStream(fs);
os.writeObject(w);
os.close();
} catch (FileNotFoundException e) {
System.out.println("File not found");
System.exit(0);
} catch (IOException e) {
System.out.println(e);
System.exit(0);
}
}
/** A Nested Toolkit Class for generalizing world */
private class GeneralizeHelper {
/** Returns true if the position is out of the limitation */
private boolean isOut(Position p) {
if (p.getXPos() < 1 || p.getXPos() > WIDTH - 2
|| p.getYPos() < 1 || p.getYPos() > HEIGHT - 2) {
return true;
}
return false;
}
/** Choose a position to start random walk and make sure there is enough space to
* generate the wall */
private Position startPosition() {
int x = 2 + r.nextInt(WIDTH - 1);
int y = 2 + r.nextInt(HEIGHT - 1);
return new Position(x, y);
}
/** GO up, down, left or right randomly and the "PATH" should
* leave enough space to generate the wall */
private Position randomWalk(Position p) {
int chooseDirection = r.nextInt(4);
Position newPos;
// Choose a direction randomly
switch (chooseDirection) {
case 0: newPos = new Position(p.goUp()); break;
case 1: newPos = new Position(p.goDown()); break;
case 2: newPos = new Position(p.goLeft()); break;
default: newPos = new Position(p.goRight()); break;
}
// Leave enough space to generate the wall
if (isOut(newPos)) {
return randomWalk(p);
}
return newPos;
}
/** Generate walls around the path */
private void generateWalls(Position p, TETile[][] t) {
Position[] positions = new Position[8];
int x = p.getXPos();
int y = p.getYPos();
positions[0] = new Position(x - 1, y + 1);
positions[1] = new Position(x , y + 1);
positions[2] = new Position(x + 1, y + 1);
positions[3] = new Position(x - 1, y);
positions[4] = new Position(x + 1, y);
positions[5] = new Position(x - 1, y - 1);
positions[6] = new Position(x , y - 1);
positions[7] = new Position(x + 1, y - 1);
for (int i = 0; i < 7; i++) {
int xpos = positions[i].getXPos();
int ypos = positions[i].getYPos();
if (t[xpos][ypos].equals(Tileset.NOTHING)) {
t[xpos][ypos] = Tileset.WALL;
}
}
}
}
/** Generalize a new world randomly (Version 1.0)*/
public void generalizeWorld() {
// Fill the TETile 2D Array with `Nothing`
for (int i = 0; i < WIDTH; i++) {
for (int j = 0; j < HEIGHT; j++) {
world[i][j] = Tileset.NOTHING;
}
}
// Generalize "Path" and "Room" randomly
double roomRatio = 0.4 + 0.2 * r.nextDouble();
int distance = 1;
GeneralizeHelper gh = new GeneralizeHelper();
Position ptr = gh.startPosition();
world[ptr.getXPos()][ptr.getYPos()] = Tileset.FLOOR;
while ((double) distance / (WIDTH * HEIGHT) < roomRatio) {
ptr = gh.randomWalk(ptr);
System.out.println(ptr.toString());
world[ptr.getXPos()][ptr.getYPos()] = Tileset.FLOOR;
distance++;
}
// Generalize "Walls" around the "Path"
for (int i = 0; i < WIDTH; i++) {
for (int j = 0; j < HEIGHT; j++) {
if (world[i][j].equals(Tileset.FLOOR)) {
gh.generateWalls(new Position(i, j), world);
}
}
}
}
public TETile[][] getWorld() {
return world;
}
}
算法效果与结果反思
创建了一个TestWorld.java,用于测试生成算法
package byog.Core;
import byog.TileEngine.TERenderer;
public class TestWorld {
public static void testGeneralizeWorld() {
World w = new World(80, 30, 217);
TERenderer ter = new TERenderer();
ter.initialize(80, 30);
w.generalizeWorld();
ter.renderFrame(w.getWorld());
}
public static void main(String[] args) {
testGeneralizeWorld();
}
}
运行代码,结果如下
发现算法确实做到了所有的“走廊”连通了,但是存在以下几个问题:
- 占用面积过小,地图中出现大面积留白
- “走廊”这一概念的实现不好,没有单层的地板
- 生成墙的算法有bug,有些九宫格位置并没有生成墙
通过输出行走路径的坐标我发现了占用面积过小的原因
文章来源:https://www.toymoban.com/news/detail-839237.html
我发现在随机等概率行走的状态下,指针会反复经过一个区域。另外,因为我是等概率选择方向,从数学上看,这样的随机游走会生成一个二维的高斯分布,和最终需要的形态存在出入,所以具体的行走逻辑需要更改,也许可以考虑随机生成“走廊”和“房间”的概念,或者递归生成。文章来源地址https://www.toymoban.com/news/detail-839237.html
到了这里,关于CS61B Project2:关于生成地图算法的讨论(一)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!