还在用随机游走?带你用 WFC 算法“手搓”一个无限肉鸽地

2026-01-12 13:56:10
文章摘要
文章介绍了用WFC算法生成Roguelike游戏地图的方法。传统随机游走和柏林噪声生成地图有缺陷,WFC能保证局部规则,生成合理多变的地图。文章围绕核心逻辑拆解、设计接口、编写传播逻辑、回溯处理矛盾、性能与视觉优化展开,还借助通义灵码辅助编程,降低实现难度,同时给出参考资料与开源库。

前言:告别“全随机”的垃圾关卡

做 Roguelike 游戏的兄弟们都知道,地图生成是核心体验的基石。最早我们用 随机游走(Random Walk),生成的地图像蚯蚓,太线性,没探索欲。后来我们用 柏林噪声(Perlin Noise),生成的地图太自然,缺乏人工设计的建筑结构感。直到 WFC(波函数坍缩) 横空出世。它就像一个“高智商的数独玩家”,能在保证局部规则(比如:墙壁必须连接墙壁,水流不能直接接岩浆)的前提下,生成全局合理千变万化的地图。这也是《Townscaper》、《Bad North》等神作背后的核心技术。但 WFC 难就难在代码实现:熵值计算、约束传播、回溯机制,任何一个环节写错,地图就会“坍缩失败”,留给你一堆报错。今天,我们不谈枯燥的量子力学理论,直接打开 Unity 和 通义灵码,手把手写一套工业级的 2D WFC 地牢生成器。


模块一:核心逻辑拆解 —— WFC 到底在算什么?

在使用 AI 辅助写代码之前,我们必须先理解算法的物理意义。你可以把 WFC 想象成在填一个超级复杂的数独。

WFC 的三个核心概念:

  1. 叠加态
    • 地图上每一个格子(Slot),在没生成之前,它既是墙,又是路,又是门
    • 初始状态下,每个格子的可能性(熵)*是最大的。
  2. 观察
    • 找到当前地图上熵值最低(可能性最少,比如只剩“墙”和“路”两种可能)的那个格子。
    • 强行让它坍缩成某一个确定的 Tile(比如,我拍板决定这个格子就是“墙”)。
  3. 传播
    • 这是最难的一步。一旦某个格子变成了“墙”,它周围的邻居就不能是“岩浆”了(假设规则如此)。
    • 这种“约束”会像水波纹一样向四周扩散,削减周围格子的可能性,直到整个地图稳定。

图片描述

  • 图注:左图为初始叠加态(所有格子都是混沌的);中图为一次观察后,选定中心为草地;右图为传播后,周围格子自动排除了“深海”的可能性。
  • 目的:用可视化图表建立算法直觉。

模块二:设计“乐高积木”的接口

WFC 的强大取决于“邻接规则” (Socket System)。我们需要告诉程序,每块瓦片(Tile)的上下左右能连接什么。这就像乐高积木的凸起和凹槽。

1. 瓦片数据结构

我们不需要自己手写繁琐的 ScriptableObject,让通义灵码帮我们生成这个模板。

Prompt 指令(发送给通义灵码)

"在 Unity 中,我需要一个 WFCTile 的 ScriptableObject 类。 属性包含:

  1. GameObject prefab: 对应的预制体。
  2. float weight: 出现的权重(概率)。
  3. string[] sockets: 定义 上、下、左、右 四个方向的接口类型(例如 'Road', 'Wall', 'Grass')。
  4. Rotation rotation: 是否允许旋转(0, 90, 180, 270)。 请生成 C# 代码,并添加详细注释。"

2. 规则配置

在 Unity 编辑器中,我们创建几种 Tile:

  • 地面 (Floor): Sockets = [A, A, A, A] (全向连接 A)
  • 直墙 (Wall_Straight): Sockets = [B, B, A, A] (上下接墙B,左右接地面A)
  • 转角 (Wall_Corner): Sockets = [B, A, A, B] (上接墙B,右接墙B)

重点:接口标签(A、B)必须严格匹配。A 只能连 A,B 只能连 B。

图片描述

  • 图注:Unity Inspector 面板。展示了 WFCTile 的配置细节,Sockets 数组清晰地定义了四个方向的连接规则。
  • 目的:展示数据驱动的设计思路。

模块三:熵值最小堆与传播逻辑

这是整个系统的引擎。我们将利用通义灵码来编写最容易出错的 传播 (Propagation) 逻辑。

1. 单元格类 (Cell Class)

每个格子需要记录当前还剩下哪些可能的 Tile。

public class Cell {
    public int x, y;
    public bool isCollapsed = false;
    public List<WFCTile> possibleOptions; // 当前还能放哪些 Tile
// 熵值:可能性越多,熵越大
public int Entropy =&gt; possibleOptions.Count; 

}

2. 编写传播函数 (AI 辅助)

这是一个递归或堆栈处理过程,手写极易出现死循环或索引越界。

Prompt 指令(发送给通义灵码)

