roguelike地牢生成算法


文章原地址

上一個地圖生成算法,這一次是一個地牢的生成算法,是一個國外的人寫的算法,用dart語言寫,我把它改成了unity-c#。

原作者博客地址:Rooms and Mazes: A Procedural Dungeon Generator

當然,我看英文很吃力,好不容易找了一篇翻譯后的文章,分享給英語不太好的人。

一個翻譯后的版本:房間和迷宮:一個地牢生成算法

然后原作者的算法代碼地址(dart):github

算法的原理請看原文地址或者翻譯地址,那里有各種動態演示圖,講解的也很清楚,代碼可以看原作者的代碼,因為我沒有學過dart,改寫c#的過程很有可能有錯誤,請見諒!

原作者的代碼用到了第三方的一個庫,github地址,可以參考這個看原作者代碼。

c#代碼:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.Threading;

class Directions{
    public static Vector2 none = new Vector2 (0,0);
    public static Vector2 up = new Vector2 (0,1);
    public static Vector2 down = new Vector2 (0,-1);
    public static Vector2 left = new Vector2 (-1,0);
    public static Vector2 right = new Vector2 (1,0);

    public static Vector2[] all = {up,down,left,right};

}

//確保地圖的長寬是奇數
public class generateDungeon2 : MonoBehaviour {
    //嘗試生成房間的數量
    public int numRoomTries = 50;
    //在已經連接的房間和走廊中再次連接的機會,使得地牢不完美
    public int extraConnectorChance = 20;
    //控制生成房間的大小
    public int roomExtraSize = 0;
    //控制迷宮的曲折程度
    public int windingPercent = 0; 
    public int width = 51;
    public int height = 51;
    public GameObject wall, floor,connect;

    private Transform mapParent;
    //生成的有效房間
    private List<Rect> rooms;
    //正被雕刻的區域的索引。(每個房間一個索引,每個不連通的迷宮一個索引,在連通之前)
    private int currentRegion = 0;
    //原文https://github.com/munificent/piecemeal Array2D
    //改成int[,]
    private int[,] _regions;
    private Tiles[,] map;

    void Start () {
        rooms = new List<Rect> ();
        map = new Tiles[width,height];
        _regions = new int[width,height];
        mapParent = GameObject.FindGameObjectWithTag ("mapParent").transform;
        Generate ();
    }

    void Update () {
        if (Input.GetKeyDown (KeyCode.Q)) {
            Generate ();
        }
    }

    public void Generate(){
        if (width % 2 == 0 || height % 2 == 0) {
            Debug.Log ("地圖長寬不能為偶數");
            return;
        }
        InitMap ();
        AddRooms ();
        FillMaze ();
        ConnectRegions ();
        RemoveDeadEnds ();
        InstanceMap ();
    }


    /*
     *生成房間
     *1.隨機房間(隨機大小,奇數)
     *2.查看是否重疊,否則加入房間數組
     */
    private void AddRooms(){
        for (int i = 0; i < numRoomTries; i++) {
            //確保房間長寬為奇數
            int size = Random.Range(1,3+roomExtraSize)*2+1;
            int rectangularity = Random.Range (0, 1 + size / 2) * 2;
            int w = size, h = size;
            if (0 == Random.Range (0, 1)) {
                w += rectangularity;
            } else {
                h += rectangularity;
            }
            int x = Random.Range (0, (width - w) / 2) * 2 + 1;
            int y = Random.Range (0, (height - h) / 2) * 2 + 1;
            Rect room = new Rect (x,y,w,h);
            //判斷房間是否和已存在的重疊
            bool overlaps = false;
            foreach (Rect r in rooms) {
                if(room.Overlaps(r)){
                    overlaps = true;
                    break;
                }
            }
            //如果重疊,拋棄該房間
            if (overlaps)
                continue;
            //如果不重疊,把房間放入rooms中
            rooms.Add(room);
            //設置新房間索引
            StartRegion();

            for (int j = x; j < x + w; j++) {
                for (int k = y; k < y + h; k++) {
                    Carve (new Vector2 (k, j));
                }
            }
        }
    }

