如果你在关卡里手工摆过围墙,一定经历过这种场景:一段几百米的城墙,复制了数百个墙段,刚摆整齐,策划走过来说一声“路线往右挪十米”,于是你默默删掉所有墙段,重新来一遍。这个场景几乎每个做静态关卡的人都会遇到。问题并不在于你不够熟练,而在于把时间花在了“重复摆放”上,而不是“设计路径”上。程序化围墙系统要解决的,就是这件事。
本文基于虚幻引擎UE5蓝图,实现一个不写C++、不依赖PCG的纯蓝图程序化围墙生成器。上半部分会带你完成最核心的路径生成能力:创建一条Spline,拖动控制点,围墙自动贴合路径重新生成;修改墙段长度,整条墙自动调整;首尾自动补上柱子。你学到的不只是一个围墙工具,更是UE5里“用Spline驱动Construction Script批量生成实例”的通用工作流。
读完这篇文章,你能得到一个可以放进任何关卡工程的 BP_ProceduralWall 蓝图,也会理解程序化生成里最容易被忽略的问题:可控性。下一步做门洞、转角、材质随机和地形贴合时,这套基础架构可以直接复用。
1. 这篇文章真正要解决的问题
很多刚接触UE5的开发者,一听到“程序化”三个字,首先想到的是随机生成、无尽关卡、开放世界大面积地形。但实际关卡开发里,程序化最常见的价值不是“随机”,而是“规则重复”。
围墙就是一个极其典型的场景。它数量大、样式统一、沿着一条路径排列,手工摆放效率极低,而且修改成本极高。假设一段城墙需要200个墙段、40根柱子,手工摆可能花掉一个下午。更痛苦的是,如果路径规划变了,所有墙段都要重新摆一次。运气好你只是平移,运气不好要重新对着地形描线,一描又是一下午。
这个痛点本质上是“数据与表现没有分离”。手工摆放时,墙体位置是硬编码在关卡里的,路径信息没有独立保存。而程序化方案把“路径”抽离成一条Spline,墙段只是这条路径上的采样结果。修改路径,墙自动重新生成;修改规格,墙自动重新计算。数据改一处,表现全局更新。
本文要讲清楚的,就是这套“路径即数据”的思维方式。对你来说,读完可以获得三个实际收益:第一,掌握UE5蓝图里Spline沿路径采样位置和旋转的方法;第二,理解Construction Script编辑期实时生成机制,知道为什么参数一改墙壁立刻变化;第三,学会用HISM高性能实例化组件批量产出墙段,避免上百个独立Static Mesh组件拖垮性能。
这篇文章适合三类读者:经常做关卡地编、被重复劳动折磨的开发者;刚入门UE5蓝图、想通过一个完整项目理解程序化思路的初学者;以及准备做RTS、模拟建设、开放世界等需要大量重复建筑的项目的技术美术。如果你只是随手点开想了解一下,前四章的概念和架构设计也值得读完,它会帮你判断项目里到底该不该用程序化。
2. 程序化围墙系统的核心概念与设计边界
动手前,先把几个关键概念理清,否则后面做的时候容易混。
2.1 用Spline定义围墙路径
Spline是UE5里最常用的路径工具之一,本质上是一条由控制点和切线方向确定的多段曲线。你可以把它理解成“一条可以随时拖动形状的绳子”。UE5的Spline组件提供了两个核心采样方法:Get Location at Distance Along Spline 和 Get Rotation at Distance Along Spline。前者根据距离得到路径上的坐标,后者得到该点切线的朝向。围墙生成的本质,就是在Spline上每隔一段距离取一个点,生成一个墙段实例。
2.2 Construction Script让修改即时可见
Construction Script中文一般叫“构造脚本”,它在编辑器中任何参数变化时自动执行。这是本方案能落地的前提。手工摆放为什么效率低?因为每一段都是独立操作。而构造脚本可以把“生成逻辑”跑在编辑器里,一旦你在Details面板里修改SegmentLength、更换网格体、拖动Spline控制点,整个围墙立刻重新计算。所见即所得。
2.3 HISM高性能实例化
HISM全称Hierarchical Instanced Static Mesh Component,即分层实例化静态网格组件。它能把多个同种网格体合并到一个组件里管理,渲染时批量提交,性能远好于一个个独立的Static Mesh Component。在围墙这种大量重复的场景里,使用HISM几乎是唯一正确的选择。
2.4 为什么不直接选Spline Mesh Component或PCG
这是技术上最容易踩坑的决策点。很多初学者看到Spline,第一反应是Spline Mesh Component。它确实能把网格体沿Spline自动拉伸贴合,适合管道、绳索、道路护栏这类“连续形状”的物体。但围墙是模块化拼接,是离散排出的一段一段,不是连续拉伸。用Spline Mesh Component控制段数、段距、首尾柱子,异常别扭。
PCG则是UE5官方推出的程序化内容生成框架,适合植被散布、石头堆、区域型随机分布等场景。它的优势是密度控制和规则分区域生成,但用在“一条路径上按固定间距排一段墙”上,配置成本过高,调试也麻烦。围墙是强规则路径,Spline加循环足够,不需要额外引入一套框架。
| 方案 | 适合场景 | 围墙项目里的表现 | 选型建议 |
|---|---|---|---|
| Spline Mesh Component | 连续网格体贴合路径 | 控制段数困难,模块拼接麻烦 | 不推荐 |
| Spline + HISM + 循环 | 规则重复模块沿路径排列 | 完全匹配围墙需求 | 推荐 |
| PCG | 区域散布、规则随机分布 | 功能过剩,调试成本高 | 不推荐 |
| Houdini | 高度自定义资产管线 | 对于简单围墙太重 | 不推荐 |
3. 环境准备与美术资源要求
本方案在UE5.0及以上版本均可运行,UE5.3、UE5.4等新版本使用体验更好。本文演示的逻辑属于蓝图基础能力,不依赖任何特殊插件,也不需要开启C++工程,只要新建一个纯蓝图的空白项目即可。如果你后续想扩展到地形贴合、网络同步,可能还需要Landscape和Gameplay的相关模块,但上半部分用不到。
美术资源方面,建议准备以下三类网格体:
| 资源名 | 建议Mesh | 原点位置建议 | 朝向建议 |
|---|---|---|---|
| 墙段 | SM_Wall_Segment | 底面中心作为原点 | 前向朝向+Y轴 |
| 柱子 | SM_Pillar | 底面中心作为原点 | 无需严格朝向,但尽量一致 |
| 可选转角 | SM_Wall_Corner | 底面中心作为原点 | 前向朝向+Y轴,下一阶段使用 |
墙段的原点位置非常关键。推荐把模型的根放在底部中心,前向朝向+Y轴,原因是Spline的采样旋转在水平路径上会把Z轴朝上、Y轴指向路径切线方向。如果模型本身朝向+X轴或者原点偏在墙角,生成出来的墙段就会出现整体偏移和旋转不正确,排查起来非常痛苦。这块属于“资源规范”的一部分,程序化生成越强大,对资源规范的要求就越高。
网格体本身建议使用简单的盒体碰撞,或者统一由关卡中的BlockingVolume负责碰撞,不要在墙段上叠加过多复杂碰撞体,否则几百段围墙同时启用复杂碰撞,运行时开销会非常大。
4. 蓝图Actor主框架搭建
接下来进入实操部分。我们先把蓝图的壳子建好,再逐步填充生成逻辑。
4.1 创建蓝图Actor
在内容浏览器中右键,选择 Blueprint Class,父类选择 Actor,命名为 BP_ProceduralWall,保存到蓝图书签文件夹下。这个类会作为程序化围墙的总控制器,所有生成逻辑都集中在这里。双击打开蓝图编辑器。
4.2 添加基础组件
点击左上角Components面板,依次添加以下组件:
- 根组件 Scene Component,命名 Root
- Spline Component,命名为 WallPath
- Hierarchical Instanced Static Mesh Component,命名为 WallHISM
- Hierarchical Instanced Static Mesh Component,命名为 PillarHISM
添加HISM组件时,在组件搜索框输入 Hierarchical Instanced,即可看到 Hierarchical Instanced Static Mesh Component 选项。这个组件负责墙体实例和柱子实例的批量渲染。
此时不要急着写逻辑。先把蓝图保存一次,然后查看组件细节面板。WallHISM和PillarHISM上面各有一个Static Mesh槽位,先留空,后面通过蓝图变量动态指定。这样做的目的是让同一个蓝图能复用不同的墙体网格体,而不是把资源写死在组件里。
4.3 设计蓝图变量
在蓝图编辑器的My Blueprint面板中点击添加变量,按以下表格创建变量:
| 变量名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| WallMesh | Static Mesh | None | 墙体中段网格体 |
| PillarMesh | Static Mesh | None | 柱子网格体 |
| SegmentLength | Float | 300 | 单段墙体长度 |
| bSpawnStartPillar | Boolean | True | 是否在起点生成柱子 |
| bSpawnEndPillar | Boolean | True | 是否在终点生成柱子 |
| YawOffset | Float | 0 | 墙段朝向修正角度 |
变量设置完成后,编译一次。然后在关卡中拖入一个BP_ProceduralWall实例,在Details面板中把WallMesh、PillarMesh指定为你的美术资源。这里会看到蓝图变量的强大之处:不同实例可以配置不同的墙段网格体,后期做城墙、栅栏、篱笆可以共用一套逻辑。
4.4 设置Spline初始控制点
选中关卡里的BP_ProceduralWall,在其Components面板选中WallPath,然后在视口中打开Spline编辑模式。默认情况Spline只有一个点,我们需要添加两个控制点。选中Spline点后,在Details面板点击Add Spline Point,或者在视口中按快捷键直接添加。
一个简单的直线路径至少需要两个点:起点和终点。先按住Ctrl点击视口,添加几个点,拖成一条折线。此时你还没有写任何生成逻辑,所以视口里看不到墙段,这是正常的。WallPath只是提供了一条路径数据,真正把路径变为墙体,靠的是Construction Script。
5. Construction Script核心逻辑拆解
现在来到整个教程的精华部分。我们打开BP_ProceduralWall的Construction Script事件图表,开始编写生成逻辑。
5.1 第一步:清空旧实例
Construction Script的重要特点是:它会在你对蓝图做任何修改时反复运行。如果你不先清空旧数据,每次生成都会叠加新实例,这一点是新手最容易踩的坑。
在Construction Script开始位置,分别调用 WallHISM → Clear Instances 和 PillarHISM → Clear Instances。这两个节点会把之前生成的墙段和柱子全部清理干净,然后再用后续逻辑重新生成。
这个清理动作非常关键。如果你没有这一步,第一次看效果还好,第二次修改参数后,墙段数量直接翻倍,第三次三倍,墙体重叠到无法直视。记住这个原则:任何构造脚本,第一步永远是清理旧结果。
5.2 第二步:计算墙段数量
我们知道了Spline的总长度,也知道每一段墙的固定长度,那么墙段数量就等于总长度除以单段长度。在蓝图中,先调用 WallPath → Get Spline Length 获取路径总长度,然后调用数学节点的 Floor,将总长度除以SegmentLength后的结果向下取整。得到的结果就是需要生成的墙段数量。
为什么要向下取整?因为路径最终剩下的零头可能不足一整段。如果强制生成,墙段会伸出终点或者与其他段重叠。默认策略是忽略尾巴,把最后一段不完整的部分留空。后面如果需要处理残段,可以在本方案基础上扩展一个拉伸末段的功能。
5.3 第三步:循环采样生成墙段
使用蓝图里的 For Each Loop 节点,循环次数从0循环到SegmentCount减1,每次循环都执行以下采样逻辑:
- 计算当前采样距离 Distance = Index * SegmentLength
- 调用 WallPath → Get Location at Distance Along Spline,坐标空间选择World Space
- 调用 WallPath → Get Rotation at Distance Along Spline,同样选择World Space
- 用这两个结果构造一个Transform,缩放设为1
- 调用 WallHISM → Add Instance,把Transform添加进去
这一段的节点连接比较多,建议在图表里画成一条直线流,避免连线交叉影响理解。核心思想并不难:沿着Spline从起点开始,每隔SegmentLength取一个点,在那个点的位置上放一个墙段。
默认情况下,Add Instance需要主动传入World Space为True,这样采样坐标会直接被解释为世界坐标。如果传False,坐标会被当成局部坐标,结果会出现在你意想不到的位置。
5.4 第四步:生成首尾柱子
墙体主体生成后,我们处理首尾柱子。柱子本质上也是沿Spline采样的实例,只是采样距离固定在0(起点)和 SplineLength(终点)。分别调用 Get Location at Distance Along Spline 和 Get Rotation at Distance Along Spline,然后Add Instance到 PillarHISM。
这里有一点需要注意:柱子的朝向。如果直接使用Spline旋转,柱子可能会横过来,因为Spline旋转在终点处依然沿切线方向。解决方式是给柱子Transform的Rotation加一个Yaw角偏移,或者干脆把柱子的Rotation设置为Yaw=0的纯旋转。具体哪种适合你,取决于柱子网格体的美术朝向。这类调节问题没有标准答案,但通过YawOffset统一控制可以避免逐个修改。
5.5 节点逻辑的代码化描述
为了帮助你快速理清蓝图节点逻辑,下面给出这段生成算法的等价文本逻辑,你可以对照蓝图节点检查自己的连线:
Construction Script ├─ WallHISM → Clear Instances ├─ PillarHISM → Clear Instances ├─ SplineLength = WallPath.GetSplineLength() ├─ SegmentCount = Floor(SplineLength / SegmentLength) ├─ For each Index in [0, SegmentCount - 1]: │ ├─ Distance = Index * SegmentLength │ ├─ Location = WallPath.GetLocationAtDistanceAlongSpline(Distance, WorldSpace) │ ├─ Rotation = WallPath.GetRotationAtDistanceAlongSpline(Distance, WorldSpace) │ ├─ WallTransform = MakeTransform(Location, Rotation, Scale=1) │ └─ WallHISM.AddInstance(WallTransform, bWorldSpace=true) ├─ 起点柱子:Distance = 0,采样并AddInstance到PillarHISM └─ 终点柱子:Distance = SplineLength,采样并AddInstance到PillarHISM如果你想更深入理解这套逻辑的算法本质,下面是用C++风格的伪代码表示,仅用于帮助理解,不需要真的去写C++:
// 辅助理解:蓝图逻辑对应的算法伪代码 // 注意:本项目使用蓝图实现,这里仅描述数据流程 void AProceduralWall::RebuildWall() { // 1. 清理旧实例 WallHISM->ClearInstances(); PillarHISM->ClearInstances(); // 2. 获取路径总长度,计算需要生成多少段 const float SplineLength = WallPath->GetSplineLength(); const int32 SegmentCount = FMath::FloorToInt(SplineLength / SegmentLength); // 3. 循环采样,生成墙段 for (int32 Index = 0; Index < SegmentCount; Index++) { const float Distance = Index * SegmentLength; const FVector Location = WallPath->GetLocationAtDistanceAlongSpline( Distance, ESplineCoordinateSpace::World); const FRotator Rotation = WallPath->GetRotationAtDistanceAlongSpline( Distance, ESplineCoordinateSpace::World); const FTransform WallTransform(Rotation, Location, FVector::OneVector); WallHISM->AddInstance(WallTransform, true); } // 4. 处理首尾柱子 if (bSpawnStartPillar) { const FVector StartLocation = WallPath->GetLocationAtDistanceAlongSpline( 0.0f, ESplineCoordinateSpace::World); PillarHISM->AddInstance(FTransform(FRotator::ZeroRotator, StartLocation), true); } if (bSpawnEndPillar) { const float EndDistance = WallPath->GetSplineLength(); const FVector EndLocation = WallPath->GetLocationAtDistanceAlongSpline( EndDistance, ESplineCoordinateSpace::World); PillarHISM->AddInstance(FTransform(FRotator::ZeroRotator, EndLocation), true); } }注意,这段伪代码中的HISM相关函数在实际C++工程里的调用方式和具体参数可能略有差异,本文只用于让你理解蓝图节点在做什么。Blua 明确告诉你,这套教程完全不需要写C++。
6. 关键参数与算法细节
蓝图连接成功只是第一步,要让围墙真正可用,还需要理解几个关键参数背后的意义。
6.1 SegmentLength必须和美术资源长度匹配
SegmentLength不是随意取的数值,它必须等于墙段网格体在X轴或Y轴方向上的实际长度。如果美术资源做的是3米长墙段,这里就要填300(UE5默认单位是厘米,3米等于300厘米)。如果填大了,墙段之间会出现间隙;如果填小了,墙段互相穿插。这个参数是整个系统的基础参数,就像音乐里的节拍,错了全盘都错。
实际操作中,可以这样验证:先让Spline是一条直线,然后微调SegmentLength,直到视口里墙段之间没有缝隙也没有重叠。如果资源建模时长度统一,这个值只需要设置一次,后面的墙体全部复用。
6.2 残段处理策略
当Spline长度不能被SegmentLength整除时,路径末端会剩下一截“零头”。默认策略是忽略,墙直接结束在最后一个完整段位置。这对于城门、围墙缺口等场景是合理的,但如果你希望每一端都完美贴合,可以在上一节基础上增加一个EndCap Mesh变量,专门处理末端残片。
残段处理不是本篇文章的重点,但它揭示了程序化系统设计的一个重要原则:所有业务规则都要参数化。边界情况不是实现完再补,而是在设计阶段就预留好开关。比如bSpawnStartPillar、bSpawnEndPillar这两个变量,就是为不同场景准备的开关控制。
6.3 墙段旋转修正
如果你生成的墙段没有沿着路径方向,而是横了过来,或者头尾颠倒,主要原因是模型坐标轴没有对齐。Spline的Get Rotation节点返回的旋转,是这条路径在该点处的切线坐标系。如果墙段本身是沿着+Y轴建模的,效果就会正常;如果沿着+X轴建模,需要把模型在导入UE5之前旋转到+Y方向。
当模型已经不能轻易调整时,也可以在蓝图中通过YawOffset统一修正。例如在采样到Rotation后,执行一个Combine Rotators,把Rotation和(Z轴旋转了YawOffset的Rotator)合并。这样写的好处是调整参数不需要改模型原始资源。
6.4 为什么选择HISM而不是Static Mesh Component循环
有人可能会问,既然每次只需要生成几十个墙段,为什么不用Add Static Mesh Component节点循环创建独立组件?原因很简单:性能。HISM将实例数据一次性提交给GPU,DrawCall通常只有一个,而独立Static Mesh Component每段都增加DrawCall。在编辑器里几十个组件感觉不到区别,但当你把这条围墙复制到关卡多个位置,总墙段数上千后,DrawCall会直接爆炸。
从工程角度看,选用HISM是在为未来做冗余。程序化系统的优势之一就是“一次设计,多场景铺量”。如果底层组件类型不支持大规模实例化,这个优势就打了一半折扣。
7. 运行验证与编辑器内实时预览
完成Construction Script编写后,点击蓝图编辑器顶部的Compile按钮。然后回到关卡视口,你会看到围墙已经沿着WallPath生成了。这里有几种验证方法。
首先,确认墙体的整体走向是否正确:沿着Spline的路径依次排列,没有断墙,没有穿插。其次,点击选中WallPath,进入Spline编辑模式,在视口中拖动任何一个Spline控制点。注意观察,墙体会实时重新生成,整条围墙跟着路径变化。这就是Construction Script的编辑器内生成威力。
接着,打开BP_ProceduralWall的Details面板,把SegmentLength改大或改小,例如从300改成500,围墙段数会自动减少,间距会自动拉大。再把bSpawnStartPillar改为False,起点柱子会消失,终点柱子保留。这些实时反馈的效果,是手工摆放完全无法做到的。
判断生成成功的一个重要标准是实例数量。选中关卡中的BP_ProceduralWall后,在Outliner面板展开它的层级,选中WallHISM组件,Details面板底部会显示当前实例数。正常情况下,实例数应该等于你计算出来的SegmentCount。如果实例数是0,说明Getter采样没有正确返回数据,或者循环没有被正确执行。
还有一个小技巧:打开视口的碰撞显示模式,确认每段墙体都有正确的碰撞体。HISM的实例碰撞由网格体资源自身的碰撞设置决定。如果生成的墙体没有碰撞,检查墙段Mesh Asset的Collision Complexity设置,确认不是“Use No Collision”。
8. 常见问题与排查思路
程序化蓝图第一版跑不出来是常态,下面整理一些高频率问题,按“现象 → 原因 → 排查 → 解决”的方式给出,供你对照处理。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 墙段之间重叠或明显缺口 | SegmentLength和网格体实际长度不匹配 | 测量墙段资源长度,核对蓝图变量数值 | 修改SegmentLength,直到无缝拼接 |
| 构造脚本每次编译后墙体越来越多 | 没有调用Clear Instances清理旧实例 | 检查Construction Script开头是否清空HISM | 在生成前添加WallHISM和PillarHISM的Clear Instances |
| 墙体方向横过来或朝向错误 | 模型朝向与Spline切线坐标系不一致 | 在模型编辑器里观察朝向,确认世界坐标轴 | 统一模型朝向为+Y,或在蓝图中用YawOffset修正 |
| 修改Spline控制点后墙体不更新 | 蓝图未编译,或修改未触发构造脚本 | 在蓝图编辑器点击Compile,再看视口 | 重新编译蓝图,确认Spline点变更后构造脚本执行 |
| 实例数为0,没有任何墙体生成 | 循环逻辑未执行或采样距离错误 | 在Get Spline Length节点后加PrintString调试 | 确认SplineLength大于0,确认For Each Loop的循环次数 |
| 柱子位置正确但朝向不对 | 柱子Transform直接使用了路径切线旋转 | 观察Rotator的Pitch/Yaw/Roll值 | 柱子单独使用固定旋转,或加Yaw方向修正 |
| 生成的墙体没有碰撞响应 | HISM实例沿用Mesh Asset的碰撞设置 | 检查Mesh Asset的Collision预设 | 将碰撞设置为BlockAll或使用自定义简单碰撞 |
排查这类蓝图问题时,建议按照“先数据、后显示”的顺序。先确定SplineLength、SegmentCount这些数值是否符合预期,再用Print String节点把关键变量打印到视口,最后检查HISM的实例数量。不要一开始就怀疑渲染和碰撞问题,大多数程序化生成的bug都出在“计算出的数据不对”这一步。
9. 工程化建议与下篇路线
到这里,一个基础的UE5程序化围墙系统已经可用了。但作为工程级方案,还有几个建议值得提前考虑。
9.1 把蓝图拆成“数据配置”和“生成逻辑”两层
BP_ProceduralWall目前包括两类内容:一类是WallMesh、SegmentLength这些配置数据;一类是Construction Script里的生成逻辑。在实际项目里,更推荐把你常用的围墙配置存成子蓝图类,比如BP_CityWall、BP_WoodFence、BP_StoneWall,它们继承BP_ProceduralWall,只修改默认变量值,不修改生成逻辑。这样新关卡直接拖入不同的子蓝图,就能快速铺出不同风格的围墙,同时底层逻辑保持单点维护。
9.2 开放变量而不是开放逻辑
蓝图程序化生成系统很容易被后来的美术同学改坏。推荐把暴露到Details面板的变量加上分类前缀和Tooltip说明,比如Segment_Length、Segment_Mesh。当美术同事在关卡里调整参数时,能看到明确的提示“墙段长度必须和墙段模型实际长度一致,单位为厘米”。这能省掉大量沟通成本。
9.3 有关键数据变更时手动触发重建
Construction Script在编辑器里很好用,但它只在编辑器环境执行,打包后的运行时不会自动运行。如果你需要在游戏运行时动态修改围墙路径或墙段,应该把Construction Script里的逻辑提取成一个自定义事件RebuildWall,然后在运行时需要时手动调用。这也是下一步做可破坏墙体和“开关门”动画的基础。
9.4 后续学习方向
本文作为《虚幻引擎UE5程序化围墙系统 蓝图开发教程》的上半部分,已经把最核心的Spline路径生成架构讲完了。下半部分可以继续深入几个进阶点:
- 转角自动识别:检测Spline上相邻切线的角度变化,自动插入转角Mesh
- 门洞与开合:在Spline上指定一个区间,用门框替换墙段,并让门支持开关动画
- 与地形贴合:采样Landscape高度,让墙段根节点自动吸附地面
- 材质随机化:根据段索引或距离渐变材质,避免整条墙完全一致
- NavMesh与碰撞优化:让动态墙体正确阻挡AI寻路,并控制碰撞开销
- 网络同步场景:如果围墙会在地图运行中动态变化,需要同步路径数据,这涉及UE5网络同步相关设计
你的下一步实践很简单:先把上半部分的BP_ProceduralWall搭出来,放一段直线路径验证效果,再手动拖动Spline点感受实时更新。跑通之后,你会对“用数据描述关卡、用逻辑生成关卡”有完全不同的体会。程序化不是让游戏内容变随机,而是让开发者从重复劳动里解放出来,把精力放到真正需要设计的地方。