"编写 WFC 算法中的 Propagate 函数。 输入参数:Vector2Int startPos (刚坍缩的格子坐标)。 逻辑:

  1. 使用 Stack<Vector2Int> 存储待检查的格子。
  2. 遍历当前格子的四个邻居。
  3. 检查邻居的 possibleOptions 是否与当前格子的 Socket 兼容。
  4. 如果邻居的可能性被削减了(Constrain),将邻居加入 Stack 继续传播。
  5. 如果邻居的可能性降为 0,抛出异常(生成失败)。"

AI 生成代码片段(经过人工 Code Review 修正)

void Propagate(Vector2Int startPos) {
    Stack<Vector2Int> stack = new Stack<Vector2Int>();
    stack.Push(startPos);
while (stack.Count &gt; 0) {
    Vector2Int current = stack.Pop();
    Cell currentCell = grid[current.x, current.y];

    // 遍历四个方向:上、下、左、右
    foreach (var dir in directions) {
        Vector2Int neighborPos = current + dir;
        if (!IsValid(neighborPos)) continue;

        Cell neighborCell = grid[neighborPos.x, neighborPos.y];
        
        // 核心逻辑:检查兼容性,移除不可能的选项
        bool changed = Constrain(currentCell, neighborCell, dir);
        
        if (changed) {
            stack.Push(neighborPos); // 产生涟漪效应,继续传播
            if (neighborCell.Entropy == 0) {
                throw new ContradictionException(&quot;生成死锁!&quot;);
            }
        }
    }
}

}

  • 图注:展示了当中心格子确定为“墙壁”后,红色的“不可用标记”迅速向四周扩散,周围的格子自动剔除了“水流”的可能性。
  • 目的:直观展示算法的运行过程。

模块四: 遇到“死胡同”怎么回溯?

WFC 最让人头疼的问题是 矛盾。有时候生成到一半,发现某个格子无论放什么 Tile 都对不上邻居的接口(比如被四面墙围住,但中间必须是路)。如果不处理,程序就会卡死或报错。传统的做法是:全部重来。这在 100x100 的大地图上是不可接受的,效率太低高级的做法是:回溯 (Backtracking)

1. 建立快照机制

我们需要在每次“观察(坍缩)”之前,保存当前地图的状态。

Prompt 指令

"优化上面的 WFC 代码,加入回溯机制。

  1. 定义一个 HistoryState 类,深拷贝保存 grid 中每个 Cell 的 possibleOptions
  2. 使用 Stack<HistoryState> 保存历史。
  3. Propagate 抛出 ContradictionException 时,执行 Backtrack():从 Stack 中弹出一个状态,恢复 Grid,并把导致死锁的那个选项从可能性中移除,重试。"

2. 优化后的实操逻辑

通义灵码会帮你生成深拷贝(Deep Copy)的代码,这在 C# 中写起来很繁琐。

void RunWFC() {
    try {
        // ... 选择最小熵格子 ...
        SaveState(); // 存档
        CollapseCell(target); // 坍缩
        Propagate(target); // 传播
    }
    catch (ContradictionException) {
        Debug.LogWarning("死锁检测!开始回溯...");
        RestoreState(); // 读档
        // 关键:把刚才导致死锁的那个随机选项移除,换一个选
        RemoveFailedOption(); 
        RunWFC(); // 递归重试
    }
}

通过这种“时光倒流”的机制,只要规则本身没有逻辑硬伤,WFC 总能找到一个解,或者至少不会让游戏崩溃。

图片描述

  • 图注:Unity Console 截图。黄色警告显示“检测到死锁,回退至 Step 45”,随后绿色日志显示“地图生成成功”。
  • 目的:证明回溯机制的有效性。

模块五:性能与视觉优化

代码跑通了,但要变成商业级产品,还得做两件事。

1. 分块生成 (Chunking)

不要一次性生成 1000x1000 的地图。 将地图分为 16x16 的 Chunk。当玩家接近边缘时,触发新 Chunk 的 WFC 生成。

  • 技巧:新 Chunk 的边缘必须继承旧 Chunk 边缘的 Socket 约束,这样地图才是无缝连接的。

2. 协程可视化 (Coroutine)

为了不卡死主线程(避免掉帧),将 while 循环放入 IEnumerator 中。 yield return new WaitForSeconds(0.01f); 这不仅能平滑帧率,还能让玩家看到地牢“逐格生长”的酷炫动画,甚至可以作为 Loading 界面的视觉效果。


结语

WFC 算法是程序化生成领域的一座高山,但有了 AI 辅助编程工具,爬山的难度降低了 80%。 通义灵码 帮我们搞定了繁琐的 Stack 操作、深拷贝和邻居遍历。


参考资料与开源库

Tags: #游戏开发 #WFC #Roguelike #地图生成 #Unity #通义灵码 #算法实战#

声明:该内容由作者自行发布,观点内容仅供参考,不代表平台立场;如有侵权,请联系平台删除。