《城市生命力》P1|开放世界的人口编排

系列《城市生命力》· P1 · 行人调度

上一篇(系列总览):在 UE5 上重建一座会自己运转的城市。本篇承接总览,进一步分析其中的人口系统。

本篇讨论街头人群从何而来、去向何方、何时出现以及何时回收,覆盖人口编排、稳定存根、三类运行形态、生成预算和依托车道图的人群流动。运动学、动画与群体表现属于后续美术表现篇的讨论范围。

导言:人群来自统一编排,而非独立 AI 的简单叠加

城市人群不是大量独立 AI NPC 的简单集合。若每个路人持续运行完整的感知、决策与行为链,数百个对象同屏时会产生显著的 CPU 开销。

原版(一款成熟的大型开放世界 RPG)设置了一层独立的人口编排系统,连接关卡中的社区数据与运行时 NPC。该系统统一管理存根、生成服务、分层运行形态、预算,以及车道图、存档和跨系统事件之间的桥接。街头人群并非彼此独立地生成,而是根据玩家周边的需求逐层调度。

全文依次分析存根与实体表示的关系、远景视觉层、异步生成队列、Actor 挂载预算、车道图上的人群流动、异常清理与持久化,并据此提出在 UE5.8 中自建面向数据人口运行时的迁移方案。Mass 可以作为局部实现手段,但不是前提。

人口生成跨越多个阶段和异步令牌,并可能失败、取消或超时。请求受理只表示工作已进入调度流程,不表示实体已经可用。后文的生成、挂载与回收均以此为前提。

术语与证据口径

本文用语 含义
存根(stub) 带稳定实体 ID 的轻量身份数据,不是 Actor
表示(representation) 一个身份当前采用的运行时呈现方式
实体表示(Actor representation) 可挂载到存根上的 Actor;可按 crowd / simple / occupant 等档位裁剪组件
远景点(distant dot) 匿名视觉数据,不对应稳定身份
挂载 / 卸载(attach / dispose) 存根取得或释放实体表示
表示交接(handoff) 不同表示层之间的协调切换;不默认意味着同一身份跨越远景点
异步令牌(token) 跟踪跨帧请求状态与取消结果的凭据
工作点(work-point) 与车道或场景位置关联的行为锚点

正文采用三种口径:“代码直接显示”用于源码可观察事实;“从调用关系可抽象为”用于职责解释;“本文建议”用于 UE5.8 迁移方案。三者不会混写成同一层面的结论。


一、存根优先:身份先于实体表示

① 源码观察与职责抽象

整个人口系统的基石,是存根优先(stub-first)。一个进入稳定身份系统的“人”,第一存在形式不是实体表示,而是存根(stub)——它有稳定的实体 ID、位置 / 朝向和必要状态,但不含 Actor 的整套组件(AI、动画、物理、感知……)。匿名远景点不属于这个身份系统。

实体表示的挂载 / 卸载不改变存根身份。 存根可按需 attach Actor 实体表示,取得 AI、动画与交互能力;相关能力不再需要时再 dispose 该表示。任务、存档和关系系统由此可以关联稳定身份,而不必绑定易失的 Actor。大世界通常存在人口 ID、持久化 ID 和运行时表示句柄等多套标识;本文所称“身份”,是供上层系统建立关联的稳定身份层,并非全局共用的单一 ID。

存根由专门的存根控制器管理。代码显示,控制器保存裸的存根指针,并依靠删除回调在锁保护下清除引用。该结构不产生引用计数开销,但源码不足以证明这就是选用裸指针的唯一原因。可以确认的是,任何绕过回调的删除路径都可能留下悬空指针。

裸指针与删除回调的生命周期约束

代码采用裸指针 + 删除回调:存根销毁时,在锁保护下清除持有方保存的引用。该结构避免了引用计数,也把安全性集中到删除路径;任何绕过回调的销毁,都可能留下悬空指针。

迁移到 UE 时,普通结构体或 SOA 存根不能使用 TWeakObjectPtr。更稳妥的起点是索引 / 句柄 + generation(代数):句柄解引用时同时核对槽位与代数,防止回收后的槽位被误认成旧对象。TWeakObjectPtr 只适用于 UObject / Actor 形式的实体表示。并发删除时,应在锁内完成“标记、从集合移除、收集待通知项”,释放锁后再执行外部回调;只有证明回调不会重入时,才考虑在锁内通知。

异步创建中的令牌、待定表与失效校验

存根创建采用异步令牌机制,而非同步构造。请求发起后,创建器取得一个令牌并将其写入待定创建表;此时存根对象尚不存在。创建完成后触发回调,回调首先检查令牌状态,成功后才接管存根并加入容器。容器接管本身不再执行拒绝逻辑,相关校验发生在更早阶段。

主要风险来自失败处理与竞态。待定数据保存了指向交通槽的弱指针,因为存根创建期间,该槽可能已被交通系统删除。完成回调因此需要再次校验依赖:即使存根创建成功,只要目标槽已经失效,也要作废存根,并同步回收槽、清理工作点令牌。槽请求还设置了每帧固定的重试上限,避免大量失败请求集中消耗当前帧预算。

