Infinite procedurally generated city with the Wave Function Collapse algorithm
英文原址:https://marian42.de/article/wfc/
这是一个游戏,在这你可以走在一个无限的用程序生成的城市用程序生成,而且会边走边生成内容。它是由一组具有波函数坍缩算法的板块组成的。
你可以下载游戏的可玩版本 itch.io 。您可以在source code on github. 上获得源代码。以下是我在一个生成的城市中漫步的视频:
算法
我将使用单词“slot”来表示三维体素网格中可以包含块(或为空)的位置,并使用单词“module”来表示可以位于这样一个slot中的块。slot是位置或者插口或者槽,module是模块,之后以此翻译。
坍缩
算法为世界中的每个位置选择模块。一组位置被认为是一个未被观测的波函数。这意味着每个位置都有一组可能放在那里的模块。在量子力学的语言中,可以说“slot是所有module的叠加”。世界从一个完全不被察觉的状态开始,在这个状态下,每个模块都可能出现在任何slot中。一个接一个,每个slot都被坍缩。也就是说,从一组可能的模块中随机选择一个模块。接下来是约束传播步骤。对于每个模块,只允许将模块的一个子集放置在其附近。每当slot坍缩时,仍有可能放置在附近插槽中的模块集需要更新。约束传播步骤是算法中计算代价最高的部分。
熵减
算法的一个重要方面是决定要坍缩哪个slot。算法总是以最小熵entropy坍缩slot。算法总选择(或混乱)最少的slot。1.假定每个模块的出现概率相同,则拥有最少module的slot被视为有最小的熵(可能出现的情况少,则不混乱,熵减)。一般来说,选择模块的概率不同。2.slot_A 拥有两个概率相同的module,slot_B拥有两个概率不同的module,则slot_A熵 > slot_B熵。
译者补充:作者用量子力学的概念比喻自己的算法。量子物理中,未观测的粒子处于叠加态,观测时坍缩为特定状态。在此算法中,位置A在未观测时处于多个模块的叠加态,每个模块都有一定概率出现。随后作者定义了熵减规律,并规定两个熵计算规则,1模块少的slot熵少。2模块数量相同的slot,模块概率不一致的slot熵少。
你可以在Wave Function Collapse algorithm here. 找到更多的信息和波函数折叠算法的一些漂亮的例子。提出了一种基于单个实例生成二维纹理的算法。在这种情况下,模块概率和邻接规则是根据它们在示例中的发生方式来确定的。在我的情况下,它们是手动供应的。算法的进行:https://marian42.de/article/wfc/wfc.mp4
关于街区,原型和模块blocks, prototypes and modules
这个世界是由一组大约100个街区组成的,我用blender做的。我从几个街区开始,每当我有空闲时间时,我就做更多。
算法需要知道哪些模块可以放在一起。每个模块有6个可能的邻居列表,每个方向一个。但我想避免手工创建这个列表。我还想要一种方法来自动生成块的旋转变体。
两者都可以通过使用我称之为模块原型来实现。这是一个可以在Unity编辑器中方便编辑的MonoBehaviour。模块以及被允许的相邻模块列表和旋转变量都是从这些模块原型中自动创建的。
一个困难的问题是如何建立邻接信息的模型,使这个自动过程正常工作。以下是我想到的:
每个块有6个连接器,每个面一个。连接器有一个号码。此外,水平连接件不是翻转的,就是对称的。垂直连接器的旋转索引介于0和3之间(屏幕截图中为b、c、d),或者它们被标记为旋转不变。
基于此,我可以自动检查哪些模块允许彼此相邻。相邻模块必须具有相同的连接器号。并且它们的对称性必须匹配(垂直旋转索引相同,水平翻转而非翻转对),或者它们必须对称/不变。
有些排除规则允许我禁止某些相邻模块。一些具有匹配连接器的块在彼此旁边看起来并不好看。下面是不使用排除规则生成的映射的示例:很难看
到达无限
原始的波函数折叠算法生成有限个映射。我想有一个世界,当你走过它时,它会伸展越来越大。
我的第一个方法是生成有限大小的大块chunks,并使用相邻大块的连接器作为约束。如果生成了大块,并且已经生成了相邻大块,则只允许生成与现有模块匹配的模块。这种方法的问题是,每当一个slot坍缩时,约束传播将限制几个slot之外的slot。在这张图片中,您可以看到一个坍缩的slot影响的所有位置,影响太多是个问题。
当一次只生成一个大块时,约束不会传播到相邻的大块。这导致在考虑其他块时不允许在块中选择模块。当算法试图生成下一个大块时,它找不到任何解决方案。
我没有使用大块,而是将映射存储在一个字典中,该字典将插槽位置映射到插槽。它只在需要时填充。算法的某些部分需要对此进行调整。在选择要折叠的插槽时,不能考虑所有无限插槽。取而代之的是,当玩家到达地图时,地图上只会同时生成一小块区域。约束仍然传播到此区域之外。
在某些情况下,这种方法不起作用。考虑一个模块集,上面截图中的隧道段是笔直的,但是没有隧道入口。如果算法选择这样一个隧道模块,这就预先确定了一个无限的隧道。约束传播步骤将尝试分配无限数量的插槽。我设计模块集是为了避免这个问题。
The original Wave Function Collapse algorithm generates finite maps. I wanted to have a world that expands further and further as you walk through it.
My first approach was to generate chunks of finite size and use the connectors of adjacent chunks as constraints. If a chunk is generated and an adjacent chunk was already generated, only modules are allowed that fit with the existing modules. The problem with this approach is, whenever a slot is collapsed, the constraint propagation will limit the posibilities even a few slots away. In this image you can see all the places affected from collapsing just one slot:
When just generating a single chunk at once, constraints were not propagated to adjacent chunks. This led to modules being selected within the chunk that would not be allowed when considering the other chunks. When the algorithm would then try to generate the next chunk, it could not find any solution.
Instead of using chunks, I store the map in a dictionary that maps a slot position to a slot. It is only populated when needed. Some parts of the algorithm needed to be adjusted to this. When selecting a slot to collapse, not all infinite slots can be considered. Instead, only a small area of the map is generated at once, when the player reaches it. Constraints are still propagated outside of this area.
In some cases this approach doesn’t work. Consider a module set with the straight tunnel piece from the screenshot above, but no tunnel entrance. If the algorithm selects such a tunnel module, this predetermines an infinite tunnel. The constraint propagation step would try to allocate an infinite amount of slots. I designed the module set to avoid this problem.这段没整太明白大家自己看看
边界约束
有两个重要的边界约束:位于地图顶部的面必须具有“空气air”连接器。地图底部的面必须具有“实体solid”连接符。如果不满足这些限制条件,地面上就会有洞,屋顶也会丢失。
在有限的映射中,这很容易做到:对于顶层和底层的所有slot,删除所有带有不需要的连接器的模块。然后使用约束传播移除不再有效的其他模块。
在无限贴图中,这不起作用,因为在顶层和底层有无限多的slot。刚开始时,我只会在创建槽后删除顶层和底层的这些模块。但是,删除顶层插槽中的模块意味着对其相邻插槽的约束。这将导致级联效应cascading effect,再次无限地分配slot。
我通过创建一个1×n×1的地图来解决这个问题,其中n是高度。此地图使用连续平铺world wrapping来传播约束。这就像吃豆人,你离开右边边界,进入左边边界。现在在这个地图上我可以应用所有的边界约束。无论何时在无限映射中创建一个新的slot,都会使用该映射中相应位置的模块集对其进行初始化。
错误状态和回溯
有时WFC算法会达到一个slot没有可能的模块的状态。在有限世界的应用程序中,您可以丢弃结果并重新开始。在无限的世界里,这是行不通的,因为世界的一部分已经展示给玩家了。我从一个解决方案开始,在出现错误的地方生成一个白色块。
我现在的解决办法是回溯。将坍缩的顺序和有关约束传播的信息被存储为历史记录以方便回溯。如果WFC算法失败,一些历史记录将被撤消。这在大多数情况下都有效,但有时错误被识别得很晚,这会导致许多步骤被回溯。在极少数情况下,玩家所在的位置会重新生成。
在我看来,这种限制使得WFC方法不适合用于商业游戏的无限世界。
见解
当我看到 talk by Oskar Stålberg who uses the WFC algorithm to generate levels in Bad North.,我就开始研究这个问题。大多数基础都是在procjam实现的。
我对未来的改进有一些想法,但我不确定是否会增加游戏性。如果我这么做了,那可能就不是你想象中的吃鸡战场游戏了。但如果你想看到你最喜欢的游戏中添加到这个,就自己动手吧!源代码毕竟是可用的,而且是麻省理工学院授权的