城市模拟的 UE5 运行时实现|多车通行、排队与动作占用

灯已经绿了,来车却没有进入路口。它前面的普通车辆刚刚完成短程任务,仍停在出口附近。对来车来说,目的地没有取消,信号也允许这个方向通行,但它准备经过的空间还没有清空。

每辆车都有自己的任务,也都有能驱动车体的控制器。把它们放进同一条路,问题就变成了:眼前还能走多远,前方哪些空间已经被别人占用,停车以后又该等什么条件解除?

上一篇《NPC 跟车、受限绕行与自主恢复》把目的地、交通限速、换道和恢复接到同一辆物理车上。本篇继续向多车推进:先收集同一轮车身观察,让路口许可、队列距离和已提交的换道动作共同限制每辆车,再把结果交回各车的驾驶控制。底层沿用玩家驾驶与四轮受力,驾驶资格仍由座位关系与驾驶权接口检查。

下面保留六段完整实录,依次看信号排队、无信号交叉、出口受阻、共享出口、施工绕行和清障续行。它们来自独立 PIE 会话,每段只有两辆或三辆受测车,合计十五辆次。这里讨论的是局部普通车辆案例;背景停放车不计入名册,也没有把这些片段当成全城交通、常态人车混流或长期稳定性的证明。

文章目录
绿灯出口被占时的真实画面
实录画面|第三段片内约 9.47 秒。A 已停在出口附近,B 在入口外等待,信号仍绿。后文结合这轮快照解释 B 为什么具备申请资格却没有获准。

目的地保留,当前一步受共享空间约束

一辆车在红灯前停下,不需要业务层再发一次目的地。旁车先通过,也不意味着它自己的任务被别人替换。目的地回答“这趟要去哪里”,共享空间的判断只决定“这一物理步允许怎样继续”。把两者合在一个当前目标字段里,停车、改道和任务结束就很容易互相覆盖。

当前行程保存目的地、巡航速度、意图编号 IntentSerial,以及有向道路条目、车道点、游标 Cursor 和计划编号 PlanSerial。意图编号对应一次有效新任务;即使坐标相同,重新接受 Start 也可能产生新意图。交通停等只保留原意图,绕出、返回或重新接路则可以在同一意图下更新计划。

计划编号在同一宿主生命周期内累计,不保证新任务总从某个固定编号开始。后文三车绕行的 Intent=1、Plan=3 是各自实例中的结果,不能据此跨录像比较同一辆持久车辆。不同 PIE 会话中的实体编号即使重复,也没有这种含义。

保存的信息 状态所有者 等待时怎样处理
目的地、巡航速度、Intent 行程对象 普通交通停等继续保留,接受新任务或取消时单独处理
道路条目、车道点、Cursor、Plan 行程中的道路计划 按实测位置推进;候选获准后才替换相关计划
信号组、相位、剩余时间 信号运行时 按模拟时间推进,提供入口灯色资格
路口到达顺序、准入与清空持有 路口运行时 区分尚未进入和已经越线,保留合法的在途清空责任
前车及允许推进距离 队列计算 根据当前车身和前车允许量,收紧后车本步距离
当前换道动作、Intent、Plan 局部动作占用记录 持有已提交动作,按实测车尾或生命周期条件释放
下一提交帧及对应建议 每车控制包 只能被匹配的物理步和任务版本消费
司机资格、命令所有权 人物与车辆关系接口 每次接受驾驶命令时继续核对
实际姿态、速度、轮接触 同一 Chaos 车体 持续参与物理,停等不会直接清零速度

这些约束保护的范围不同。路口保留车辆穿过控制区的剩余路径,换道占用保留当前一次横移动作,队列只限制本步还能向前多少。读到“允许通行”时,需要先看是哪一层给出的允许:一个 GO 只解除对应限制,其他约束仍然存在。

UE 侧由一个显式登记车辆的协调宿主取得全队快照,调用原生运行时并发布控制包。各车的执行宿主继续负责路线评估、目标控制、驾驶命令接受、轮力提交和物理回读。决策规则放在运行时里,Actor 的阶段回调负责把它们接到当前场景;这是局部名册的执行入口。

任务、共享空间与物理执行的状态所有权
图 1|状态分别由行程、交通约束和物理执行持有。信号、队列与动作占用收紧当前控制,车体的实际运动仍由驾驶执行与 Chaos 决定。图示对应当前局部名册。

还有一个贯穿全文的区别:任务完成只改变任务状态,实体仍然存在。车停在出口并报告 Completed 后,不能从其他车的观察里消失。反过来,车辆现在停着,也不能推断任务已经完成;它可能只是等灯、等前车或等一个冲突动作清空。

先取得全队观察,再把建议交给各车

单车控制可以只读自己的姿态,多车准入还需要知道别人目前占在哪里。如果第一辆车读取旧车位、第二辆车读取新车位,再各自推导“路口是空的”,即使每辆车的局部算法看起来正确,组合以后也可能互相抢同一段空间。

当前协调器在 PrePhysics 阶段先用模拟时间步推进信号,再读取已加载路网和登记车辆的实际 Body 状态。登记车辆必须已经 Ready,且没有尚未消费的物理提交;某个仍在名册中的参与者暂时不可观察,这一轮就不发布新包。各车拿不到匹配包时请求制动,不把半份观察当成完整队列继续使用。

快照记录实际位置、车头方向、水平速率、车身边界和当前有向道路身份。UE 位置使用厘米,道路计算使用米,位置和车身边界都要在接口处换算。队列中的七米间隔与几百厘米的车身半长,必须先统一单位才能比较。

先观察已有换道动作是否已经结束,再建立路口输入。普通交通车和显式静态障碍分别登记:一辆车速度为零,仍可能是在排队或已经到达,不会因此自动变成允许绕过的施工物。这个类别区分也决定后续空隙和出口判断走哪条路径。

路口更新先在候选状态中计算

整队路口入口会检查帧号是否递增、车辆身份是否重复、每车策略是否齐全、模型尺寸及坐标速度是否有效。通过基本校验后,复制一份路口控制状态,在副本上计算新观察与许可。全部结果有效,才替换正式控制器并发布输出。

