第17章 · PCG:规则驱动的内容生产线

📑 目录

导语:「回响峡谷」第一次尝试程序化铺植被时,技术美术用 Surface Sampler 默认参数撒了两千棵树。评审会上一眼假:同一棵松树在 30 度陡坡上立正,在湖边沼泽里立正,在路边十厘米处还是立正;更糟的是树与树之间的距离均匀得像阅兵——大自然从来不阅兵。那次失败让我们把 PCG(Procedural Content Generation)的定义改写了一遍:它不是"随机生成器",它是一台约束求解机。随机只是求解过程中的扰动项,约束才是本体。本章讲怎么把约束写对。

本章地图

mindmap
  root((PCG 规则驱动))
    世界观
      点云是通用货币
      图是数据流程序
      随机服从约束
    组合学
      采样器造候选
      过滤器做减法
      生成器落成网格
    三条规则链
      森林 坡度 海拔 密度场
      乱石滩 样条线 泊松盘
      废墟 建筑边界内点云
    共存
      保护区一票否决
      手工资产优先
    选型
      运行时 PCG
      烘焙期 PCG
      MMO 选烘焙
    远港管线
      高度图到可玩区域
      落入 World Partition 格子

17.1 PCG 图的本质:点云上的数据流

先纠正一个流传很广的误解:PCG 是约束求解机,"程序帮你随机摆东西"纯属误读。如果真是那样,开篇那次评审就不会翻车——随机恰恰是问题本身,随机二字在方案里只配做扰动项。

PCG 的正确心智模型是:场景里的一切摆放,都是一组约束条件的解集。树应该长在平地上、长在特定海拔带、成片但不等距、离路三米以外、不进村庄——这些全是约束。PCG 图(PCG Graph)就是把这些约束写成一条数据流管道,让引擎替你求出满足全部约束的点集合。随机数只在两个地方出场:在约束允许的范围内做抖动,以及在密度场里做"接受还是拒绝"的抽签。

这个模型的数据载体是点云(Point Data)。UE 的 PCG 框架里,几乎所有节点处理的都是点:每个点带一个 Transform(位置/旋转/缩放)和一组任意属性(Attribute),比如密度值、坡度值、材质索引、网格软引用。采样器从地形、样条线、Mesh 表面造出候选点云;过滤器按属性做减法;变换节点调整 Transform;最后的 Static Mesh Spawner 把点落成实例化静态网格(Instanced Static Mesh, ISM)。点和点之间不存储拓扑关系——这让整个管线天然并行友好,几万个点分块处理互不干扰。

类与资产的关系一张图就能立起来:

classDiagram
    class UPCGGraph {
        +Nodes : 图节点集合
        +"输入/输出 Pin"
    }
    class UPCGComponent {
        +Graph 引用
        +GenerationTrigger
        +Seed
        +Generate()
        +Cleanup()
    }
    class UPCGSettings {
        +每个节点一份配置
        +Debug 选项
    }
    class UPCGData {
        <>
    }
    class UPCGPointData {
        +TArray~FPCGPoint~
    }
    class UPCGSpatialData {
        +空间查询接口
    }
    class APCGActor {
        +持有 Component
    }
    UPCGGraph "1" o-- "*" UPCGSettings : 节点配置
    APCGActor "1" *-- "1" UPCGComponent
    UPCGComponent --> UPCGGraph : 执行
    UPCGData <|-- UPCGPointData
    UPCGData <|-- UPCGSpatialData
    UPCGComponent ..> UPCGPointData : 产出

注意 UPCGComponent 上那个 Seed。它暴露了 PCG 的第二条纪律:同一组约束加同一个种子,必须产出同一个世界。这不只是调试需要——MMO 里服务器和一万个客户端要对"哪块石头在哪个坐标"达成一致,底牌是同步"图 + 种子"。网络同步每个实例?那才是走不通的死胡同。确定性是 PCG 能进生产线的门票,做不到确定性的方案连评审会都进不了。

