CS61B Project2:关于生成地图算法的讨论(一)

文章描述了一种基于TETile的2D游戏世界生成算法,通过伪随机生成房间、走廊和走廊连接,但存在占用面积小和走廊实现不理想等问题。作者提出改进思路,如调整行走逻辑和引入“走廊”与“房间”概念。
摘要由CSDN通过智能技术生成

问题介绍

Project 2介绍了一个渲染引擎,可以根据传入的TETile[][]数组来在对应的位置生成砖块(TETile对象表示各种不同的砖块类型,包括地板、墙、花等元素),我们的任务是完成一个用于生成可探索世界的引擎,并设计和实现一个基于2D图块的游戏(就像塞尔达传说:织梦岛一样)。

在Phrase 1的部分,我们的目标是编写一个世界生成器,这个生成器有以下这些要求:

  1. 世界必须是一个2D网络,使用提供的TERenderer.java引擎渲染
  2. 世界必须是伪随机生成的,随机数种子应该由用户输入
  3. 生成的世界必须包括房间和走廊(走廊是单行地板,房间是多行地板),以及可能的室外空间
  4. 至少应该存在一些矩形房间
  5. 生成的走廊需要包含转弯或者相交的部分
  6. 房间和走廊的数量应该是随机的
  7. 房间和走廊的位置应该是随机的
  8. 房间的宽度和高度应该是随机的
  9. 走廊的长度应该是随机的
  10. 房间的走廊和墙壁必须在视觉上与地板不同,墙壁和地板在视觉上应与未使用的空间不同
  11. 房间和走廊应该相连,即相邻房间或走廊之间的地板不应该有间隙
  12. 世界每次都应该有很大不同,即不应该具有相同的基本布局和易于预测的功能

针对这些要求,给出了一个合格世界的示例,在该图中,#代表墙壁,点代表地板,一个金色的墙代表一扇锁着的门。所有的未使用空间都留空

Project 2还有一些关于游戏进入界面和游玩方式的要求,因为本文仅谈论地图生成算法,所以先暂时略过(可能会在之后的博客中展示实现)

实现思路

通过对问题的拆解,我发现在这些要求中,最应该首先把握住的是房间和走廊连通的问题,从这个角度出发我思考出了第一版算法,他包含三个部分:

  1. 初始化TETile[][]数组,将其所有位置填充为`Tileset.NOTHING`
  2. 随机指定一个开始点,确保其坐标在数组内部,并以这个开始点随机游走,每次随机往上下左右四个方向之一前进一格,直到前进的次数除以总的格子数达到一个随机指定的“空间使用率”,对于每个经过的坐标(x, y),在TETile[x][y]中填入`Tileset.FLOOR`(要限制不能走到地图边缘,因为FLOOR周边应该包裹一层WALL)
  3. 遍历数组中的每个元素,如果这个元素是`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();
    }
}

运行代码,结果如下

发现算法确实做到了所有的“走廊”连通了,但是存在以下几个问题:

  1. 占用面积过小,地图中出现大面积留白
  2. “走廊”这一概念的实现不好,没有单层的地板
  3. 生成墙的算法有bug,有些九宫格位置并没有生成墙

通过输出行走路径的坐标我发现了占用面积过小的原因

我发现在随机等概率行走的状态下,指针会反复经过一个区域。另外,因为我是等概率选择方向,从数学上看,这样的随机游走会生成一个二维的高斯分布,和最终需要的形态存在出入,所以具体的行走逻辑需要更改,也许可以考虑随机生成“走廊”和“房间”的概念,或者递归生成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值