3D迷宫生成

主要功能

通过java代码实现3D迷宫的随机生成, 目前200阶需要1分钟左右, 可以先参考之前的文章2D迷宫生成.

代码实现

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.*;

/**
 * 3D迷宫生成, 参照2D迷宫生成
 * 由一面面很薄的墙组成, 拆除最少的墙, 使所有的单元格连通
 * 阶数  需要拆的墙   总墙数
 * 1	1 + 1	    1*6 = 6
 * 2	8 + 1	    4*6+4*3*(2-1) = 24+12=36
 * 3	27 + 1	    9*6+9*3*(3-1) = 54 + 54 = 108
 * 4	64 + 1	    16*6+16*3*(4-1) = 96+144 = 240
 * ...
 * n	n^3 + 1	    n^2*6 + n^2*3*(n-1) = 3n^2(1+n)
 */
public class MazeGenerate3D {

    // 迷宫的阶数
    private int sqrt = 2;

    // 迷宫的大小
    private int size = sqrt * sqrt * sqrt;

    // 用于存储每个单元格所属的集合
    private int[] array = new int[size];
    // 用于存储已经合并过的单元格
    private HashSet<Integer> allCell = new HashSet<Integer>();

    /**
     * 返回 i 所在集合的最大值
     *
     * @param i 单元格编号
     * @return
     */
    public int find(int i) {
        int result = i;
        while (array[result] != -1) {
            result = array[result];
        }
        return result;
    }

    /**
     * 将 i 和 j 所在集合进行合并
     *
     * @param i 单元格编号
     * @param j 单元格编号
     */
    public void union(int i, int j) {
        int result1 = find(i);
        int result2 = find(j);
        if (result1 == result2) {
            return;
        }
        if (result1 > result2) {
            array[result2] = result1;
            allCell.add(result2);
        } else {
            array[result1] = result2;
            allCell.add(result1);
        }
    }

    /**
     * 判断是否需要继续拆墙
     *
     * @return
     */
    public boolean isContinue() {
        // 第一个单元格和最后一个单元格是否连通
        if (find(0) != (size - 1)) {
            return true;
        }

        // 是否所有的单元格都连通
        if (allCell.size() < size - 1) {
            return true;
        }
        return false;
    }

    /**
     * 获取所有的可拆的墙, 迷宫由一面面很薄的墙组成, 除了表面的墙, 都是可拆的墙
     * 表面的墙都不可以拆除, 除了起始两面墙
     * 阶数  需要拆的墙   可拆墙数
     * 1	1 + 1	    0
     * 2	8 + 1	    4*3*(2-1) = 12
     * 3	27 + 1	    9*3*(3-1) = 54
     * 4	64 + 1	    16*3*(4-1) = 144
     * n	n^3 + 1	    n^2*3*(n-1) = 3*(n-1)*n^2
     */
    public List<Wall> getAllWalls() {
        // 保存所有可拆的墙
        ArrayList<Wall> allWalls = new ArrayList<>();

        // 迷宫单元格的标记, 从俯视图的左上角开始, 从左往右, 从上往下, 从前往后依次标记
        // 二阶迷宫一共8个单元格, 依次为0,1,2,3, 4,5,6,7
        // 一共size个元素
        for (int i = 0; i < (size - 1); i++) {
            // 右边的单元格
            int k = i + 1;

            // 下面的单元格
            int l = i + sqrt;

            // 后面的单元格
            int m = i + sqrt*sqrt;

            // 排除掉最右边的墙
            if (((i + 1) % sqrt) != 0) {
                allWalls.add(new MazeGenerate3D.Wall(i, k));
            }

            // 排除掉最下面的墙
            if ((sqrt*sqrt - sqrt) > (i%(sqrt*sqrt))) {
                allWalls.add(new MazeGenerate3D.Wall(i, l));
            }

            // 排除掉最后面的墙
            if (i < (size - sqrt*sqrt)) {
                allWalls.add(new MazeGenerate3D.Wall(i, m));
            }
        }

        return allWalls;
    }