② 对 UE5.8 迁移的启示

UE 的 Actor、Mass 和 ISM 各自提供对象生命周期、数据批处理或实例渲染能力,但不会直接给出“稳定身份 + 分层表示 + 预算化异步挂载 + 可恢复交接”的整套语义。本文建议把稳定身份放在独立存根层中,实体表示按需挂卸;是否使用 Mass,取决于项目版本、数据规模与团队已有管线。

③ 移植到 UE5.8


// 以下均为职责级架构伪代码,用于表达所有权和状态边界,
// 不代表已完成、可编译或唯一的 UE5 API 设计。
struct FPopulationStub
{
    FPopulationEntityId Id;
    FTransform Transform;
    FPopulationPersistentState State;
    FRepresentationHandle CurrentRepresentation;
};
// UPopulationRuntimeSubsystem 管存根池。
// 存根引用使用带 generation 的数据句柄;Actor 表示可使用 TWeakObjectPtr。

迁移原则:存根身份与实体表示解耦;Actor 表示可反复挂卸,存根 ID 保持稳定,存根池独立管理生命周期。不宜采用“spawn 即建立全部身份与表示”的单层模型。


二、三级成本层:Actor 表示 / 存根 / 远景点

① 源码观察与职责抽象

人群运行时按距离、重要性与语义需求分成三类成本层:

  • 实体表示:存根按需挂载的 Actor。Actor 仍可按用途分为 full、crowd、simple、occupant 等组件档位;近处需要交互的档位成本最高。
  • 存根:带稳定实体 ID、变换与必要状态的轻量数据层,本身不拥有 Actor 组件。实体表示可反复 attach / dispose,存根身份不变。
  • 远景点:匿名视觉数据,沿预处理车道插值并批量绘制;不拥有实体 ID,不参与交互,也不承诺与随后出现的某个存根是同一个人。

这三层发生的是表示协调,不是一条身份从远景点一路延续到 Actor 的必然链。代码直接显示,存根超出范围时会按车道与位置请求补充远景点;反向接近时,系统依据区域密度创建常规存根。两层在视觉密度上衔接,但当前证据不足以证明远景点与新存根之间存在一对一身份交接。

行人与车流都按距离和交互需求分配运行成本:越接近玩家,表示承担的交互语义越多。两者的层级、槽位与交接规则仍分别定义。

图 · 身份与表示解耦:稳定身份从存根开始,远景点只维持匿名视觉密度
图 · 身份与表示解耦:稳定身份从存根开始,远景点只维持匿名视觉密度

时序走查:区域人口从匿名密度到有身份表示的衔接

  1. 最远处只维护匿名远景点。
  2. 玩家接近后,人口系统按区域需求创建带稳定 ID 的存根,同时避免远景点与常规人群重叠。
  3. 预算允许时,存根挂载合适档位的 Actor 表示。
  4. 交互状态写入存根或其持久化数据。
  5. 玩家离开后,Actor 表示卸载,身份仍可停留在存根层。
  6. 存根超出范围并被回收时,系统可在相近车道位置请求新的匿名远景点,以维持视觉密度。

稳定身份只覆盖存根与其实体表示,不覆盖前后的远景点。

升级由重要性排序决定,而非单一距离阈值

升级并不由距离或时间直接触发,而是由所有候选对象参与的多因素重要性评分决定。评分较高的对象优先占用有限的完整实体名额。代码结构表明,该评分综合了以下维度;具体权重与组合方式仍以实现为准:

  • 身份优先级:剧情、任务、社区和背景人群具有不同基础优先级。
  • 类型权重:对象类型参与评分;具体类型之间的大小关系仍需结合配置与调用点核验。
  • 距离与高度差:水平距离和垂直高差影响候选优先级。
  • 可见性与兴趣区域:视野和 POI 等因素可提高候选优先级;具体增量仍以配置为准。

在人口调度更新中,候选对象会被评分,并在可用名额内尝试挂载实体表示。源码可观察到类型、距离、高度、可见性与兴趣区域等因素;准确更新频率、权重次序和组合方式仍需结合配置核验。

实体表示挂载时的组件裁剪

按 LOD 裁剪组件发生在实体表示上,不发生在存根上。源码为 crowd、simple、occupant 等 Actor 档位配置了不同的组件过滤表;crowd 档会排除状态效果、小队成员、视觉感知、肢解、投射物生成与物体携带等组件,更精简的 simple / occupant 档还会移除更多能力。

这些组件类型通过一次性初始化和类型指针缓存查询,避免运行时反复字符串匹配。存根保持纯数据职责,组件过滤仅作用于 Actor 表示。

② 对 UE5.8 迁移的启示

UE5.8 不直接提供上述完整语义。MassCrowd 能否承载其中一部分,取决于引擎版本和项目的 Mass 管线。迁移设计仍需区分三类对象:远景点属于匿名视觉层,存根属于身份数据层,Actor 表示再按 full、crowd、simple、occupant 等档位配置组件。