这样,计算途中发现结果无效时,不会已经在正式路口状态里消耗部分车辆的到达序号,或给某辆车留下半次准入。这个发布边界只覆盖路口控制器及其输出:信号推进和此前的动作 Observe 各自已经发生,不会随路口拒绝一起回滚。它也没有提供跨线程异步规划、网络分布式一致性或整轮通用回滚。

路口更新接受后,允许推进量再进入队列计算。队列把前车限制向后传播,协调器取最小值合并回每车建议,随后统一发布下一物理提交包。这样,各车使用的是同一轮快照推导出的建议,单车物理回调的先后不会让其他车临时改读它的新位置。这里的统一发布是当前同步调用顺序,不是把信号、路口、动作记录和车体一起提交的全局事务。

全队帧号与单车物理帧号不是同一个计数器

协调器的 FleetFrame 表示全队观察更新次数,各车的 SubmittedFrames 表示自身物理提交次数。二者有不同生命周期,不能假设数字永远相等。发布时,内部观察的 Frame 会重新标为该车的 SubmittedFrames + 1,外层包的 SubmitFrame 使用同一个值。

使用控制包前,要同时核对外层提交帧、Intent、Plan 和内部路口观察的 Frame。任务相同但路线已经换过,旧建议仍会失效;外包装帧号正确,内部观察过期,也同样拒绝。

校验对象 防止什么情况 当前拒绝结果
下一物理提交帧 把上一步或尚未对应的建议重复用于当前步 建议速度和距离归零,请求制动
Intent 新任务误用旧目的地的交通结果 拒绝旧包,等待新快照
Plan 路线已变,仍沿用旧移动的准入 本步制动,后续按新计划重新计算
内部 Observation.Frame 外包装正确却夹带过期原生观察 同样拒绝,不能绕过原生入口检查

有一种正常失效就发生在本步内:协调器为旧计划生成包后,单车执行入口先评估路线,再运行交通策略。策略可能在这时提交新的换道计划,使 Plan 增加。随后控制包检查发现版本已变,旧包被拒绝,本步请求制动。下一轮观察到新计划,再为它生成匹配包。

所以,换道计划刚提交的这一物理步可能仍在制动,实际横移要等匹配新计划的包。短暂等待保住了版本边界,避免新路线沿用旧路线的通行结果。

全队观察、控制包和物理回读的更新顺序
图 2|一轮更新的阶段关系。副本发布边界仅覆盖路口状态和输出,不回滚此前的信号及动作观察。单车提交新 Plan 后,旧包可能失效制动,后续快照再发匹配包。两个 Frame 计数器分别标记。

有效建议最终交给目标控制器,生成油门、制动和转向,再通过当前司机与命令所有权检查,进入传动和四轮受力。Chaos 完成实际积分后,PostPhysics 发布姿态、速度和测量帧,供下一轮继续判断。全队协调没有获得直接挪动车身的接口。

红灯排队:灯色只打开一层入口条件

第一段在原城的小十字路口安排两组车辆。西侧 A1、A2 同向排队,南侧 B 先行。需要观察的不只是灯头变化,还包括后车能否跟着前车停稳,以及放行后是否继续各自原来的目的地。

视频 1|交叉方向先通过,原队列等绿灯后放行,约 39.27 秒。A1、A2 在红灯前排队,B 先通过。三辆车各有任务,不是三辆同时等待红灯。

片内约 7.83 s,B 中心经过所检查的交汇位置。A1、A2 随后形成稳定等待,低于 0.05 m/s 的共同采样区间约为片内 11.90—17.60 s,跨度 5.70 s。东西组约 17.63 s 首次变绿,A1、A2 约 21.37 s 和 28.10 s 先后经过检查中心线,约 32.90 s 和 37.20 s 确认到达。

B 约 15.00 s 已完成自己的任务,早于西侧队列放行。车体中心经过交汇位置、离开路口保护范围和完成整趟任务,是三个观察时点。影片能看出顺序,精确时间仍来自同轮记录。

本场景两组信号交替运行:一组初始绿灯十八个模拟秒,全红清空一秒,再切到另一组绿灯。片头已裁去初始化,第一张实录对应原生日志约 1.3667 s,所以不能把“原生十九秒变绿”直接写成播放器第十九秒。

实体灯头材质读取实际信号控制器状态,画面中的灯色确实来自运行时。不过这里只配置绿灯和全红清空,模型有黄灯镜片不代表黄灯阶段已经实现;临时灯头也不表示原图所有永久灯位都完成了绑定。

灯绿、具备资格、获得许可、实际越线分别保存

ApproachSignalOpen 是来自信号组的真实灯色输入。内部适配还会把入口距离、前车与出口条件合入请求,形成申请资格。内部字段 Request.SignalOpen 名称像灯色,含义却已经更宽,不能把它直接画成美术灯泡的开关。

状态 对当前车辆意味着什么 尚不能据此推出什么
信号入口开放 该方向的灯色条件允许申请 冲突空间一定空闲
Eligible 综合入口条件满足,可以申请 已经得到本次移动的持有
Admitted 路口已授予具体移动 车身已经越过停止线
Committed/日志 Inside 实际越过入口,进入清空阶段 中心一过就能释放全部空间
Cleared 按当前实际位置满足保护范围的离开条件 整趟行程已经到达目的地

未越线的预准入可以在条件失效时撤回。已经实际进入的车辆,则要继续承担清空责任;横向变绿,也不能直接忽略里面还没通过的车身。这个跨相位保留由冻结源码和合成状态测试支持,第一段没有另行构造“车恰好在区内变红”的专项实车对照。

信号资格、准入、越线与清空的区别
图 3|从入口资格到实际清空。灯色约束尚未提交的入口,已越线持有按实际车身清空。等待请求没有自动获得 GO 占用,状态图也没有补出黄灯阶段。

GO 只解除路口自身的速度建议。队列给出的推进量、当前感知、世界碰撞保护、目的地减速和驾驶资格仍然有效。冻结单测特意检查过:包中的 Command 为 GO,但几何允许推进量为零时,最终速度仍应为零。

后车要继承前车的允许距离,才能稳定排队

红灯首先拦住队首,后车不一定已经靠近同一条停止线。如果后车只检查自己的信号资格,又认为前车接下来可以正常巡航,就会不断接近前车,最后依靠短距离碰撞保护刹一下、松一下。车没撞上,并不代表队列限制已经正确接通。

当前队列输入包含每车实际位置、方向、尺寸、有向链接和本步允许推进量。同一逻辑车道使用统一参考轴排序,再结合横向投影和高度范围确定是否仍共享物理走廊。转弯或换道时,如果每辆车按各自车头投影排序,结果会受世界坐标位置影响;统一坐标轴能避免这种排序歧义。

逻辑节点也不是物理队尾。原路网节点较密,两辆实际相邻的车可能已经属于相邻链接。实现继续跨节点寻找方向相容、位于前方且共享走廊的实际前车,观察范围为 max(25 m, 4 × minimumGap),并进行有限轮次的约束传播。

为了说明关系,先看直线同向、同一参考轴的简化式:

dFollower = min(dFollower,
                max(0, centreGap − minimumGap + dLeader))

dLeader 是前车本步已经获准的距离,centreGap 是两车中心间距,minimumGap 是当前跟随中心间隔。后车能够使用的是现有间距超出保留量的部分,再加上前车确实还能向前移动的距离。真实实现另含前后车方向对参考轴的投影,不能直接把这条同轴式用于任意朝向。

本场景的 minimumGap=7 m 指中心间隔。车身简化半长为 2.36 m,同向直线时对应保险杠间余量约为:

bumperGap ≈ 7 − 2 × 2.36 = 2.28 m

车型变化以后,中心间隔和净距的关系也会变。实际前车选择还检查车身宽度、方向和高度,避免两个逻辑上同车道、物理上已分开的对象继续互相限制。跨节点松弛最多执行 min(车辆数, 8) 轮,当前有限传播并不保证任意长队列一帧内完全收敛。

先修正前车“还可以走一百米”的虚假输入

开发中曾给每个队列请求固定填入 100 m。队首已经被红灯限制,后车读到的前车允许量却仍是长距离。对后车而言,前车仿佛马上就能离开,队列无法提供真正的停车约束,只剩下最后的碰撞保护反复制动。

修正以后,传入队列的是每辆车真实的路口允许推进量,先限制在 0—100 m。这里的一百米是局部请求截取上限,不能解释成车辆可以无条件开出一百米。队首得到零,后面的传播才知道队首暂时没有可用前移距离。

非零的小许可也必须合并

另一个问题出在输出接线:早期只在 HeldByLaneQueue=true 时合并队列结果。但 Held 表示接近零的保持状态,小而非零的允许量同样是约束。

例如中心间距 8 m、保留间隔 7 m,前车允许推进 0 m,后车最多只剩 1 m。这时它还没有 Held 标志,却不能把这一米丢掉,重新使用未受队列约束的巡航距离。最终实现对每个匹配车辆的 Grant 都取最小值合并,不再只读停车标志。

前车允许推进量怎样沿队列传播
图 4|同轴简化的队列距离关系。一米非零许可也要被消费。中心间隔与车身净距分别标记,实际实现还检查方向投影、走廊重叠并限制传播轮次。

这两处修复分别补上输入和输出:前车真实允许量传进队列,后车再使用每一份有效限制。最终红灯队列能形成实际停稳区间,与这条距离约束接线有关;完整尾随人格和跟车上下文仍未全部接通。

速度建议和物理制动分开处理

路口接近还复用了 AI handling 的制动距离尺度。当前计算可写为:

ratio = desiredSpeed / referenceSpeed       # referenceSpeed > 0
D = max(minimumDistance,
        ratio × (maximumDistance − minimumDistance))

参考速度为零时,不执行除法,比例项取零,再由最小距离保护。当前已审阅构造默认值是 20 m、60 m、40 m/s,对应最小距离、最大距离字段和参考速度。式中没有额外再加最小距离,最大距离字段也不是结果的硬上限。

与上一篇的威胁减速尺度相比,这里的路口辅助还用最小距离作下限,两个消费者不能混成同一公式。这里输入的是建议速度,不是实测速度平方。它给接近策略一个距离尺度,不能直接当成轮胎的物理刹停公式,也没有导入全车型和驾驶人格参数。红灯接近在相应距离范围内按 clamp((distance − extra) / D, 0, 1) 收紧建议速度;让行分支另有接近和最低建议速度,但仍受独立几何推进量限制。

UE 轮力执行边界另外增加速度保护:

speedLimit = sqrt(4 × max(0, permittedDistance − 0.2))

距离使用米,输出使用米每秒。系数四可按 2a 理解为本宿主采用的尺度;在这个单位约定下,它带有 m/s² 量纲。0.2 m 是本地余量,不是所有附着条件下都适用的停车保证。实际水平速度还会用于额外制动需求:

requiredDeceleration = actualSpeedXY² /
                       (2 × max(0.15, permittedDistance − 0.2))

该需求再按 6 m/s² 归一到制动请求,限制在零到一,制动显著时撤掉油门。感知距离的额外制动另用 0.6 m 余量,两条保护不要混成同一个距离参数。建议速度负责限制请求,实际减速仍由传动、轮力与接触实现。

停等滞回保留“已经决定停住”的历史

只靠比例减速,车辆可能在拒绝线前以很小的建议速度长期爬行;刹车后的轻微回弹,也可能让条件一松就再次给油。最终停等逻辑保存独立 Hold,完整条件为:

enterHold = d < 0.6 m
            || (Stopping && d < 2 m && advisorySpeed < 0.5 m/s)
releaseHold = d > 2 m && (!Stopping || d >= 3 m)

这里 d 是允许推进距离,advisorySpeed 是已经收紧的建议速度,不是车体实测速率。进入条件优先处理;既未进入也未释放时,沿用之前的 Hold。Hold 中建议速度为零,Stopping 保持为真。缺包或版本不匹配也会进入这个保持状态。

单测用不同阶段检查它:拒绝接近的尾段剩 1.5 m、建议已足够低时进入 Hold;回弹到 2.2 m 但仍受拒绝时不释放;前车实际离开,允许量恢复到 8 m 后可以解除。这些是合成输入对状态边界的检查,不是三次实车制动距离测量。

停车保持的进入、保留与释放条件
图 5|最终停等滞回。距离、Stopping 和收紧后的建议速度共同参与判断,不能简化为只有零点六米进入、两米退出。参数属于当前宿主策略。

失效控制包请求制动,也不等于车体瞬间静止。一般控制生产者的 Fault 另有中性滑行等政策,不能把所有异常都归成同一种立即停车。协调器退出时保留必需包门、之后缺少匹配包走制动,是源码中的关闭策略;六段录像没有另做一次拔除协调器的实录。

无信号交叉:等对方实际清空,再继续原路线

红灯能够解释入口为什么关闭,没有信号时,两辆车就需要根据各自准备经过的空间判断冲突。第二段关闭信号控制器,也不生成这轮临时灯头,让西向东的 A 与南向北的 B 进入同一个小路口。

视频 2|无信号交叉,一车先行,另一车等清空后继续,约 27.20 秒。看 B 先通过时 A 是否仍停在入口外,以及 A 随后能否沿原目标到达。两车使用不同出口。

B 约在片内 7.83 s 中心经过交汇,A 约 13.53 s 随后经过;B 约 15.00 s 完成,A 约 25.13 s 完成。A 入口低速等待约 3.23 s,这个统计使用 Stopping=1 且实测速率低于 0.1 m/s。若改用低于 0.05 m/s 的筛选,得到约 3.17 s 的更窄样本跨度,差异来自条件,不是两份记录互相矛盾。

A 没有一个“暂停三点二三秒后启动”的专用计时器。这段时间由对方实际运动、已有许可和本车制动共同形成。目的地仍是原目标,空间解除以后继续评估原行程。

路口身份要能说清从哪条车道去哪个出口

只用“在矩形里”描述车辆,不能区分两辆可以同时经过的车与两辆会交叉的车。当前输入保留有向 From/To/Lane 身份,未来车道步骤还带节点语义。经过计划来自原编译路网,控制矩形只界定检查范围,并不创建原来不存在的转弯连接。

适配器沿未来路线求进入与离开位置,形成经过路径。若短路线窗口里没有找到完整出口,就保守处理,不能把当前看见的最后一个点当成“出口一定在这里”。否则一辆车在路口深处的转弯可能落到未检查范围,两辆车各自拿到许可后才在那里相遇。

冲突检查因此保留完整的剩余路口移动,而不是每轮只检查入口附近一小段。当前实现把成对几何冲突编码为帧内冲突区标识,交给原生仲裁;这些标识服务本轮请求,不是从地图中恢复出的永久完整冲突区资产。

已有持有先保留,新请求再比较

仲裁先清理无效、离开或已经清空的记录。对于仍合法的持有,保留其经过路径和已经越线标记;新请求再根据到达序号等规则检查与已有移动的冲突。这能防止已经开进路口的车因为下一轮重新排序,被另一辆新来的车夺走前方空间。

到达序号来自被接受的请求状态。新观察中同时出现的请求先按当前实现的身份顺序登记,之后再按保存的到达关系处理。这里的“先到”是运行时观察和请求序列,不应描述成对现实驾驶法规的完整解释。

等待请求也不是已获准车辆。源码对未决请求保留有限的来源上下文优先规则,未知或不支持的组合采用保守边界;没有把所有 WAIT 计划都当成已经拿到 GO 的空间占用。完整主次路、全部左右转、替代出口、公平性和反饥饿策略仍不在本轮等价范围内。

实际通过的路径逐步释放

已准入路径不会被每轮新目标随意重写。适配器根据实测位置和朝向,在保留路径上找能够证明已经经过的部分,再逐步释放。只因空间上靠近另一段路径,不能跳到后面宣布清空;循环或折返的路线还需要维持原有条目顺序。

控制区周围另加 300 cm 的保守车身清空扩展,避免车中心刚出矩形,转弯车身和车尾仍占着交汇区域。这个数属于当前矩形适配,不是参考引擎固定要求的三米规则。它与换道动作的车尾释放条件也不是同一套状态。

交叉路径与共享出口的两种冲突
图 6|不同出口的交叉与同一出口的汇入。箭头表示有向移动,受保护的是相应经过路径;示意不按真实比例绘制,也不把控制矩形当成道路连接。右侧对应后面的第四段。

第二段验证了当前两车交叉场景中的等待、清空和续行。它没有测量任意方向混合流的吞吐,也不足以判断某种优先策略在持续来车时是否会饥饿。这些需要更长、更复杂的输入,不能从一次先后通过反推。

绿灯出口被占:已完成的车仍然有车身

第三段回到开头的问题。出口附近的 A 是普通交通车,初始就预置在控制范围内,先完成一个很短的真实任务,然后停住。B 从上游驶来,始终准备去原来的东侧目标;它所在的信号组全片保持绿灯。

视频 3|灯绿,但普通出口车仍占着空间,约 35.33 秒。观察 B 停等时 A 的实际位置,以及 A 离开后 B 怎样继续。只有 A 后来接到新的驶离目标,B 没有重新接单。

A 约在片内 2.83 s 首次 Completed,仍实际停在出口附近。B 约 8.47 s 开始形成稳定停车窗口。约 10.47 s,场景给 A 新的驶离任务;A 动起来以后,B 还要等实际占用清空,才进入。A 第二个任务约 28.87 s 完成,B 约 33.27 s 完成。

A 从出口附近开始这段短程,场景用它构造“任务已经完成,车身仍占空间”的条件。后续 A 从 Intent 1 变为 Intent 2,B 则始终保留 Intent 1、Plan 1 和原目的地。

看一次具体快照,比只看 GO/WAIT 更清楚

片内约 9.47 s,对应原生日志 10.8333 s,关键状态如下:

观察项 出口车 A 来车 B
行程 已完成短程任务 仍在执行原行程
意图与计划 Intent 1、Plan 1 Intent 1、Plan 1
Admitted 1,持有仍在保护范围内 0,未获准进入
Inside 1 0
Eligible 本表重点为已有占位 1,具备申请资格
当前实际结果 接近零速,车身未清空 Stopping 1,允许距离约 0.535 m
冲突来源 当前普通车辆 冲突阻挡指向 A

B 的灯色和申请资格已经满足,拒绝发生在共享空间冲突上。A 即使 Completed,也继续提供有向身份和实际 Body 观察;任务结束后不再假定它将主动前移,原有清空保护仍能保留。

绿灯出口被普通车辆占用时的状态
图 7|第三段约九点四七秒的状态关系。来车 Eligible 为一、Admitted 为零,出口车已完成却仍 Inside。实际阻挡来自普通车占位,静态出口标志为零。

同一快照中,StaticExitBlocked=0,DownstreamBlockerRoute=-1,DownstreamDepth=0。它们把原因限定得很清楚:这一刻阻止 B 的是普通车冲突持有,静态出口分支和下游深层阻挡链没有在本例触发。

源码另有出口慢车与真实驾驶阻挡链观察,使用实际速度、前车和驾驶冲突阻挡关系,以及当前和此前路口身份。它要区分真实实体拥堵和两辆车互相等待许可的关系,后者不能仅因为都有 WAIT 就递归当作下游车流堵住。本片实际覆盖的是普通车辆占用造成的冲突保留。

外部给出口车新任务,来车仍等自己的条件解除

场景在 B 稳定等待约两秒后,给 A 新目标,让出口条件发生变化。这个刺激来自脚本;B 的准入仍由原生观察决定。A 接到任务并不等于它已经离开,控制器不能在发令那一刻就把真实车身删掉。

两个脚本事件相隔 2.0000 s,按左闭右开区间筛选的日志样本首尾跨度则是 1.9667 s。物理记录每秒三十个样本,末端释放样本被排除,出现一个采样间隔的差别很正常。这里的“约两秒”描述稳定等待;精确核对时,事件间隔和样本首尾跨度仍要分开。

本例保留原任务的是来车 B。A 明确接到了新的驶离目标,B 则一直等待原行程的通行条件。两个不同的任务变化共同构成了这段场景。

直行和右转共用出口,先保留已经进入的移动

第二段两车交叉后去不同方向,第四段让它们最终进入同一东向出口。B 从西侧直行,A 从南侧右转。即使二者在入口附近看似互不占道,后面的经过路径仍会汇到一起。

视频 4|直行与右转共享出口,约 35.27 秒。B 较早接近并先获得通行,A 在入口外等候,然后沿原曲线路线右转进入同一东向车道。这里不是两条平行车道缩成一条。

片内约 12.60 s 的观察中,B 已在路口或出口移动,Admitted 和 Inside 都为一。A 具备申请资格,却尚未准入,仍在入口外,Stopping 为一,允许推进距离约 0.640 m。A 的冲突阻挡指向本轮 B,而不是一个不明来源的暂停标记。

A 入口低速等待约 2.17 s,统计条件仍是 Stopping 且实测速率低于 0.1 m/s。B 约 29.20 s 完成,A 约 33.17 s 完成。两车目的地特意错开,避免先完成的车又在后车终点前形成另一个堵塞场景。

如果只检查目标车道这一瞬间有没有车,A 可能看到空出口就申请成功,而 B 正沿已获准路径驶向那里。保护完整剩余移动,才能把“现在不在那儿,但这个已提交动作会经过那儿”纳入冲突检查。已有持有保留,新的冲突请求等待,这是本片共享出口的核心。

这次先行来自 B 较早接近并获得的许可。片中没有持续车流,无法据此判断拉链交替、公平等待或反饥饿,也没有提供法定主次路体系的完整验收。当前能核对的是这两条路线的到达关系、冲突和实际清空。

多车施工绕行:目标位置空着,中途也可能冲突

上一篇已经说明单车绕行要检查相邻车道、后车接近、返回几何和碰撞。多车一起到达施工区以后,还需要把别的车已经提交的横移动作算进去。它们未必已经进入相邻车道,但可能正准备使用同一片空间。

视频 5|三车依次绕过施工并返回,约 48.07 秒。观察后车如何保留跟随约束,以及绕出和返回为何分开发生。每车最终保留原意图,经历两次换道提交;计数增加不是车尾已经释放的时刻。

场景沿原有平直走廊放置单车道施工物,保留合法相邻同向车道。A、B、C 依次接近;画面中的绿色停放车属于美术背景,没有第四份受测 NPC 任务。

车辆 首次观察到绕出提交 首次观察到返回提交 首次确认到达
A 约 1.83 s 约 10.87 s 约 28.47 s
B 约 14.33 s 约 22.63 s 约 37.53 s
C 约 26.13 s 约 34.07 s 约 45.97 s

各车最终 Intent 都为一,绕出后 Plan 为二,返回后为三,换道提交计数为二。这里只按同一段录像比较这些实例。时点表示采样首次观察到新 Plan 或计数变化,不能拿来给横移完成和内部预约释放打时间戳。

后车仍然要看普通前车。远处登记了施工物,不表示可以看穿近处排队车、提前抢旁边的绕行空间。只有显式静态障碍会提出可选绕行,普通前车即使停止也不改类别。这个约束保留了排队语义,没有把施工区变成每辆车各自寻找最短超车线的竞争。

当前动作从实际车位到本次换道点

当前动作请求由实际 Body 位置起步,终点是这次 lane-waypoint,并补上车身相对道路高度的偏移,使用当前 Body 包围盒。它保护完整的这一段局部动作,绕出和返回各有一次独立请求。

返回几何预览只说明后续路线允许回来,不代表已经得到未来交通许可。真正返回时,仍要检查当前车尾是否越过障碍、当前实体和已提交动作是否冲突,以及世界扫掠是否通过。当前实现还保留已通过障碍的历史约束,防止目的地车道对齐过早把车横回障碍旁。

这与路口的“完整剩余经过路径”需要分开理解。路口保护车辆穿过控制区域的剩余移动;这里保护的是实际起点到当前换道点的动作。两处都要检查完整对象,但“完整”的对象并不是整个余下行程。

为什么不能只检查两个端点

假设起点和目标点都没有其他车,中间横移段却穿过一辆车的车头。端点检查会通过,实际执行仍会接触。已有动作也存在同样问题:另一辆车还没到冲突点,当前身体不在那里,只检查实际车体也会漏掉未来交叉。

准入因此分别检查候选动作对实际实体,以及候选动作对已经提交的动作。候选自身无效、同一车辆已经有未释放动作、中途与实体相交,或与其他持有动作交叉,都会拒绝。没有把“目标车道当前看起来空”直接当作通过条件。

完整当前换道动作的平移和转动包络
图 8|计划几何需要覆盖当前动作中途。平移包络和方向转换包络一起参与空间检查,再与实体及已提交动作比较;图中包络不是任意侧滑或轮胎动力学的可达性证明。

平移段按运动方向构造车身扫过的有向包络。方向变化还要考虑绕路线原点的车角运动,不能假定局部包围盒原点永远等于车身几何中心。实现把转角分成至多五度的小段,分别求区间端点姿态的角点范围。

只包住这些端点,仍可能漏掉端点之间的弧线鼓出。为此按车角半径加入弓高余量:

margin = cornerRadius × (1 − cos(deltaAngle / 2))

角度以弧度参与余弦。该余量扩张区间端点包络,让相应的旋转弧线被包含。随后先比较高度区间,再用平面分离轴判断包络是否相交。高度分离的对象不会仅因俯视图重叠就自动冲突。

这里保护的是计划路径与车身几何。实际车辆仍可能受速度、附着、接触和侧滑影响,所以它不证明任意高速、任意轮胎都能精确落在这个包络内。世界扫掠、物理限速和后续实测反馈仍然需要保留。

从候选到占用,释放要等实际车尾

动作记录不能在刚看见候选时就建立,否则每个失败尝试都可能留下虚假的占位。也不能等实际碰到别人时才登记,那时后来的车辆已经可能拿到了冲突路线。

当前流程先预检查候选完整动作,累积空隙稳定时间;连续通过 0.6 s 后,提交入口再次调用 CanAdmit。复核通过,再检查行程版本和游标,提交正式换道路线、生成新 Plan,然后记录这份动作对应的 Intent 和 Plan。

换道准入、计划提交与持有的生命周期
图 9|当前同步入口里的写入顺序。提交前复核空间,先接受正式计划,再保存动作身份;入口没有为第二步登记失败配置通用计划回滚。版本替换和实体退出走各自清理。

空隙稳定表达的是过去连续观察通过,提交前复核表达的是现在仍能通过。二者各有作用:没有稳定观察,条件在边缘变化时容易频繁尝试;只保留稳定计时,又可能在最后一轮空间变化后仍提交旧候选。

计划提交接口核对当前 Intent、Plan、Cursor 和候选条目,限定候选能修改哪一版路线。顺序是先接受 RoadJourney 的新计划,再把动作与提交后的 Intent、Plan 记入占用表,后面的 Observe 用这份身份核对车辆。前置复核减少了登记失败的机会,但这两个写入没有被包装成通用回滚:若后面的动作登记返回失败,入口不会自动撤销已经接受的计划。这里说明的是当前同步调用的实际顺序,不把它扩展成任意失败下都原子成功或原子撤销。

中心到达换道点,车尾可能还没过去

如果车辆中心刚到动作终点就释放,后面的车可能立即进入车尾仍占据的空间。道路计划已经消费中间目标,也不等于这段空间完全清空;路线进度和车身占用的结束条件需要分开。

Observe 把实测位置与动作终点之差投影到动作轴上,另查侧向偏差。下面的位置和尺寸统一使用厘米,actionDirection 是水平单位方向:

alongPast = dot(actualPosition − actionEnd, actionDirection)
sideError = abs(cross(actualPosition − actionEnd, actionDirection).z)
release = alongPast > rearExtent + 50 cm && sideError < 150 cm

rearExtent 来自动作记录中的本车局部后向边界,动作方向取起终点的水平单位方向。严格超过后向尺寸加五十厘米,并且侧向误差小于一百五十厘米,才删除这条持有。这里只描述当前局部直段的释放政策,不把中心投影扩展为任意弯曲车辆轮廓的精确清空算法。

车中心到点与实际车尾清空的区别
图 10|中心到终点时仍持有,实际轴向与侧向条件同时满足后才释放。绕出和返回各有独立动作,示意中不为单条持有编造精确释放秒数。

冻结单测明确构造过这组差别:动作已提交,中心到终点时记录仍存在;继续观察到车尾越过边界后记录才删除。另有中途实体阻挡、交叉动作拒绝、禁止同车嵌套申请、意图计划替换,以及实体从名册消失后的清理断言。这些是对机制边界的合成测试。

当前没有“等够若干秒就强制释放”的超时通路。车迟迟没走完,记录也不能仅为提高通行率自行消失。相反,Intent 或 Plan 已替换、宿主退出名册时,会清掉已经放弃的动作,以免形成幽灵持有。

清计划并不会清物理车身。旧动作因版本变化释放以后,车若仍停在原位置,下一次实体观察依然会阻挡别的候选。这让“放弃旧计划”与“场地已经空了”保持各自含义。

可以并行的是当前被判定不冲突的动作

第五段的聚合动作占用计数最高为二。它说明当前场景允许两份被几何判断为不冲突的动作同时存在,不是一辆车完成整趟绕行并到达以后,下一辆才获得出发资格。

录像记录只保存聚合计数,没有逐个动作对象的内部释放事件。因此,A 的换道计数变化、B 开始绕出和总计数下降,需要各按自己的含义读取。逐车车尾规则由源码和单测支持;实录能核对整体动作与最大并存数量,无法确定每条持有的精确释放时刻。

这也解释了为什么不需要把所有车辆全局串行化。互相冲突的动作等待,空间分离的动作可以并存;规则只对当前已知实体和计划有效。更大规模中的扫描成本、调度公平与稳定性,还需要下一阶段另测。

清障以后,原队列怎样继续

第六段把任务延续放回一个容易看懂的场景:三车在全路幅障碍前停住,外部移除障碍以后,它们继续各自原来的行程。这里关闭绕行,也不开路口信号,让读者只观察队列和实际前方空间。

视频 6|原队列停稳,清障后继续到达,约 23.73 秒。障碍由场景关闭碰撞并隐藏;车辆没有重建实体,也没有重新接受目的地。观察前方空间解除后,原车怎样恢复。

片内约 7.20 s,三车进入稳定停车区间;约 9.20 s 移除外部路障,形成约两秒全队停稳。随后恢复行驶的仍是片内正式采样期间的同一车辆和同一司机,Goal 与 Intent 保持不变。

障碍解除首先改变当前可用空间,队首获得新的推进量;后车还要结合前车实际位置和允许量重新计算。它们不是在同一时刻被脚本一齐写入非零速度。前车动起来、间距展开、后车继续,仍经过普通驾驶与四轮物理执行。

脚本只控制外部障碍,没有施工人员 AI,也没有 NPC 搬走路障。这个边界让环境刺激和运行时响应可以分开检查:刺激发生一次,后续各车能否继续,由任务、交通约束和物理结果决定。

三车终点不同,到达顺序也与队首顺序不同:C 约 20.97 s 先完成,A 约 21.10 s,B 约 21.67 s。通过同一个障碍,只完成了各自行程中的一段;后续还要走到自己的目的地。

等待保留的是任务,解除的是当前约束

如果停车时直接结束任务,清障后就需要业务层再次 Start。若把限制写回目的地,障碍解除以后也可能只剩一个临时停止点,原来的任务意图已经丢失。当前实现把原目的地留在行程里,停车限制由本步控制包和停等历史保存,因此恢复只需重新得到合法的推进条件。

这段也可以检查清理是否过度。车辆没有重新生成、换司机或瞬移到新起点,轮和传动状态仍属于同一车体。等待结束只恢复正常控制,不重置整个车辆运行时。先前积累的真实位置仍然决定当前游标与前视目标。

本片的两秒是场景观察到的停稳窗口,不是队列约定的统一等待时间。只要障碍还在、前车空间不足或包不匹配,车辆就不能因为时间到了而强行前进。这和第三段“出口车接到新任务,来车仍等实际清空”使用的是同一个分工。

到达必须由物理后的连续观察确认

路口清空、换道动作清空和任务到达分别有结束条件。进入目标附近,前车也许已经完成,但当前车是否停稳还要看自己的物理结果。目标速度为零只是一项请求,不能直接触发下车、对话或下一业务任务。

本轮交通宿主启用实测到达。行程先在末段建立 ArrivalPending,再由 PostPhysics 的实际姿态和完整速度累计保持时间。要求由宿主配置,没有把所有调用行程对象的入口都改成同一种默认模式。

较紧的进入线与稍宽的保持线

水平距离严格小于 1.5 m、完整实测速率严格小于 0.2 m/s,且高度差小于 2.5 m 时,行程进入到达等待,目标速度置零。随后允许车体在水平距离小于 1.8 m 的范围内制动收敛。越过较紧进入线的一点回弹,不会立即再次给油。

保持阶段仍检查高度;超出保持范围或高度不符,就清掉到达等待和时间。它与交通停等 Hold 分开保存:障碍前刹住不代表已经靠近业务目的地,到达等待也不能拿红灯停车历史来充数。

物理后观察还要求实际输入有效、时间步满足 0 < dt <= 0.1 s,行程阶段仍是 JoiningRoad 或 Driving,并且存在本轮待回读提交、NPC 模式正常、宿主没有故障。经过这些门以后,再检查:

horizontalDistance < 1.8 m
heightDifference < 2.5 m
length(actualVelocityMps) < 0.05 m/s
qualifyingPostPhysicsTime >= 0.5 s

任一条件失败,连续保持时间归零。这里累计的是满足条件的物理后更新时间,不是发出制动命令后的墙钟时间,也不是片头已经播放了多久。本轮三十赫兹采样下约半秒对应十五个更新间隔,但浮点累计和观察起点仍会影响首次成立在哪一步,不能承诺固定第十五帧完成。

停车候选、物理回读与完成保持
图 11|目标附近先进入停车准备,PostPhysics 连续低速成立后才确认完成。Completed 以后的显示保持计数可能归零,不应只读最后一行推断之前没有保持。

最后一行 Hold 为零,不代表没有停稳确认

查看证据时需要取第一次 Completed 的样本,该样本中的 ArrivalHold 约为 0.5 s。后续观察已经不再处于待确认阶段,显示保持计数会归零。若只读取最终 result 或最后一行,很容易把正常的完成后清理误认为“没有执行半秒保持”。

和换道计数一样,最终值未必保留过程。核对 ArrivalHold 时,要看它何时建立、在哪个阶段累计,以及完成后怎样清理。影片的“已到达”字幕跟随完成结果,首次完成样本才保留这次保持通过的证据。

全部最终目标的平面误差约为 0.61—1.41 m,处于当前接受范围内。这里测量的是平面误差,不是三维总误差,也不是零误差或厘米级泊车。车身最终朝向、车位几何和任意道路精确停放,还需要单独的停车任务约束。

Completed 也不会使车身失去物理意义。第三段出口车已经给出了例子:任务可以结束,车还在共享空间里。每一种结束结果只处理自己拥有的状态,其他观察者继续按实际车身判断。

源机制、适配策略和本轮证据怎样对应

这轮参考成熟开放世界引擎的车辆智能、路口控制和驾驶辅助机制:交通预更新整理路口与前车观察,入口出口和车道语义决定冲突,接近辅助先限制允许速度,低层驾驶再生成控制。车身清空与任务目标分开,符合条件的驾驶员才能提交命令。

参考代码还包含更丰富的驾驶人格、主次路语义、替代出口、尾随上下文和分帧更新。本轮保留与当前场景有关的职责、输入和阶段,没有声称所有原分支逐行等价。配图整理这些职责和状态之间的关系,内部审稿材料另保留对应源码定位。

