从零开始在Unity中创建类似于Rogue的游戏:地牢生成器

图片

这次,我们将深入研究地牢生成器算法的实现。在上一篇文章中,我们创建了第一个房间,现在我们将生成其余的地牢级别。

但是在我们开始之前,我想修复先前文章中的一个错误。实际上,最近几周我学到了一些新知识,这就是为什么我所做的一些工作已经过时了,我想谈一谈。

还记得我们创建的Position类吗?实际上,Unity已经有一个内置的类,可以执行完全相同的功能,但控制性稍好-声明和处理都更容易。此类称为Vector2Int。因此,在开始之前,我们将从MapManager.cs中删除Position类,并将每个Position变量替换为Vector2Int变量。


在DungeonGenerator.cs脚本中的多个地方都需要做同样的事情。现在让我们开始讨论算法的其余部分。

第七阶段-房间/大厅生成


我们将对上次创建的函数FirstRoom()进行小的更改。我们无需创建另一个函数来生成地图的所有其他元素并复制一堆代码,而只是将其转换为通用的GenerateFeature()。因此,将名称从FirstRoom更改为GenerateFeature。

现在我们需要将参数传递给该函数。首先,您需要知道它产生什么功能-房间或走廊。我们可以传递一个叫做type的字符串接下来,函数需要知道元素的起点,即它来自哪面墙(因为我们总是从较旧元素的墙创建一个新元素),为此,传递Wall参数就足够了。最后,要创建的第一个房间具有特殊的特征,因此我们需要一个可选的bool变量,该变量告诉该项目是否是第一个房间。默认情况下,它为false:bool isFirst = false。因此,函数标题将与此不同:


在此:


精细。下一步是更改计算元素的宽度和高度的方式。当我们计算它们时,在房间的高度和宽度的最小值和最大值之间获取一个随机值-这对于房间来说是理想的,但不适用于走廊。因此,到目前为止,我们有以下内容:


但是,走廊的宽度或高度将恒定为3,具体取决于方向。因此,我们需要检查元素是-房间还是走廊,然后执行适当的计算。


所以。我们检查物品是否是房间。如果是这样,那么我们将做与以前相同的操作
-在高度和宽度的最小值和最大值之间的间隔中获得一个随机数。但是,现在在其他的相同的,如果你需要做的东西有点不同。我们需要检查走廊的方向。幸运的是,生成墙时,我们会保存有关墙朝向的信息,因此我们可以使用它来获取走廊的方向。


但是我们尚未声明变量minCorridorLength。您需要回到变量声明并在maxCorridorLength上方声明它。


现在回到我们的条件开关语句。我们在这里做的是:获得方向的值,即墙在看的位置,从中走过的走廊。方向只能有四个可能的值:南,北,西和东。对于南边和北边,走廊的宽度为3(中间有两堵墙和一层地板),高度可变(长度)。对于西方和东方,一切都将相反:高度将始终等于3,宽度将具有可变的长度。因此,让我们开始吧。


哇。这就是我们最终确定新项目大小的地方。现在,您需要确定放置位置。我们将第一个房间放置在相对于地图中心的阈值内的随机位置。


但是对于所有其他元素,这将行不通。它们应从生成元素的墙的随机点开始。因此,让我们更改代码。首先,我们需要检查元素是否是第一个房间。如果这是第一个房间,那么我们将以与以前相同的方式定义起点-为地图的宽度和高度的一半。


其他情况下,如果元素不是第一个房间,那么我们将在生成元素的墙壁上获得一个随机点。首先,我们需要检查墙的大小是否为3(这意味着它是走廊的终点),如果是,则将始终选择中间点,即墙阵列的索引1(具有3个元素,该阵列具有索引0、1、2)。但是,如果大小不等于3(墙不是走廊的终点),则我们在点1与墙的长度减去2的长度之间的间隔中取一个随机点。这对于避免在拐角处创建通道是必要的。也就是说,例如,在长度为6的墙上,我们排除索引0和5(第一个和最后一个),并在点1、2、3和4中选择一个随机点。


现在,我们具有将在墙上创建新元素的点的位置。但是我们不能仅仅从那里开始生成一个元素,因为这样一来,它就会被已经放置的墙壁所阻挡。同样重要的是要注意,该元素从其左下角开始生成,然后向右和向上执行增量,因此我们必须根据墙壁的方向将初始位置设置在不同的位置。此外,第一列x和第一行y将是墙,如果我们在墙的某个点旁边开始一个新元素,则可以在房间的一角而不是墙的合适位置创建一条走廊。

因此,如果墙指向北,则必须使该元素从y轴向北的一个位置开始,但沿x轴向西的随机位置在1到Room-2的宽度范围内开始。在南向,x轴的作用相同,但是y轴的起始位置是墙上点的位置减去房间高度的位置。西墙和东墙遵循相同的逻辑,只是轴是倒置的。

但是在执行所有这些操作之前,我们需要将壁点的位置保存在Vector2Int变量中,以便以后可以对其进行操作。


大。来做吧。


因此,我们生成了具有大小和位置的元素,下一步是将该元素放置在地图上。但是首先,我们需要确定地图上此位置上是否确实存在该元素的空间。现在,我们只调用CheckIfHasSpace()函数。它将以红色突出显示,因为我们尚未实现它。在完成GenerateFeature()函数中需要在此处完成的操作后,我们将立即执行此操作。因此,请忽略红色下划线并继续。


在下一部分中,将创建墙。直到我们接触它,第二个for循环中的片段除外


在撰写本文时,我注意到这些if-else构造是完全错误的。例如,其中某些墙的长度将为1。这是因为,当要向北墙添加位置时,如果该位置在与东墙的转角处,则不会按原样将其添加到东墙。这导致生成算法中令人讨厌的错误。让我们消除它们。

修复它们非常简单。删除其他所有内容就足够了,以便该位置通过所有if构造,并且在返回true时不停在第一个位置。然后将最后一个else(不是if的另一个)更改为if,它检查位置是否已添加为“墙”,如果尚未添加,则将其添加为“地板”。


太神奇了,我们在这里差不多完成了。现在,我们有了一个全新的元素,该元素在正确的位置创建,但它与我们的第一个房间相同:它完全被墙壁包围。这意味着玩家将无法到达这个新地方。也就是说,我们需要转换墙上的一个点(我们记得,该点存储在Vector2Int类型的变量中)和地板上新元素的墙上的对应点。但仅当元素不是第一个房间时。


这段代码检查新项目是否是第一个房间。如果不是,它将把墙的最后位置转换为地板,然后检查墙的方向,以检查新元素的哪一块应变成地板。

我们到了GenerateFeature()函数的最后一部分。它已经有添加有关该函数创建的元素的信息的行。


在这里我们需要改变一些东西。首先,元素类型并不总是等于Room。幸运的是,所需的变量作为参数(即类型字符串)传递给函数。因此,我们在这里用type替换“ Room”。


好。现在,为了使生成游戏所有元素的算法正常工作,我们需要在此处添加新数据。即,一个int会计算创建的项目数以及所有创建的项目的列表。我们到达声明所有变量的地方,并声明一个名为countFeatures的int以及一个名为allFeatures的元素列表。所有元素的列表必须是公共的,并且int计数器可以是私有的。


现在返回到GenerateFeature()函数,并在最后添加几行:增加变量countFeatures并将新元素添加到allFeatures列表中。


因此,我们的GenerateFeature()即将完成。稍后,我们将需要返回到它来填充空的CheckIfHasSpace函数,但是首先我们需要创建它。这就是我们现在要做的。

阶段8-检查是否有地方


现在,让我们在GenerateFeature()函数完成后立即创建一个新函数。她需要两个参数:元素开始的位置和元素结束的位置。您可以将两个Vector2Int变量用作它们。该函数应返回布尔值,以便可以在是否检查空间时使用它


它带有红色下划线,因为到目前为止它还没有返回任何东西。很快我们将修复它,但是现在我们将不关注。在此函数中,我们将遍历元素开始和结束之间的所有位置,并检查MapManager.map中的当前位置是否为null或已经有东西。如果那里有东西,那么我们停止该函数并返回false。如果不是,则继续。如果函数到达循环末尾但未满足填充的位置,则返回true。

另外,在检查位置是否为空之前,我们需要一行来检查位置是否在地图内。因为否则,我们可能会得到数组索引错误和游戏崩溃。


精细。现在回到我们在GenerateFeature()函数中插入此函数的地方。我们需要解决此调用,因为它没有传递必要的参数。

在这里,我们要插入一条if语句来检查元素是否有足够的空间。如果结果为假,则无需在MapManager.map中插入新元素即可结束函数。


我们需要传递必需的参数,即两个Vector2Int变量。首先,一切都很简单,这是元素起点的x和y坐标位置。


第二个难度更大,但幅度不大。这是起点加上y的高度和x的宽度,两者都减去1(因为已经考虑了起点)。


现在让我们继续下一步-创建一个算法来调用GenerateFeature()函数。

阶段9-调用生成的元素


返回本文前面部分中创建的GenerateDungeon()函数。现在看起来应该像这样:


由于我们更改了此函数的名称,因此对FirstRoom()的调用用红色下划线标出。因此,让我们称第一代房间。


我们传递了必要的参数:“ Room”作为类型,因为第一个房间将始终是Room,new Wall(),因为第一个房间将不会从其他任何房间创建,因此我们只传递了null,这是很正常的。可以使用null代替新的Wall(),这是个人喜好问题。最后一个参数确定新元素是否是第一个房间,因此在本例中,我们传递true

现在我们来谈重点。我们使用一个for循环,该循环将运行500次-是的,我们将尝试添加元素500次。但是,如果创建的元素数(countFeatures变量)等于指定的最大元素数(maxFeatures变量),则我们中断此循环。


此循环的第一步是声明将从中创建新元素的元素。如果我们仅创建一个元素(第一个房间),那么它将是原始元素。否则,我们将随机选择一个已创建的元素。


现在,我们将选择将使用该元素的哪面墙来创建新元素。


请注意,我们还没有此ChoseWall()函数。让我们快速编写它。转到函数末尾并创建它。它应该返回一堵墙,并使用一个元素作为参数,以便函数可以选择此元素的墙。


我在CheckIfHasSpace()和DrawMap()函数之间创建了它。请注意,如果您正在与Unity一起安装的Visual Studio中工作,则可以使用左侧的-/ +字段折叠/展开部分代码以简化工作。

在此功能中,我们将找到尚未从中创建元素的墙。有时我们会获得带有一个或多个墙的元素,其中的其他元素已经连接,因此我们需要一次又一次地检查任意随机墙是否空闲。为此,我们使用一个for循环重复十次-如果在这十次之后没有找到空闲墙,则该函数返回null。


现在返回到GenerateDungeon()函数,并将原始元素作为参数传递给ChoseWall()函数。


该行if (wall == null) continue;表示如果wall搜索函数返回false,则原始元素无法生成新元素,因此该函数将继续循环,即,它无法创建新元素并继续进行循环的下一个迭代。

现在我们需要为下一项选择类型。如果源元素是一个房间,则下一个必须是走廊(我们不希望该房间直接通向另一个房间,而它们之间没有走廊)。但是,如果这是一个走廊,那么我们需要确定下一个走廊或房间将成为下一个走廊的可能性。


精细。现在,我们只需要调用GenerateFeature()函数,将墙壁和类型作为参数传递给它。


最后,转到Unity检查器,选择GameManager对象并将值更改为以下内容:


如果现在单击“播放”按钮,那么您已经可以看到结果了!


正如我所说,这不是最好的地牢。我们有很多死胡同。但是它具有完整的功能,并且可以保证您不会有没有其他任何房间的房间。