UE5.8 在表示层和批处理层已有成熟设施:ISM/HISM 可绘制大量远景实例,Mass 的 fragment 与 processor 适合批处理,SmartObject 可承载交互点,动画预算分配器可以控制人群动画开销。需要自建的是将这些设施组织成“匿名远景密度 + 稳定存根身份 + 分档 Actor 表示 + 预算化 attach”的编排语义,而不是重新实现 ISM 或动画预算器。

③ 移植到 UE5.8


enum class ECrowdRepresentationTier : uint8
{
    FullActor,
    CrowdActor,
    SimpleActor,
    OccupantActor,
    DistantDot
};
// Stub:独立身份数据,不是本枚举中的 Actor 档位。
// DistantDot:匿名视觉数据;与存根只做密度协调,不承诺身份连续。

迁移原则:远景点、存根和 Actor 表示分别承担视觉密度、稳定身份和交互能力;组件裁剪只属于 Actor 表示;稳定 ID 从存根开始。


三、人口编排:从社区数据到运行时人群

① 源码观察与职责抽象

常规人群需求来自关卡中预先配置的社区数据(community),其中记录区域人口类型、密度与作息。人口编排层负责在运行时将静态社区数据转换为动态人群:依据玩家位置、时间和密度参数,决定生成位置、人口类型与成本层级。

社区数据的核心是一张作息时间表。社区按时段划分为若干生成阶段(spawn phase):每个阶段绑定一个时段,并记录生成数量(quantity)、生成所使用的标记点或 spot 节点,以及是否按序生成(sequence)。早高峰通勤人群、深夜低密度人群与正午商业区人流由不同时段的生成阶段驱动,而非硬编码条件分支。

编排层连接社区数据、实体存根、运行时生成服务、交通车道、远景人群渲染、响应单位需求、存档和跨系统事件。它负责形成并协调人口需求,但不直接构造 Actor。人口类型与数量的决策完成后,实体仍需经过异步生成链路。

图 · 人口编排层:桥接中枢,六方解耦,由作息时段表驱动
图 · 人口编排层:桥接中枢,六方解耦,由作息时段表驱动

prevention 的职责边界

原版内部名 prevention 容易被按字面理解为“抑制人口生成”。代码直接显示,它提供按等级请求生成单位、按等级批量撤销和查询当前生成数量等能力;从接口与调用关系看,本文暂将其抽象为通缉 / 敌对响应生成协调器。至于它是否覆盖完整通缉状态机,仍需继续核对上层调用点。

同一个源码类还持有死体计数器,并按距离、可见性和数量阈值提交回收请求。这是已确认的代码归属,但不代表迁移设计必须把“响应单位生成”和“全局死体清理”合并为一个业务子系统;两者可以共享人口运行时的生成 / 回收接口。

这类响应单位需求不同于维持社区密度的常规人口需求。死体回收虽位于同一个源码类中,但经由人口运行时的统一清理接口执行。

尸体清理的三层范围、资源压力删除与 ID 延迟复用

尸体清理并非每帧执行,而是以较低频率运行。系统首先收集全部死体,再依据其到玩家的距离平方分为三层:

  • 最外圈(超过视野外清除距离):立即强制删除。
  • 中圈(在中间距离带):提交软删除请求,仅在满足常规删除条件时执行。
  • 近圈(玩家附近):暂不删除,同时记录视野内与视野外距离最远的候选对象,供资源压力路径使用。

资源压力达到阈值时还存在一条强制删除路径:当死体数量超过立即清除阈值、总实体数达到上限,且本轮常规清理未删除任何对象时,系统优先强制删除距离最远的视野外死体;若不存在视野外候选,则删除视野内距离最远的对象。正常情况下,系统遵守可见性约束;只有资源压力达到阈值时才允许越过该约束。

实体删除后,其 ID 被插回 ID 池前端,而分配从另一端进行。该双端策略延长了已释放 ID 的再次使用间隔,降低脚本旧引用发生身份混淆的风险。

作息调度器的时段切换与回调注册

社区作息由时段调度器执行。设置时段集合时,系统先注销旧回调、更新时段表,立即按当前游戏时间应用一次,再注册新回调。单时段配置直接使用该时段;多时段配置从后向前查找第一个“起始小时 ≤ 当前小时”的条目。若没有命中,则回绕到最后一个时段,以覆盖跨午夜区间。

只有配置多个时段时,系统才注册时间回调;单时段社区无需监听时间变化。回调还会按帧号去重,避免同一帧内重复处理。

生成与回收的完成通知

源码可观察到实体生成器广播器与完成回调,用于把部分生成结果传递给下游,而不要求所有消费者轮询人口状态。

当前证据同时显示,attach 调度中的广播可能早于全部下游副作用结束,因此不能把它概括为“所有相关事件都严格延迟到完全完成”。完成通知至少需要区分 Actor 已挂载与玩法状态已就绪。