    /**
     * 随机生成迷宫
     *
     * @param allWalls list
     * @return List<Wall>
     */
    public Set<String> generateMaze(List<Wall> allWalls) {
        Random random = new Random();

        // 用于存储待拆除的墙
        HashSet<String> toDelWalls = new HashSet<String>();

        // 拆除首尾节点的墙, 这是固定的, 进入迷宫的墙和离开迷宫的墙
        toDelWalls.add("1,1,0");
        toDelWalls.add(sqrt + "," + (2 * sqrt-1) + "," + (2 * sqrt));

        // 初始化各个单元格所属的集合
        for (int j = 0; j < size; j++) {
            array[j] = -1;
        }

        int count = 0;
        while (isContinue()) {
            count++;
            // 随机获取一面墙
            int currentWall = random.nextInt(allWalls.size());
            Wall wall = allWalls.get(currentWall);
            int firstCellCode = wall.getFirstCellCode();
            int secondCellCode = wall.getSecondCellCode();
            // 判断墙对应的两个单元格是否是连通的
            if (find(firstCellCode) == find(secondCellCode)) {
                // 如果连通,墙不需要拆除
                continue;
            } else {
                // 如果不连通,拆除墙
                int maxIndex = allWalls.size() - 1;
                allWalls.set(currentWall, allWalls.get(maxIndex));
                allWalls.remove(maxIndex);

                // 将两个单元格连通
                union(firstCellCode, secondCellCode);

                // 添加到需要拆除的墙集合里面
                toDelWalls.add(wall.getCoordinate());
            }
        }

        System.out.println("count: " + count);
        return toDelWalls;
    }

    /**
     * 打印迷宫到控制台
     *
     * 由于迷宫是3D的, 打印时需要像3D打印机那样一层一层的打印
     * 俯视图, 从上往下依次打印
     * 有两类墙, 一种与地面平行, 一种与地面垂直
     * 例:
     * 二阶平行墙俯视图
     *  _ _
     * |X|_|
     * |_|_|
     * 表示4块平行于地面的墙拼在一起, 打叉的表示被拆掉, 还剩3块墙
     *
     * 二阶垂直墙俯视图
     *  _ _
     * |_ _|
     * |_|_|
     * 表示12块垂直于地面的墙拼在一起, 中间上方丢失的那一竖表示被拆掉, 还剩11块墙
     *
     * @param toDelWalls
     */
    public void printMaze(Set<String> toDelWalls) {
        // z轴
        for (int k = 0; k < 2 * sqrt + 1; k++) {
            // 行
            for (int i = 0; i < (sqrt + 1); i++) {
                // 列
                for (int j = 0; j < (2 * sqrt + 1); j++) {
                    String toPrintStr = "";
                    String temp = i + "," + j + "," + k;
                    if (i % 2 == 0) {    // 奇数行
                        if (j % 2 == 0) {    //奇数列
                            if (i == 0) {    // 第一行
                                toPrintStr = " ";
                            } else {
                                toPrintStr = "|";
                            }
                        } else {              // 偶数列
                            toPrintStr = "_";
                        }
                    } else {                  // 偶数行
                        if (j % 2 == 0) {    //奇数列
                            toPrintStr = "|";
                        } else {              // 偶数列
                            toPrintStr = "_";
                        }
                    }
                    if (toDelWalls.contains(temp)) {
                        if (k % 2 == 0) {
                            toPrintStr = "X";
                        } else {
                            toPrintStr = " ";
                        }
                        toDelWalls.remove(temp);
                    }
                    System.out.print(toPrintStr);
                }
                System.out.println();
            }
        }
    }