有了世界观,看一遍数据的完整旅程:

flowchart TD
    A[地形高度图 / Landscape] --> B[Surface Sampler
表面采样器] C[样条线资产 道路 河岸] --> B B --> D[候选点云
每点带密度属性] D --> E[属性计算
坡度 / 海拔 / 曲率] E --> F{过滤器链
按约束做减法} F -->|不满足| G[丢弃] F -->|满足| H[Transform Points
缩放/朝向抖动] H --> I[Static Mesh Spawner
落成 ISM 实例] I --> J[进入关卡
可被 HLOD 与流送接管] K[保护区体积] -.->|一票否决| F L[Seed] -.->|确定性| B

这张图就是全章的骨架:后面三个实战案例,本质都是往这条管道的不同位置挂不同的约束。区别只在采样源选什么、过滤器写几条、最后生成什么网格。

补一条调试纪律,越早养成越省钱:排查 PCG 问题,永远是逐节点看点云,而不是盯着最终结果猜。PCG 编辑器里每个节点都能单独检查输出的点数量与属性分布(5.8 的 Debug 可视化,具体入口以官方文档 PCG → Debugging 词条为准)。评审会上"树怎么没了"这类问题,九成能在五分钟内定位到某一个过滤器:采样器出来十万个点,过完坡度过滤剩三万,过完海拔剩八千,过完密度场剩两百——哪一步数量断崖不合理,哪一步就是嫌疑人。把点云想象成数据库里的一张表,每个过滤器是一条 WHERE 子句,逐节点检查就是逐条执行 SQL 看行数。写过 SQL 的人对这套排错节奏不会陌生。

17.2 采样器与过滤器:组合学,以及为什么均匀随机一眼假

补课:本节补美术基础——自然分布的"成簇+噪点"美学。美术出身的同事可跳过前半,但泊松盘那段对写规则有用,建议读完。

PCG 框架内置的节点在 5.8 里已经有上百个,但你 90% 的时间只会用到四类:

类别代表节点干什么
采样器Surface Sampler、Spline Sampler、Mesh Sampler、Bounds Sampler从几何上造候选点
属性计算Normal To Density、Attribute 运算类节点给点贴上坡度、海拔等数值
过滤器Density Filter、Distance Filter、Density Remap、Self Pruning按属性减点
生成器Static Mesh Spawner、Actor Spawner点变实例

组合学的要点:采样器决定"候选集有多大",过滤器决定"真实感有多强"。新手把精力全花在采样参数上,老手把精力全花在过滤条件上。因为候选集只是原材料,过滤条件才是你向自然界的观察交的作业。

候选集还有个成本账要算。采样间距每缩一半,候选点数量翻四倍,后面每个过滤器的开销同步翻四倍。一个常见的浪费是:拿 2 米间距的密采样去跑最终只留 5% 的稀疏规则——95% 的点出生即死,白算了一路属性。正确姿势是先粗后细:粗采样跑完便宜的约束(坡度、海拔这种一次法线查询就完事的),只在幸存者周围再加密采样跑贵的约束。这跟数据库优化器"先执行选择性高的谓词"是同一个道理,约束重排序省下来的都是实打实的生成时间。

回到那个失败案例,回答"均匀随机为什么一眼假"。Surface Sampler 默认行为近似均匀随机:每个候选点独立抽签,接受概率处处相同。这在数学上叫空间泊松过程,它的视觉特征恰好是灾难性的——点与点之间没有任何相关性,于是必然出现两棵树贴脸、又必然出现一块足球场大的空地,远看是一块灰度均匀的"噪点棋盘"。人眼对这种统计均匀极其敏感,因为我们在真实世界里从没见过它。

真实森林的分布长什么样?站进任何一片次生林量一量:

  • 成簇:母树种子散落在树冠投影范围内,幼苗围绕母树聚成家族。空间上呈现多尺度的团块。
  • 空隙:林窗(倒木砸出的空地)、溪流、岩壁切开簇与簇的边界。没有空隙的森林像绿墙。
  • 边缘渐变:簇的边缘密度从簇心向外连续衰减,像墨滴在宣纸上洇开。