    /*
     * 填充迷宮(洪水填充)
     * 
     */ 
    private void FillMaze(){
        //0處為牆
        for (int x = 1; x < width; x += 2) {
            for (int y = 1; y < height; y += 2) {
                Vector2 pos = new Vector2 (x,y);
                //if (map [pos] == Tiles.Wall) {
                if (map [x,y] == Tiles.Wall) {
                    GrowMaze (pos);
                }
            }
        }
    }

    /*
     * 生成迷宮
     */ 
    private void GrowMaze(Vector2 start){
        List<Vector2> cells = new List<Vector2> ();
        Vector2 lastDir = Directions.none;
        StartRegion ();
        //cells添加之前需要變成Floor
        Carve (start);
        cells.Add (start);
        while (cells != null && cells.Count != 0) {
            Vector2 cell = cells [cells.Count - 1];
            //可以擴展的方向的集合
            List<Vector2> unmadeCells = new List<Vector2> ();
            //加入能擴展迷宮的方向
            foreach (Vector2 dir in Directions.all) {
                if (CanCarve (cell, dir)) {
                    unmadeCells.Add (dir);
                }
            }
            if (unmadeCells != null && unmadeCells.Count != 0) {
                Vector2 dir;
                //得到擴展方向 windingPercent用來控制是否為原方向
                if (unmadeCells.Contains (lastDir) && Random.Range (0, 100) > windingPercent) {
                    dir = lastDir;
                } else {
                    dir = unmadeCells [Random.Range (0, unmadeCells.Count - 1)];
                }

                Carve (cell + dir);
                Carve (cell + dir * 2);
                //添加第二個單元
                cells.Add (cell + dir * 2);
                lastDir = dir;
            } else {
                //沒有相鄰可以雕刻的單元,就刪除
                cells.Remove (cells[cells.Count - 1]);
                //置空路徑
                lastDir = Directions.none;
            }

        }
    }

    /*
     * 連通房間和迷宮
     */ 
    private void ConnectRegions(){
        //找到區域所有可連接的空間牆wall
        Dictionary<Vector2,List<int>> connectorRegions = new Dictionary<Vector2, List<int>> ();
        for (int i = 1; i < width - 1; i++) {
            for (int j = 1; j < height - 1; j++) {
                //不是牆的跳過
                if (map [i, j] != Tiles.Wall)
                    continue;
                List<int> regions = new List<int> ();
                foreach (Vector2 dir in Directions.all) {
                    int region = _regions [i + (int)dir.x, j + (int)dir.y];
                    //如果周圍不是牆(牆的索引為regions的初始值為0)
                    //去重
                    if (region != 0 && !regions.Contains(region))
                        regions.Add (region);
                }
                //如果這個牆沒有連接一個以上的區域,那就不是一個連接點
                if (regions.Count < 2)
                    continue;
                connectorRegions [new Vector2 (i, j)] = regions;
                //標志連接點
                //SetConnectCube(i,j);
            }
        }
        //所有連接點
        List<Vector2> connectors = connectorRegions.Keys.ToList<Vector2>();
        //跟蹤哪些區域已合並。將區域索引映射為它已合並的區域索引。
        List<int> merged = new List<int>();
        List<int> openRegions = new List<int> ();
        for (int i = 0; i <= currentRegion; i++) {
            merged.Add (i);
            openRegions.Add (i);
        }
        //使區域連接最終只剩下一個
        while (openRegions.Count > 1) {
            //隨機選擇一個連接點
            Vector2 connector = connectors[Random.Range(0,connectors.Count-1)];
            //連接
            AddJunction(connector);
            //合並連接區域我們將選擇第一個區域(任意)和
            //將所有其他區域映射到其索引。
            //connectorRegions[connector]
            List<int> regions = connectorRegions[connector];
            for (int i = 0; i < regions.Count; i++) {
                regions[i] = merged[regions[i]];
            }
            int dest = regions[0];
            regions.RemoveAt (0);
            List<int> sources = regions;
            //合並所有受影響的區域
            for(int i=0;i<currentRegion;i++){
                if (sources.Contains (merged [i])) {
                    merged [i] = dest;
                }
            }
            //移除已經連接的區域
            foreach (int s in sources) {
                openRegions.RemoveAll (value => (value==s));
            }
            connectors.RemoveAll (index=>IsRemove(merged,connectorRegions,connector,index));
        }
    }
        