② 对 UE5.8 迁移的启示

UE5.8 不提供从社区数据直接生成运行时人群的完整编排层。原版将人口数量、位置与类型从硬编码条件转换为数据驱动编排:内容作者配置社区数据,编排层根据玩家位置和运行规则形成实际人群分布。迁移时需要自建这一协调层。

③ 移植到 UE5.8


// 重建工程:人口编排层(桥接社区数据 / 存根 / 生成 / 车道 / 存档 / 事件)
// 不采用单一 Tick 统一处理全部人口;拆分为显式相位,接入自定义 World 调度,而非普通 Actor Tick。
class UPopulationRuntimeSubsystem : public UWorldSubsystem
{
    void TickPopulationPhase(float Dt);   // 编排:按作息表计算人口需求与表示优先级
    void TickSpawnPhase(float Dt);        // 生成相位:消化异步请求队列、节流放行
    void TickDespawnPhase(float Dt);      // 回收相位:延迟删除器 + 孤儿容器 + 批量清
    // 输入:社区作息表(时段→生成阶段:数量/标记/spot/是否按序) + 玩家位置 + 时间
    // 协调:存根池 / 实体生成服务 / 交通车道 / 远景渲染 / 存档 / 事件广播
    // 响应单位需求与社区人口需求分开输入;死体回收复用统一清理管线。
};

迁移原则:编排层负责协调而不直接构造实体;社区作息表驱动常规人口;响应单位作为另一类需求源;完成通知区分 Actor 已挂载与玩法状态已就绪。


四、生成是一个请求队列,不是即时调用

① 源码观察与职责抽象

人口系统将生成一个人定义为异步请求,而非即时调用。添加实体、请求生成以及启用或禁用人群等接口返回的是请求已受理或已入队的信号,并不表示实体已经可见。

一次生成需要依次经过存根创建 token → 人口注册队列 → 实体生成 token → attach 调度。各阶段均可跨帧等待,也可能失败或取消。生成请求返回时,Actor 表示通常尚未建立;依赖实体可交互状态的逻辑需要等待后续完成信号。

这种分阶段设计源于流送与预算约束:请求位置可能尚未完成流送,当前帧生成预算可能已经耗尽,attach 也可能需要等待实体生成服务。同步生成既可能集中产生 Actor 构造成本,也无法在依赖未就绪时安全排队。

本文用于解释异步边界的四阶段模型

源码并不存在名为“四个 token”的统一结构,但生成路径上的异步状态与凭据可以抽象为四个阶段。该模型用于解释异步边界和中断位置,不代表原版存在同名结构:

  1. 存根创建 token:请求首先创建存根。若目标位置尚未完成流送,则等待依赖就绪;位置无效时进入失败状态。
  2. 人口注册队列:存根完成后进入人口系统有序表。注册操作先入队,在人口系统处理节拍到来时正式登记。
  3. 实体生成 token:注册后的存根请求 Actor 实体表示。Actor 构造受实体预算约束,预算已满时继续排队。
  4. attach 调度:实体完成后 attach 到存根,挂载组件、AI 和动画,并恢复持久态。该阶段可能跨帧执行,完成后对象才具备交互能力。

任一阶段都可能因流送、处理节拍、预算或跨帧执行而延迟,也可能因位置无效或请求撤销而失败。因此系统使用 排队中 / 存根已建 / 实体生成中 / 已挂载 / 失败 / 超时 等分级状态。将整条链压缩为单一成功或失败布尔值,会丢失处理中、撤销和超时等必要状态。

图 · 从多条源码路径抽象出的四阶段模型:用于解释受理、等待、挂载与中断边界
图 · 从多条源码路径抽象出的四阶段模型:用于解释受理、等待、挂载与中断边界

② 对 UE5.8 迁移的启示

UE 的 SpawnActor、deferred spawn 等设施可以完成 Actor 构造,但不会自动提供一条可跨帧取消、受流送与预算约束、并能报告多级完成状态的人口请求链。迁移时仍需在最终 Actor 构造之外增加人口编排。

若在产生需求时直接调用 SpawnActor 并立即使用返回对象,规模扩大后会出现同帧构造成本集中、流送依赖尚未就绪以及缺少人口预算约束等问题。SpawnActor 适合作为异步请求链的最终构造步骤,其前置流程包括入队、节流、流送校验、预算检查与分级状态管理。

③ 移植到 UE5.8


// 重建显式建模源码中分散的异步边界,不声称复刻同名状态机。
FPopulationSpawnHandle UPopulationRuntimeSubsystem::RequestSpawn(
    const FPopulationSpawnDesc& Desc);
// 返回 handle(受理),不是 actor(完成)。内部阶段:
//   存根创建 token → 人口注册队列 → 实体生成 token → attach 调度
enum class EPopulationSpawnStatus : uint8
{
    Queued, StubCreated, ActorSpawning, ActorAttached, ReadyForGameplay,
    Failed, Cancelled, TimedOut
};
// 依赖实体可用状态的逻辑等待所需完成级别,不依赖 RequestSpawn 的返回值。