这三件事用 PCG 的语言翻译过来就是一句:密度不能是常数,必须是空间函数 D(x, y)。用一张低频噪声图当密度场,波峰处抽签容易中、波谷处全拒,成簇和空隙自动出现;给噪声加一个衰减权重,边缘渐变也有了。这就是"成簇+噪点"的全部秘密——宏观成簇来自低频密度场,微观不规则来自逐点抽签的噪点。

均匀随机撒点:密度为常数左上贴脸挤团,右中一片空地——棋盘感密度场驱动:D(x,y) 是空间函数道路样条线:两侧 3m 密度归零保护区成簇 + 空隙 + 边缘渐变 + 硬约束留白

均匀随机还有一个致命缺陷:它保证不了最小间距。两棵树贴脸不只是难看——树干穿插会让 Nanite 的簇剔除失效、阴影穿帮。自然界解决这个问题的机制是竞争:两株幼苗抢同一片光,活一棵。PCG 里的对应物是泊松盘采样(Poisson Disc Sampling),它的约束一句话写完:

pi,pjS,ij:pipjr\forall\, p_i, p_j \in S,\; i \neq j :\quad \lVert p_i - p_j \rVert \;\geq\; r

读法:解集 S 里任意两个不同点的欧氏距离不得小于 r。这个最小间距约束的几何意义是——每个点独占一个半径为 r/2 的圆盘,圆盘之间不许重叠。为什么它长得像自然分布?因为自然界的"竞争排斥"就是这个不等式的物理版:树冠半径、根系范围、领地意识,全是半径各异的排斥盘。纯随机是"点与点无对话",泊松盘是"每个点都对邻居提了一个最低要求",视觉上的"呼吸感"就是这么来的。

经典 Bridson 算法的思路很工程:维护一个"活跃点"列表,每轮从活跃点周围的环形带(内半径 r、外半径 2r)里随机试投候选,落点若与所有已接受点满足上式就接受并加入活跃列表;一个点连续 k 次(常用 k=30)投不出合法候选就标记为死点。环形带设计是精髓——候选必然靠近已有结构,于是点云像潮水一样从种子向外推,天然致密。

在 UE 的 PCG 里没有必须手写的道理:Distance Filter(按最小距离过滤)和 Self Pruning(自剪枝)组合起来就能逼近泊松盘效果,蓝噪声(Blue Noise)采样资产也有现成的用法。你真正要记住的是这个不等式本身——r 取多大、是否随密度场缩放,决定了场景的"年龄"和"拥挤度"。幼树林 r 小,原始森林 r 大;乱石滩的巨石区 r 大、砾石区 r 小。一个参数,一种叙事。

实操里 r 的初值从资产量起:树冠平均直径乘以 0.7 到 0.9 作为起步,再按评审截图微调。别从"感觉"出发乱填,r 和网格尺寸脱钩是新手最常犯的错——r 小于树冠半径,树就必然互相穿插,后面加多少密度场都救不回来。约束之间是有顺序的:间距约束保证不穿模这条底线,密度场负责底线之上的美学。先物理,后美学,这个顺序和角色动画里"先解决脚踩地,再解决姿态帅"一模一样。

17.3 三条实战规则链:森林、乱石滩、废墟

理论够了,上战场。「回响峡谷」到「远港 Online」的野外区域,最终收敛到三条规则链,覆盖了大地图 80% 的自然内容。每条都给出完整链条:采样源 → 属性计算 → 过滤条件 → 生成目标。

规则链一:森林 = 坡度 + 海拔 + 密度场

森林是最典型的"三约束"问题。策划给的需求文档只有三句话:树不长在陡坡上;不同树种有各自的海拔带;整体要成簇。翻译成规则链:

# 伪代码:森林规则链——每个候选点要连闯四关
def forest_rule_chain(terrain, density_field, seed):
    rng = Random(seed)
    # 采样源:Landscape 表面,每 12 米一格候选
    candidates = surface_sample(terrain, spacing=12.0)
    kept = []
    for p in candidates:
        # 关卡一:坡度约束。法线与竖直方向夹角 > 25° 拒收
        if slope_degrees(terrain.normal_at(p)) > 25.0:
            continue
        # 关卡二:海拔带。松 300-1400m,桦 100-800m,按树种查表
        h = terrain.height_at(p)
        if not (300.0 <= h <= 1400.0):
            continue
        # 关卡三:保护区一票否决(村落/道路/任务点)
        if in_protected_zone(p):
            continue
        # 关卡四:密度场抽签。低频噪声 D∈[0,1] 决定成簇与空隙
        if rng.next() > density_field.sample(p.x, p.y):
            continue
        kept.append(point(p,
            scale = rng.uniform(0.8, 1.6),      # 树高分级
            yaw   = rng.uniform(0, 360),
            pitch = clamp(slope_tilt(p), -6, 6))) # 微微顺坡倾斜
    return kept

图上对应的做法:Surface Sampler 出候选 → Normal To Density 把坡度写进密度属性 → Density Filter(坡度区间)→ 海拔转密度的 Attribute 运算 → Density Remap 映射密度场噪声 → Self Pruning 保证最小间距 → Transform Points 做缩放朝向抖动 → Static Mesh Spawner 出树。注意链条里 pitch 那一行:真树在缓坡上不是绝对竖直的,顺坡倾斜 3-6 度这个细节值半天的评审时间——它消除了"插上去的感觉"。

树种混种也在这一步做:密度场不只输出一个标量,而是输出一张"树种权重图",Spawner 按权重从网格数组里抽签。针叶林向阔叶林过渡的地带,权重渐变,混交林就出来了。

规则链二:乱石滩 = 样条线 + 泊松盘

河滩与海岸线的乱石分布有强烈的方向性——它们是被水流搬运后沉积的,所以沿水岸呈带状分布,近岸密、远岸稀,大石少、小石多。均匀撒点在这里会死得更惨,因为带状结构根本不均匀。

采样源换成 Spline Sampler:让关卡美术沿河岸画一条样条线,PCG 沿样条线按弧长均匀取点,再在每个点的法线方向做横向偏移,铺出几条平行带。泊松盘约束负责最后的长相:

# 伪代码:乱石滩——样条线给走向,泊松盘给间距
def rocky_shore_rule_chain(coast_spline, r_min=3.0, seed=7):
    rng = Random(seed)
    belt = []
    step = 5.0 / coast_spline.length
    for t in arange(0.0, 1.0, step):
        c  = coast_spline.position(t)
        n  = rotate90(coast_spline.tangent(t))   # 横向法线
        # 三条平行带:近岸 8m 密、中 18m、远 30m 稀
        for side in (-1, +1):
            for w, keep_prob in ((8, 0.9), (18, 0.5), (30, 0.2)):
                if rng.next() < keep_prob:
                    belt.append(c + n * side * w)
    # 最小间距约束:任意两块石头 ≥ r_min
    accepted = poisson_disc(belt, r_min, k=30)
    return [point(p,
                scale = rng.lognormal(1.0, 0.4),  # 大石少小石多
                yaw   = rng.uniform(0, 360),
                pitch = rng.uniform(-8, 8))       # 半埋进沙里
            for p in accepted]

对数正态分布(lognormal)选型是刻意的:正态分布会给出负尺寸和"平均主义",对数正态的长尾正好模拟"遍地鹅卵石、偶见几人高的巨岩"。pitch = ±8° 同理——真石头没有端端正正坐在地面上的,半埋半斜才有沉积感。评审里"像真的"和"像摆的",差别经常就是这几度。

规则链三:废墟 = 建筑边界内点云

废墟类 POI(兴趣点)的难点反过来:约束的方向变了——此前是"排除哪里",现在要回答"只在哪里生成"。瓦砾、破箱、杂草只允许出现在建筑轮廓内,且贴着墙根长——现实的废墟里, debris 是被墙挡住堆积起来的,空地中央反而干净。

做法:Get Actor Data 类节点拉取策划摆好的建筑壳(白模或成品 Mesh)的包围体,Bounds Modifier 收紧边界 → Mesh Sampler 在建筑楼板上撒点 → 属性计算"到墙的距离"→ Density Remap 让靠墙 1.5m 内密度拉满、中心区域压到 0.2 → Self Pruning → Spawner。瓦砾走 ISM,可交互的破箱走 Actor Spawner(因为它们要带碰撞和交互组件,后文有账要算)。

三条链摆在一起看,骨架完全一致:谁提供几何约束(地形/样条线/建筑),谁提供统计约束(密度场/泊松盘/权重分布),最后都归并到"过滤后的点云"这一种中间产物。下次接到新需求——比如"山坡上长一片草甸"——你不会再问"有没有现成节点",而是问"几何约束是什么、统计约束是什么"。这就是约束求解思维替换随机思维的过程。

最后强调一条工程纪律:规则链是资产,资产就要有变更管理。PCG 图文件进版本库,改阈值要走评审——这是拿血泪换来的教训,跟官僚主义扯不上关系。有次美术把森林密度场的频率参数调高了一档想"让簇更碎一点",没告诉任何人,夜间烘焙后半个大陆的植被分布变了,三处任务点的隐蔽灌木消失,测试组第二天炸了锅。PCG 的蝴蝶效应比代码大:代码改一行影响一个功能,密度场改一个数影响一片大陆。所以远港的规则是:规则链变更必须附带"影响范围评估"——改的是哪张图、覆盖哪些格子、重烘焙要多长时间,写清楚了才准提交。

17.4 手工与 PCG 共存:保护区必须一票否决

纯 PCG 的世界没有叙事。村落、任务点、解谜机关这些承载玩法的内容,永远由策划手工摆放——这没问题。出问题的是交界:PCG 不知道村落存在,一棵树正好堵住村口;或者一片灌木盖住了任务道具的交互点。

解法在架构上只有一条:手工资产的位置信息必须反向进入 PCG 的约束集,而且优先级最高。我们叫这个机制"保护区"(Protected Zone):每个手工区域带一个体积(通常用简单 Box/Sphere,村落用拼合的凸包),PCG 过滤链的末尾统一过一个点云减法——落在任何保护区内的点,无条件删除。

"降低密度"不行——做过"保护区内密度乘以 0.3"的版本,结果是村落里稀稀拉拉还是长树,测试提了四十几个 bug 才认命改成硬删除。软约束适合美学,硬约束适合玩法——玩法区域必须 100% 可控,这是策划的底线,谈都不要谈。

UE 里的落地有两层。轻量的:PCG 图内用 World Volumetric Query 类节点查体积,配合 Difference(点云差集)节点直接减。重一点、要跨多个 PCG 图复用的:封装成自定义节点,全项目统一行为。骨架长这样(接口名以 5.8 官方文档 PCG → Custom Nodes 词条为准):

// 自定义 PCG 节点:保护区剔除。输入点云,输出"不在任何保护区内"的点云
UCLASS()
class UPCGProtectedZoneElement : public UPCGBlueprintElement
{
    GENERATED_BODY()
protected:
    virtual bool ExecuteInternal(FPCGContext* Context) const override
    {
        const UPCGPointData* In = GetInputPointData(Context);
        UPCGPointData* Out = NewObject<UPCGPointData>();
        // 保护区列表由关卡里的标记 Actor 提供,集中注册、全图统一查
        const TArray<FBox>& Zones = FProtectedZoneRegistry::Get(Context->World);
        for (const FPCGPoint& Pt : In->GetPoints())
        {
            const FVector Pos = Pt.Transform.GetLocation();
            const bool bBlocked = Zones.ContainsByPredicate(
                [&](const FBox& B){ return B.IsInsideOrOn(Pos); });
            if (!bBlocked)
                Out->GetMutablePoints().Add(Pt);   // 不在任何保护区内才放行
        }
        OutputPointData(Context, Out);
        return true;
    }
};

三个要点。一是这步必须放在过滤链末尾,位置抖动之后再剔除,否则抖进保护区的点会漏网。二是保护区数据要集中注册成一份,别让每个 PCG 图各自维护列表——十几张图各抄一份的时候,漏改一个就是线上事故。三是保护区本身要进版本管理并能被策划可视化编辑,它本质是关卡数据——代码常量承担不了这个角色。

缓冲区(Buffer Zone)是配套的第二个概念:保护区边界往外再加 3-5 米渐变带,密度从 0 缓升到正常值。否则村口的树被齐刷刷切出一个硬边,又是一眼假。硬删除管玩法,软渐变管观感,两个都要。

还有一个和第 16 章数据层(Data Layer)的联动值得交代。远港的手工内容按"村落层""道路层""任务点层"分层管理,保护区注册表就是从这些数据层自动生成的——策划往村落层里摆一栋房子,房子的包围体当晚自动进入注册表,次日烘焙的 PCG 自然绕开它。千万别反过来让策划"摆完房子再手动圈一遍保护区",人工步骤一定会被忘记,而自动化步骤不会。工具链的原则和运维一样:能由机器保证的不变式,就不要写进操作手册里求人类自觉。

17.5 运行时 PCG 还是烘焙期 PCG:MMO 没有第二种答案

同样的图、同样的种子,生成可以发生在两个时机:玩家跑到附近时现场算(运行时),或者开发期算好、把结果序列化进关卡包(烘焙期)。这张取舍表决定架构:

quadrantChart
    title "内容生产方式:成本与可控性"
    x-axis "可控性低" --> "可控性高"
    y-axis "边际成本低" --> "边际成本高"
    quadrant-1 "精工区"
    quadrant-2 "工匠区"
    quadrant-3 "灾难区"
    quadrant-4 "规模区"
    "全手工摆放": [0.9, 0.85]
    "运行时PCG": [0.45, 0.25]
    "烘焙期PCG+手工修正": [0.75, 0.45]
    "纯随机撒点": [0.15, 0.1]

两种时机的硬指标对照,选型时照着打分:

维度运行时 PCG烘焙期 PCG
生成成本由谁付每台玩家机器,每格每会话一次一台开发机,一次
包体只装图与规则,小装全部生成结果,大
跨端一致性靠种子与浮点纪律,有风险结果序列化,天然一致
静态光照/HLOD多数吃不到全部吃到
可人工修正产物不可,世界是算出来的可,产物是普通关卡数据
适合场景roguelike、动态变化地形MMO 大世界、固定叙事地图

运行时 PCG 的卖点是包体小、能动态响应(比如洪水过境后重铺 debris),代价是每端每格都要现场算一遍:CPU 时间花在玩家机器上,帧预算里要给它留位置;更麻烦的是 ISM 实例在运行时才创建,跟静态光照、预计算可见性这些"假设世界静态"的系统天生犯冲。单机游戏调节奏型玩法(roguelike 地牢)可以选它。

MMO 选烘焙期,理由有四条,条条是钱:

  1. 确定性跨端一致。服务器不算渲染,但它要算碰撞和寻路——"那棵树能不能挡箭"在服务器上必须有答案。烘焙期生成意味着碰撞体和导航网格在开发机上一次算好、全端一致;运行时生成则要求服务器也跑一套 PCG 求出同样的世界,多花的 CPU 是服务器的,服务器 CPU 是全项目最贵的资源。
  2. 性能归属正确。烘焙把生成成本从 10 万台玩家机器搬到 1 台开发机。一个 256m 格子的野外内容生成大约几百毫秒到几秒,玩家机器上这是卡顿,开发机上这是喝杯咖啡。
  3. 能吃静态红利。烘焙产物是普通静态网格实例,Lumen 的光照缓存、HLOD 的合并代理、预计算遮挡全都照常工作。运行时生成的实例很多静态优化吃不到。
  4. 可人工兜底。烘焙后美术还能进编辑器手动挪走几棵碍事的树——产物是普通关卡数据,改起来和手工摆的没区别。运行时生成的世界,美术碰都碰不到。

代价也摆在明面上:包体变大(生成结果全在包里)、改规则要重烘焙、大世界全量烘焙一次以小时计。这笔账在第 16 章的流送预算里已经埋过伏笔——烘焙产物正是按格子组织、按格子流送的。

包体账值得算细一点,免得评审时拍脑袋。一个 256m 野外格子的 PCG 产物:ISM 实例的 Transform 数据(每实例 40 字节上下,压缩后更低)加网格与贴图引用。两万实例的密林格子,实例数据也就 1MB 量级——真正占体积的是网格和贴图资产本身,而它们跨格子共享,只进包一次。所以"烘焙期让包体爆炸"在植被场景里其实是个被夸大的恐惧:远港野外 977 格的实例数据总量估在 1-2GB,对比整个游戏几十 GB 的资产包,换来的是全端一致性和静态优化红利,这笔买卖做得过。真正要盯的是另一种格子:废墟 POI 里走 Actor Spawner 的可交互物,每个都是独立 Actor 带组件状态,实例数据翻几倍不说,还不能合 ISM——这类内容要严格控制密度,一个格子别超过三位数。

17.6 「远港 Online」野外管线:从高度图到可玩区域

把前面所有零件装起来,看一遍远港野外区域的完整生产线。输入是地形组交付的高度图与材质分层图,输出是可以进版本库、能被流送系统按格加载的可玩区域。

flowchart LR
    A[高度图 + 材质分层
地形组交付] --> B[按 256m 格子
分区触发 PCG 图] C[规则链库
森林/乱石滩/废墟] --> B D[保护区注册表] --> B B --> E[ISM 实例
落在格子归属内] E --> F[重建 HLOD
远景代理] F --> G[光照/导航
预计算] G --> H[人工验收
美术修正个别实例] H --> I[提交版本库
OFPA 一 Actor 一文件]

两个设计点展开说。

生成按格子分区,而非按图分区。 第 16 章定过远港的切割:8km×8km 主大陆,256m 一格,约 977 格。PCG 的分区生成与这个网格严格对齐——每个格子内的点云只生成归属本格的实例,跨格的内容(比如一棵树横跨边界)按点位置归属到单一格子,宁可切树不可双份。这样流送系统加载格子时拿到的永远是自洽的一份内容,卸载时不留孤儿。UE 侧对应的机制是 PCG 与 World Partition 的分区生成集成(命令行批处理入口在官方文档 PCG → World Partition 相关词条下,版本间名字有变动,以 5.8 文档为准),它能按网格分块调度生成,避免一张图一口气算完 64 平方公里把内存吃爆。

落到数据上还有一层讲究:每格输出按网格类型聚合的 ISM 组件,告别散装 Actor——格子里两千棵松树合并成少数几个 ISM,Draw Call 从两千降到个位数,这正是第 6 章渲染管线里最贵的那项指标。这些 ISM 组件作为 Actor 归属格子,走 OFPA(一 Actor 一文件,见第 16 章)各自成文件,于是两个人同时改不同格子的规则链不会互相冲突,版本库的合并冲突概率也跟着降下来。HLOD 重建只碰受影响格子的代理,8km 地图改一条河滩规则,重烘焙的范围是沿河几十格,不是全图 977 格——局部重烘焙这一条,直接决定规则迭代的周期是按小时算还是按天算。

烘焙是一道需要严格控制时序的流水线。 一次点击完成不了——完整的夜间批处理长这样,时序上有严格的依赖顺序:

sequenceDiagram
    participant CI as 夜间构建机
    participant PCG as PCG 分区生成
    participant WP as World Partition
    participant HLOD as HLOD 构建
    participant P4 as 版本库
    CI->>PCG: 拉取最新地形与规则链,按格调度
    PCG->>PCG: 每格:采样→过滤→剔除保护区→生成 ISM
    PCG->>WP: 实例按点位置写入对应格子 Actor
    WP->>HLOD: 触发受影响格子的代理重建
    HLOD->>CI: 远景合并完成
    CI->>CI: 光照/导航预计算 + 自动校验(密度/穿模抽查)
    alt 校验通过
        CI->>P4: 提交生成结果(OFPA 文件)
    else 校验失败
        CI->>PCG: 标记失败格子,次日人工介入
    end

校验这一步别省。自动抽查几条:格子的实例数是否在预期带宽内(规则改坏的第一信号是数量爆炸)、保护区周边 1m 内是否存在零实例(漏接注册表的信号)、随机抽 20 个点射线检测是否悬空或穿模。这些是规则链回归测试,和第 28 章 CI/CD 那套是同一种思维:内容生产线也是代码,也要有测试。

最后交代远港的选型结论:野外自然内容(森林/乱石/草甸/废墟装饰)走烘焙期 PCG,估占野外实例数的 85%;玩法实体(怪物、NPC、可采集物)不进 PCG,走策划配置表由服务器刷怪系统管理——因为它们的生命周期归服务器管,和第 21 章复制系统、第 25 章 Zone 拆分是一条线。两类内容在视觉上是邻居,在架构上永不见面。划清这条线,后面卷三的每一章都会轻松一些。

小结

PCG 是约束求解,不是随机生成:点云是解集,过滤器是约束,随机只是扰动项与种子化的确定性来源。自然感的配方是"低频密度场成簇 + 逐点抽签噪点 + 泊松盘最小间距",均匀随机在空间统计上就是错的。三条规则链共用一副骨架:几何约束(地形/样条线/建筑边界)定范围,统计约束(密度/间距/分布形态)定长相。保护区必须一票否决且集中注册;缓冲区做边缘渐变——软约束管美学,硬约束管玩法。MMO 选烘焙期 PCG:确定性跨端一致、成本归开发机、吃静态光照与 HLOD 红利,代价是包体与重烘焙时间。

上手任务

  1. 在一片 1km² 的 Landscape 上复刻森林规则链:坡度阈值 25°、海拔带一条、Perlin 噪声密度场一张。验收:截图中无陡坡树、无越界树,且能指出至少三处明显的簇与三处林窗空隙。
  2. 把密度场换成常数 0.5 重新生成,与任务 1 并排截图。验收:能用自己的话讲清"棋盘感"出现在哪里、为什么——讲不清就回去重读 17.2。
  3. 沿任意样条线生成乱石滩,Self Pruning 最小间距从 1m 调到 5m 各生成一版。验收:导出两版点云,写脚本验证任意两点距离满足设定阈值,误差为 0。
  4. 在村落模型外围画一个保护区体积,接入剔除节点。验收:生成后保护区内实例数为 0,边界外 3-5m 内密度渐变过渡,无硬切边。

下一章

内容生产线铺完了,世界能看也能跑了——但"能跑"和"跑得快"之间隔着一整套测量方法论。第 18 章用 Unreal Insights 把「回响峡谷」按在 4K/60fps 的指标上摩擦。

延伸阅读

  • 官方文档:Unreal Engine → Procedural Content Generation (PCG) → Overview 与 Custom Nodes 词条
  • 官方文档:Unreal Engine → World Partition,配合 PCG 分区生成的相关小节
  • Bridson, "Fast Poisson Disk Sampling in Arbitrary Dimensions"(2007,SIGGRAPH sketch)——两页纸的经典
  • GDC 相关内容:程序化生成方向,搜索 "Procedural Generation in Unreal" 与 Ghost of Tsushima 的地形与植被管线分享
  • 源码参考:Engine/Plugins/PCG/Source/PCG/Private/Elements/ 下的采样与过滤节点实现