    /*
     * 簡化迷宮
     */ 
    private void RemoveDeadEnds(){
        bool done = false;
        while (!done) {
            done = true;
            for (int i = 1; i < width - 1; i++) {
                for (int j = 1; j < height - 1; j++) {
                    if (map [i, j] == Tiles.Wall)
                        continue;
                    int exists = 0;
                    foreach (Vector2 dir in Directions.all) {
                        if (map [i + (int)dir.x, j + (int)dir.y] != Tiles.Wall) {
                            exists++;
                        }
                    }
                    //如果exists==1則是三面環牆
                    if (exists != 1) {
                        continue;
                    }
                    done = false;
                    _regions [i, j] = 0;//變成牆
                    map [i, j] = Tiles.Wall;
                }
            }
        }
    }

    /*
     *保存區域索引
     * 
     */ 
    private void StartRegion() {
        currentRegion++;
    }

    /*
     * 雕塑點,設置這個點的類型,默認地板
     * 
     */ 
    private void Carve(Vector2 pos,Tiles type=Tiles.Floor) {
        int x = (int)pos.x, y = (int)pos.y;
        map [x, y] = Tiles.Floor;
        _regions [x,y] = currentRegion;
    }

    //dir是方向
    private bool CanCarve(Vector2 pos,Vector2 dir){
        Vector2 temp = pos + 3*dir;
        int x = (int)temp.x, y = (int)temp.y;
        //判斷是否超過邊界
        if (x < 0 || x > width || y < 0 || y > height) {
            return false;
        }
        //需要判斷方向第二個單元的原因是cells中需要添加下一個cell
        //所以下一個cell要變為Floor,然后需要判斷是否第二個單元是否為牆
        //如果不為牆,則第一個cell被變為Floor為,和第二個單元就連通了,不可行
        //判斷第二個單元主要用來判斷不能&其他房間或走廊(regions)連通
        temp = pos + 2 * dir;
        x = (int)temp.x;
        y = (int)temp.y;
        //是牆則能雕刻迷宮
        return map [x, y] == Tiles.Wall;
    }

    private void AddJunction(Vector2 pos){
        map [(int)pos.x, (int)pos.y] = Tiles.Floor;
    }

    /*
     * 刪除不需要的連接點
     */ 
    private bool IsRemove(List<int> merged,Dictionary<Vector2,List<int>> ConnectRegions,Vector2 connector,Vector2 pos){
        //不讓連接器相連(包括斜向相連)
        if((connector-pos).SqrMagnitude() < 2){
            return true;
        }
        List<int> temp = ConnectRegions[pos];
        for(int i=0;i<temp.Count;i++){
            temp[i] = merged[temp[i]];
        }
        HashSet<int> set = new HashSet<int>(temp);
        //判斷連接點是否和兩個區域相鄰,不然移除
        if(set.Count>1){
            return false;
        }
        //增加連接,使得地圖連接不是單連通的
        if(Random.Range(0,extraConnectorChance)==0) AddJunction(pos);
        return true;
    }

    private void SetConnectCube(int i,int j){
        GameObject go = Instantiate (connect, new Vector3 (i, j, 1), Quaternion.identity) as GameObject;
        go.transform.SetParent (mapParent);
        go.layer = LayerMask.NameToLayer ("wall");
    }

    /*
     * 地圖全部初始化為牆
     * 
     */ 
    private void InitMap(){
        for (int x = 0; x < width; x ++) {
            for (int y = 0; y < height; y ++) {
                map [x, y] = Tiles.Wall;
            }
        }
    }

    private void InstanceMap (){
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                if (map [i, j] == Tiles.Floor) {
                    GameObject go = Instantiate (floor, new Vector3 (i, j, 1), Quaternion.identity) as GameObject;
                    go.transform.SetParent (mapParent);
                    //設置層級
                    go.layer = LayerMask.NameToLayer ("floor");
                } else if (map [i, j] == Tiles.Wall) {
                    GameObject go = Instantiate (wall, new Vector3 (i, j, 1), Quaternion.identity) as GameObject;
                    go.transform.SetParent (mapParent);
                    go.layer = LayerMask.NameToLayer ("wall");
                }
            }
        }
    }

}

效果圖:

這個是一開始的生成房間的效果圖:

下圖是對地圖空白部分進行迷宮填充:


下圖是對迷宮進行連接點計算:

下圖是對地圖的區域進行連接:

最后是對地圖中的死胡同進行消除:



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM