随机二维洞穴发生器

前言


如果您懒得打发时间,为游戏打水漂,那么您来对地方了。

本文将以高地和洞穴为例,详细介绍如何使用许多其他生成方法之一。 我们将考虑Aldous-Broder算法以及如何使生成的洞穴更美丽。

在阅读本文结束时,您应该会得到以下内容:

总结


理论


山地


老实说,这个洞穴可以从头开始,但是它会很难看吗? 在用于放置地雷的“平台”中,我选择了山脉。
这个山很简单地生成:让我们有一个二维数组和一个可变高度 ,初始高度等于第二维数组长度的一半; 我们只需遍历其列,然后将列中所有行填充为可变的高度值,并以随机的机会向上或向下更改它。

山洞


为了自己生成地牢,在我看来 ,我选择了一种出色的算法。 简单来说,可以解释如下:即使我们有两个(也许是十个)变量XY以及由50 x 50组成的二维数组,我们也会在数组中给这些变量提供随机值,例如X = 26Y = 28 。 之后,我们会多次执行相同的操作:我们会得到一个从零到

2

在我们的情况下,最多四个 ; 然后,根据退出的数量,我们进行更改
我们的变量:

switch (Random.Range(0, 4)) { case 0: X += 1; break; case 1: X -= 1; break; case 2: Y += 1; break; case 3: Y -= 1; break; } 

然后,当然,我们检查是否有任何变量超出了我们领域的范围:

  X = X < 0 ? 0 : (X >= 50 ? 49 : X); Y = Y < 0 ? 0 : (Y >= 50 ? 49 : Y); 

完成所有这些检查之后,我们在数组的新XY值中执行某些操作(例如:向元素添加一个)

 array[X, Y] += 1; 

准备工作


为了简化实现和方法的可视化,我们将绘制结果对象吗? 我很高兴你不介意! 我们将使用Texture2D做到这一点。

要工作,我们只需要两个脚本:
ground_libray是本文的重点。 在这里,我们生成,清理并绘制
ground_generator是我们的ground_libray将使用的
让第一个是静态的,并且不会继承任何东西:

 public static class ground_libray 

第二个是正常的,只是我们不需要Update方法。

另外,让我们使用SpriteRenderer组件在舞台上创建一个游戏对象

实践部分


它由什么组成?


为了处理数据,我们将使用二维数组。 您可以采用不同类型的数组,从byteintColor ,但是我相信最好做到这一点:

新类型
我们将此内容写在ground_libray中

 [System.Serializable] public class block { public float[] color = new float[3]; public block(Color col) { color = new float[3] { col.r, col.g, col.b }; } } 


我将通过允许我们保存数组并在必要时对其进行修改的事实来对此进行解释。

地块


在开始生成山峰之前,让我们指定要存储山峰的地方。

ground_generator脚本中我这样写:

  public int ground_size = 128; ground_libray.block[,] ground; Texture2D myT; 

ground_size-我们的字段的大小(即数组将由16384个元素组成)。
ground_libray.block [,] ground-这是我们要产生的领域。
我们将借鉴Texture2D myT

如何运作?
与我们合作的原则如下 -我们将从ground_generator调用一些ground_libray方法, 首先给出我们的ground字段。

让我们在ground_libray脚本中创建第一个方法:

造山
  public static float mount_noise = 0.02f; public static void generate_mount(ref block[,] b) { int h_now = b.GetLength(1) / 2; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < h_now; y++) { b[x, y] = new block(new Color(0.7f, 0.4f, 0)); h_now += Random.value > (1.0f - mount_noise) ? (Random.value > 0.5 ? 1 : -1) : 0; } } 

马上,我们将尝试了解这里发生的情况:正如我所说,我们只需遍历数组b的列,同时更改高度变量h_now ,该值最初等于一半128 (64) 。 但是仍然有一些新东西-mount_noise 。 该变量负责更改h_now的机会,因为如果您经常更改高度,山峰将看起来像梳子

色泽
我立即设置了略带褐色的颜色,让它至少成为一些颜色-将来我们将不再需要它。

现在,转到ground_generator,并在Start方法中编写以下代码:

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); 

一旦需要完成,我们就初始化变量Ground
之后,无需解释,将其发送到ground_libray
因此,我们产生了这座山。

