与瓦片贴图一起使用的程序模式,第 2 部分

ETHAN BRUINS / UNITY TECHNOLOGIES Contributor
Jun 7, 2018|14 Min
与瓦片贴图一起使用的程序模式,第 2 部分
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

第 1 部分中,我们了解了使用各种方法(如 Perlin Noise 和 Random Walk)以程序化方式创建顶层图层的一些方法。在本篇文章中,我们将介绍一些使用程序生成创建洞穴的方法,让您对可能出现的各种变化有所了解。

我们将在本博文中讨论的所有内容都可以在这个项目中找到。欢迎下载这些资产并试用程序算法。

图片
图片

本博文遵守与第一部分相同的规则:

  • 我们区分是否是瓦片的方法是使用二进制。1 表示开启,0 表示关闭。
  • 我们将把所有地图存储到一个二维整数数组中,并在每个函数结束时返回给用户(渲染时除外)。
  • 我将使用数组函数GetUpperBound()来获取地图的高度和宽度。这意味着每个函数的变量数量会减少,从而使代码更加简洁。
  • 我经常使用Mathf.FloorToInt(),这是因为瓦片贴图坐标系从左下方开始,使用 Mathf.FloorToInt() 可以将数字四舍五入为整数。
  • 本博文提供的所有代码均使用 C#。
柏林噪声(Perlin noise)

在上一篇博文中,我们介绍了一些使用Perlin 噪声创建顶层的方法。幸运的是,我们还可以使用佩林噪声来创建一个洞穴。为此,我们需要获取一个新的 Perlin 噪声值,该值包含当前位置的参数乘以一个修改器。修改器的值介于 0 和 1 之间。修改器值越大,生成的 Perlin 就越混乱。然后,我们将该值四舍五入为 0 或 1 的整数,并将其存储到映射数组中。看一下实施情况:

未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器

我们使用修改器而不是种子的原因是,当我们将数值乘以 0 到 0.5 之间的数值时,Perlin 生成的结果会更好看。数值越小,效果越块状。请看一些结果。该 gif 从修改器值 0.01 开始,以 0.25 为增量。

图片
图片

从这张图片中可以看出,Perlin 生成器实际上只是在每一个刻度处放大图案。

随机漫步

在上一篇博文中,我们看到可以通过掷硬币来判断平台是上升还是下降。在本帖中,我们将使用相同的想法,但增加了左右两个选项。随机漫步算法的这种变体让我们可以创建洞穴。我们的做法是获取一个随机方向,然后移动我们的位置并移除瓷砖。我们继续这个过程,直到达到需要销毁的地板数量。目前,我们只使用 4 个方向:上、下、左、右。

未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器

我们在功能开始时

寻找起始位置

计算我们需要拆除的地砖数量

移除起始位置的瓷砖

楼层数增加一个

接下来,我们进入 while 循环。这将为我们创建洞穴:

未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器

那么,我们在这里做什么呢?

首先,我们使用随机数来决定移动的方向。接下来,我们用 switch case 语句检查新方向。在该语句中,我们会检查位置是否为墙。如果不是,我们就从数组中移除瓦片。我们继续这样做,直到达到所需的最低限额。最终结果如下所示:

图片
图片

我还创建了该功能的自定义版本,其中也包括对角线方向。这个函数的代码有点长,如果你想看,请查看本博文开头的项目链接。

定向隧道

定向隧道从地图的一端开始,然后向另一端延伸。我们可以通过输入函数来控制隧道的曲线和粗糙度。我们还可以确定隧道部件的最小和最大长度。让我们看看下面的执行情况:

