城市人口运行时:UE5 实现篇|行人生成、回收复用与局部避让

同一段人行道上,普通行人在观察位置退远后被回收,剧情接管的行人却继续保留。返回街段时,系统补足人口;如果复用了刚才的对象,它还应当从新的合法位置继续行走。这些过程涉及人口需求、实体身份、剧情归属和路线状态,单靠显示或隐藏角色模型无法完成。

此前《城市人口运行时(上):世界基础层、人口调度与感知决策》讨论了世界怎样托管实体,以及人口、智能和任务之间的职责。本篇回到 UE5 原型,沿出生、活动、回收和再次准入展开实现。人物与车辆关系承接《车辆驾驶运行时:UE5 实现篇》;服务事件与派遣属于后续业务,不在这里重新建立一套人物生命周期。

先看下面这段合片。它依次展示人口增减、剧情归属、区域保护、淡出初始化、退休复用、起停折返与三人交汇。画面中的行人由实际运行时生成和推进,录制脚本负责镜头、操作与字幕,不代替行人修改坐标。

总览视频:65.2 秒,由正文中的主片顺序拼接。合片为 1280×720、15 fps,共 978 帧;这是交付视频的采样与编码规格,不是 UE 运行性能。淡出段的黑场来自真实摄像机功能。各段是独立案例,不能连读成同一批行人的连续任务。

本文的实现与验收以这次交付材料对应的版本为范围,不把工作区后续改动自动计入视频结论。这次完成的是局部街段中的生成准入、空间回收、剧情保护、复用与行走执行。完整城市人口密度、模型流送、场景行为、联网人口和人物简化/完整表示转换仍有缺口。下文会区分参考机制、当前工程适配和实际测试范围;视频里出现了什么,也不替代源码与日志中的状态检查。

人口、实体和显示分别保存什么

先约定称呼:Ped 指逻辑人物实体,保存人物身份、归属、事件、任务和关系;骨骼显示代理读取这些状态,负责场景中的角色外观。人口成员表示某个来源当前管理了哪些人物,人口目标表示这个来源希望保持多少人。目标、成员数量和世界里仍可查询的实体数量不必始终相等。

例如,目标需要补人,但候选位置被占用时,需求可以继续等待,世界里不应提前出现一个被藏起来的新人物。人物退出活动人口进入复用池后,成员数已经减少,逻辑实体和原句柄却仍然保留。剧情接管后,人物移出普通人口计数,但仍占用来源的实际容量。这些中间状态分别由后续接口消费。

UE 的 WorldSubsystem 是宿主,世界运行时拥有实体身份、分阶段执行和生命周期。人口调度在自己的阶段处理需求、准入、成员和退休;人物任务及路线负责后续行动。场景 Actor 提供物理查询和呈现,不以各自 Tick 取代世界层的状态权威。

状态 主要持有者 保存的内容 不能据此推断什么
人口目标与需求 人口管理器 目标数量、待补缺口、来源与类型 有需求不代表已经生成
候选与预约 生成准入流程 实际候选位置、路线进度、同批占位 预约成功不代表实体已加入世界
活动成员 来源记录 当前人物句柄、来源关系与代次 成员退休不代表逻辑对象已删除
人物实体 世界与人物自身 身份、类型、剧情归属、任务、事件和车辆关系 句柄仍有效不代表正在参与更新
复用池条目 人口复用管理器 原句柄、类型匹配、来源代次、隔离帧与恢复信息 池中有对象不代表可以立即使用
路线计划 正式路线执行器 目的地、进度、完成状态和当前位置写入权限 旧诊断记录不代表仍有活动路线
显示与动画 UE 显示代理 渲染位置、朝向、步态与姿态 播放走路动作不代表逻辑人物发生了位移
文章目录
人口需求、实体状态、路线与显示的职责
图 1:职责示意。人口管理器决定何时申请和撤回成员,世界持有实体,路线消费实际运动,显示代理读取结果。各状态通过现有接口交接。

本篇保留这种分工,原因会在复用案例中变得很具体:人口已经取出一个旧对象,路线却仍认为自己拥有它的位置,新的出生落点就可能无法生效。各模块的局部记录可能都符合自身规则,交接时仍会发生冲突。

从宿主接口追到实际写入者

阅读实现时,可以先从 UE 宿主提供的操作进入,再追到保存状态的运行时。宿主负责取得当前世界、校验实体句柄并转交请求;它并不因为暴露了一个蓝图接口,就成为任务、人口和位置的共同所有者。当前世界调度层把人口处理接在 Control 链中,来源记录随后连接具体街段的实例化与路线执行。读源码时若只查找“创建人物”的调用,很容易漏掉此前的候选检查和此后的成员挂接。

数据沿这条链传递时,保存周期也不同。本轮摄像机矩阵与淡出量是一份观察快照,下一次 Control 会重新读取;来源、待处理需求和人物句柄则跨更新保留。路线中的活动计划又比一次局部避让建议活得更久。若将这些状态集中为一个 Actor 的临时变量,调用顺序就会成为隐含契约:先 Tick 的对象看到新观察,后 Tick 的对象可能已经看到新的成员表,结果难以复查。

当前实现把这个问题约束在各自的消费边界上。人口周期读取世界本轮保存的空间观察,实例化流程读取已经确定的需求和来源,路线消费当前有效计划,显示端再读取运动结果。这并不等于工程拥有了跨全部子系统的原子快照;例如任务保护仍要在实际退休时检查。它的作用是明确一份数据在哪里生成、由谁保存,以及哪一步允许重新解释它。

排查问题时也按这条依赖链提问:世界是否仍能解析句柄,来源是否仍登记成员,需求是否还在等待,实例是否已挂接,路线是否拥有位置写入权,显示代理是否读到了新变换。活动人数恢复,只回答了其中一部分。后面的复用故障正发生在成员已经恢复、位置交接仍未完成的阶段。

出生前检查实际落点

先沿一次出生看调用顺序。生成流程从需求取得候选,再检查这个候选是否能够使用,随后建立预约并进行实体创建或复用。检查位置必须与最终使用的位置一致:如果检查路线中点,却在初始化时把人物统一放到路线起点,前面的可见性和占位检查就失去了作用。

当前候选包含路线进度。准入、同批预约与实例挂接沿用这份进度,新人物进入正式路线时也保留已经批准的位置。原生空间模式下,不再统一退回路线零点,也不额外走一段用于旧入口的过渡路径。旧兼容入口仍有自己的处理,不应把一个分支的行为概括到所有调用方。

视频 1:人口增减,9.4 秒。观察位置退远后,正式人口由 2 降到 0;返回后补足到 2 并继续行走。街段保持加载,人数变化来自人口生命周期。这段使用近景生成覆盖配置,不能从画面距离推断默认生成范围。

可见性来自真实本地视图

地区处于 Active、Warm 或 Inactive,只描述地区的运行策略,不能代替“玩家是否看得到这个点”。当前空间模式在世界 Control 阶段读取本地视图,将投影矩阵、摄像机位置、人口控制中心和淡出状态保存成本轮观察。人口周期共用这份观察,避免同一轮不同尝试读取不同镜头状态。

视锥使用 UE 本地玩家的实际投影数据,候选以边界球参与视锥检测。距离通常使用候选根位置到人口控制中心的水平距离;可见候选的近距检查还包含到摄像机的三维距离。人口中心在当前宿主中优先取玩家 Pawn 位置,摄像机位置用于视图判断,两者并非总在同一点。

生成条件 当前默认距离 使用条件
普通可见候选 90–130 m 候选边界球进入实际视锥
普通屏外候选 50–75 m 候选处于屏外
隐藏启动候选 1–30 m 有启动原因,且相关有效本地视图均满足真实淡出条件

这些距离用于本轮对应的配置分支,不能理解成任何人物、任何场景下都相同。正文多数主片为了近景观察采用 3–35 m 拍摄覆盖,淡出初始化片保留表中的正常与隐藏范围。拍摄配置和默认配置在这里分别说明,避免用近处出生画面解释远距生成规则。

多本地视图下,还要避免在另一视角眼前生成。当前检查会拒绝对任一有效视图过近的候选,并要求至少进入一个观察的允许范围。原生空间模式缺少有效视图时延后生成;没有观察数据,不等于已经证明候选在屏外。无视图工具使用的显式旧模式不计入真实摄像机准入验收。

物理占位与逻辑人物占位分别查询

候选还要通过胶囊占位、信号冲突和同批预约检查。出生检查使用的胶囊半径 35 cm、半高 90 cm 是当前原型几何,不能与后面避让使用的身体半径混为一个参数。

只做 UE 碰撞查询仍有一个缺口:当前骨骼显示代理没有阻挡碰撞,另一名逻辑行人站在那里,胶囊查询也可能返回可用。因此又补了跨来源的正式人口成员占位检查。它以运行时人物位置为依据,覆盖显示代理不能提供的身体占位;它仍不是完整世界人物空间网格,不能据此宣称检查了所有未登记对象。

补充视频:已有行人占位出生,约 6.67 秒。已有逻辑人物占据候选时拒绝新生,空出位置后重新准入。它检查跨来源人物占位;物理障碍挡住出生位置的另一段补片检查胶囊碰撞,两者不互相替代。补片保留原拍摄版本。

拒绝后保留需求,等待后续周期重新检查。不能先创建人物、把模型藏起来,再用“画面没有重叠”作为准入成功的证据;那会让计数、实体身份与可见画面从出生第一步就不一致。

生成请求、空间观察、占位与实际创建的顺序
图 2:生成准入示意。最终落点沿用已通过检查的候选。未通过时保持待处理需求,不发布一个临时隐藏的实体。

资源预选、候选预约与实例发布怎样衔接

当前实例化入口处理的是一份已经登记的来源需求。需要模型绑定时,它先预览模型选择,保存资源准入原因和可用模型信息;资源未就绪便返回,尚不会创建人物。这里的“预览”与后面的“提交”有明确距离:先确认有足够的绑定可供本批使用,再尝试落点与实体挂接,整批挂接完成后才提交模型选择。资源准备完成并不等于世界成员已经发布。

原生候选分支把准入回调交给候选预约器。回调用候选的路线进度重建根位置,随后检查过街冲突、有限坐标、禁止生成区域和空间观察。占位检查又分成本街段现有逻辑人物、其他活动人口来源、当前批次已接受位置,以及有物理场景时的胶囊查询。已接受位置保存在本次调用的局部列表里,后续候选可以据此排除已占用位置,提前发现本批次内的位置重叠。

这份局部位置列表覆盖“前一个候选通过,但实体尚未发布”的时间窗口;正式成员仍由来源与实例化记录管理。跨来源查询的范围同样限于当前已登记的正式人口,剧情人物、独立演示对象和未登记实体尚未统一纳入空间索引。物理场景缺失时,胶囊分支不能提供原生碰撞证据,逻辑查询通过也不能替代这份缺失的证据。

胶囊的坐标约定同样需要明确。路线给出的是脚部根位置,查询中心在它上方加半高及少量余量;若直接把根位置当作胶囊中心,下半部分会落进地面,得到与实际站立身体不同的占位结果。生成几何和行走几何分别配置,不能只因两者都使用胶囊就共用全部尺寸。

通过预约后,实例化逐个取得实体:优先询问复用池,未找到兼容条目时才走新建路径;若已有兼容条目尚在隔离,则当前路径延后本批,而不是直接额外创建一个新人物绕过隔离。成功取得对象后,挂接继续使用获准候选的路线进度。原生空间模式缺少登记的候选图时也返回阻塞,不退回未经本轮观察验证的旧位置分配器。

“资源阻塞”“候选阻塞”和“复用等待”分别对应不同恢复条件。资源阻塞要等待绑定就绪,候选阻塞要等待位置重新满足条件,复用等待要等待隔离及匹配条件。这些状态在画面上都可能表现为没有新增人物,诊断记录应保留具体原因,并由对应条件决定何时重试。

批次失败如何撤回已经完成的局部步骤

实例化过程中会同时记录已挂接句柄、该句柄是否来自复用,以及原复用条目。这些记录服务于失败处理。若其中一个实体无法挂接,或整批挂接后模型选择提交失败,调用方就需要处理此前已经成功的部分,不能把一份完整批次需求默默改成部分成功。

当前项挂接失败时,先按它的新建或复用来源,分别请求移除或归还池;随后,批次失败处理再按反向顺序尝试撤回此前已成功挂接的成员。新建对象走世界的延迟移除请求;复用对象走归还入口,重新进入受抑制的池化状态。候选预约也随失败释放。归还操作保留原对象、句柄及隔离记录,后续取用继续沿这些记录判断。

失败清理本身也有结果:能否从场景挂接中撤回、能否归还或申请移除,都参与清理完成判断。清理不完整时保留专门失败状态。这里实现的是当前实例化过程的有界补偿,并不是对所有世界状态提供通用事务或完整回滚;例如 Acquire 恢复更新、挂接新落点、路线接管仍发生在各自边界。

下面归纳批次补偿部分,进入这里前,当前挂接失败项已经在自己的分支尝试过移除或归还;它不属于 attachedItems。名称作了中性化,并省略结果结构和错误记录。这是调用顺序摘要,不能代替完整函数:

if attachmentFailed or resourceCommitFailed:
    cleanupComplete = true
    for item in reverse(attachedItems):
        detached = detachFromSource(item)
        released = restorePoolEntry(item) if item.wasReused else requestWorldRemoval(item)
        cleanupComplete = detached and released and cleanupComplete
    releaseCandidateClaims()
    return cleanupComplete ? failureReason : RollbackFailed
publishCompletedDemand()

只有整批满足要求且资源提交成功后,需求才退出 Pending,来源的待处理索引才被移除,正式实例化结果才进入记录。这样读返回值时,必须区分“曾经创建过临时对象”和“需求已经完成发布”。前者即使发生,也可能在失败路径被撤回,不能计作最终新增人口。

人口周期怎样分配生成机会

位置准入确定候选能否使用,周期调度则确定本轮能够分配多少处理机会。人口目标决定缺口,周期调度分配处理机会。当前行人与车辆各自保存周期欠账,不共用一个每帧生成计数。两者有不同速率,也有各自尚未处理完的工作;改变一方配置不应把另一方的欠账清空。

用 debt 表示累计处理机会,rate 表示每秒周期数,dt 表示本次经过时间,基本关系可以写成:

rate = frameRate × cyclesPerFrame
acceptedDt = min(dt, maximumFrameTime)
debtNext = debt + acceptedDt × rate

这里的周期是人口处理机会,并非渲染帧,也不是保证成功出生一个对象。当前实现为行人和车辆分别保留 frameRate 与 cyclesPerFrame,没有把它们折成一个失去来源的统一配置项。计算使用双精度中间量,取整边界还保留了 1e-6 容差;上述公式只说明欠账关系,不取代代码中的边界处理。

追赶处理仍然有上限

车辆欠账达到补偿条件时,可以处理多个周期。行人侧保留交替限额:车辆发生补偿的连续更新中,行人交替受到单周期限制,未处理的欠账留到后续更新。未受这一限制时,行人上限由最大接受帧时间和自身周期速率计算。

这避免某次长帧后所有类型同时无界追赶。当前最大接受帧时间默认是工程适配值 0.1 秒,超过的时间不全部计入这一人口时钟;它不是已确认的原版统一时间常量。暂停时不增加新欠账,也不借暂停清掉既有小数和延后工作。

单次生成失败仍会消耗所属类型的一次周期,需求本身可以等待重试。如果失败不计成本,一个被占用的候选就可能在本轮反复尝试,既绕过处理预算,也让其他工作迟迟得不到机会。周期消耗描述尝试成本,需求保留描述业务仍未满足,两者应分开记录。

批量创建怎样抵扣后续机会

已有原子批次有时会在一次处理内生成多个成员。当前实现没有把多出来的创建量忽略,而是先累加到该类型的创建抵扣记录,再按后续周期逐次扣除;抵扣未消完时,阻止这一类型继续发起新生成。

以下是便于理解的教学例子:原先没有创建抵扣,一次周期成功创建一批 4 个对象。先记录 4,再抵扣本周期 1 次,余下 3 次由后续周期偿还。这不表示批次被拆成四次可见出生,也不表示普通单人需求每次都创建四人。当前原子批次上限属于项目配置,不能写成参考引擎固定采用的批量大小。

独立周期欠账与批量创建抵扣
图 3:调度示意。时间产生处理机会,尝试消耗机会;批量创建另记抵扣。图中的教学批次只解释账目变化,不是视频中的人物数量。

地区预算、作者布置的路线候选和批次预约仍是当前工程策略。它们保留了预算、机会与实际创建的区别,但没有覆盖完整城市密度、所有车辆生成器或场景人口队列。

周期计划与实际执行分别记账

周期运行时的 Advance 先生成一份处理计划,再由世界桥接层按类型执行。配置阶段检查各自帧率、每帧周期数、最大接受时间是否有效,也检查计算量是否越过整数可表达范围;配置失败不应被解释为“采用一个猜测的默认速率”。配置和 Reset 是不同操作,普通重配不等于清掉历史欠账。

有效且启用的更新先累加经过限幅的时间。车辆可执行次数取欠账加容差后的向下取整,执行计划生成后扣除对应次数,小数仍留在车辆账上。行人根据车辆本轮是否达到补偿条件以及上次是否延后来决定本轮限额,再从自己的欠账中取可用次数。交替限制保存的是跨更新状态,不能在每次调用开始时重建为同一个初值。

世界桥接层随后先处理车辆周期,再处理行人周期,队列与退休请求均按当前所有者类型过滤。当前单周期可以排入一个新的单实体管理需求,也可以移除至多一个实体;已经排队且就绪的原子批次则沿已有需求路径处理。这样“计划允许处理”“本轮实际创建”和“这一轮有多少个待处理需求”是不同数字,诊断输出不能只留一个 SpawnCount。

以失败重试为例:候选被占用,本次周期照常消耗,但需求保持待处理。下一次有机会时重新检查空间条件。若人物先被剧情接管,退休检查又会拒绝撤回,即便地区希望降低人数,也不能把预算差额解释为删除授权。生成与退休共用人口调度,并不意味着它们可以互相抵消资格门。

创建抵扣还要与时间欠账分开。时间欠账表示已经积累、尚可分配的处理机会;创建抵扣表示此前批次已经使用、后续要偿还的创建量。抵扣未消完,生成入口暂停新增;周期仍可继续推进抵扣。两份记录分别影响后续处理,合并后会丢失类型与来源信息,也无法区分暂停、失败和批量成功。

这里仍有未覆盖的实现:车辆创建驾驶员导致的跨类型抵扣没有在此完成,地区密度和作者候选仍是原型策略。因此这里解释的是当前独立人口时钟及其消费者,没有据此宣称所有人物与车辆来源已共享完整城市生成账本。

回收先看人物是否仍被其他系统使用

观察位置退远,不代表人口管理器可以立即收走所有人物。一个正在处理事件、执行剧情任务或准备进入车辆的人物,可能仍被其他系统引用。空间条件只能回答距离与可见性是否允许回收,关系检查还要回答人口系统有没有资格执行这次撤回。

当前只读资格检查先拒绝 Mission 人物和已经入池的人物,再检查占座、车辆意图与进出车交互。任务槽中仍有非人口默认的 Pending/Active 任务时拒绝;当前事件和尚未被下一次完整智能更新消费的活动事件候选也会保护人物。只剩人口自己拥有的默认任务时,才允许人口按自己的重置过程退出成员。

这里有参考机制与工程适配的区别。非默认活动任务、当前事件阻止复用,有明确机制依据;保护 Pending 任务和未消费事件候选,是当前 UE 阶段之间采取的保守准入。把这份本地检查也用于退休,是为了避免减少人口时绕过关系保护,不意味着原版删除资格与复用资格本来就是同一个谓词。

退休数量估计和实际退休提交共用检查,避免预算阶段认为可以回收,提交时却清掉了刚进入的任务。相关判断输出具体拒绝原因;调用方不能把所有拒绝都解释为“太近”,也不能因人口缺口变化自行清除事件或占座。

空间回收按水平距离和视锥共同决定

通过上述资格门后,空间策略再区分近处保留、中间距离的屏外回收,以及超过更远门槛的回收。设水平距离为 d,屏外回收阈值为 Rout,更远阈值为 Rin,当前规则可概括为:

d <= Rout:保留
Rout < d <= Rin:只有屏外且允许激进清理时才可回收
d > Rin:空间条件允许回收,不再要求屏外

Rout 和 Rin 已包含当前生效的倍率,且这仍只是空间条件。剧情、任务、事件、车辆关系或禁止回收区域仍可拒绝。d=Rout 时保留,d=Rin 时仍按中间区间的屏外规则判断。使用水平距离也意味着人物在高处或低处时,不能只凭三维斜距把它提前回收。

多本地视图采取保守保护:任一有效观察要求保留,就不因另一个观察已经退远而回收。观察缺失或无效也不作为自动删除许可。世界、来源或场景的显式销毁另有生命周期权力,不受这套普通人口策略完全约束。

任务关系保护与空间回收条件
图 4:退休与空间回收示意。资格检查先保护仍有用途的人物,距离与视锥检查随后决定空间上是否允许撤回。区间图使用符号阈值,没有把拍摄距离写成通用回收常量。

逐人物倍率只改变回收距离

逐人物回收倍率只缩放回收距离,不改变生成距离,也不越过任务和归属检查。人物同时保留模型默认值与运行时覆盖值;Reset 读取当前绑定模型的默认值,并非永远写回 1。新生或复用后的普通人口会重新应用模型默认,避免前一段使用留下的临时倍率进入新生命周期。

当帧保护由 Control 消费

全世界的“当帧抑制激进清理”请求由下一次 Control 消费。本次 Control 内的所有人口周期读取同一快照,反复请求只保持为真。下一轮不再重申就恢复普通规则;人口没有生成成功、没有街段,或人口处理暂停,都不应自动把已经消费的保护延长到以后。世界没有推进时,还没有到达消费边界,待处理请求则仍保留。

这一请求在对应配置下扩大回收距离阈值,也就是扩大保留范围,并禁止中间区间的屏外激进清理;超过扩大的更远距离后仍可进入回收检查。默认特殊倍率为 1.6,它不是永久免删除。每个人物的范围倍率最后参与计算,不能替代这个按世界 Control 消费的请求。

任务标志依照任务更新重置

普通额外距离标志和任务额外距离标志也有不同重置边界:前者在人物 Control 重置,后者只在完整任务更新前重置。部分或被抑制的任务更新保留任务标志,活动主任务可在完整更新中重新声明。把两者都改成“过若干秒清除”,会改变原有阶段语义。

当前警务分类只是资源上的布尔投影。存在额外距离标志且属于该分类时使用 cops 倍率,其余满足条件的分支使用 special 倍率,最后再乘逐人物倍率;cops 与 special 不连乘。完整人物类型和完整任务树仍未接入,这些标志的存在不代表相应追捕或救护任务已经在本文全部运行。

资格查询为何必须保持只读

撤回资格入口读取人物、智能状态和关系,返回第一项阻止原因,不负责帮调用方消除阻止条件。若它在查询途中取消任务或清除事件,预算估计第一次调用就可能改变场景,实际退休时就会读到已被查询改写的状态。这样同一个查询调用一次和调用多次会得到不同副作用,问题也很难在日志中定位。

当前任务检查既看活动标志,也看 Pending 与 Active 生命周期值。只检查“是否已经开始执行”会留下一个阶段空隙:任务已经提交,下一次完整智能更新尚未激活,人口却把实体退休。活动事件候选的保护解决同类问题。候选进入智能层与成为当前事件之间存在时间差,等待选择不代表没有使用者。

人口默认任务通过专门的归属判断识别。判断重点是任务是否由人口默认行为拥有,不能把所有名称像 Walk 的任务都当作可清理任务;剧情也可能要求人物沿同一条路行走。只有任务性质和当前归属均落在人口允许清理的范围内,才由人口执行自己的重置过程。

返回原因还决定调用方应等待什么。任务保护等待任务结束或取消,事件保护等待智能层消费与清理,车辆关系保护等待关系接口完成退出。人口管理器不能为了降低预算,替这些系统宣布完成。实际提交前再使用同一只读规则,是为了保证检查口径一致;资格查询不提供锁定,实际提交仍需重新判断,先前结果不应作为长期许可缓存。

剧情接管怎样改变归属与人口计数

剧情需要保留人物时,通过归属接口明确登记保护关系。当前公开类型只覆盖 Ambient 和 Mission:普通人口接管后转为 Mission,并保存调用方提供的归属标识;释放后恢复 Ambient。

视频 2:剧情归属,7.4 秒。A 被接管,B 保持普通人口;观察位置退远后 B 回收,A 继续保留。释放 A 后恢复普通回收。画面检查归属保护,没有展示新增剧情任务或剧情动作。

接管保留原句柄和当前任务,不通过删除再创建伪造“变成剧情人物”。同一归属重复接管是幂等操作;若人物属于另一个脚本,必须显式允许接管才能转移。释放由当前归属方执行,其他调用方不能拿到句柄就替它结束保护。

操作 人物身份与类型 计数变化 任务与关系
普通人口被接管 原句柄保留,Ambient 转 Mission 移出 ambient 计数,来源实际容量仍占用 保留当前任务与关系
同归属再次接管 身份与归属不变 不重复增减 不重复初始化
外部归属申请 未授权转移时拒绝 不变 拒绝不清旧状态
当前归属方释放 Mission 转 Ambient 后续协调按当前类型计算 当前实现采用保留任务策略
释放后人口回收 再次经过普通资格与空间检查 提交退休后减少成员 不因释放自动清空其他保护

Mission 人物不参与普通人口退休与复用,人口默认行为也不应替它安装、暂停或完成普通任务。ambient 计数下降后,原人物仍占用来源的实际容量;后续补人继续受该容量约束。

释放只解除这段剧情归属。当前实现保留任务、事件、座位与车辆意图,如果这些关系还在,普通回收仍须等待。原版完整清理还涉及更多玩法属性与任务替换,本篇没有把这些尚未移植的部分藏进一个 Release 成功返回值。

归属标识目前由调用方以唯一会话身份提供。没有完整脚本虚拟机替每个请求自动附加当前脚本,也没有脚本结束后的全套自动清理、联网归属迁移或流送引用锁。调用方需要保存自己的身份并在结束时显式释放。

剧情接管、人口类型与来源容量的变化
图 5:归属转换示意。实体身份持续保留,普通人口计数与实际来源容量分别处理。释放不直接连接删除,而是回到普通资格检查。

同一个人物为什么会出现在不同计数里

接管改变的是人物的业务归属,不撤掉来源与世界之间的实体联系。来源协调时按当前人口类型计算普通成员,实际容量则仍根据已挂接实体计算。它们回答的问题不同:普通计数用于估算还差多少环境人口,实际容量用于约束这个来源已经托管了多少对象。若把 Mission 人物同时从两份计数中扣掉,每接管一个人就会释放一个不存在的容量空位,连续接管就可能让来源突破上限。

这也解释了接管入口为什么要拒绝池中人物。池条目保留有效句柄,但已经退出活动人口,更新和任务处于受抑制状态;若仅凭“世界还能找到句柄”就允许脚本接管,将出现脚本归属与池管理同时声明生命周期的情况。当前接口在这一步先拒绝,让调用者操作明确处于活动阶段的对象。

同归属接管不重新初始化,是为了让重复请求保持可预测。若脚本每轮确认自己仍持有人物,人口类型不能来回切换,任务也不能反复重建。外部归属转移则必须显式授权,接管失败不应清掉原归属。释放时还要带当前拥有者身份,曾经申请过的调用方若已失去归属,就无权释放。

当前会话标识仍由调用方管理。保留同一个 FName,却把它重新用于另一段已无关系的脚本,并不会自动得到新的所有权世代。文章所说的唯一会话身份是调用约束,不是已经实现的全局请求版本服务。之后接完整脚本生命周期时,还需要处理脚本退出、复制句柄和延迟请求;本地接管接口的成功返回没有覆盖这些工作。

禁止生成与禁止回收是独立区域

剧情有时需要暂时保持一个街角空置,另一个街角则需要保留已有群众。这两个需求的时间方向不同:前者限制未来出生,后者保护当前成员。用一个“人口禁用区”同时完成两件事,容易误删已有角色,或让本应停止补人的地方继续生成。

当前世界分别保存一个禁止生成盒和一个禁止回收盒。每种盒各自只有一个槽位,后设置的覆盖此前同类设置,各自可以清除。它们不是任意数量体积的集合,也没有自动合并区域。

包含判断使用人物根位置或实际出生候选的 XYZ 坐标,边界计入盒内:

inside = minX <= x <= maxX
      && minY <= y <= maxY
      && minZ <= z <= maxZ

这段用数学链式不等式说明包含关系,不是可直接粘贴的 C++ 表达式。它与前面的水平距离检查不同,区域会检查高度;只在平面上画一个覆盖范围,不足以判断高架或地下的人物是否属于同一盒。

视频 3:生成与保护区域,约 11.93 秒。已有两人时分别设置 A 区禁止生成、B 区禁止回收;设置 A 不删除已有行人,B 的人物退远后仍保留。返回只清 A,A 才恢复补人;清掉 B 保护后再退远,两边都恢复普通回收。

禁止生成区域在实体创建前检查,只阻止未来 ambient 出生。已有行人继续活动;区域清除后,等待需求也仍要满足预算、位置和资源条件。禁止回收区域保护已有 ambient 成员免于自动退休与空间回收,不撤销世界显式销毁权力。

盒状态归世界持有。本地路线刷新、摄像机观察更新或场景重新绑定不能擅自清空剧情设置,世界关闭或重建时才清理。非法设置输入先被拒绝,不能先覆盖旧盒再发现新值无效;当前实现还保留有限值、最小值与最大值次序检查。

禁止生成与禁止回收的独立影响
图 6:区域作用示意。禁止生成只作用于未来候选,禁止回收作用于已有成员;清除其中一个不会改变另一个,也不绕过其他准入条件。

隐藏初始化需要真实画面状态

加载或传送后,人口系统可能需要较快补足近处行人。但“请求快速初始化”和“玩家已经看不到画面”是两件事。如果只要 API 被调用就开放近距出生,人物仍会在可见画面中突然出现。

当前隐藏启动要同时满足启动原因与真实淡出观察。启动原因可以来自强制初始化、持续完全淡出、已有启动标记或有效快速填充请求;实际近距准入还要求相关有效本地视图的最小淡出量严格大于 0.9。

hiddenStartup = hasStartupReason
             && allRequiredViewsValid
             && minimumLocalFade > 0.9

没有视图、缺少淡出信息或仅部分视角隐藏,都不能证明所有相关画面已被遮蔽。恢复画面后立即回到普通可见/屏外规则,不沿用上一次隐藏启动的近距许可。

视频 4:淡出初始化,11 秒。可见时请求初始化与填充,近处仍保持空置;真实 PlayerCameraManager 淡出后,隐藏期间补入人口,恢复画面后正常行走。这段保留默认距离。连续完全淡出本身也能成为启动原因,不能把出生唯一归因于隐藏期间的第二次 API 调用。

初始化窗口与快速填充使用不同计时

强制初始化默认持续 0.7 秒,内部按毫秒保留边界。到达结束时刻与已经超过结束时刻不同,后者才满足到期检查;没有待处理人口需求时,也要在经过严格大于 100 ms 后才走提前结束路径。重复请求可以延长结束时刻,不缩短已有效的窗口。公开接口使用秒,内部转换与有效范围检查仍需保留。

快速填充则按人口处理帧推进,默认持续 10 次,每次附加 2 个 Ped 周期。请求已经有效时再次请求,不把计数重新补满。计数每次人口 Control 更新只减一次,不按出生人数、候选数量或追赶循环次数扣减。

隐藏启动还提供有界的额外生成尝试。当前作者候选适配的隐藏尝试上限为 100,普通情况下为 2;没有生成工作时提前退出。这些尝试仍受人口目标、位置、资源和关系检查约束,成功创建仍进入抵扣记录。这里没有移植完整路径服务的同步刷新、取消和场景/野生动物异步队列。

快速处理不能刷过复用隔离

复用隔离按世界 Control 计时,每个有效更新推进一次。同一帧中的快速填充和追赶循环不能各自推进隔离时钟。否则配置写着“隔离两个更新”,一次突发循环就可能提前满足,刚退出人口的对象尚未完成阶段交接就被重新取用。

启动原因、真实淡出与快速填充时钟
图 7:隐藏初始化示意。真实淡出控制近距许可;快速填充增加有界处理机会;复用隔离仍按 Control 推进。三者分别保存状态,不由同一个循环次数代替。

拍摄这一功能时还发现过显示代理泄漏:正式人口尚未成功出生,演示原先用于启动的角色却仍可见。早期遥测只检查新增代理,遗漏了这批既有显示对象。修复把正式人口显示归属的交接提前到场景准备后,让旧演示代理在首次需求被拒绝前就退出显示;没有隐藏已准入人口来伪造空场景。最终录像额外核对了这些启动代理,失败素材留在诊断目录,没有混进正文视频。

初始化状态如何避免被重复请求改写

初始化运行时保存当前 Control 时间、强制窗口起止、连续完全淡出次数和快速填充剩余次数。强制窗口的请求先检查输入有效性,再把时长量化为毫秒。新请求第一次建立起止;已有窗口则取原结束时间与新结束时间中更晚的一项。这样一个较短的重复请求不会让先前较长的初始化意外提前结束。

观察阶段与时间推进也有不同职责。时间推进检查是否已经超过截止时刻;读取本轮观察时再处理无待处理需求的提前完成条件,并更新完全淡出连续计数。完全淡出连续计数使用接近全黑的门槛,隐藏准入使用严格大于 0.9 的门槛,两者不能只因为都读 fade 就合并成一个布尔值。前者参与启动原因,后者决定当前是否允许隐藏近距出生。

快速填充则在剩余次数为零时建立请求,已有请求不会因重复调用恢复满额。结束人口处理后才扣减本次处理帧;它不根据成功生了几个人扣减。否则一个没有合法落点的更新会永久保留快速模式,而一个批量成功的更新又可能立刻消耗整个窗口。当前设计把请求生命周期从出生结果中分离出来。

复用隔离也不能放进这里的内层尝试循环。隔离帧由世界所属更新推进一次;本轮尝试更多候选,只表示做了更多准入工作,不表示对象已经经过更多世界阶段。初始化、周期追赶和隔离在实现上保留各自状态,才不会因为提高填充预算,就顺带改变复用时序。

排查淡出初始化时,先查是否存在有效观察和启动原因,再查当前淡出是否满足近距许可,最后查资源、落点和容量。初始化请求被接受但没有人物出生,可以是这些后续门仍未打开;仅看 API 返回成功无法判断是哪一步阻塞。

退休入池之后,原句柄还在

退休先撤回人口成员,再由复用管理器接管对象。入池时关闭相应更新,清理当前已覆盖的人口默认任务及池化状态,保存恢复所需信息并开始隔离。路线执行器和空间投影仍有各自的状态所有权,不由这里统一清空。世界仍保留逻辑实体和句柄,直到真正驱逐或销毁才终止身份。

这与“角色离开画面以后删掉 Actor,下次生成一个同模型人物”有不同验收标准。入池阶段应检查成员已经撤回、更新关闭、任务重置和来源关系;重新使用时检查匹配、隔离、新落点、任务恢复与运动。旧句柄仍能查到是当前复用实现的正常结果,不能用“必须查不到”判断退休成功。

视频 5:退休复用,9 秒。池数经历 0→2→0,原句柄再次准入,两人恢复更新并分别继续行走。该片录于最终重准入修复之后,连续黑场来自实际淡出流程。计数变化与画面运动共同检查,不能只靠池数判断成功。

拒绝入池不能先破坏其他对象

有界池满时需要驱逐旧条目,但应先检查当前请求的身份、类型、重复入池和保护关系。若先腾空间,再发现候选受剧情保护,本次请求虽然失败,池中另一个有效对象却已经被销毁,拒绝就产生了无关副作用。

当前流程先只读检查,再处理容量压力和重置。人物保护沿用退休资格;车辆侧也检查驾驶员、命令所有者、乘员和预约,避免只保护驾驶员却把仍有乘客的车辆收走。这里保留关系不变量,并没有扩展为完整车辆复用机制的验收。

取用还要匹配来源与隔离

池条目保留类型、模型绑定、退出来源、来源代次和退出 Control 帧。取用者还要匹配世界、类型和所需绑定。同来源重新使用要求新的来源代次,跨来源则要求相应观察安全条件,随后才检查隔离是否满足。

当前两次 Control 更新的隔离与来源 epoch 都是项目策略,不能写成原版常量或全局身份代际机制。epoch 在这里用于区分人口来源的不同使用阶段,不意味着网络、脚本和渲染等所有消费者都有统一的失效令牌。

取用成功会解除池标记和任务抑制,并请求恢复相应更新阶段;随后还要让人口实例化流程挂接新位置与来源。池的 Acquire 成功不应被宣传成“对象的所有下游状态已经原子恢复”。挂接和计划初始化还有各自的结果,失败分支必须继续按实际返回处理。

退休、隔离、重新取用与真正销毁
图 8:当前复用生命周期示意。入池保留身份,真正终止驱逐或销毁才让旧句柄失效;取用之后仍需完成新落点和路线挂接。箭头是工程流程归纳,不是原版统一状态枚举。

池条目记录的是哪一次退休

池不只保存一个可用句柄列表。条目还保存实体类型、模型绑定、退出来源及代次、退出帧、入池序号、重置版本、原更新阶段标志和退出时的观察安全条件。前面几项用于判断能否匹配,原更新标志用于后续恢复,序号与版本则保留这次入池身份和重置记录。来源代次属于人口来源,不能与人物句柄本身的身份混为一谈。

同来源取用要求当前来源代次晚于退出时代次;仅仅再次调用 Acquire 不会自动满足。跨来源取用不依赖这一相同来源关系,而要求退出端与新来源都满足相应观察安全条件。通过来源条件后,还要检查世界、人物类型、模型绑定和隔离时间。匹配到尚未结束隔离的条目会记录“兼容但待隔离”,与“池里完全没有兼容对象”分别返回。

来源条件可以进一步写成便于阅读的布尔摘要,外层仍需校验世界、类型、绑定及请求本身。下式比较的是来源代次,不是实体句柄值:

sameSourceEligible = sameSource && retiredGeneration < currentGeneration
crossSourceEligible = !sameSource && safeAtRetirement && safeAtDestination
sourceEligible = sameSourceEligible || crossSourceEligible
quarantineReady = currentControlFrame − retiredControlFrame >= requiredFrames

成功取用先从池记录移除条目,再解除人物池标记与任务抑制,并向世界请求恢复原有的更新阶段。随后外层实例化才负责新落点挂接。Acquired 表示池侧操作完成;新的来源和路线关系仍由外层挂接流程建立。

入池端也有同样的分层。它先清除无效缓存记录,再只读拒绝重复条目、错误实体类型和受保护关系,随后才考虑容量驱逐与重置。这里的“拒绝不破坏其他对象”针对合法缓存和候选状态,并不意味着函数绝不整理已经失效的缓存记录。容量驱逐与重置也是后续操作,并没有被包装成跨世界的不可失败事务。

实例化后续失败时,Restore 使用保存的池条目恢复记录,并重新执行池化重置;如果原先隔离已经成熟,归还条目不因这个接口自动被当成第一次退休。恢复入口本身仍可能失败,因此外层必须检查返回结果,而不能只把条目重新塞进数组就宣布整个批次已回滚。

复用后卡住:新准入被旧路线覆盖

现象与排查:取用成功,运动没有恢复

这轮最后的失败发生在取用之后。人物已经从池中移出,原句柄还在,活动人数也恢复了,但后续位置没有按预期推进。只检查“是不是原对象”“池里是否少了两个条目”,都可能把这次运行判成成功。

问题分为两个相邻环节。先是旧的路线/空间投影仍保留位置写入权限,实例挂接新候选时被旧权限抑制。之后即使挂接能够执行,规划器里已关闭的历史记录还可能被当成新路线的起点,覆盖新准入位置。

挂接修复:成功取用后撤回旧位置权限

正式路线运行期间,有理由阻止其他模块随意重写人物位置。但对象退出旧人口、进入池并再次被取用后,这份旧权限不能自动继续阻挡新出生位置。

修复落在成功取用之后、新实例挂接之前:只对复用对象清掉旧的路线和空间投影权威,再使用已经通过准入的路线进度进行挂接。没有对所有活跃人物无条件清标志,因此正常路线转移仍保留原规则。

下面是中性化后的调用顺序摘要,省略对象创建与错误分支:

成功取得符合条件的复用条目
若本次确为复用:撤回旧路线与空间投影的位置写入权限
应用当前模型默认回收参数
按已批准候选的路线进度挂接人物
由新路线按当前状态接管后续运动

规划修复:历史记录需要继续使用的依据

规划器保留最近一次计划便于诊断,这份历史有价值,不能简单全部删除。但“最近有一条记录”不足以说明本次请求应当沿它续接。

当前修复只在以下条件同时成立时,不使用旧历史推断起点:没有活动计划,没有显式指定来源端点,也没有由当前人口来源拥有的已完成路线。此时将其视为一次新的隐式准入,从刚批准的实际位置建立计划。

显式续接、仍然活动的路线转移,以及当前来源拥有的已完成路线继续走原有路径。否则为了修复复用而删除全部路线记录,会把正常连续行走也破坏掉。这次修复保留诊断记录,仅限制旧历史参与新计划的条件;位置写入权则在挂接前单独撤回。

复用卡住的原因与有条件修复
图 9:实现案例。旧位置权威与已关闭历史分别影响挂接和新计划起点;修复在各自消费处约束旧状态,没有将所有路线信息统一清空。

验收:检查原句柄与后续位移

最终复用视频核对原句柄 78、79 保留。重新准入后,两人的实际移动量分别约 308.711 cm、572.000 cm,最小间距约 537.000 cm。这些值来自该片对应的记录,是本次具体轨迹的结果,不是通用速度或间距参数。

复用后同一视频两个时刻的人物位置
运行截图 1:修复后的复用视频在 5.31 秒与 8.37 秒的实际画面。两张图来自同一连续片段,分别完整缩放,没有拼贴人物或修改场景。下方移动量引用该片对应的整段核验记录,不是这两张截图之间的位移。它展示修复后恢复行走,不是修复前后的画面对比。

回归还补了更小的反例:旧路线已经关闭但诊断仍在时,重新准入应保留已批准的路线进度;旧对象持有位置权限时,复用挂接仍应到达新候选。小反例检查原因是否被修复,正式视频再检查两个人是否真的重新走动。两类证据各自有用途。

为什么在消费旧记录的地方修复

这次故障可以按状态依赖再拆开看。池提供“同一个实体仍可使用”的身份事实;挂接需要“允许把它放到新候选”的位置权限;规划需要“从哪个状态建立未来路线”的起点依据。这几件事在新建路径中容易同时成立,因为对象没有历史;复用路径则必须逐项交接,不能默认旧对象等同于空白对象。

如果只修池,让它恢复更多标志,挂接仍可能受到旧路线权威阻止。如果只强制写一次新坐标,下一次规划又可能读取历史起点把人物拉回。若反过来删除全部历史,正常路线转移和显式续接又会失去依据。因此修复分成挂接前撤回旧位置权限、规划入口限制历史可用性,分别约束真正消费旧状态的位置。

规划判断尤其强调来源。最近的已完成记录只能证明过去完成过,不证明当前来源拥有这份完成状态;当前来源拥有的 completed route,才有资格进入对应续接分支。显式来源端点又是调用者主动提供的连续性要求,应当区别于系统自己从诊断记录猜测起点。无活动计划、无显式端点、无当前来源完成依据时,新的实际准入位置才应成为新计划依据。

验证据此分成不同反例:一个把旧位置权限留在复用对象上,检查新候选能否实际写入;另一个保留关闭历史,检查新计划是否仍从批准位置建立。最后再观察正式视频中两人持续移动。这些测试没有把所有可能路线历史都证明正确,但能避免只改一个外观症状,另一个消费者下一轮又把状态覆盖回去。

动画读取运动结果,不能反过来推进人口

行人逻辑上已经停下,walk clip 却仍在走,或者折返时模型朝向突然跳变,都会使正确的任务结果难以观察。表现层需要改善,但不能让 Root Motion 取代已有路线的位移来源。

当前显示代理随逻辑实体同步,动画 Root Motion 不参与逻辑移动。步态按实测速度播放,静止时停止 walk clip 推进;近景 idle 采用 92% 参考姿态与 8% 现有 idle,配合已有腿部和手臂双骨骼 IK 调整平地姿态,腕方向跟随解算后的前臂,不靠拉长骨骼修正。

视频 6:起停折返,11.8 秒。真实 walk 请求驱动起步、到达和折返,显示层按实际速度播放并在停止后收步。此段使用专用 walk host,是近景表现检查,不代表所有正式人口宿主和远距动画都经过同样验收。

起步与停步混合使用不同时间,当前分别为 0.16 秒、0.22 秒;实测速率窗口为 0.1 秒,hold 为 0.08 秒。现有固定步显示插值也已开启,用来连接相邻逻辑状态。逻辑转向仍可在任务一步内改变,渲染朝向通过插值连续过渡,没有因此生成一个新的转身任务。

任务运动、渲染插值与动画播放
图 10:表现层示意。逻辑位置、速度和朝向是输入,插值与步态是显示结果;没有从动画根位移返回路线进度的箭头。

这些是现有资源上的程序化适配。没有新增起步、停步、180°转身动作,也没有 Motion Matching、完整参考动画网络或地形 IK。近景看起来连贯,只说明这条显示路径更容易辨认,远距低频动画和更复杂地面仍需独立检查。

显示插值怎样接在逻辑运动之后

显示端读取逻辑变换与实测速度,再决定姿态和步态。测量窗口用于平滑速度观察,短暂 hold 用于抑制起停附近的反复切换;起步与停步使用不对称混合,使进入行走与退出行走分别响应。窗口、保持时间和混合时长描述不同滤波阶段,不能把它们加在一起解释成一个固定的“人物反应延迟”。

固定步显示插值处理的是采样之间的可见变换。逻辑位置可能按自己的阶段离散更新,渲染帧根据已保存样本平滑呈现;不能把插值位置再写回逻辑实体,否则显示刷新率会反过来改变路线进度与碰撞输入。转向也一样:当前逻辑允许任务单步改变朝向,渲染端平滑这次变化,并不等于已经有完整转身动作或转身任务。

近景姿态修正则作用于骨骼。参考姿态混合和已有双骨骼 IK 调整手脚,手腕跟随求解后的前臂方向,避免用拉长骨段掩盖姿态误差。这些操作既不修改人口类型,也不解除车辆关系或改变实际位移。静止时停止推进行走片段,防止只因动画播放仍在继续就给读者“人物还在走”的错觉。

因此,验收要分别看逻辑位移、显示位置与骨骼运动。走路动画正常而逻辑位置不变,是表现不能证明移动;逻辑位置正常而显示抖动,则应检查代理同步和插值,不应先改路线成本。近景主片和专用 walk host 用于检查表现适配,正式人口的路线与生命周期另有对应测试。

行走由路线提交,避让只给方向建议

出生和回收正确之后,活动中的行人还要继续完成路线。同向跟随、对向避让、静态障碍等待和交叉冲突,都发生在这一执行过程里。局部避让可以给出方向建议,但目的地、进度与完成状态仍由原任务和路线维护。

正式人口的原生 Sidewalk 分支复用行走模块中的方向选择与身体间距帮助器,读取真实人物位置、朝向和速度观察,不再另建一份独立推进的行走身份。已有排队与物理 crossing hold 优先,过街、跨块转移和接近路线入口的分支保留自己的权力,不被统一的侧让逻辑覆盖。

计划进度读取实际位移的纵向投影

设当前直线连接从 S 到 G,水平长度为 L,单位方向为 a,连接代价为 C。候选实际位置为 Pnext,它在连接上的纵向距离是:

alongNext = dot(Pnext − S, a)
projectedCost = alongNext / L × C
acceptedCost = clamp(projectedCost − currentProgress, 0, stepBudget)

这是当前直线人行道适配的关系式。侧向绕行走过的总路程不能直接加到计划进度,朝障碍反复试探也不能让人物逻辑上提前到终点。候选没有通过地面、碰撞和身体间距检查时,不提交新的物理步,也不偷推进路线。

完成还要检查人物是否实际接近终点。绕到侧面后即使跨过终点的纵向平面,仍不能把距离终点较远的位置当成完成。当前适配还约束反向投影和越界候选,避免绕行破坏原连接的单调推进。

实际位移、纵向进度与显示的关系
图 11:当前直线连接的投影示意。斜向实步包含纵向与侧向位移,路线只消费对应的纵向代价;实际候选未通过检查时不提交。图中点位是教学几何,不对应某一段录像。

正式 Sidewalk 分支怎样换算运动预算

路线成本不必等于世界长度。设当前连接成本为 C,连接端点的水平长度为 L,本步路线预算为 B,局部执行器先将它换算成允许尝试的物理步长。物理检查通过后,再把实际纵向进度转换回同一成本单位。下式中的 S 沿用前文定义,表示当前连接的起点,而非人物本步开始的位置;previousProgress 是本步之前已累计的连接进度。下式只描述当前直线 Sidewalk 适配,不涵盖所有连接类型:

requestedStep = max(B, 0) × L / C
alongNew = dot(Paccepted − S, routeAxis)
acceptedCost = clamp(alongNew / L × C − previousProgress, 0, B)

这样既能保持规划器的成本表示,也能让碰撞层使用厘米坐标。连接成本或长度接近零时不走这条换算,不能把除零异常交给碰撞查询处理。实际位置偏离中心线时,也不能直接用到目标的斜向总距离改写规划成本。

同向跟随先缩短可用物理步长。实现将相邻人物分解成沿路线的 Ahead 与横向 Side;横向仍落在身体宽度范围内时,根据圆体几何计算所需纵向余量,再给后续提交保留一个本步距离余量。该余量针对当前不同来源可能先后提交的执行顺序,不应被写成通用人类跟随距离。前方没有空间时,局部输出保持原位置,路线预算归零。

随后才决定是否启用局部转向:对向身体、其他方向的预测冲突、已偏离中心线或前方物理阻挡,均可能进入帮助器。队列或过街等待已经持有当前位置时,帮助器无权从侧面绕开这份约束。直行没有冲突且无需修正时,可以继续既有路线处理;这并不是每个人每步都无条件重建一份导航路径。

末端检查也必须看实际到达。候选纵向位置不允许倒退或越过连接边界;达到末端纵向范围后,横向仍离目标太远也会拒绝。否则人物可能从终点侧面经过,只因跨过一个平面就被算成已经到达,下一条路线接续时再发生跳变。

身体预测怎样发现交叉冲突

只检查“正对面有人”会遗漏斜向和垂直交汇。当前共享帮助器把人物看成水平身体圆,以相对运动检查候选时间窗内是否接近。设自己当前位置为 Ps,候选终点为 E,对方当前位置为 Po,对方观察速度为 vo,预测时长为 h:

r0 = Ps − Po
r1 = E − Po − vo × h
closest = 三维原点到线段 [r0, r1] 的最近点
clear = lengthXY(closest)² >= (2 × bodyRadius + padding)²

这里特意保留“三维最近点、水平长度比较”的代码次序,不能直接改写为纯二维最近时刻公式再宣称执行完全相同。当前帮助器忽略已退休对象,并对身体高度差作过滤;停住或已经到达的邻居仍会占位置,不会因为速度为零而消失。

预测时间通常来自前视距离除以期望速度,对方速度来自观察窗口。当前身体半径 30 cm、padding 3 cm,对应安全直径项 63 cm;前视 100 cm、对向扫描 250 cm、速度窗口 0.2 秒都是项目参数。它们与出生胶囊、显示大小和动画播放速率各自独立。

视频 7:三人交汇,约 4.67 秒。正式人口从不同方向接近,局部避让后继续各自原路线,到原目标停稳。录制没有脚本手动排队、改坐标或重设目的地。它证明这个小场景,不是大规模拥挤人流或全局路口优先级算法的验收。

方向选择与物理检查仍然分开

当前候选方向取期望方向附近的固定角度扇形,测试 0°、±25°、±50°、±75°、±90°。候选先检查地面投影、可通行性和预测身体间距,再比较与期望方向的点积;正角度候选增加 0.03 的偏好,用于减少完全对称的迎面选择。

这是当前 UE 适配,不是完整恢复的切线候选与评分系统。地面查询还依赖场景标记和物理证据,使用有限高度差与采样间距,候选胶囊 sweep 通过后,实际短步仍会再次检查。候选方向看起来安全,不保证下一步实际位移已经获准。

相对身体预测、方向候选与等待
图 12:等高俯视的身体预测示意。左侧画出两个人在同一预测时长内的运动,右侧转为相对线段,检查它到原点的最近距离。身体圆、相对轨迹和阈值圆用于解释关系,不按运行坐标比例绘制;代码仍先求三维最近点,再比较水平长度。

候选方向如何经过场景检查

方向评分优先保留朝向目标的分量,并用固定侧偏好打破完全对称的选择。偏好只影响已通过安全检查的候选,不能给穿墙方向加分后直接提交;它也不是路口通行权或人物优先级。

每个方向先投影到可行走表面,再检查可通过性与相对身体净空。正式 Sidewalk 适配要求地面射线命中带指定行走表面标记的对象,并检查法线是否足够朝上。沿路径还会分段采样地面,避免端点合法、途中却跨过缺口;胶囊 sweep 则检查身体体积能否通过。这些条件由 UE 场景回调提供,帮助器本身不拥有地图语义。

直接前方被静态几何堵住时,还要检查侧移之后能否重新向前。只有侧面一步可行、后面仍是连续墙体,不构成局部出口。若缺少这道检查,左右候选都可能在不同更新获得高分,人物持续横移试探却没有前进。当前做法是在候选评价中补出口探测,实际短步提交仍独立重查,不把远处探测通过当成眼前运动一定安全。

候选胜出后,执行层按本步预算构造较短终点,重新做地面、胶囊和身体净空检查。规划时预测的是一段观察窗口,提交时验证的是当前实际步,两者输入与用途不同。任何一项失败都保持当前位置,不提前消费路线进度;下个更新仍可根据新的障碍与身体位置重新评价。

这里保留的是当前有界局部适配。它没有完整切线候选评分、全局拥挤成本或群体死锁解算;指定侧偏好也不能保证任意密度、任意顺序下都收敛。三人交汇片能显示这个小场景的实际结果,不能用来证明全城人流容量。

没有出口时,保留原计划等待

早期面对整面墙时,侧向短步有时仍然安全,人物便在墙前左右试探。位置在变化,却没有可以继续原路线的局部出口。后续在静态阻挡候选中加了一段沿原路线方向的前向出口探测;找不到候选时就等待,不把横移本身当成进展。

补充视频:阻挡等待与恢复,7.6 秒。实际全宽障碍挡住路线时人物等待,移除障碍后原计划继续。该案例不替换人物,也不请求新的路线规划;它检查有限局部处理,不能推广为任意凹形障碍的全局绕行。补片来自此前拍摄版本。

正式路线的这个等待恢复,与专用 walk host 中可能触发的重规划不是同一条路径。共享的是局部帮助器,任务生命周期和失败策略仍由各自消费者决定。正文不把其中一个宿主的 Replan 行为写成所有人口路线的统一恢复方式。

狭窄通道、排队恢复和运行时启用/移除障碍另有补片。运行时障碍片改变的是静态几何碰撞的启用状态,不证明系统已能预测高速移动车辆;通道片也没有通用走廊预约或全局死锁求解器。现场没有安全候选时先等待,是当前明确保留的结果。

专用行走宿主怎样拒绝迟到路径

正式人口路线与专用 walk host 共用几何帮助器,但并没有合并成同一个任务执行器。专用行走状态保存目标、路径点、当前拐点、请求代次、等待状态和阻塞时间。新目标请求增加代次,撤回旧路径,并重置新请求所需的速度、转向和进展记录;取消也增加代次,使尚未返回的旧查询不能重新激活任务。

接受路径时同时检查实体索引、请求代次以及当前是否仍有待处理查询。旧回包即使带着一条几何上有效的路径,也不能因为晚到就覆盖新目标。通过身份检查后还要看查询是否失败、路径是否为空,以及末点是否到达当前目标的允许范围;通过这些检查才切换到行走并安装新路径。这个机制保护的是专用宿主的异步边界,没有据此宣称正式人口所有计划接口都采用相同回包结构。

用请求时保存的代次核对回包,可以把“过期结果”和“当前查询失败”分开。以下为该入口的分支摘要,省略索引检查及返回结构:

if reply.generation != state.generation or !state.pathPending:
    rejectStaleReply()
else:
    state.pathPending = false
    if queryFailed or pathEmpty or endpointOutsideGoalTolerance:
        state.status = Failed
    else:
        installPath()
        state.status = Walking

一个旧查询被拒绝不应把新查询标成失败;只有身份匹配的当前查询,才有资格更新当前等待状态。否则“旧失败晚到”也能取消已经换过目标的新任务。

专用宿主还区分便宜的运动更新与较贵的转向评价。Control 依据远近状态调整转向更新节奏,并在从远处进入近处或定时站立结束时要求重新评价;实际运动仍检查本步地面与身体。运动阶段把经过时间拆成有上限的小步,而不是在卡顿后只走一小段就丢弃其余时间。这与前面人口生成时钟的最大接受时间不同,两个限幅不能统一替换。

推进拐点后也不能直接沿原始目标方向走一步。新的局部方向先经过同一帮助器,否则每次重试都可能绕过障碍判断,逐次向墙体靠近。当前宿主还分别累计阻塞与无进展时间,超过各自门槛才请求重规划;重规划次数也有上限。这套恢复属于专用行走入口,正式人口的队列等待和过街约束仍沿正式路线规则处理。

用失效原因选择观察数据

同样是人物静止,调试入口应当不同。缺口尚未形成时先看来源目标和计数;已有待处理需求但没有实例时看资源、候选、预约与隔离原因;已有实例却不动时看任务资格、路线活动状态、位置权威及本步拒绝。逻辑位置已经移动但模型仍静止,才沿显示同步、速度测量与动画播放查下去。诊断信息应保留这些阶段差异,以便定位实际阻塞点。

复用验收尤其要留住同一对象的身份链。记录退休来源和代次、池条目、取用结果、新准入进度,再对照后续实际位置。人数回到目标值可能由新对象补足,原句柄重现也可能只是原地恢复显示,单一数字都不足以证明复用闭环。本文保留句柄、计数和位移,是为了分别回答这些问题,不是把它们包装成一个综合成功分数。

读参数时也要带上消费者。以下数值来自本文核对的交付分支,长度使用厘米、时间使用秒,法线分量没有长度单位;它们不是原机制全局常量,也没有作为本次写作新增的调参结果:

当前参数或检查 数值 消费位置与含义
路径末点容差 各坐标分量绝对差不超过 15 cm 专用 walk host 用 FVector::Equals 接受路径;这是逐分量比较
最终到达距离 三维距离不超过 15 cm 专用 walk host 路径耗尽后的目标距离检查,不等于上面的逐分量容差
拐点推进距离 严格小于 5 cm 专用 walk host 进入下一拐点;随后重新过滤第一步方向
单个运动子步 最大 0.05 s 专用 walk host 拆分经过时间,不是丢弃其余时间
连续阻塞触发 严格大于 1.5 s 专用 walk host 请求重规划
无进展触发 严格大于 2 s 专用 walk host 按到拐点的改善情况累计
重规划次数 超过 3 次进入失败 专用 walk host 的有界恢复,不是正式人口的统一重试上限
地面法线 Z 不低于 0.8 正式 Sidewalk 地面投影资格,仍需指定表面标记
地面沿途采样间距 最多约 25 cm 正式 Sidewalk 可通过性检查,还需高度偏差与胶囊检查

这张表不能用来给正式人口任意添加超时。例如过街等待可能由信号或占用规则合法维持,不能套用专用宿主的阻塞时间就删掉路线。复用隔离又属于 Control 帧,不应转换成表中的秒数去比较。参数不仅有数值和单位,还需要保留所属状态机与生效阶段。

现有回归和视频分别覆盖这些局部契约,本文没有重新执行全部工程测试,也没有新增多人或性能验收。补充实现解释来自已核对的源码与交接记录;它不会把历史补片升级为同一最终构建的连续证明。后续核查可以沿输入、状态写入者、拒绝原因和输出逐项展开,再与对应画面核对。

这轮测试分别检查了什么

最终功能记录为构建成功,142 项唯一自动回归中,57 项成功、85 项带警告成功,0 失败、0 未运行。已有 world cleanup 警告保留在报告中,不能写成无告警。这里的 142 是自动回归项数,不是渲染场景数,也不是参考系统的功能覆盖百分比。

正文主片来自原城市实际 UE 画面,逐段保存了对应操作、采样与核验记录。主片 5 在最后的重准入修复后录制;其他主片主要来自此前的动画修订版本,所展示行为未变。主片 6 复用已经验收的近景源片。不能为了叙述方便,把它们写成同一个最终 DLL 的一次连续测试。

证据 可以检查什么 不能替代什么
参考机制阅读与接口合同 职责、阶段、参数来源与已知偏离 不能证明 UE 分支已经实际执行
当前源码 调用顺序、状态写入、失败路径 不能单独证明镜头中角色表现正确
自动回归 边界、保护、计数、复用与计划反例 不能替代所有真实物理场景
对应运行采样 句柄、位置、计划、成员与池状态 不直接等同于完整视频里每个细节
实际视频与抽帧 连续可见行为、淡出、起停、避让结果 不证明未展示分支、多人和性能

本次写作读取交接、机制说明、相关源码和已有验收记录,核对视频哈希并查看抽帧序列,没有重新构建或执行 UE 回归。图片使用真实编码帧,按不透明 RGB 导出;机制图另外标注用途,不能把图中的教学路径当成运行测量。

主片与补片怎样对应正文

主片按交付顺序就近嵌入,先看生成、保护和复用,再看显示与正式路线避让。文末补片索引保留所有素材,方便按问题查阅;各片不是新增的独立实验次数。

主片 时长 本文检查重点 主要限制
人口增减 9.4 秒 空间回收、返回补足与正式人口继续走 使用近景距离覆盖,街段未卸载
剧情归属 7.4 秒 接管保护、释放后恢复普通回收 没有新增剧情动作
生成与保护区域 约 11.93 秒 两类区域独立设置与解除 每类一个世界盒,不是任意体积集合
淡出初始化 11 秒 实际淡出与隐藏近距准入 默认范围;不能唯一归因于第二次请求
退休复用 9 秒 原句柄、池数、重新准入后的实际移动 本地复用策略,不含完整流送与联网
起停折返 11.8 秒 实测速度驱动动画与显示转向 专用 walk host,近景表现
三人交汇避让 约 4.67 秒 正式路线的身体避让和原目标完成 小场景,未验收大人群性能

补片保留此前拍摄版本,未全部按最后动画重新录制。索引中的“对向行走”与“正式人口路线对向避让”有不同宿主,物理占位出生与逻辑人物占位出生也有不同检查对象,应按各自说明使用。

补片 时长 用途与边界
同路线跟随间距 11.03 秒 同路线间距与出生进度;独立历史拍摄
阻挡出生 10.03 秒 真实物理障碍阻止出生;区别于逻辑人物占位
对向行走避让 7.53 秒 专用行走宿主的对向观察;不替正式人口接线验收
正式人口路线避让 5.67 秒 正式人口需求到原路线的对向避让
静态障碍绕行 6.20 秒 有限局部静态绕行;不是全局寻路
阻挡等待与恢复 7.60 秒 全宽阻挡等待,移除后原计划继续
排队恢复 10.00 秒 前方阻塞影响后随者,解除后恢复
运行时障碍出现与移除 8.47 秒 静态几何运行时启用/移除;不是高速车辆预测
交叉路径避让 4.47 秒 非对向身体冲突与原目标保留
狭窄通道等待与放行 9.73 秒 狭窄通道等待;无通用走廊预约与全局解锁
已有行人占位出生 6.67 秒 跨来源逻辑人物占位;代理无碰撞也拒绝重叠出生
逐行人回收倍率 7.40 秒 逐人物倍率与恢复模型默认;不绕过任务保护
当帧清理保护 10.73 秒 持续重申与不重申恢复;不是永久免回收

从局部闭环继续接入城市

这轮把一个街段中可重复观察的过程接了起来:需求经过准入生成成员,人物沿正式路线活动,剧情可以接管与释放,人口在关系允许时退休,复用对象经过新准入重新行走。复用卡住的修复还表明,成员重新出现之后,要继续追查位置与路线有没有真正恢复。

已有实现为后续任务、车辆和服务系统提供了可用的人物身份与生命周期接口,但还不能据此说完整城市人口已经还原。完整全城密度、原始模型流送、场景/室内/野生动物人口、网络归属与全任务树仍需各自接入。Active/Warm/Inactive 是地区策略,不能冒充人物 Dummy/Real 表示转换;其他实体类型上的转换代码也不能替人物证明这一点。

事件到 Pending 任务、活动槽保护与任务抑制已有代码和测试基础,正文主片没有专门展示完整感知反应链,因此这里只在退休资格处说明其保护作用。完整事件反应还需核对生产者、任务桥和执行结果,并由对应场景检查。

后续接入应继续保留现在的职责:人口决定是否申请和撤回,任务与路线决定怎样行动,物理查询约束实际短步,动画读取结果。换成更完整的候选服务、角色资源或行为实现时,再对这些接口重新验收。

AI 协作复盘

AI 协助把分散的机制阅读、人口接口、路线执行和录制记录整理到同一条运行过程里,并核对生成位置、剧情归属、复用条件与动画输入。已有开发中最有价值的检查,并非一次成功编译,而是把“池数恢复但人不走”和“正式人口未生成但旧代理仍可见”保留下来追查。

两次问题都暴露了观察范围的缺口。前者只看句柄与计数会漏掉实际运动,后者只采样新增代理会漏掉旧显示对象。修复分别落在新准入的位置权限、规划历史消费和显示归属交接处,之后补对应反例与实际画面。不能把它们统称为一次“重置全部状态”,那会遮掉改动究竟影响了哪条路径。

写作时也按同样方式处理材料:历史合同中的缺口结合后续记录判断,项目参数与参考机制分开,补片与最终主片保留各自版本。这些区分让后续修订可以回到具体接口与运行记录,保留失败原因和修复依据。

发表评论

了解 AI Native Game Development 的更多信息

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

继续阅读