我为什么看不到我的山?


现在让我们来画画我们得到的!

为了进行绘制,我们将在ground_libray中编写以下方法:

画图
  public static void paint(block[,] b, ref Texture2D t) { t = new Texture2D(b.GetLength(0), b.GetLength(1)); t.filterMode = FilterMode.Point; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) { t.SetPixel(x, y, new Color(0, 0, 0, 0)); continue; } t.SetPixel(x, y, new Color( b[x, y].color[0], b[x, y].color[1], b[x, y].color[2] ) ); } t.Apply(); } 

在这里,我们将不再给别人一个领域,我们只会给它一个副本(尽管由于class这个词,我们提供的不仅仅是一个副本) 。 我们还将这个方法提供Texture2D

前两行:我们创建字段大小的纹理,并删除过滤

之后,我们将遍历整个数组字段,并且不创建任何内容(该类需要初始化) -我们绘制一个空框,否则,如果它不为空-我们绘制保存到元素中的内容。

并且,当然,完成后,我们转到ground_generator并添加以下内容:

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); //   ground_libray.paint(ground, ref myT); GetComponent<SpriteRenderer>().sprite = Sprite.Create(myT, new Rect(0, 0, ground_size, ground_size), Vector3.zero ); 

但是无论我们在纹理上绘制多少,在游戏中我们只能通过将画布放置在某些物体上才能看到它:

SpriteRenderer任何地方都不接受Texture2D ,但是没有什么可以阻止我们从此纹理创建一个Sprite - Sprite.Createtexture具有左下角和右上角坐标的矩形 ,即轴坐标 )。

这些行将被称为最新行,其余的将在paint方法之上添加!

我的


现在我们需要用随机洞穴填充我们的字段。 对于此类操作,我们还将在ground_libray中创建一个单独的方法。 我想立即解释该方法的参数:

 ref block[,] b -     . int thick -    int size -         Color outLine -   

山洞
  public static void make_cave(ref block[,] b, int thick, int size, Color outLine) { int xNow = Random.Range(0, b.GetLength(0)); int yNow = Random.Range(0, b.GetLength(1) / 2); for (int i = 0; i < size; i++) { b[xNow, yNow] = null; make_thick(ref b, thick, new int[2] { xNow, yNow }, outLine); switch (Random.Range(0, 4)) { case 0: xNow += 1; break; case 1: xNow -= 1; break; case 2: yNow += 1; break; case 3: yNow -= 1; break; } xNow = xNow < 0 ? 0 : (xNow >= b.GetLength(0) ? b.GetLength(0) - 1 : xNow); yNow = yNow < 0 ? 0 : (yNow >= b.GetLength(1) ? b.GetLength(1) - 1 : yNow); } } 

首先,我们声明了变量XY ,但我分别将它们分别称为xNowyNow

第一个,即xNow ,从零到第一维中字段的大小获取随机值。

第二个-yNow-也获得一个随机值:从零到第二维的字段中间。 怎么了 我们是从中间产生山峰的,它长到“天花板”的机会并不大 。 基于此,我认为与空中产生洞穴无关。

之后,循环立即进行,其滴答声的数量取决于size参数。 每次滴答时,我们都会在xNowyNow位置更新字段,然后才自己更新它们(字段更新可以放在结尾,您不会感到有区别)

还有一个make_thick方法,在参数中我们传递了我们的字段洞穴笔触宽度,洞穴当前更新位置笔触颜色

中风
  static void make_thick (ref block[,] b, int t, int[] start, Color o) { for (int x = (start[0] - t); x < (start[0] + t); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - t); y < (start[1] + t); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) continue; b[x, y] = new block(o); } } } 

该方法采用传递给它的开始坐标,并在距离t周围重新绘制所有颜色为o的块-一切都很简单!


现在,将这一行添加ground_generator中

 ground_libray.make_cave(ref ground, 2, 10000, new Color(0.3f, 0.3f, 0.3f)); 

您可以将ground_generator脚本作为组件安装在我们的对象上,并检查其工作方式!