    /**
     * 将迷宫保存到TXT文本中
     * 由于迷宫是3D的, 打印时需要像3D打印机那样一层一层的打印
     * 俯视图, 从上往下依次打印
     * 有两类墙, 一种与地面平行, 一种与地面垂直
     * 例:
     * 二阶平行墙俯视图
     *  _ _
     * |X|_|
     * |_|_|
     * 表示4块平行于地面的墙拼在一起, 打叉的表示被拆掉, 还剩3块墙
     *
     * 二阶垂直墙俯视图
     *  _ _
     * |_ _|
     * |_|_|
     * 表示12块垂直于地面的墙拼在一起, 中间上方丢失的那一竖表示被拆掉, 还剩11块墙
     *
     * @param toDelWalls
     * @param fileName
     */
    public void saveToText(Set<String> toDelWalls, String fileName) {
        File file = new File(fileName);
        try {
            Writer writer = new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8");
            StringBuilder builder = new StringBuilder();


            // z轴
            for (int k = 0; k < 2 * sqrt + 1; k++) {

                // 行
                for (int i = 0; i < (sqrt + 1); i++) {
                    // 列
                    for (int j = 0; j < (2 * sqrt + 1); j++) {
                        String toPrintStr = "";
                        String temp = i + "," + j + "," + k;
                        if (i % 2 == 0) {    // 奇数行
                            if (j % 2 == 0) {    //奇数列
                                if (i == 0) {    // 第一行
                                    toPrintStr = " ";
                                } else {
                                    toPrintStr = "|";
                                }
                            } else {              // 偶数列
                                toPrintStr = "_";
                            }
                        } else {                  // 偶数行
                            if (j % 2 == 0) {    //奇数列
                                toPrintStr = "|";
                            } else {              // 偶数列
                                toPrintStr = "_";
                            }
                        }
                        if (toDelWalls.contains(temp)) {
                            // 3D迷宫的打印分为两类墙, 一种是与地面平行的墙, 一种是与地面垂直的墙
                            if (k % 2 == 0) {
                                toPrintStr = "X";
                            } else {
                                toPrintStr = " ";
                            }
                            toDelWalls.remove(temp);
                        }
                        builder.append(toPrintStr);
                    }
                    builder.append("\r\n");
                }
            }

            writer.write(builder.toString());
            writer.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 保存关于墙所有的信息
     */
    public class Wall {
        // 墙对应的第一个单元格
        private int firstCellCode = 0;
        // 墙对应的第二个单元格
        private int secondCellCode = 0;

        // 这边的坐标以迷宫俯视图的左上角为起点, 向下为x轴, 向右为y轴, 向里为z轴
        // 坐标在后面画图会用到
        // 横坐标
        private int x = 0;
        // 纵坐标
        private int y = 0;
        // 竖坐标
        private int z = 0;

        // x,y,z组成的坐标系
        private String coordinate = "";

        public Wall(int firstCellCode, int secondCellCode) {
            this.firstCellCode = firstCellCode;
            this.secondCellCode = secondCellCode;

            if (sqrt * sqrt == (secondCellCode - firstCellCode)) {
                this.y = ((secondCellCode % (sqrt * sqrt)) % sqrt) * 2 + 1;
                this.z = ((secondCellCode / (sqrt * sqrt)) % sqrt) * 2;
            } else if (sqrt == (secondCellCode - firstCellCode)) {
                this.y = ((secondCellCode % (sqrt * sqrt)) % sqrt) * 2 + 1;
                this.z = (secondCellCode / (sqrt * sqrt)) * 2 + 1;
            } else if (1 == (secondCellCode - firstCellCode)) {
                this.y = ((secondCellCode % (sqrt * sqrt)) % sqrt) * 2;
                this.z = (secondCellCode / (sqrt * sqrt)) * 2 + 1;
            }

            this.x = (firstCellCode % (sqrt * sqrt)) / sqrt + 1;

            this.coordinate = x + "," + y + "," + z;
        }

        public int getFirstCellCode() {
            return firstCellCode;
        }

        public int getSecondCellCode() {
            return secondCellCode;
        }

        public String getCoordinate() {
            return coordinate;
        }
    }


    @Test
    public void test() {
        long startTime = System.currentTimeMillis();
        // 1.获取所有可拆卸的墙的信息
        List<Wall> allWalls = getAllWalls();

        long getWallTime = System.currentTimeMillis();
        System.out.println("getWallTime: " + (getWallTime - startTime) + "ms");

        // 2.随机生成迷宫,并记录需要被拆除的墙
        Set<String> toDelWalls = generateMaze(allWalls);

        long generateMazeTime = System.currentTimeMillis();
        System.out.println("generateMazeTime: " + (generateMazeTime - getWallTime) + "ms");

        // 3.打印迷宫到控制台
        printMaze(toDelWalls);
        // 输出迷宫到TXT文本
        //saveToText(toDelWalls, "d:" + File.separator + "10.txt");

    }

}

打印结果

效果图(2阶立体迷宫)
俯视图, 从上往下, 水平墙,垂直墙依次打印, 一共5层
在这里插入图片描述

图片解释

  • 二阶平行墙俯视图

在这里插入图片描述
表示4块平行于地面的墙拼在一起, 打叉的表示被拆掉, 还剩3块墙

  • 二阶垂直墙俯视图

在这里插入图片描述
表示12块垂直于地面的墙拼在一起, 中间上方丢失的那一竖表示被拆掉, 还剩11块墙

另附一百阶3D迷宫某一层效果图, 感觉进去就别想出来了
在这里插入图片描述

PS.以后星际时代的游乐园里面肯定会有这个娱乐项目

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前面几日我重新实现了迷宫的自动生成方法,并且添加了迷宫自动寻路方法。有了一个独立于GUI的迷宫库,我开始迫不及待地实现三维迷宫了! 下面是我在开发迷宫程序中遇到的问题。 1、怎样将迷宫类的行和列映射到真实的三维坐标中?迷宫应该用哪个参考系来描述? 其实我在制作的时候为了简化,将二维迷宫的左上角与三维的原点重合,二维迷宫的右对应三维的X轴正方向,迷宫的下对应Z轴的正方向。 2、迷宫的“上、下、左、右”在三维中应该叫做什么? 在确定好迷宫的位置后,我们将迷宫的上对应Z轴的负半轴,下对应Z轴的正半轴,左对应X轴的负半轴,右对应Y轴的正半轴。 3、三维点绘制顺序以及OpenGL裁剪模式造成的一些面不可见问题。 这个问题是我在编写二维迷宫没有想到的。主要是因为二维迷宫中描述墙是用一条直线,而到了三维则是一个面。由于在OpenGL中有裁剪模式可以选择,我使用了 glFrontFace( GL_CW ); // 顺时针的绘制为正面 glEnable( GL_CULL_FACE ); // 剔除不是正面的面 进行设定,也就是说,所有在摄像机看来是逆时针绘制的图形都无法显示。因此我不得不用同样的顶点绘制两个面。下面是相关的函数: void DrawInnerWall( Point3F& p1, Point3F& p2, Point3F& p3, Point3F& p4 ) { glTexCoord2f( 0.0f, 1.0f ); glVertex3fv( p1 ); glTexCoord2f( 1.0f, 1.0f ); glVertex3fv( p2 ); glTexCoord2f( 1.0f, 0.0f ); glVertex3fv( p3 ); glTexCoord2f( 0.0f, 0.0f ); glVertex3fv( p4 ); } void DrawOuterWall( Point3F& p1, Point3F& p2, Point3F& p3, Point3F& p4 ) { glTexCoord2f( 1.0f, 1.0f ); glVertex3fv( p1 ); glTexCoord2f( 0.0f, 1.0f ); glVertex3fv( p2 ); glTexCoord2f( 0.0f, 0.0f ); glVertex3fv( p3 ); glTexCoord2f( 1.0f, 0.0f ); glVertex3fv( p4 ); } 在编写这些函数的时候尤其注意纹理坐标的绘制顺序。可以在纸上绘制一个草图。下面是我调用的代码: if ( Cell_UpWall( cell ) ) { DrawInnerWall( p8, p7, p3, p4 ); DrawOuterWall( p7, p8, p4, p3 ); } 4、怎样设置阻挡? 设置阻挡的基本原则还是先检测后执行。首先我先尝试着执行走一步,再判断这一步是不是出现了越界问题。如果出现了越界问题,那么不执行这一步,否则执行这一步。为了不让我们无限地靠近墙,我设定了一个gap,即摄像机必须与墙保持gap的距离。 下面是我相关的代码: bool View3D::CanGo( Maze& maze, float step ) { static float gap = m_CellSize.w / 8.0f;// 摄像机与墙最近不能超过的间隔 const Point3F& pos = m_Camera.Pos( ); Point3F tryPos; if ( pos.y > 0 && pos.y ( row - 1 ) * m_CellSize.w ); if (

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值