迁移原则:生成接口返回 handle 而非 actor;完成过程跨越多个令牌和阶段;依赖实体在场的逻辑等待 attach 完成回调。

生成节流与请求抵消

生成队列包含两项调度约束。其一,每帧生成数量有硬性上限,只允许固定数量的存根进入实际生成。这样可以将区域切换时集中到来的请求分摊到多个帧,避免存根创建和实体 attach 成本集中。

其二,生成与删除请求可以相互抵消。 删除队列处理前会检查对象是否仍处于生成队列;若尚未生成,则直接撤销生成并回收 ID,避免先构造再删除。新的生成请求还会使既有待删请求暂缓。

ID 池采用对称的双端策略:分配从后端弹出,回收插入前端,使刚释放的 ID 经过更长间隔后才会重新分配。这与通缉响应系统的 ID 回收策略一致。


五、生成预算:Actor 表示的挂载条件

① 源码观察与职责抽象

三类成本层中,交互能力最完整的 Actor 表示成本最高。系统通过独立预算限制同时挂载的高成本实体表示数量;超出 attach 预算的请求继续等待,直到其他表示 dispose 并释放名额。

该预算控制人口规模对帧成本的影响:远景点消耗实例池、更新和渲染预算,存根消耗数据池预算,Actor 表示则占用更高成本的挂载与模拟预算。玩家视角变化时,系统在高成本名额内回收低优先级表示,并把名额重新分配给更高优先级对象。

存根池本身也有独立预算,与 Actor 挂载预算分开。因此,大量身份可以低成本停留在存根层,只有当前更可能参与交互的对象才竞争高成本实体表示。

图 · 重要性竞争 + 分层预算闸门:升格与否由排序 + 名额决定
图 · 重要性竞争 + 分层预算闸门:升格与否由排序 + 名额决定

② 对 UE5.8 迁移的启示

UE5.8 不直接提供“Actor attach 预算 + 存根池独立预算”的完整人口语义。分层预算为高成本模拟设置上限,使实体表示数量不随区域理论人口同步增长。

两级预算、画质配置与未挂载暂存

源码中的两级预算可概括为已挂载实体预算与未挂载实体预算,分别记录当前用量和上限。上限按画质档从配置表读取,使高成本实体表示数量可以随性能档位缩放。

生成算法按重要性对队列排序,并依次尝试进入已挂载预算。预算已满时,对象不会立即丢弃,而是转入未挂载实体预算暂存;实体已经完成构造,但尚未 attach 完整表示,待已挂载预算释放后继续处理。位置无效等实际生成失败则立即取消并从队列移除,不进行无限重试。

迁移时应将预算设计为画质档可配置的两级结构:已挂载实体承担完整模拟成本,未挂载实体保存已构造但等待 attach 的对象,实际生成失败则终止请求。

③ 移植到 UE5.8


// Actor attach / 模拟预算:高成本名额,按画质与运行状态配置
// 存根预算:数据池容量与更新成本
// 远景点预算:实例池容量、更新批次与渲染成本
// 玩家移动时,高成本名额在候选之间重新分配。

迁移原则:远景点不占用完整实体 attach 名额,但仍消耗实例池、更新和渲染预算。高成本实体槽位应受控,并可随画质、平台与运行时压力调整。


六、人群与车道图:共享拓扑,分别执行

① 源码观察与职责抽象

人群也使用交通车道基础设施。 源码中可以观察到人群监听交通车道事件、按车道片段计算密度,并通过交通槽与工作点协调移动。这些调用说明人群与车辆共享车道图核心数据和部分协调机制,但不能据此认定两者使用完全相同的槽位、占用语义或空间索引。

人口编排通过人车协调器接收车道事件并使用交通槽机制。工作点(work-point)为候车、浏览橱窗或过街等行为提供空间锚点,人群生成时可被分配到相应位置。

车道事件处理还体现了分阶段原则:除监视器状态外,人群不会在收到车道封锁事件时立即删除对象,而是将死端与堵塞清理推迟到删除阶段统一批处理。

车道段密度如何驱动生成与删除

系统通过按车道段维护的密度账决定人口增减。每条车道划分为若干段(fragment),每段记录目标人数与实际人数之差。差值为正时提交生成请求;差值为负时触发删除。删除候选还会按过剩程度排序,使偏离目标最大的车道段优先减员。

生成与删除共用同一项密度目标,避免两套独立规则产生冲突。密度目标来自社区数据,并随时段变化传递到各车道段;目标值与实际值的差同时驱动生成和删除。

图 · 人车共享车道拓扑核心数据,但使用不同查询与占用规则;行人密度按段维护
图 · 人车共享车道拓扑核心数据,但使用不同查询与占用规则;行人密度按段维护

远景人群点的车道归属

专用的远景人群车道查找器负责为远景点分配流动路线。查找器在点位周边检索合适的人行道车道并建立绑定,避免远景人群偏离步行区域或进入机动车道。

该问题与车辆生成都属于车道空间检索,但筛选条件与占用语义不同。源码是否让两者共享同一个 BVH 实例尚未确认。

工作点(work-point)也在这一阶段分配。人群点绑定车道后,可以前往附近的候车、橱窗或过街工作点,执行对应行为后再继续移动,从而避免人群长期以单一速度沿人行道循环。

② 对 UE5.8 迁移的启示

UE5.8 中可用的人群与交通能力取决于插件和引擎版本。重建工程可以共享道路 / 人行道拓扑与空间坐标基础,再按对象类型定义查询过滤和占用规则,为过街、让行与密度协调提供统一的空间基准。

③ 移植到 UE5.8


// 重建工程:统一车道拓扑,按对象类型配置过滤与占用语义
// 人行道是车道图的一类边;行人、车辆、远景点使用不同查询策略。
// UPopulationTrafficCoordinator:协调车道事件、行人槽与工作点。
// 车道封锁事件:忽略(除监视器状态),死端清理推迟到删除阶段批处理。

迁移原则:人群与车辆共享车道图核心数据,不默认共享完全相同的槽位和占用规则;工作点为行人行为提供空间锚点;车道封锁相关清理推迟到删除阶段批处理。


七、清理:停滞、超范围与生成失败

① 源码观察与职责抽象

人口对象持续生成,也需要持续回收。原版将以下情况统一交给删除阶段批处理,而不是在任意时刻分散执行:

  • 超出范围:对象离开玩家周围的活跃区域。
  • 停滞(stuck):对象受几何或交通状态影响,持续无法移动。
  • 生成错误:位置无效、依赖失效或生成过程未能完成。
  • 车道死端 / 堵塞:对象所在车道不再具备有效通行条件。

所有删除均经过统一回调。由于存根控制器保存裸指针,相关集合的标记、移除与待通知项收集需要受锁保护;外部通知宜在释放锁后执行,避免回调重入造成死锁。集中批处理还可以避免遍历人群时并发修改集合所导致的迭代器失效与数据竞争。

延迟删除器的淡出、FIFO 与预先降级

存根被判定需要删除后,多数情况会进入延迟删除器。系统依次将其设为非常驻、降低到人群优先级、触发淡出,再连同时间戳写入 FIFO 队列。每帧仅在队头对象满足淡出时长后执行实际删除。

预先降级可以防止对象在淡出期间重新参与重要性竞争并再次升格。FIFO 使删除顺序与请求顺序一致,便于预测内存释放。只有视野外且不再需要的对象才允许同步删除,其余对象均经过淡出流程。

删除采用“多入口、单出口”结构。到达终点、交通阻塞、死亡、超出范围和车道人口过剩等原因最终汇入同一删除函数,由该函数统一将存根转交给孤儿容器、从交通系统移除槽并更新范围计数。不同入口仍保留各自的可见性、距离和时间门槛:阻塞对象仅在视野外且距离足够远时删除;超出范围的对象先向远景系统请求 dot 替身;人口过剩路径避开刚刚可见的对象。

孤儿容器承担删除前的中转状态

存根从运行集合移除后不会立即销毁,而是先转交给孤儿容器。容器按实体 ID 排序,并通过 lower_bound 定位插入位置,使按 ID 查询保持对数复杂度。存根进入容器时还会创建事件桥接对象,最终删除时同步销毁,使脚本层在对象退役期间仍能接收必要事件。

运行集合移除与最终销毁之间仍需完成淡出、通知并等待安全时机。延迟删除器负责计时,孤儿容器负责排序存放与事件桥接,两者共同承载对象退役阶段。

图 · 平滑回收:延迟删除器 + 孤儿容器 + 尸体三层清理范围
图 · 平滑回收:延迟删除器 + 孤儿容器 + 尸体三层清理范围

② 对 UE5.8 迁移的启示

UE 的 Actor 销毁设施不会自动解决人口集合的跨系统引用、淡出和批量修改边界。重建时仍需明确删除相位:锁内修改所有权与索引,锁外发送可能重入的通知。

③ 移植到 UE5.8


// 重建工程:人群清理(多原因集中到删除阶段批处理)
enum class EPopulationDespawnReason : uint8 { OutOfRange, Stuck, SpawnError, LaneDeadEnd };
// 收集待删存根 → 锁内标记/移除/收集通知 → 解锁后执行回调
// 避免在人群遍历期间并发删除,以免迭代器失效或发生数据竞争。

迁移原则:多类回收原因集中到删除阶段批处理;锁内只处理所有权、索引和待通知列表;外部回调默认在锁外执行;遍历期间不并发修改集合。


八、远景人群:视觉数据不等于可交互实体

① 源码观察与职责抽象

人口运行时最远的一层是远景人群(distant crowd)。它由沿车道插值并通过 GPU 实例化渲染的点构成,提供的是视觉数据,而不是完整的人群实体。