有关洞穴的更多信息...
  • 要制造更多的洞穴,可以多次调用make_cave方法(使用循环)
  • 更改大小参数并不总是会增加洞穴的大小,但通常会变得更大
  • 通过更改thick参数,可以显着增加操作数:
    如果参数为3,则半径为3的平方数将为36 ,因此,如果参数size = 40,000 ,则操作数将为36 * 40,000 = 1440000


溶洞校正




您是否已注意到,从这个角度看,洞穴看起来不是最好的? 过多的细节(也许您的想法有所不同)

为了摆脱一些#4d4d4d的包含,我们将在ground_libray中编写以下方法:

清洁工
  public static void clear_caves(ref block[,] b) { for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) continue; if (solo(b, 2, 13, new int[2] { x, y })) b[x, y] = null; } } 

但是,如果您不知道solo函数的作用,那么将很难理解这里发生的事情:

  static bool solo (block[,] b, int rad, int min, int[] start) { int cnt = 0; for (int x = (start[0] - rad); x <= (start[0] + rad); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - rad); y <= (start[1] + rad); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) cnt += 1; else continue; if (cnt >= min) return true; } } return false; } 

在此功能的参数中,必须显示我们的字段点验证半径“破坏阈值”要检查的点坐标
这是此功能的详细解释:
int cnt是当前“阈值”的计数器
接下来是两个循环,检查围绕其坐标传递到起点的所有点。 如果有一个空的点 ,则将一个加到cnt上 ,在达到“破坏阈值”时,我们返回真相-该点是多余的 。 否则,我们不会碰她。

我将销毁阈值设置为13个空点,验证半径为2 (即它将检查24个点,不包括中心点)
例子
因为只有9个空白点,所以这一点不会受到伤害。



但这并不算幸运-多达14个空点



该算法的简要说明: 我们遍历整个领域并检查所有要点,看是否需要它们。

接下来,我们只需将以下行添加到ground_generator中

 ground_libray.clear_caves(ref ground); 

总结


如我们所见,大多数不必要的粒子只是消失了。

添加一些颜色


我们的山看起来很单调,我觉得很无聊。

让我们添加一些颜色。 将level_paint方法添加到ground_libray

在山上绘画
  public static void level_paint(ref block[,] b, Color[] all_c) { for (int x = 0; x < b.GetLength(0); x++) { int lvl_div = -1; int counter = 0; int lvl_now = 0; for (int y = b.GetLength(1) - 1; y > 0; y--) { if (b[x, y] != null && lvl_div == -1) lvl_div = y / all_c.Length; else if (b[x, y] == null) continue; b[x, y] = new block(all_c[lvl_now]); lvl_now += counter >= lvl_div ? 1 : 0; lvl_now = (lvl_now >= all_c.Length) ? (all_c.Length - 1) : lvl_now; counter = counter >= lvl_div ? 0 : (counter += 1); } } } </ <cut />source>           .    ,       ,   .       ,      .          <b>Y </b>  ,      . </spoiler>     <b>ground_generator </b> : <source lang="cs"> ground_libray.level_paint(ref ground, new Color[3] { new Color(0.2f, 0.8f, 0), new Color(0.6f, 0.2f, 0.05f), new Color(0.2f, 0.2f, 0.2f), }); 

我只选择了3种颜色: 绿色深红色深灰色
当然,您可以更改颜色的数量和每种颜色的值。 原来是这样的:

总结


但是在颜色上添加一些随机性看起来仍然太严格了,我们将在ground_libray中编写以下属性:

颜色随机
  public static float color_randomize = 0.1f; static float crnd { get { return Random.Range(1.0f - color_randomize, 1.0f + color_randomize); } } 

现在在level_paintmake_thick方法中 ,在我们分配颜色的行中,例如在make_thick中

 b[x, y] = new block(o); 

我们将这样写:

 b[x, y] = new block(o * crnd); 

并在level_paint中

 b[x, y] = new block(all_c[lvl_now] * crnd); 


最后,所有内容应如下所示:

总结



缺点


假设我们有一个1024 x 1024的字段,我们需要生成24个洞穴,其边缘的厚度将为4,其大小为80,000。

1024 * 1024 + 24 * 64 * 80,000 = 5,368,832,000,000次运算。

此方法仅适用于为游戏世界生成较小的模块, 不可能一次生成非常大的内容

Source: https://habr.com/ru/post/zh-CN416419/


All Articles