内容 本轮怎样使用 来源与范围
路口预更新、前车与任务分工 先观察,再给任务允许量,执行后读回实际结果 参考机制的职责关系
有向入口出口与冲突持有 使用原路网身份和剩余经过计划,保留已进入车的清空责任 原生机制的当前子集,未覆盖全部法规与公平策略
UE 阶段桥接 全队 PrePhysics、单车实际物理提交、PostPhysics 回读 目标引擎接入
中心间隔与有限传播 当前七米参数和最多八轮跨节点传播 项目策略,不能当成所有车型通用常量
停等与额外制动 距离、建议速度和历史共同限制推进 当前物理宿主保护政策
局部换道持有 记录完整当前动作,版本校验,实测车尾释放 本项目的同步局部占用实现
控制矩形、临时灯头、场景刺激 选择原图局部范围,构造明确可观察条件 展示与适配,不是全城永久交通部署

合成测试、实车记录和视频检查分别回答问题

历史相关选择回归中,271 项成功,零失败、零未运行、零带警告成功。它已经包含控制包身份、信号与车身清空、完整动作持有这三项专项测试,不能写成二百七十一再加三。选择范围包括有关控制、路口、行程、恢复、感知、轮力和关系的测试,并非全项目测试总量。

这些测试用合成记录检查边界,例如内部观察过期时拒绝、已越线的车不因红灯丢掉清空持有、动作中心到终点仍不释放。它们不经过完整渲染物理场景。六段原图实车补上真实车身、轮力、驾驶接受、连续运动和最终任务完成的检查。

本轮使用的冻结 DLL 与前述历史回归一致。最终补录主要调整选址、实体灯头和镜头,没有因此修改原生代码或重新编译。写作使用冻结源码和当轮记录,没有重新运行 UE;文件哈希核对、全片解码和浏览器播放只能证明材料身份与媒体可读,不能算作又跑了一轮运行时测试。

片段 时长与受测车数 实录核对内容 结论边界
红绿灯排队 39.27 s,三车 交叉方向先行,两车停稳排队后按原任务放行 没有黄灯、全城配时或跨相位清空专项实录
无信号交叉 27.20 s,两车 不同出口的冲突等待与续行 没有任意方向持续流公平性结论
绿灯出口受阻 35.33 s,两车 普通车已完成仍占位,来车有资格但未获准 静态出口标志为零,没有深阻挡链实测
共享出口 35.27 s,两车 直行已进入,右转在入口外等待 不是缩道、拉链规则或恒定法定优先
施工绕行 48.07 s,三车 每车分别绕出返回,当前不冲突动作可以并存 没有逐条内部持有的精确释放时间日志
清障续行 23.73 s,三车 原队列停稳,移除外部障碍后原意图继续 不是施工人员行为或自动业务重新派单

六片共 3133 帧,以 15 fps 编码,总时长约 208.8667 s。逐片帧数为 589、408、530、529、721、356,由帧数计算的总时长与各片两位小数相加可能有舍入差别。原生物理采样为 30 Hz,每两个连续源样本取一张真实画面,保持仿真时长,片内没有跳切或加速。

全部受测车辆完成各自最终任务,记录接触计数为零。这只覆盖这些有界输入。十五辆次不是十五车同场,也不是同一批车连续跨越所有场景;车内原生驾驶资格不代表已展示完整司机动画。录制速度和输出帧率都不作为实时性能结论。记录中受测车辆为 NPC 模式;即使报告保留 Replay 标志,也不表示它们在照抄预先编好的油门和转向序列。脚本负责启动任务与改变外部条件,驾驶命令仍由行程和交通策略生成。

所有播放器时间以首录帧映射,近似为原生时间减 1.3667 s。画面粒度约为十五分之一秒,物理记录粒度为三十分之一秒,因此本文时间均按“约”解释;首次观察到状态,也不等于函数内部精确提交的 CPU 时点。

源码、测试、实车与媒体的证据范围
图 12|不同材料支持不同结论。历史选择回归验证状态,六段局部实车验证当前执行,媒体检查验证可读性。持续街区、混流与规模耐久另列为后续工作。

从局部共享空间走向持续街区

当前可以观察到:每车保留自己的目的地,信号和冲突决定入口,队列把前车真实允许量传向后车,局部动作持有保护换道经过,实际车尾与物理停稳分别结束对应阶段。这条链已经能够解释录像里为什么停、保留了什么、条件解除后怎样继续。

接到持续街区以后,还要处理常态人口的生成回收、更多车辆与行人的混合任务、观察者移动、远近表示切换,以及扫描和更新预算。更长时间下的拥堵传播、公平性和性能稳定,也都需要新的输入与记录。不能把局部零接触直接放大成全城无碰撞。

下一阶段会围绕“让城市持续运行”继续展开。这里先把局部共享空间的状态边界收住:任务不因为等待被丢掉,许可不越过实际占位,计划变化不偷用旧包,结束状态也不让仍在场的车身消失。后续规模增加时,才能继续追查每辆车正在等什么,而不只是看到一片停住的模型。

AI 协作复盘

本篇先读取交接、逐车时间线和冻结源码,再检查六段最终录像及关键帧。AI 用于梳理全队与单车更新顺序、整理队列修复的输入输出、核对动作持有和物理清空条件,并绘制机制图。写作没有改工程、扩功能或重录,也没有把历史回归写成这次新跑的测试。

最需要避免的是凭名字猜机制。绿灯并不等于来车已获准,出口被占也不等于静态探针触发;换道计数增加不是车尾已经释放,任务 Completed 更不等于实体退出。第三段的实际字段、第五段的源码和合成断言分别约束这些说法,不能只从画面相似就串成同一种“等待状态”。

时间与数量也单独核对:原生日志先映射到片内时间,事件间隔与采样跨度分开;六个独立会话只合计辆次,不合计并发;三项专项已经包含在历史选择回归中。内部资料保留定位与映射,正文围绕这些状态如何影响实际驾驶展开。

仍需继续验收的内容留在边界里:未逐条记录的动作释放时点不补猜测,没有展示的深阻挡链和公平调度不写成实测,正在开发的持续街区能力也不放进本轮完成清单。图用于解释因果,真实结果仍回到对应录像和同轮物理记录。

发表评论

了解 AI Native Game Development 的更多信息

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

继续阅读