Unity程序化生成实战:ProceduralToolkit核心功能与地下城构建

📅 2026/7/14 6:55:07
Unity程序化生成实战:ProceduralToolkit核心功能与地下城构建
1. 项目概述为什么你需要ProceduralToolkit如果你在Unity里做过地形、建筑、植被或者任何需要大量、规则或随机内容生成的项目那你一定体会过手动摆放的痛苦。一个关卡成百上千棵树一座城市无数栋形态各异的建筑一个地下城迷宫般的房间和走廊。靠美术同学手搓工期爆炸。自己写脚本硬编码灵活性为零改个需求就得重写。这就是程序化生成Procedural Generation的价值所在——用算法和规则让计算机自动、智能地生成内容。而ProceduralToolkit正是Unity生态中一个被严重低估的“瑞士军刀”。它不是某个单一功能的插件而是一个工具箱一个方法论集合。很多开发者第一次接触它可能只是为了画个程序化网格或者生成个简单迷宫但用深了你会发现它提供的是一整套构建复杂、可重用、高性能程序化系统的底层积木。网上很多教程只讲其一不讲其二导致大家觉得它就是个“画形状的库”这实在太片面了。今天我们就抛开那些浅尝辄止的介绍直接深入核心。我将结合自己多个项目从开放世界地形到策略游戏地图生成的使用经验带你从零开始彻底掌握ProceduralToolkit的10大核心功能。我们的目标不是学会调用几个API而是理解其设计哲学掌握如何将这些功能组合起来搭建属于你自己的、坚如磐石的程序化生成管线。你会发现从“会用它”到“用它构建系统”中间只差这一篇深度解析。2. 核心功能深度解析与设计哲学在拆解具体功能前我们必须先理解ProceduralToolkit的设计核心。它与许多“一键生成”的资产商店插件不同其理念是提供基础构件而非完整解决方案。这带来了极高的灵活性但也要求开发者具备一定的系统设计能力。它的10大核心功能可以归纳为三个层次几何构建层、算法逻辑层和实用工具层。2.1 几何构建层一切可视化的基础这是ProceduralToolkit最直观的功能层负责生成基础的网格Mesh数据。Unity中一切3D物体的形状都由网格定义因此这是程序化生成的物理基石。2.1.1 网格构建器 (MeshDraft)MeshDraft是核心中的核心。它不是最终上传到GPU的Mesh对象而是一个中间数据结构用于方便地组装顶点、三角形和UV。// 创建一个简单的四边形平面 var meshDraft MeshDraft.Quad(10f, 5f); // 此时可以方便地进行顶点级操作比如整体偏移 meshDraft.Move(Vector3.up * 2); // 或者添加更多几何体进行合并 meshDraft.Add(MeshDraft.Triangle(Vector3.zero, Vector3.right, Vector3.forward)); // 最后再转换为Unity Mesh Mesh finalMesh meshDraft.ToMesh();注意MeshDraft在内存中操作非常高效因为它直接操作ListVector3等数据结构。但务必记得在完成所有修改后调用ToMesh()并赋值给MeshFilter.sharedMesh或MeshFilter.mesh才会真正在场景中显示。频繁调用ToMesh()会产生GC垃圾回收最佳实践是在一帧内完成所有构建逻辑最后统一生成Mesh。2.1.2 基础图元生成库内置了从2D形状到3D体的丰富图元生成方法远超Unity内置的GameObject.CreatePrimitive。2D图元Quad四边形、Triangle三角形、Circle圆盘可设置分段数、Ring圆环、FlatSpiral平面螺旋。这些是构建复杂平面结构如UI面板、地面贴片的基础。3D图元Cube立方体、Sphere球体支持多种细分算法如Icosphere、Cylinder圆柱、Cone圆锥、Pyramid棱锥、Torus圆环体。这些是构建基础物体的快速方案。2.1.3 挤出与放样 (Extrusion Lofting)这是从2D轮廓创建3D物体的关键技术。挤出将一个2D轮廓沿着一个方向通常是法线方向拉伸形成3D体。例如用一个Circle轮廓挤出得到的就是一个圆柱体。ProceduralToolkit允许你自定义挤出路径不仅仅是直线也可以是曲线从而创建出弯曲的管道、扭曲的柱子等。放样在多个2D轮廓之间进行平滑过渡生成网格。这是创建复杂有机形状如花瓶、山脉轮廓、自定义模型的利器。你需要提供一系列在3D空间中排列的轮廓比如一系列大小不一的圆圈库会自动在轮廓之间生成平滑的曲面。实操心得使用放样功能时轮廓的顶点数和顺序必须严格一致。例如如果你用Circle(20)生成第一个轮廓20个顶点那么后续所有用于放样的轮廓也必须恰好是20个顶点并且顶点顺序环绕方向要相同否则会导致三角形缠绕错误产生撕裂的网格。一个技巧是先用一个基础轮廓生成MeshDraft然后通过缩放、旋转、位移这个MeshDraft的顶点列表来创建后续轮廓这样可以保证顶点数和顺序的绝对一致。2.2 算法逻辑层生成规则与智能有了几何构建能力接下来就需要规则来驱动生成。这一层功能决定了生成内容的“灵魂”——是完全随机还是遵循某种规律。2.1.4 迷宫与网格生成 (Maze Grid)这是ProceduralToolkit的招牌功能之一。它不直接生成视觉上的迷宫模型而是生成一个表示迷宫结构的数据模型通常是二维布尔数组或图结构你可以再用这个数据模型去驱动几何构建层生成墙壁、地板。迷宫算法库实现了多种经典算法如深度优先搜索递归回溯、Prim算法、Kruskal算法等。不同算法生成的迷宫风格迥异深度优先倾向于生成长走廊Prim算法更均匀Kruskal算法则分支更多。网格数据结构提供了Grid和Cell等类方便你将游戏逻辑如房间、地块与迷宫结构关联起来。2.1.5 噪声与随机化 (Noise Randomization)程序化生成的“自然感”很大程度上来源于噪声。ProceduralToolkit内置了Perlin噪声、Simplex噪声等实现但更强大的是它的随机化工具集。RandomE类提供了比Unity自带Random更丰富、更可控的随机方法如随机点单位圆/球内、随机颜色梯度采样、随机列表元素带权重等。可控随机通过设定随机种子可以确保每次运行生成完全相同的内容这对关卡调试和网络同步至关重要。2.1.6 几何算法 (Geometry Algorithms)这是一组处理点、线、面关系的数学工具常用于生成过程的决策。多边形三角剖分将任意简单多边形包括带洞的分解为三角形集合这是渲染复杂2D形状或3D物体截面的前提。凸包计算给定一组散点找到能包裹所有点的最小凸多边形。可用于快速计算物体边界、进行碰撞检测预处理等。线相交、点包含测试判断两条线段是否相交或一个点是否在多边形内部。这些是构建地块划分、区域检测等功能的基础。2.3 实用工具层提升开发效率这一层功能让整个开发流程更顺畅它们不直接参与生成逻辑但能极大提升代码质量和运行效率。2.1.7 调试绘制 (Debug Drawing)在编辑器模式和游戏运行时将算法中间状态可视化是调试程序化生成逻辑的必备手段。Unity自带的Debug.DrawLine功能较弱。ProceduralToolkit的DebugE类提供了绘制带持续时间的线、箭头、网格、球体、包围盒等功能颜色和持续时间可自定义让你能清晰地看到噪声图的范围、迷宫的通路、生成区域的边界等。2.1.8 网格操作与优化在MeshDraft层面提供了一系列优化和变形工具。合并与拆分可以将多个MeshDraft合并为一个减少Draw Call也可以按材质或子网格拆分。网格变形如Bend弯曲、Twist扭曲、Spherize球面化等可以让你在生成基础形状后再进行二次艺术化处理。UV与法线计算自动生成平面、球形或圆柱形投影的UV坐标以及平滑或硬边的顶点法线。自己写正确的UV和法线计算非常容易出错这个功能节省了大量时间。2.1.9 扩展性与组合性这是ProceduralToolkit最强大的隐性功能。它的所有组件都是松散耦合的。你可以用迷宫算法生成布局数据用几何算法处理房间形状用图元生成房间模型再用噪声来扰动墙壁的细节最后用调试绘制来验证整个过程。这种“乐高积木”式的设计让你可以自由组合创造出无限可能例如生成一个迷宫其中每个房间是一个通过放样生成的、形状不规则的洞穴。用地形噪声生成高度图然后用多边形三角剖分生成网格再挤出成立体地形。用随机点生成凸包作为岛屿轮廓再在轮廓内用泊松圆盘采样放置树木。2.1.10 性能与最佳实践库在设计上考虑了性能。例如MeshDraft大量使用值类型struct和对象池思想来减少GC压力。但要想发挥最大性能你需要遵循一些最佳实践批量操作尽可能在单帧内完成一个完整物体的所有MeshDraft构建和修改最后再调用一次ToMesh()。重用对象对于频繁创建和销毁的相同形状网格考虑使用对象池来缓存MeshDraft或最终的Mesh对象。LOD多层次细节对于远处物体使用顶点数更少的图元如低分段数的球体。ProceduralToolkit的图元生成方法通常允许你指定分段数这为动态LOD提供了便利。Job System/Burst兼容性虽然ProceduralToolkit本身不是基于ECS/Job System构建的但其核心算法如噪声、几何计算是纯数学函数你可以很容易地将这些计算剥离出来放入IJob中利用Burst编译进行多线程加速而仅将结果用于构建MeshDraft。3. 实战演练构建一个程序化地下城生成器理论说再多不如动手。现在我们综合运用上述功能来构建一个简易但完整的地下城关卡生成器。这个生成器将包含随机迷宫布局、不同大小的房间、连接房间的走廊、以及基础的3D网格生成。3.1 第一步生成迷宫与房间布局数据我们不会直接生成网格而是先规划好关卡的“平面图”。using ProceduralToolkit; using System.Collections.Generic; public class DungeonGenerator : MonoBehaviour { public int mazeWidth 30; public int mazeHeight 30; public int roomAttempts 20; // 尝试放置房间的次数 public int minRoomSize 3; public int maxRoomSize 7; private Maze maze; private ListRectInt rooms new ListRectInt(); void Start() { GenerateLayout(); VisualizeLayoutInEditor(); // 先用调试绘制看看布局 // GenerateGeometry(); // 后续步骤 } void GenerateLayout() { // 1. 生成基础迷宫例如使用深度优先算法 // Maze.Generate 返回的是一个二维布尔数组true代表是通路墙被挖开 maze Maze.Generate(mazeWidth, mazeHeight, MazeAlgorithm.RecursiveBacktracking); // 2. 在迷宫网格中随机放置房间 // 房间是矩形区域我们会把该区域内的所有“墙”都变为“路” for (int i 0; i roomAttempts; i) { int roomW Random.Range(minRoomSize, maxRoomSize 1); int roomH Random.Range(minRoomSize, maxRoomSize 1); int roomX Random.Range(1, mazeWidth - roomW - 1); // 留出边界 int roomY Random.Range(1, mazeHeight - roomH - 1); RectInt newRoom new RectInt(roomX, roomY, roomW, roomH); // 检查新房间是否与已有房间重叠 bool overlap false; foreach (var room in rooms) { // 给房间之间留出至少1格走廊的间距 if (newRoom.Overlaps(room.Expanded(1))) { overlap true; break; } } if (!overlap) { // 房间有效将其区域在迷宫中标记为通路 CarveRoom(newRoom); rooms.Add(newRoom); } } // 3. 连接房间与迷宫 // 确保每个房间至少有一条通道连接到主迷宫系统 ConnectRoomsToMaze(); } void CarveRoom(RectInt room) { for (int x room.x; x room.x room.width; x) { for (int y room.y; y room.y room.height; y) { maze.cells[x, y] true; // true 表示是地面通路 } } } void ConnectRoomsToMaze() { // 简化逻辑将每个房间的中心点连接到迷宫最近的通路 // 更复杂的实现可以使用A*算法寻找最短路径 foreach (var room in rooms) { Vector2Int roomCenter new Vector2Int(room.x room.width / 2, room.y room.height / 2); // 寻找最近的通路点这里简化处理实际可能需要搜索 // 然后调用一个“挖隧道”函数将两点之间的迷宫单元格设为true DigCorridor(roomCenter, FindNearestPassage(roomCenter)); } } // ... FindNearestPassage 和 DigCorridor 方法实现略 ... }这段代码生成了关卡的逻辑布局一个布尔类型的二维数组maze.cells表示哪里是墙false哪里是路true以及一个房间列表rooms。这是所有后续视觉构建的数据基础。3.2 第二步根据布局数据生成3D网格现在我们将“地面”和“墙壁”的平面图变成真正的3D模型。public GameObject floorPrefab; // 一个简单的平面Prefab带MeshFilter和MeshRenderer public GameObject wallPrefab; // 一个立方体Prefab用于墙壁 public float cellSize 2.0f; // 每个逻辑格子对应的世界单位大小 public float wallHeight 3.0f; void GenerateGeometry() { // 清理旧的地牢 foreach (Transform child in transform) Destroy(child.gameObject); // 生成地面 for (int x 0; x mazeWidth; x) { for (int y 0; y mazeHeight; y) { if (maze.cells[x, y]) // 如果是通路地面 { Vector3 position new Vector3(x * cellSize, 0, y * cellSize); Instantiate(floorPrefab, position, Quaternion.identity, transform); } } } // 生成墙壁 - 更高效的方案是合并网格这里为清晰起见使用实例化 for (int x 0; x mazeWidth; x) { for (int y 0; y mazeHeight; y) { if (!maze.cells[x, y]) // 如果是墙 { // 只在墙的“顶部”和“右侧”放置墙壁模型避免重复 // 检查当前墙格子的右侧邻居是否是路需要一堵东墙 if (x 1 mazeWidth maze.cells[x 1, y]) { PlaceWall(x, y, Direction.East); } // 检查当前墙格子的上侧邻居是否是路需要一堵北墙 if (y 1 mazeHeight maze.cells[x, y 1]) { PlaceWall(x, y, Direction.North); } // 对于迷宫边界也需要墙壁 if (x 0) PlaceWall(x, y, Direction.West); if (y 0) PlaceWall(x, y, Direction.South); } } } } enum Direction { North, East, South, West } void PlaceWall(int gridX, int gridY, Direction dir) { Vector3 position new Vector3(gridX * cellSize, wallHeight * 0.5f, gridY * cellSize); Quaternion rotation Quaternion.identity; Vector3 scale new Vector3(0.2f, wallHeight, cellSize); // 墙壁较薄 switch (dir) { case Direction.North: position new Vector3(cellSize * 0.5f, 0, cellSize); rotation Quaternion.Euler(0, 0, 0); break; case Direction.East: position new Vector3(cellSize, 0, cellSize * 0.5f); rotation Quaternion.Euler(0, 90, 0); break; // South 和 West 类似... } var wall Instantiate(wallPrefab, position, rotation, transform); wall.transform.localScale scale; }这个生成方法虽然直观但实例化大量GameObject一堵墙一个对性能不友好。接下来我们就用ProceduralToolkit的核心功能来优化它。3.3 第三步使用ProceduralToolkit进行网格合并与优化我们的目标是为所有地面生成一个合并的Mesh为所有墙壁生成另一个合并的Mesh。这样Draw Call就从成千上万减少到2个。using ProceduralToolkit; using UnityEngine; void GenerateOptimizedGeometry() { MeshDraft floorDraft new MeshDraft(); MeshDraft wallDraft new MeshDraft(); // 1. 生成所有地面网格并合并 for (int x 0; x mazeWidth; x) { for (int y 0; y mazeHeight; y) { if (maze.cells[x, y]) { // 创建一个单位平面图元然后平移到正确位置 MeshDraft tile MeshDraft.Quad(cellSize, cellSize); tile.Move(new Vector3(x * cellSize, 0, y * cellSize)); // 可以在这里为每个瓦片设置不同的UV偏移用于纹理变化 floorDraft.Add(tile); } } } // 2. 生成所有墙壁网格并合并 // 思路不再检查每个墙格子而是遍历所有“边界”。 // 即遍历所有通路格子检查其四个方向是否是墙或边界如果是则在该方向放置一面墙。 for (int x 0; x mazeWidth; x) { for (int y 0; y mazeHeight; y) { if (maze.cells[x, y]) // 只从通路格子出发 { Vector3 basePos new Vector3(x * cellSize, 0, y * cellSize); // 北墙 if (y 1 mazeHeight || !maze.cells[x, y 1]) { MeshDraft northWall CreateWallSegment(basePos, new Vector3(cellSize * 0.5f, 0, cellSize), Vector3.zero, cellSize); wallDraft.Add(northWall); } // 东墙 if (x 1 mazeWidth || !maze.cells[x 1, y]) { MeshDraft eastWall CreateWallSegment(basePos, new Vector3(cellSize, 0, cellSize * 0.5f), Vector3.up * 90, cellSize); wallDraft.Add(eastWall); } // 南墙和西墙逻辑类似... } } } // 3. 创建GameObject并应用合并后的Mesh CreateMeshGameObject(ProceduralFloor, floorDraft.ToMesh(), transform, 0); CreateMeshGameObject(ProceduralWalls, wallDraft.ToMesh(), transform, 1); // 墙壁层级可以放上面 } MeshDraft CreateWallSegment(Vector3 basePos, Vector3 offset, Vector3 rotation, float length) { // 创建一个扁平的立方体作为墙段 MeshDraft segment MeshDraft.Cube(0.2f, wallHeight, length); // 厚0.2高wallHeight长length segment.Rotate(Quaternion.Euler(rotation)); segment.Move(basePos offset Vector3.up * (wallHeight * 0.5f)); return segment; } void CreateMeshGameObject(string name, Mesh mesh, Transform parent, int layerOrder 0) { GameObject go new GameObject(name); go.transform.SetParent(parent); MeshFilter mf go.AddComponentMeshFilter(); mf.mesh mesh; MeshRenderer mr go.AddComponentMeshRenderer(); // 这里需要你提前准备好材质球并赋值例如通过public变量暴露 mr.material (name.Contains(Wall)) ? wallMaterial : floorMaterial; // 如果需要碰撞可以添加MeshCollider MeshCollider mc go.AddComponentMeshCollider(); mc.sharedMesh mesh; }通过这种方式无论地下城有多大我们只创建了两个GameObject地面和墙壁各自拥有一个合并后的大Mesh。性能得到质的飞跃。这就是ProceduralToolkit在实战中提升效率的典型应用。4. 高级技巧与性能调优指南掌握了基础构建和合并我们来看看如何让生成的内容更精致、更高效。4.1 利用噪声增加细节与自然感纯粹由算法生成的迷宫和房间看起来会很“机械”。我们可以用噪声来扰动增加自然感。4.1.1 扰动墙壁位置在生成墙壁时不让墙段严格对齐网格而是给其位置一个微小的随机偏移。Vector3 jitter new Vector3( Random.Range(-jitterAmount, jitterAmount), 0, Random.Range(-jitterAmount, jitterAmount) ); segment.Move(basePos offset Vector3.up * (wallHeight * 0.5f) jitter);4.1.2 创建不规则房间之前我们的房间是严格的矩形。可以用噪声来生成房间的轮廓。例如在房间矩形的基础上对每条边上的点进行径向扰动。void GenerateOrganicRoom(RectInt baseRect, float noiseScale, float maxOffset) { ListVector2 points new ListVector2(); int segmentsPerSide 5; // 每条边采样5个点 // 采样矩形四条边上的点... for (int i 0; i segmentsPerSide * 4; i) { float t (float)i / (segmentsPerSide * 4); Vector2 pointOnRect ...; // 计算矩形边界上的点 // 计算该点指向矩形中心的方向 Vector2 dirToCenter (new Vector2(baseRect.center.x, baseRect.center.y) - pointOnRect).normalized; // 使用Perlin噪声获取一个扰动值 float noise Mathf.PerlinNoise(pointOnRect.x * noiseScale, pointOnRect.y * noiseScale); float offset (noise * 2 - 1) * maxOffset; // 映射到[-maxOffset, maxOffset] // 沿中心方向扰动 pointOnRect dirToCenter * offset; points.Add(pointOnRect); } // 现在points里是一个不规则多边形的顶点轮廓 // 使用ProceduralToolkit的三角剖分功能将其转为MeshDraft var roomOutline new MeshDraft(); // ... 将points转换为MeshDraft ... // 然后可以挤出这个轮廓形成3D房间 }4.1.3 创建起伏的地面地面的高度也可以使用噪声图来调制形成缓坡或坑洼。float sampleNoise Mathf.PerlinNoise(x * groundNoiseScale, y * groundNoiseScale); float height sampleNoise * maxGroundHeight; tile.Move(new Vector3(x * cellSize, height, y * cellSize)); // 注意移动后需要重新计算该瓦片的顶点法线或者直接使用整个合并后的MeshDraft统一计算平滑法线。 // ProceduralToolkit的MeshDraft.RecalculateNormals()方法可以帮我们做这件事。4.2 动态加载与LOD管理对于超大地图不可能一次性生成全部网格。需要结合空间分区如四叉树、网格进行动态加载。分块生成将整个世界划分为固定大小的区块Chunk。每个区块独立运行地下城生成算法但需要处理区块边界的衔接问题例如区块边缘的迷宫通路要对齐。基于距离的LOD为每个区块准备多个细节层次的模型。当玩家远离时使用低模更少的顶点更简单的墙壁和地面形状。ProceduralToolkit的图元生成函数通常允许指定细节参数如球体的分段数可以方便地生成不同LOD的网格。异步生成将耗时的网格生成逻辑放在后台线程例如使用C#的Task或Unity的JobSystem避免卡顿主线程。注意Unity的API如Mesh创建必须在主线程调用但MeshDraft的构建过程可以放在后台。后台线程准备好MeshDraft后在主线程调用ToMesh()并赋值。4.3 材质与着色器优化合并网格减少了Draw Call但一个大Mesh使用单一材质可能会失去纹理变化。解决方案是纹理图集将不同的地面纹理石板、泥土、草地和墙壁纹理合并到一张大贴图中。在生成MeshDraft时根据格子类型房间地面、走廊地面、石墙、砖墙计算对应的UV坐标指向图集的不同区域。顶点颜色可以在生成网格时为顶点附加颜色信息。然后在着色器中利用这些顶点颜色进行色调调整或纹理混合实现简单的视觉变化而无需增加Draw Call。细节贴图使用一张平铺的细节法线贴图或高度贴图通过Shader叠加到基础材质上可以极大地增加表面细节而无需增加几何复杂度。5. 常见问题排查与实战心得即使掌握了所有功能在实际项目中还是会遇到各种坑。这里分享一些高频问题的解决思路。5.1 生成的网格UV错误或法线朝向不对问题模型显示为纯黑、纹理拉伸或背面可见。排查UV检查生成图元时是否正确地设置了UV。MeshDraft的图元方法通常有默认UV但如果你对网格进行了复杂的变形如弯曲、挤出可能需要手动重新计算UV。使用MeshDraft的FlipFaces()方法可以反转三角形缠绕顺序有时能解决背面显示问题。法线在完成所有顶点位置修改后务必调用meshDraft.RecalculateNormals()或meshDraft.RecalculateNormalsFlat()。前者计算平滑法线适合有机曲面后者计算硬边法线适合立方体等有棱角的物体。技巧在编辑器下将MeshRenderer的材质临时替换为显示法线的调试Shader可以直观地看到法线方向。5.2 性能问题生成卡顿或内存过高卡顿生成上万格子的地图时单帧计算量过大。解决方案是分帧生成。使用Coroutine协程或async/await每帧只生成一部分区块。或者将纯计算部分如迷宫算法、噪声采样移至Job System。内存高生成了大量未合并的GameObject或Mesh。坚持使用MeshDraft合并策略。对于不再需要的Mesh对象使用DestroyImmediate(mesh)进行销毁而不仅仅是取消引用。5.3 随机生成的结果不可复现问题每次运行游戏生成的地图都不一样给测试和存档带来困难。解决在生成逻辑开始前设置随机数种子。UnityEngine.Random.InitState(seed)。ProceduralToolkit的随机工具也尊重Unity的随机状态或者你可以直接使用System.Random并传入种子。确保所有随机决策如房间位置、大小、噪声偏移都基于这个被初始化的随机发生器。5.4 组合功能时逻辑冲突问题比如先用地形噪声生成了高度再在上面生成迷宫结果墙壁嵌入了地面之下。解决规划好生成管线。通常顺序是布局生成2D-地形塑造3D高度-几何构建3D网格-细节装饰放置道具。每一步都基于上一步的结果进行调整。例如在生成墙壁时其底部Y坐标应该取所在位置的地面高度而不是固定的0。5.5 扩展性维护困难问题生成逻辑写在一个巨大的脚本里难以添加新特性如河流、桥梁。解决采用模块化设计。将生成过程分解为独立的“生成器”组件。例如LayoutGenerator负责生成2D的迷宫和房间数据。TerrainGenerator负责根据布局计算高度图。MeshBuilder负责将2D布局和高度图转换为3D网格。PropPlacer负责在网格上随机放置树木、宝箱等道具。 每个组件通过定义良好的接口如共享一个GenerationContext数据类进行通信。这样要添加河流只需插入一个新的RiverGenerator组件在TerrainGenerator之前运行修改高度图即可。ProceduralToolkit不是一个“傻瓜式”的解决方案它赋予你的是构建复杂系统的能力。初学时可能会觉得它提供的都是零散的部件但当你真正理解每个部件的用途并学会将它们像乐高一样组合起来时你就会发现限制你的不再是工具而是你自己的想象力。从今天开始别再只把它当做一个画图工具尝试用它去设计规则去构建系统去创造那些独一无二、充满生命力的程序化世界吧。