远处广场的人群在渲染上可见,但逻辑层不存在逐个执行决策的 NPC,只有沿路线移动并批量绘制的数据点。玩家接近后,系统减少或重分配远景点,并按区域人口需求创建常规存根;这两者维持视觉连续性,但不承诺一对一身份连续。

远景点的重新分配条件

远景点每帧更新,并在以下任一条件满足时重新分配:车道索引失效、距离玩家过远、连续若干帧停止移动、当前车道需要减少点数,或进入应由常规人群接管的距离范围。若远景点位于玩家视锥内且接近该范围,也会触发重新分配,避免纯数据点与新建的实体表示重叠。系统最多尝试固定次数,每次选择一条按概率加权的车道,并重置速度、位置与停止计数。

停止计数用于维护远景层的连续流动。远景点不参与真实拥堵求解;连续停止后,系统将其重新分配到其他位置。近处实体则通过正式的阻塞处理与清理路径解决问题。两层采用不同策略,是因为远景层只承担群体流动表现。

系统在初始化时将车道预处理为一组入度与出度均不超过 1 的直路链,远景点沿这些链插值移动。构建过程包含环检测;发生回环的链会被标记为无效并保留调试信息。直路链省去了寻路与路口决策,仅承担连续移动。

图 · 远景人群数据流:匿名点沿预处理直路链批量更新并通过 ISM 渲染
图 · 远景人群数据流:匿名点沿预处理直路链批量更新并通过 ISM 渲染

② 对 UE5.8 迁移的启示

UE 的 ISM/HISM 已能高效绘制大量实例。这里需要补充的是远景视觉数据与可交互实体表示之间的语义边界:ISM 实例不承担稳定身份或完整交互逻辑。

“常量直路链 + 停止后重新分配”的运动模型不执行寻路、决策或避让,只沿预处理路线插值。即使单个轻量 AI 的成本较低,数千个远景点累积后仍可能形成显著开销。因此,远景层只负责形成持续流动的人群或车流;玩家附近另行创建的存根与 Actor 表示才处理个体目的地和绕障逻辑。

③ 移植到 UE5.8


// 重建工程:远景人群 = 视觉数据(ISM 批量画),不是实体
// 沿车道插值的点 → UInstancedStaticMeshComponent 批量渲染
// 纪律:视觉数据有效 ≠ 那里有可交互的人;靠近时协调密度并另建存根。

迁移原则:远景人群属于视觉数据而非实体表示;ISM 负责批量绘制,逻辑层不提供稳定身份与交互能力。


九、持久化:只保存需要延续的身份与状态

① 源码观察与职责抽象

人口系统需要支持存档与流送恢复,但持久化范围不覆盖全部背景人群。对于需要保留的对象,存档记录存根的实体 ID 与必要持久态;Actor 表示按需重建。

因此,任务相关、带持久标记或与玩家发生过重要交互的对象可以跨流送与读档保留;纯背景人群则回收后重新生成,不承担逐人记忆成本。

新建与恢复路径的对称性

存根构造包含新建与恢复(读档 / 流式载入)两种模式。新建路径依次启用持久化、创建基础数据持久态和各组件持久态;恢复路径跳过初始设置,访问既有持久态,并向各组件发送恢复通知。两条路径具有相反的前置条件:新建前要求组件持久态不存在,恢复前要求其已经存在;条件不满足时直接触发断言。

析构函数进一步约束这种对称性:存根析构时断言基础数据持久态已经置空。存根只能通过正式删除流程销毁,由该流程先处理持久态;直接 delete 会触发断言,从而暴露绕过清理流程的调用。

持久化问题通常跨越一次保存与加载后才暴露。对称的新建 / 恢复路径、前置存在性校验和析构断言,使错误能够在状态不一致时提前暴露。

可存档 ID 与不可存档 ID

实体 ID 分为可存档与不可存档两类。剧情相关、具有持久标记或与玩家发生过重要交互的 NPC 使用可存档 ID;可再生的背景人群使用不可存档 ID,回收后不保留逐个体状态,读档时再按区域需求生成。

该区分用于控制存档规模。若城市中的数千个存根全部写入存档,文件体积与读写成本都会显著增长。系统只持久化玩家交互过、带剧情标记或与任务相关的对象,纯背景人群保持可再生且不持久。可存档与不可存档 ID 的区分应在存根设计阶段确定。

图 · 持久化与恢复:保存存根身份与必要状态,按运行时预算重新挂载表示
图 · 持久化与恢复:保存存根身份与必要状态,按运行时预算重新挂载表示

② 对 UE5.8 迁移的启示

UE 提供基础存档框架,但“存根层持久化 + 实体表示从存根重建”的语义仍需自建。存档只保存必要的稳定状态,无需序列化大量易失 Actor,从而降低文件规模与恢复复杂度。

③ 移植到 UE5.8


// 重建工程:人口持久化(存存根身份+持久态,不存易失实体)
// 存档 = 存根层(EntityId + FPopulationPersistentState)
// Actor 实体表示易失,随流送 / 读档从存根重建
// 只保存需要延续的身份与状态;纯背景人群保持可再生。