我希望你喜欢它!在下一篇文章中,我们将创建一个将在地牢中移动的玩家,然后将地图从ASCII转换为精灵。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Post3 : MonoBehaviour {
    public int mapWidth;
    public int mapHeight;

    public int widthMinRoom;
    public int widthMaxRoom;
    public int heightMinRoom;
    public int heightMaxRoom;

    public int minCorridorLength;
    public int maxCorridorLength;
    public int maxFeatures;
    int countFeatures;

    public bool isASCII;

    public List<Feature> allFeatures;

    public void InitializeDungeon() {
        MapManager.map = new Tile[mapWidth, mapHeight];
    }

    public void GenerateDungeon() {
        GenerateFeature("Room", new Wall(), true);

        for (int i = 0; i < 500; i++) {
            Feature originFeature;

            if (allFeatures.Count == 1) {
                originFeature = allFeatures[0];
            }
            else {
                originFeature = allFeatures[Random.Range(0, allFeatures.Count - 1)];
            }

            Wall wall = ChoseWall(originFeature);
            if (wall == null) continue;

            string type;

            if (originFeature.type == "Room") {
                type = "Corridor";
            }
            else {
                if (Random.Range(0, 100) < 90) {
                    type = "Room";
                }
                else {
                    type = "Corridor";
                }
            }

            GenerateFeature(type, wall);

            if (countFeatures >= maxFeatures) break;
        }

        DrawMap(isASCII);
    }

    void GenerateFeature(string type, Wall wall, bool isFirst = false) {
        Feature room = new Feature();
        room.positions = new List<Vector2Int>();

        int roomWidth = 0;
        int roomHeight = 0;

        if (type == "Room") {
            roomWidth = Random.Range(widthMinRoom, widthMaxRoom);
            roomHeight = Random.Range(heightMinRoom, heightMaxRoom);
        }
        else {
            switch (wall.direction) {
                case "South":
                    roomWidth = 3;
                    roomHeight = Random.Range(minCorridorLength, maxCorridorLength);
                    break;
                case "North":
                    roomWidth = 3;
                    roomHeight = Random.Range(minCorridorLength, maxCorridorLength);
                    break;
                case "West":
                    roomWidth = Random.Range(minCorridorLength, maxCorridorLength);
                    roomHeight = 3;
                    break;
                case "East":
                    roomWidth = Random.Range(minCorridorLength, maxCorridorLength);
                    roomHeight = 3;
                    break;

            }
        }

        int xStartingPoint;
        int yStartingPoint;

        if (isFirst) {
            xStartingPoint = mapWidth / 2;
            yStartingPoint = mapHeight / 2;
        }
        else {
            int id;
            if (wall.positions.Count == 3) id = 1;
            else id = Random.Range(1, wall.positions.Count - 2);

            xStartingPoint = wall.positions[id].x;
            yStartingPoint = wall.positions[id].y;
        }

        Vector2Int lastWallPosition = new Vector2Int(xStartingPoint, yStartingPoint);

        if (isFirst) {
            xStartingPoint -= Random.Range(1, roomWidth);
            yStartingPoint -= Random.Range(1, roomHeight);
        }
        else {
            switch (wall.direction) {
                case "South":
                    if (type == "Room") xStartingPoint -= Random.Range(1, roomWidth - 2);
                    else xStartingPoint--;
                    yStartingPoint -= Random.Range(1, roomHeight - 2);
                    break;
                case "North":
                    if (type == "Room") xStartingPoint -= Random.Range(1, roomWidth - 2);
                    else xStartingPoint--;
                    yStartingPoint ++;
                    break;
                case "West":
                    xStartingPoint -= roomWidth;
                    if (type == "Room") yStartingPoint -= Random.Range(1, roomHeight - 2);
                    else yStartingPoint--;
                    break;
                case "East":
                    xStartingPoint++;
                    if (type == "Room") yStartingPoint -= Random.Range(1, roomHeight - 2);
                    else yStartingPoint--;
                    break;
            }
        }

         if (!CheckIfHasSpace(new Vector2Int(xStartingPoint, yStartingPoint), new Vector2Int(xStartingPoint + roomWidth - 1, yStartingPoint + roomHeight - 1))) {
            return;
        }

        room.walls = new Wall[4];

        for (int i = 0; i < room.walls.Length; i++) {
            room.walls[i] = new Wall();
            room.walls[i].positions = new List<Vector2Int>();
            room.walls[i].length = 0;

            switch (i) {
                case 0:
                    room.walls[i].direction = "South";
                    break;
                case 1:
                    room.walls[i].direction = "North";
                    break;
                case 2:
                    room.walls[i].direction = "West";
                    break;
                case 3:
                    room.walls[i].direction = "East";
                    break;
            }
        }

        for (int y = 0; y < roomHeight; y++) {
            for (int x = 0; x < roomWidth; x++) {
                Vector2Int position = new Vector2Int();
                position.x = xStartingPoint + x;
                position.y = yStartingPoint + y;

                room.positions.Add(position);

                MapManager.map[position.x, position.y] = new Tile();
                MapManager.map[position.x, position.y].xPosition = position.x;
                MapManager.map[position.x, position.y].yPosition = position.y;

                if (y == 0) {
                    room.walls[0].positions.Add(position);
                    room.walls[0].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                if (y == (roomHeight - 1)) {
                    room.walls[1].positions.Add(position);
                    room.walls[1].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                if (x == 0) {
                    room.walls[2].positions.Add(position);
                    room.walls[2].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                if (x == (roomWidth - 1)) {
                    room.walls[3].positions.Add(position);
                    room.walls[3].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                if (MapManager.map[position.x, position.y].type != "Wall") {
                    MapManager.map[position.x, position.y].type = "Floor";
                }
            }
        }

        if (!isFirst) {
            MapManager.map[lastWallPosition.x, lastWallPosition.y].type = "Floor";
            switch (wall.direction) {
                case "South":
                    MapManager.map[lastWallPosition.x, lastWallPosition.y - 1].type = "Floor";
                    break;
                case "North":
                    MapManager.map[lastWallPosition.x, lastWallPosition.y + 1].type = "Floor";
                    break;
                case "West":
                    MapManager.map[lastWallPosition.x - 1, lastWallPosition.y].type = "Floor";
                    break;
                case "East":
                    MapManager.map[lastWallPosition.x + 1, lastWallPosition.y].type = "Floor";
                    break;
            }
        }

        room.width = roomWidth;
        room.height = roomHeight;
        room.type = type;
        allFeatures.Add(room);
        countFeatures++;
    }

    bool CheckIfHasSpace(Vector2Int start, Vector2Int end) {
        for (int y = start.y; y <= end.y; y++) {
            for (int x = start.x; x <= end.x; x++) {
                if (x < 0 || y < 0 || x >= mapWidth || y >= mapHeight) return false;
                if (MapManager.map != null) return false;
            }
        }

        return true;
    }

    Wall ChoseWall(Feature feature) {
        for (int i = 0; i < 10; i++) {
            int id = Random.Range(0, 100) / 25;
            if (!feature.walls[id].hasFeature) {
                return feature.walls[id];
            }
        }
        return null;
    }

    void DrawMap(bool isASCII) {
        if (isASCII) {
            Text screen = GameObject.Find("ASCIITest").GetComponent<Text>();

            string asciiMap = "";

            for (int y = (mapHeight - 1); y >= 0; y--) {
                for (int x = 0; x < mapWidth; x++) {
                    if (MapManager.map[x, y] != null) {
                        switch (MapManager.map[x, y].type) {
                            case "Wall":
                                asciiMap += "#";
                                break;
                            case "Floor":
                                asciiMap += ".";
                                break;
                        }
                    }
                    else {
                        asciiMap += " ";
                    }

                    if (x == (mapWidth - 1)) {
                        asciiMap += "\n";
                    }
                }
            }

            screen.text = asciiMap;
        }
    }
}

All Articles