public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness)
{
//该值从负值变为正值,在本例中,宽度值为 1 时,隧道的宽度为 3
int tunnelWidth = 1;
//将起始 X 位置设置为隧道中心
int x = map.GetUpperBound(0) / 2;

//用种子设置随机
System.Random rand = new System.Random(Time.time.GetHashCode());

//创建隧道的第一部分
for (int i = -tunnelWidth; i <= tunnelWidth; i++)
{
map[x + i, 0] = 0;
}

到底发生了什么?

我们首先设置一个宽度值。该宽度值将从负值变为正值。这样就能得到我们想要的实际尺寸。在这种情况下,我们使用的值是 1。由于我们将使用 -1, 0, 1 这三个值,因此总宽度为 3。

接下来我们要做的是设置起始 x 位置,方法是获取地图宽度的中间值。现在,我们已经设置好了第一个值,可以开始地图的第一部分。

图片
图片

现在,让我们开始绘制地图的其余部分。

未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器

生成一个随机数,与我们的粗糙度值进行核对,如果高于该值,我们就可以改变路径的宽度。我们还要检查宽度是否过小。有了接下来的这段代码,我们就可以在地图上边走边挖隧道了。每一步,我们都要做以下工作:

生成一个新的随机数,与我们的曲线值进行校验。与之前的检查一样,如果高于该值,我们就可以更改路径的中心点。我们还要检查是否偏离了地图边缘。

最后,我们将创建的新部分挖出隧道。

最终的实施结果是这样的:

图像
图像
细胞自动机

单元自动机使用单元邻域来确定当前单元是打开(1)还是关闭(0)。这些街区的基础是随机生成的单元网格。在本例中,我们将使用 C# 中的Random.Next函数生成初始网格。

因为我们有几种不同的蜂窝自动机实现方式,所以我为生成这个基础网格制作了一个单独的函数。功能如下

未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器

在这个函数中,我们还可以确定是否要在网格上设置墙壁。除此之外,其他都比较简单。我们根据填充百分比检查一个随机数,以确定当前单元格是打开还是关闭。看看结果吧:

图片
图片
摩尔街区

摩尔邻域用于帮助平滑细胞自动机的初始生成。摩尔社区是这样的

图片
图片

街区规则如下:

  • 检查每个方向是否有邻居。
  • 如果邻居是活动的瓦片,则在周围的瓦片上增加一个。
  • 如果邻居不是活动磁砖,则什么也不做。
  • 如果单元格周围有 4 块以上的瓦片,则将该单元格设为活动瓦片。
  • 如果单元格正好有 4 个环绕瓷片,则保留该瓷片。
  • 重复上述步骤,直到尝试过地图上的每一块瓷砖。

检查摩尔邻域的功能如下:

static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls)
{
/* Moore Neighbourhood 看起来像这样('T'是我们的瓷砖,'N'是我们的邻居)
*
* N N N
* N T N
* N N N
*
*/

int tileCount = 0;

for(int neighbourX = x - 1; neighbourX <= x + 1; neighbourX++)
{
for(int neighbourY = y - 1; neighbourY <= y + 1; neighbourY++)
{
if (neighbourX >= 0 && neighbourX < map.GetUpperBound(0) && neighbourY >= 0 && neighbourY < map.GetUpperBound(1))
{
//我们不想计算我们正在检查的瓦片的周围环境
if(neighbourX != x || neighbourY != y)
{
tileCount += map[neighbourX, neighbourY];
}
}
}
}
return tileCount;
}

检查完瓦片后,我们就可以在平滑函数中使用这些信息了。同样,与最初的蜂窝自动机生成一样,我们可以设置地图的边缘是否是墙壁。

public static int[,] SmoothMooreCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount)
{
for (int i = 0; i < smoothCount; i++)
{
for (int x = 0; x < map.GetUpperBound(0); x++)
{
for (int y = 0; y < map.GetUpperBound(1); y++)
{
int surroundingTiles = GetMooreSurroundingTiles(map, x, y, edgesAreWalls);

if (edgesAreWalls && (x == 0 || x == (map.GetUpperBound(0) - 1) || y == 0 || y == (map.GetUpperBound(1) - 1)))
{
//如果 edgesAreWalls 为 true,则将边缘设置为墙壁
map[x, y] = 1;
}
//默认的摩尔规则需要 4 个以上的邻居
else if (surroundingTiles > 4)
{
map[x, y] = 1;
}
else if (surroundingTiles < 4)
{
map[x, y] = 0;
}
}
}
}
//返回修改后的地图
return map;
}

在这个函数中需要注意的一个关键点是,我们有一个 for 循环来平滑通过地图一定的次数。最终,我们会得到一张更漂亮的地图。

图片
图片

我们还可以继续修改这种算法,例如,如果房间与房间之间只有两个街区,我们就可以将它们相互连接起来。

冯-诺伊曼邻域

冯-诺依曼邻域是蜂窝自动机的另一种常用实现方法。在这一代中,我们使用了比摩尔世代更简单的邻域。附近的情况是这样的

Title
Title

该街区的规则如下:

  • 检查瓷砖周围的直邻,不包括对角线。
  • 如果单元格处于活动状态,则在计数中加一。
  • 如果单元格不活动,则什么也不做。
  • 如果我们有 2 个以上的邻居,则使当前单元格处于活动状态。
  • 如果相邻单元格少于 2 个,则使当前单元格处于非活动状态。
  • 如果正好有 2 个相邻单元格,则不修改当前单元格。

第二个结果采用了与第一个结果相同的原则,但扩大了邻近区域。

我们使用下面的函数来检查邻居:

static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls)
{
/* 冯-诺依曼邻域看起来像这样('T'是我们的瓦片,'N'是我们的邻域)
*
* N
* N T N
* N
*
*/

int tileCount = 0;

//将边缘保留为墙壁
if(edgesAreWalls && (x - 1 == 0 || x + 1 == map.GetUpperBound(0) || y - 1 == 0 || y + 1 == map.GetUpperBound(1)))
{
tileCount++;
}

//确保我们没有接触到地图左侧
if(x - 1 > 0)
{
tileCount += map[x - 1, y];
}

//确保我们没有触及地图底部
if(y - 1 > 0)
{
tileCount += map[x, y - 1];
}

//确保我们没有碰到地图的右边
if(x + 1 < map.GetUpperBound(0))
{
tileCount += map[x + 1, y];
}

//确保我们没有触及地图顶部
if(y + 1 < map.GetUpperBound(1))
{
tileCount += map[x, y + 1];
}

return tileCount;
}

得出有多少个邻居后,我们就可以对数组进行平滑处理了。和之前一样,我们有一个 for 循环来遍历平滑所需的输入量。

public static int[,] SmoothVNCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount)
{
for (int i = 0; i < smoothCount; i++)
{
for (int x = 0; x < map.GetUpperBound(0); x++)
{
for (int y = 0; y < map.GetUpperBound(1); y++)
{
// 获取周围的瓷砖
int surroundingTiles = GetVNSurroundingTiles(map, x, y, edgesAreWalls);

if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1)))
{
//将我们的边缘保留为墙壁
map[x, y] = 1;
}
//von Neuemann Neighbourhood(冯-诺伊曼邻里关系)只要求将周围 3 个或更多的瓦片变为一个瓦片
else if (surroundingTiles > 2)
{
map[x, y] = 1;
}
else if (surroundingTiles < 2)
{
map[x, y] = 0;
}
}
}
}
//返回修改后的地图
return map;
}

如下图所示,最终效果比摩尔邻居看起来更块状:

图像
图像

同样,与摩尔邻里一样,我们也可以在生成的基础上运行一个额外的脚本,以便更好地连接地图上的各个区域。

结论

希望我的介绍能对你有所启发,在你的项目中开始使用某种形式的程序生成。如果您还没有下载该项目,可以从这里获取。如果你想了解有关程序生成地图的更多信息,请查看程序生成维基或 Roguebasin.com,它们都是很好的资源。

如果你使用程序生成技术做出了很酷的东西,请随时在Twitter上给我留言或在下面评论!

柏林联合展上的二维程序生成技术

想了解更多信息并获得现场演示?我还将在 6 月 20 日的柏林 Unite 展览会上,在展厅的迷你剧场中讲述如何将程序模式与瓦片贴图结合使用。讲座结束后,如果您想和我当面聊聊,我会在附近等您!