迁移原则:保存存根身份与持久态,实体表示在需要时从存根重建。该原则与逐车持久化一致。


附:区域人口的身份与表示生命周期

把源码路径压缩成六个节点:

  1. 区域提出需求:社区作息和车道密度决定当前需要多少、哪类人口。
  2. 异步建立身份:请求入队并取得实体 ID;存根创建完成时重新校验槽、车道和流送依赖。
  3. 竞争实体表示名额:调度更新依据多项重要性因素,在高成本预算内选择可挂载对象。
  4. 挂载并参与玩法:存根取得相应档位的 Actor 表示;若对象需要持久化,交互结果写入稳定状态。
  5. 卸载或退役:玩家离开后释放 Actor 表示;不再需要的存根进入统一回收路径。
  6. 回到匿名密度:存根超出范围后可以请求补充远景点,但新的点不继承该存根的身份。死亡对象则按死体清理规则回收,ID 延迟复用。

这里沿用第一章的定义:稳定身份从存根开始;远景点只在前后维持视觉密度,不参与个体身份生命周期。

图 · 区域人口的身份与表示生命周期:稳定身份从存根开始,远景点只做匿名密度衔接
图 · 区域人口的身份与表示生命周期:稳定身份从存根开始,远景点只做匿名密度衔接

十、人口运行时约束:生命周期与并发边界

前述机制可以归纳为以下跨模块约束:

  1. 跨帧依赖必须重验:令牌完成时重新检查槽、车道、流送区域和取消状态。
  2. 数据句柄要能识别复用:结构体存根使用索引 + generation;Actor 表示才使用 UObject 弱引用。
  3. 锁只保护临界数据:锁内标记、移除并收集通知,锁外执行可能重入的回调。
  4. 回收有统一出口:可见性、淡出、孤儿中转和最终销毁由明确阶段处理。
  5. 预算分别计量:远景点、存根与 Actor 表示各有容量和更新成本;高成本名额按配置与运行压力调整。
  6. 持久化路径必须对称:新建、恢复和删除各自校验状态存在性,只保存需要延续的身份与状态。

流式和并行环境中的引用与前置条件会随运行状态变化。生命周期设计需要区分当前有效状态与请求发起时的历史状态。


十一、迁移取舍:参考原版自建面向数据的人口运行时

迁移方案采用面向数据的人口运行时,Mass 作为可选的批处理载体,而非架构前提。

机制 采用方案 排除方案 设计理由
身份 存根 ID + 持久态,独立于 Actor 表示 spawn 即完整 Actor 身份 / 表示解耦
分层 匿名远景点 / 存根 / 分档 Actor 表示 把三者混成同一对象 成本与语义分层
编排 社区人口与响应单位作为不同需求源 仅靠内部名推断职责 数据驱动、边界可验证
生成 可取消的异步请求链与多级完成状态 把请求返回当完成 适配流送与预算
预算 远景、存根、Actor 分别计量 只给完整实体设总数 约束不同成本来源
坐落 共享车道拓扑,按类型定义查询和占用 假设所有槽位语义相同 统一空间基础
清理 多原因集中删除阶段批处理 随时零散删除 避免并发修改集合
远景 视觉数据(ISM),非实体 为远景点分配实体身份 视觉存在 ≠ 实体存在
持久化 只保存重要 / 交互 / 持久标记对象 存全部背景路人或易失 Actor 控制存档规模
承载 面向数据核心 + 可选 Mass / ISM 能力 预设必须或绝不能用 Mass 按项目版本选型

重建工程优先自建人口语义层:稳定身份、异步状态、预算名额与可恢复交接。底层数据更新可以落在 SOA、Mass 或混合方案上,远景绘制可以使用 ISM / HISM;具体选择取决于 UE 版本、现有管线和性能测量。Mass 提供 ECS 与批处理能力,但不包含本文所需的人口生命周期语义。

图 · 人口运行时总览:编排层连接需求、身份、预算、表示、空间、回收与持久化
图 · 人口运行时总览:编排层连接需求、身份、预算、表示、空间、回收与持久化

AI 协作复盘

  • AI 帮了什么:整理源码路径、交叉检查人口、远景与生成模块,并把机制转换成可读的职责模型。
  • 人工校核:确认远景点不携带实体 ID,修正“身份贯穿 dot”的错误;区分存根与分档 Actor 表示;保留 prevention 与完成事件的证据边界。
  • 核验方式:关键结论回溯到人口模块深读及对应源码;UE5 部分明确标为迁移建议。
  • 证据边界:正文分别标识源码事实、职责抽象和迁移方案,不把推导模型写成原版同名架构。
  • 文章边界:本文讨论人口调度的底层逻辑与 UE5 迁移原则;运动学、动画和群体视觉效果属于《城市生命力》的美术表现部分。

《《城市生命力》P1|开放世界的人口编排》有1条评论

发表评论

了解 AI Native Game Development 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读