剧情要求角色到一辆接应车旁集合。角色需要绕过围挡,在车旁减速停稳,再向剧情报告完成。途中若收到等待指令,原目的应当保留;收到改道指令,则应放弃旧路线。另一个场景里,平民因附近的燃烧物逃离,途中受到冲击倒地,站起来后还要继续远离威胁。任务意图需要跨越这些执行变化持续保存,同时在失败或取消时正确结束。
上一篇《城市模拟的 UE5 运行时实现|行人生成、回收复用与局部避让》讨论人物如何生成、保留、退休和复用。本篇讨论人物生成之后的任务执行,结合接应、剧情指令、逃离恢复和失败改派说明实现。人物仍由世界持有,场景对象提供目标和物理环境;本文讨论任务意图、移动执行和身体控制各自保存的状态,以及它们之间的结果交接。
以下场景来自已有 UE5 原型,说明以交付时的源码快照、运行记录和连续实机录像为准,不包含后续工作区改动。已经实现的是有界指令父任务、原生移动与路径请求、有限逃离策略,以及物理倒地后的安全恢复交接。完整多态任务树、环境感知、伤害系统和所有移动行为不在本轮验收范围内。起身部分使用明确标注的姿态平滑过渡占位,尚无真实起身动作。
接应任务中的目的与执行
接应片段展示一次正常执行:角色绕过围挡,在车旁停稳,移动完成后上层任务才结束。车体是场景中的真实静态对象,角色前往其接近位置,本片不展示上车。
视频 1:前往接应对象,约 11.53 秒。连续真实 PIE 画面,对象位置与围挡参与实际查询;角色到车旁停稳后,移动层返回 Arrived,指令父任务随后完成。没有移动目标追踪或进车流程。
目的需要比一条路径保存得更久
“去接应对象旁”由父任务持续保存,从等待路径返回到转向、绕障和接近目标,始终是同一任务目的。路径只是完成这一目的的一份执行结果,可能因为新障碍或身体位置变化而失效。若将目的等同于当前路径数组,取消一次查询就会丢掉业务目标;若让路径执行器自己决定替代对象,剧情又会失去失败后的决策权。
父任务和移动执行分别保存状态。指令父任务持有来源实体、任务会话、目的位置、指令计数、失败计数和当前业务状态。下层移动持有位置、朝向、目标、路径点、当前拐点、查询代次、运动状态及阻塞计时。两边都出现 Goal,是为了在消费指令时核对当前执行是否仍属于这段目的,并非让两个系统独立选择目标。
动画和物理也有自己的职责。骨骼表现读取当前动作片段和时间,显示起步、行走、停步与转身;正常位移仍由原生执行器提交。进入倒地以后,身体位置改由物理反馈提供,移动暂时失去写入资格。上层目的是否继续保留,由任务生命周期决定,不由当前正在播放哪段动画推断。
| 状态或记录 | 持有者 | 跨更新保存什么 | 结束或改变的依据 |
|---|---|---|---|
| 接应指令 | 指令父任务 | 来源、会话、目标、指令与失败记录 | 完成、取消、控制资格失效或上层改道 |
| 移动任务 | 人物的移动执行层 | 目标、实际位置、路径、拐点和运动状态 | 到达、明确失败、取消或新的目标请求 |
| 路径请求 | 导航服务与宿主请求记录 | 查询句柄、请求代次、待处理结果 | 返回、取消、服务失效或任务代次变化 |
| 安全响应 | 事件消费者形成的父任务 | 威胁来源、标签、安全点和响应状态 | 安全到达、失败、取消或来源失效 |
| 身体恢复 | 独立恢复叶 | 恢复会话、物理年龄、稳定时间和姿态进度 | 安全过渡结束;不直接决定业务任务是否恢复 |
| 动作表现 | 姿态评估与显示代理 | 当前片段、显式采样时间和混合状态 | 原生动作状态更新或物理恢复接管 |
文章目录
任务来源与会话标识
目标车确定目的位置,任务来源标识指令的发起者。当前指令入口创建一个世界持有的逻辑来源对象,在人物主任务槽中登记任务类型、来源、脚本命令和会话标签。会话内的等待、继续与改道沿用同一父任务身份,不因一次新的路径查询就重新开始业务会话。
处理指令前会核对主任务槽是否仍匹配这份记录:所属任务树和优先级位置、任务类型、任务拥有者、脚本标记、命令类型和会话标签都参与判断。父任务还必须处于活动生命周期,移动层目标也需要与父任务目标一致。拥有一个人物句柄并不等于取得修改其当前任务的许可。
当前入口限制了可以共存的关系。已有其他来源的活动或待处理主任务时,不能直接安装这个父任务;人物正在占座、有车辆意图或进出车交互时,也拒绝开始普通接应移动。取消时只关闭匹配的父任务槽,移动子任务还要符合该入口的拥有条件。它保留外部任务的状态,不通过清空整棵任务树来恢复本入口的控制。
当前指令运行时生成会话标签,并显式记录任务来源;它尚未实现完整脚本虚拟机或全局任务世代管理。跨宿主的全局命令排序、网络复制、任意层级任务克隆和脚本资源生命周期都还有各自工作。启动入口也不是覆盖所有宿主副作用的通用事务;阅读失败返回时,仍需区分外层事件取消与父任务安装所在的具体调用边界。
场景对象怎样变成路径目标
角色前往车旁,并不等于将车体中心直接交给移动。车体中心可能位于碰撞体内部,也不适合角色站立。当前接应入口接收一个实际场景对象和局部接近偏移,在发令时取得对象变换,将偏移转换成世界目标。
goalWorld = objectTransform.TransformPosition(approachOffsetLocal)
对象变换包含位置、旋转与缩放,不能只在世界坐标上加一个固定向量。正式场景里,车辆资源的原始前向为局部 +Y,接近偏移为 (-200, 0, 0) cm。这表示按该对象的局部坐标寻找车旁位置,不是把 -X 解释成所有车体统一的后方。交付中车辆保持静止,发令时记录的变换用于本次目标解析。
请求入口接收 Actor,移动层保存解析后的世界位置。车辆随后移动,并不会因此自动更新这个目标。若要持续跟随移动目标,还需要目标失效检查、采样或预测策略、重新请求节奏,以及到达条件如何变化的定义。本片的验收范围限于静态对象旁的接近和停步,不包含追踪或进车。
目标有效,路径仍可能无法建立
外层入口先检查目标对象是否存在、是否属于当前世界、是否正在销毁,以及偏移和人物索引是否有效。通过这些检查,只说明可以尝试建立目的;目标投影、路径连接和身体能否通过仍在后续执行。
下层 RequestGoal 增加任务的查询代次,保存新目标,清空旧路径与拐点,归零本次运动和阻塞记录,进入等待查询状态。它还撤掉此前的定时站立与命令等待,重置当前动作状态。新请求建立新的移动执行,Goal、路径和进度需要一并更新。
宿主另外保存正在排队的查询句柄。AbortQuery 通过服务取消句柄并移除宿主记录;任务代次则保护结果回到移动层时的身份检查。两者分别覆盖队列和消费者。取消服务请求不意味着所有可能已经形成的返回结果都已从世界消失,代次不匹配的旧结果仍须在接受入口拒绝。
下面是这一边界的中性化摘要,省略索引、来源与有限值检查,不是可直接替代完整函数的实现:
replaceGoal(newGoal):
cancelOutstandingQuery()
generation += 1
goal = newGoal
clearPathAndProgress()
clearCommandAndTimedHolds()
needsPath = true
acceptPath(reply):
if reply.generation != generation or !pathPending:
return StaleReply
pathPending = false
if reply.failed or reply.points.empty or endpointOutsideTolerance:
movement = Failed
else:
installPath(reply.points)
movement = Walking
路径末点与实际到达采用不同判定
接受路径时,当前实现用 pathEnd.Equals(goal, 15) 比较路径末点与目标:XYZ 各分量差值的绝对值都不能超过 15 cm。它是逐分量容差,对应一个轴对齐范围。最终到达则计算人物到 Goal 的三维距离,要求不超过 15 cm。两项检查数值相同,但对应的几何范围不同。
路径通过验收还没有结束任务。角色需要实际消费路径、完成末端位置检查,并在启用动作配置时完成停步阶段,移动层才发布 Arrived,父任务随后消费该结果。比如路径末点在各轴容差内,并不允许角色从更远位置直接宣布完成;播放了停步动作,也不等于世界位置已经到达目标。
接应片对应记录中,最终目标为 (10930.00, 14050.00, 25.00) cm,最终位置约为 (10930.83, 14046.32, 25.00) cm。两者不同,符合当前位置容差的含义。该案例父任务会话为 1,指令计数为 1,失败反馈为 0,记录还确认了动作停步先于原生到达结果。这里的坐标和计数只描述该次运行,不是通用接应参数。
一次更新怎样连接任务、移动与结果
当前示例通过 UE 宿主推进原生运行时。场景 Actor 提供对象、物理世界和更新入口,人物身份与任务槽仍保存在世界实体上,路径和运动状态保存在原生执行层。这里没有使用 UE 行为树替代父任务,也没有通过引擎 NavigationSystem 或 Recast 作为当前查询权威;原生服务负责已接入的表面投影和路径请求。完整导航生成不是本文主题,本文只沿它与任务之间的请求边界展开。
更新先后决定哪些结果可被消费
本入口每次 AdvanceWalk 先处理身体恢复,再处理指令父任务与安全响应,随后协调人物关系,决定移动是否仍有资格执行。接下来处理转向和实际运动,运行导航服务并提交需要的新查询,然后再次协调状态,让安全响应和指令父任务消费本轮结果。
父任务在运动前后都有检查机会,作用不同:前面的处理维护已有意图与资格,后面的处理读取刚形成的到达或失败。上层任务只在相应检查阶段消费结果,移动接口返回成功的时刻与父任务结束的时刻需要分别记录。尤其倒地发生后,恢复处理和关系协调需要先撤回移动的位置写入资格。
导航服务入口也有明确先后:先处理既有请求和可取得结果,再排入本轮新请求。新请求不会在提交调用内立刻以内联回调覆盖当前状态。返回结果仍需经 AcceptPath 检查任务代次和待查询标志。该队列支持请求取消;后台线程和其他异步调度方式不在当前实现范围内。
暂停运动时,先协调位置写入资格
关系协调检查移动槽是否仍属于预期任务,还检查更高优先级移动、外部主任务、人物任务抑制、车辆关系及身体恢复状态。任一条件阻止执行,移动进入受暂停状态,速度归零,位置从逻辑实体的当前变换读取。首次进入暂停时取消旧请求并增加代次;此前正在等查询的任务保留重新请求需求。
恢复资格时,再从逻辑实体读实际朝向、重置动作状态,并请求重规划。这样路径起点来自人物现在所处的位置,而不是暂停前的数组索引。人物因物理滚动改变位置后,旧路线可以保持原目的,却不能仍假定起点未变。
外部主任务并非一律排斥当前移动。当前指令父任务和安全响应父任务如果通过自己的完整身份检查,被视为这个移动的合法上层;其他活动主任务仍会暂停它。这项例外需要核对任务类型、来源和会话,不能仅凭“有一个活动主任务”就全面解除抑制。
记录中的 LastChildGeneration 用于反映最近观察到的移动代次,它不是所有命令都必须提交的独立令牌。实际控制资格还依赖父任务身份、移动任务归属及目标一致性。把诊断序号写成一个已经覆盖所有请求的全局版本检查,会扩大当前实现;后续若统一命令仲裁,还需要重新审视这些接口。
剧情要求等待、继续和改道时分别改变什么
接下来的片段展示同一任务会话中的指令变化:等待期间保持位置,收到 Continue 后恢复移动,再按 Redirect 指令前往备用车。
视频 2:剧情控制等待、继续与改道,15.40 秒。指令来自同一父任务会话;外部演示在等待约 3.5 秒后发送继续,运行时等待本身没有自动倒计时。对象均静止,改道取新的对象变换快照。
Hold 保留目标,Continue 解除本项等待
Hold 只在父任务正在前往目标且仍具备控制资格时接受。下层 RequestCommandHold 将命令等待设为真,速度和转向建议清零,要求下次重新评价转向。它不增加查询代次,不改写 Goal,也不清掉路径。父任务转为 WaitingForSignal,并记录这一条已接受指令。
人物因此可以在保留路径的同时停止实际位移。已经排队的路径结果仍可能返回并通过身份检查,但命令等待继续阻止执行;路径查询成功不会解除命令等待。等待状态的退出条件保存在指令层,而不是从移动速度是否再次大于零推断。
Continue 要求同一父任务仍在 WaitingForSignal。通过控制资格检查后,只解除命令等待,父任务回到 Travelling。它不调用 RequestGoal 重启移动,不借继续指令改变原查询代次。环境如果已经改变,后续实际运动仍会经过场景检查和已有重规划策略;保留路径不等于跳过新障碍。
交付记录中,命令等待覆盖约 3.4667 秒的采样区间,根位置漂移为 0 cm。继续前后查询代次保持,随后发生实际移动。演示发令时刻与逐帧采样、字幕切换存在离散取样差异,因此正文将外部等待称为“约 3.5 秒”,不把小数记录扩展为运行时自动倒计时。
Redirect 替换执行目标,父任务会话不重启
Redirect 接受新的对象接近位置后,先取消宿主保存的旧查询句柄,再更新父任务 Goal,调用新的 RequestGoal。移动代次增加,路径和进展清空,旧命令等待与定时站立撤回;随后重新激活本入口拥有的移动槽。父任务仍在当前会话,只增加指令计数,转回 Travelling。
这一分工让“同一剧情要求中途改道”与“另一个剧情任务重新接管人物”有不同记录。前者会话不变而查询代次改变,后者涉及父任务来源与会话的重新建立。仅比较最后的世界目标无法区分这两种情况,日志需要同时保留业务身份与执行身份。
旧返回结果即使晚于新指令返回,也没有资格安装旧路径。接受入口将其任务代次与当前移动代次核对,不匹配便拒绝。这保护的是路径结果消费;它不意味着动画通知、场景回调和所有未来业务事件已经统一带上相同令牌。本文只对已执行的路径请求链作出该结论。
| 操作 | 父任务会话 | 目标与路径 | 查询代次 | 结束本次停止的条件 |
|---|---|---|---|---|
| 命令等待 Hold | 保留 | 保留 | 保留 | 同一有效任务收到 Continue |
| 明确继续 Continue | 保留 | 继续使用原目的 | 不因继续本身增加 | 其他约束也允许移动 |
| 改道 Redirect | 保留 | 新目标、清旧路径 | 新目标产生新代次 | 新查询与运动检查通过 |
| 定时站立 | 不替换上层意图 | 保留 | 不因站立本身增加 | 运行时时间达到截止 |
| 身体或关系暂停 | 意图仍有效时保留 | 目标保留,旧执行需重核 | 旧请求失效,恢复重规划 | 身体与关系重新允许执行 |
| 取消 Cancel | 本意图结束 | 不再执行原路线 | 取消使旧结果失效 | 新任务须另行授权 |
取消只处理当前入口拥有的任务
Cancel 的目标是结束这段指令。当前实现对已完成、已取消或尚未开始的父任务不重复处理;需要取消时,检查移动槽是否仍为该入口拥有的任务、目标是否仍与父任务一致,符合条件才撤回其查询和移动。主任务槽也通过完整归属判断后才终止。
外部任务接管移动槽后,旧父任务结束时保留该槽的现有状态。自动回收、车辆关系与剧情指令可能在不同阶段读取人物,局部关闭必须尊重当前归属。当前代码采用的是有界匹配规则,不是任意父子层级都能自动追溯的统一引用系统。
“父任务被取消”“移动暂时无法执行”和“父任务已完成”还需要分别记录。取消不应被伪装成一次成功到达,暂时暂停也不应让上层以为整项业务已经结束。后面的倒地补充片会直接检查这条边界:身体可以恢复,已经取消的旧目的不能因此恢复。
同样是停止,命令、身体与任务关系各有解除条件
角色停住时,画面不足以辨认原因。可能是剧情等待继续、有限时间的站立、通路阻塞、更高优先级任务接管,或真实身体已经倒地。将这些原因压成同一个 Paused,再用任何一个恢复事件把它清掉,会让本应继续保持的约束失效。
当前命令等待、定时站立和身体暂停分别保存状态。移动阶段先判断身体与关系暂停;这时原生行走不推进。若身体允许,才检查命令等待、定时站立和移动生命周期。满足这些停止条件时,启用动作配置的角色可以执行停步尾段,但不提交平移。
一个恢复事件只解除它负责的约束
剧情 Continue 解除命令等待,不清除正在生效的恢复任务。恢复叶结束身体控制,也不代替剧情发出 Continue。定时站立到期只清自己的计时状态,不能取消另一个来源的任务。每种恢复都有相应所有者与条件,才能解释多个停止原因叠加后的结果。
这项分离已经体现在更新结构中,但应与测试范围区分。现有主片分别展示命令等待和身体恢复,补充片检查取消与站立阻塞;它们并不构成任意等待、接管、车辆关系和身体状态排列组合的完整证明。文章保留它们的独立状态,是为了说明实现怎样减少互相覆盖,而不是声称所有组合都已验收。
等待不应隐含成一个定时任务
参考机制中可以区分有限计时的暂停和持续站立。当前接应命令使用显式 Hold/Continue 实现业务等待,原生有限站立入口则保存截止时间。两者有相近的可见表现,但上层承诺不同:前者等待另一个决策者,后者由运行时时钟结束。
自动检查还覆盖了未发送 Continue 的情况:模拟推进 10 秒,人物位置和查询代次保持,父任务仍在 WaitingForSignal。该检查能够支持“等待没有自动在片中时长后结束”;它不是无限时长形式化证明,也不验证所有外部任务都能正确发出继续。
写场景脚本时可以安排某个时间点发送继续,用来让视频完整展示后续阶段;这属于演示的输入计划。若把这个发令时间写进运行时等待规则,行为就会从“等待上层指令”变成“过一段时间自动走”。同样,失败后停顿多久再改派,反映的是上层决策输入,而不是移动层的隐藏备用目标策略。
动作数据怎样约束路径执行与实际步长
移动执行并不只做路径点插值。角色需要先面对新的前进方向,再进入起步与循环;接近目标时,还要在允许的剩余距离内完成减速停步。动作资源可以提供形状与节奏,但地面、人物身体和障碍仍决定本步是否能移动。
动作资源提供数据,原生运行时选择采样时刻
本轮在独立角色资源里接入奔跑起步、循环和停步片段,并沿用同一骨架上的待机与转向片段。资源通过隔离的 FBX 路径进入示例,不因引用奔跑动作就接入整套外部玩法框架。加载时核对网格、骨架、片段数量和采样曲线持续时间,配置不匹配时拒绝使用。
导入处理按每个动作的持续时间划分 120 个区间,保存包含首末点的 121 个样本。前进片段的数据取根骨相对首帧的水平偏移长度,并以历史最大值约束为单调通道;它不是逐帧位移总长度的积分。对当前主要向前的资源,这条通道作为距离参数使用;若未来动作包含回退或复杂环绕,必须重新审查这一提取方式是否仍适合。
转向通道读取相邻根旋转的最短角差并累计,再归一化到片段总转角。运行时读取归一化进度,乘以本次目标与当前朝向之间的实际角差。这样同一个转向资源可以驱动当前所需角度,不把片段名里的 90 或 180 直接当作每次逻辑旋转量。
每个角色保存自己的动作阶段、片段时间、循环时间和动作序号。角色进入逃离时切换该角色的奔跑配置与期望速度,不修改共享资源中的运行状态。其他旧入口和正式人口消费者保留各自策略,本文的奔跑接入不能被理解成所有人口已经统一迁移到同一动作系统。
起步与停步从距离曲线取得本步预算
设采样距离曲线为 d(t),缩放为 scale,期望速度为 speed。起步和行进停步会先按播放率取得候选片段时间,再比较对应距离增量与允许的速度预算:
playRate = clamp(speed / referenceSpeed, 0.1, 2.5)
candidateTime = min(duration, clipTime + dt × playRate)
curveDistance = max(0, d(candidateTime) − d(clipTime)) × scale
distanceBudget = max(speed, 0) × dt
若曲线增量超过预算,实现通过有界二分缩短本步片段时间,当前二分次数为 12。目标是让姿态采样与对应根距离一起受速度约束,避免动作中的加速尖峰一次给出过大的步长。这里调整的是原生动作时间;不是先让动画播放器移动骨骼根,再把碰撞后的结果倒推成任务进度。
循环移动按期望速度生成距离预算,随后用实际接受的路程推进循环采样:
cycleTime = fmod(cycleTime + acceptedDistance / referenceSpeed, cycleDuration)
acceptedDistance 来自实际通过检查的位置变化。等待期间即使还有待机或收步姿态,循环行走时间也不因此获得虚假的路程输入。另一方面,起步或停步阶段在提出候选运动时已经可能更新动作时间;本实现没有承诺所有被拒绝短步都会将全部动作状态原子回滚。阻挡后调用停止表现,位置与循环路程则只使用检查通过的实际位移。
当前奔跑参考速度约为 594.324 cm/s,任务期望速度为 330 cm/s,名义播放率约为 0.555。这是资源曲线与任务速度之间的比例,并非每次物理步都以该比例完成位移;转向、终点和障碍仍可缩短或阻止本步。用教学时间步 dt=1/60 s,速度预算为 5.5 cm,也不代表视频按 60 fps 实时执行。
到目标停步和被命令停下采用不同处理
行进停步在剩余路线距离进入动作的停步距离范围后启动,并将剩余距离与资源停步距离之比保存为本次缩放。运动继续消费经过缩放的距离增量,直到路径末端。角色进入目标距离容差后还需要完成停止姿态,才将状态改为 Arrived。
命令等待或阻挡则不能继续消费整段停步根距离。前方已经有围挡,若仍按动作所需路程前进,就可能通过“自然停步”穿过障碍。当前 Hold 选择停步尾段,只完成姿态过渡,不提交平移。停步尾段起点取资源曲线剩余距离接近零的位置,本次奔跑配置约为 0.7 秒;它不是命令等待时长。
大幅转向时,原生动作状态可以进入转向片段,依据采样角度进度更新朝向,此阶段返回定向状态而不提出平移。运动中的较小方向调整另用受限转向速率处理;如果尚未对准允许范围,本步也可能只转向。处于转向时,阻塞或无进展计时需要区别处理,避免把有意的原地定向当成寻路卡死。
地面与身体检查仍然位于提交位置之前
动作模块提出的方向与距离先生成候选位置,执行器再做表面投影、可通过性和人物身体净空检查。当前入口还使用有限角度的局部方向候选,前方存在静态阻挡时,要求侧移后仍有继续前进的局部通路。第一步和切换拐点后的第一步也经过同一过滤,避免每次重新取得路径就向障碍内推进少量距离。
若本步失败,人物位置不前进,阻塞计时和下一次方向重评继续推进。若通过,才保存实际位置、实测速度和已走路程。对于物理查询,较长经过时间会拆成最大 0.05 秒的运动子步,不能把渲染长帧直接当成一条未经中间检查的大位移。
姿态评估端使用显式片段时间,忽略引擎根运动,并从普通片段姿态中移除执行根变换,避免同一根距离被重复应用。起步、循环、停步之间仍有显示混合;这个混合解决骨骼连续性,不具备修改原生任务位置和到达结果的权限。
燃烧物出现后,安全位置由谁选择
逃离场景从普通行走开始。应观察燃烧物出现后角色改变行为并奔跑,第二次冲击使身体真实倒地,恢复后继续逃离,最后在安全位置结束。片中片中已标明姿态过渡占位,真实 Chaos 倒地与起身表现分别说明。
视频 3:逃离威胁并在倒地后接续,约 11.53 秒。燃烧物和冲击由场景脚本触发,安全终点由运行时选择。父任务在身体中断前后保持同一响应,起身采用已标注的平滑姿态过渡占位。它不证明完整环境感知、火焰伤害或真实起身动作。
威胁点与前往目标的含义相反
普通接应给出希望接近的目标,逃离首先给出希望远离的威胁位置。若把威胁点直接送入同一前往接口,角色会向危险靠近。当前入口先调用独立逃离策略,根据人物当前位置、威胁和安全距离选择一个可达终点,再把该终点交给事件响应与移动执行。
这项策略位于任务目的和导航服务之间。场景脚本只提供威胁及安全距离,不直接指定视频里的安全点 B;策略负责检查候选落点的有效性与可达性。它仍是一个有界的原型策略,不是完整参考逃离路径搜索。选出终点之后,当前响应持续保留这个终点,没有在每个更新重新搜索所有候选,也没有追踪一个不断移动的威胁重新选点。
先过滤候选,再比较距离与路线成本
起始方向取人物远离威胁的水平单位向量。两点水平重合、无法取得方向时,策略使用显式默认前向。它避免零向量导致候选全部重合,却不是从周围威胁分布推导的最优安全方向。
away = normalizeXY(start − threat)
candidate = start + rotateAroundUp(away, angle) × candidateLength
当前角度为 0°、±30°、±60°、±90°,长度为 safeDistance+100、safeDistance+300、safeDistance+550 cm。理论上形成至多 21 个候选组合;这里的数量来自当前枚举范围,不代表导航采样已经覆盖全部逃离方向或任意距离。参数改变后候选数也会改变。
候选投影到原生人行表面,要求投影前后水平距离不超过 50 cm。这一限制避免投影将无效候选移至距离过远的可走区域。随后检查终点空间,以及投影点离威胁是否严格大于安全距离加 30 cm。只有通过这些条件,才搜索从当前起点到候选的完整路径,每个候选搜索上限为 2048 次多边形扩展。
这项候选预检是直接的有界同步查询;移动任务正式执行时仍会沿自己的请求服务重新取得路径。前者为选点提供可达性依据,后者建立本次执行的路径身份与结果。二者使用同一原生查询能力,但选点预检不替代执行期查询,新障碍仍可能使正式执行失败。
候选合格之后,按下式评分:
routeLength = sum(lengthXY(path[i] − path[i−1]))
score = distanceXY(projectedCandidate, threat) − 0.05 × routeLength
其中 projectedCandidate 是已经投影到人行表面、并通过空间、安全距离和完整路径检查的候选位置,不是投影前的理论点。
距离和路径长度都使用厘米,0.05 是当前策略的无量纲权重。它偏向远离威胁,同时轻度惩罚绕路;不是运行时成本的毫秒估计,也没有把障碍危险度、可见性或人群拥挤统一换算成这个分数。只在严格高于已有最佳分数时替换,相同分数保留先遇到的候选,枚举顺序因此属于当前策略行为。
无合格候选时,当前入口明确返回未接受,不使用未经检查的替代终点;这时尚未建立该次安全响应。它与逃离已经开始、执行过程中返回 Failed 是不同阶段,诊断和上层决策需要分别处理。入口预检失败也不能一概说成父任务已经报告过一次失败,因为父任务可能根本没有启动。
安全距离在哪些环节被检查
候选策略要求终点离威胁严格超过 safe+30 cm;通用事件排队入口还检查所提供目标超过 safe+15 cm;最终安全响应在移动 Arrived 后,再检查实际人物与当前威胁位置的水平距离严格大于 safe。这些检查属于不同接口和时间,不应挑其中一个数当成全链统一容差。
本次调用从候选策略进入,所以预检路径先经过较严格的候选余量,再进入通用事件接口。最终判定使用人物实际位置,避免将“选点当时安全”当成“现在人物已经安全”。威胁来源对象的位置可以被响应观察,但现有选点不会因此持续追踪新威胁方向;本片威胁保持静止,不能从最终检查推导出移动威胁处理已经完整。
主片配置安全距离 650 cm,最终记录离威胁约 1017.25 cm。它证明该运行到达安全距离之外,并不说明角色总会选择离威胁最远的世界位置。仍有资格、可达性和枚举上限约束,可见画面中的方向也不能替代这些查询记录。
事件怎样建立一次持续的逃离任务
选出可达终点后,还要通过人物的事件与任务桥建立上层响应。当前场景入口创建一个逻辑威胁来源,登记事件类型、来源句柄、事件标签、威胁位置、选出终点及安全距离。可见火焰和冲击解释玩家为什么看到这一行为,但并不构成完整的环境感知或伤害传播。
事件入队与响应开始不是同一步
Queue 先检查响应是否已经处于排队或活动状态、来源与标签是否有效、输入位置和安全距离是否有效,以及人物是否拥有预期的基础移动任务。已有不兼容事件到任务映射时拒绝覆盖;缺少映射时,为这个有限场景登记对应关系。成功后,响应处于 Queued,并向人物智能层提交带来源和标签的事件。
父任务开始执行时,Process 还要核对当前事件是否就是这份事件、主任务槽是否已经形成匹配的 Pending 任务、任务抑制与车辆关系是否允许,以及是否存在其他阻止执行的活动任务。任务类型相同仍不足以取得执行权,还须满足本次事件和任务身份的检查。
通过这些检查后,安全响应保存被替换的日常 Goal 供观察,取消旧查询和旧移动,安装选出的安全目标,再激活匹配的移动槽与主任务槽。此时响应转为 Active,保存初始移动代次,StartCount 增加。保留旧日常目标用于诊断,没有把它登记为身体恢复后自动返回的目的。
同一个逃离父任务可以经历不同的下层查询
主片记录的 StartCount 在逃离开始后始终为 1。这个值说明该安全响应没有因倒地恢复再次启动,不意味着移动查询代次也必须保持不变。身体位置变化后,旧查询应失效,新路径需要从实际位置建立。父任务继续,执行可以更换,父任务身份与查询代次分别记录意图和执行。
安全响应在 Active 阶段继续核对主任务槽是否仍属于自己,以及来源是否仍可用。移动 Arrived 时检查安全距离并结束,移动 Failed 或 Cancelled 则分别关闭自己的任务状态与事件。清理事件使用类型、来源和标签匹配,不能为了结束一项响应把所有人物事件清掉。
恢复期间没有自动重新排队威胁事件,也没有在恢复完成时重新执行一次初始逃离选择。保留下来的目的仍是最初选出的安全终点,查询起点则由身体实际位置更新。这是本文所说的“接续逃离”;它没有暗示原版所有逃离任务的克隆、持续重评和终止策略已经复现。
身体倒地时,原来的目的怎样保留
第二次冲击发生在奔跑期间。角色进入真实 Chaos 刚体模拟,骨骼身体获得物理速度与冲量。与此同时,独立恢复任务占据当前工程约定的高优先级移动槽,普通行走被暂停,已有查询被取消并失效。上层安全响应仍在人物主任务槽中保持活动身份。
身体接管前也要检查任务和资源
倒地入口要求有效游戏世界、有效人物索引和动作配置,拒绝已经退休、已被其他关系暂停或正在恢复的角色。人物有占座、车辆意图和进出车交互时也不接受这条普通倒地路径。这里没有覆盖车内布娃娃或所有身体控制模式。
入口取得人物显示骨骼、物理资产与恢复任务槽,开始新的恢复会话。启用骨骼物理后还要检查骨盆是否已进入模拟;若失败,撤掉模拟和本入口恢复槽,恢复状态退回非活动。是否进入模拟以骨盆的实际状态为准,不能只检查 SetSimulatePhysics 是否已调用。
模拟开始时以人物当前朝向和速度构造初始线速度,再向身体施加冲量。冲量范围在入口受限,本轮示例使用的大小属于场景配置,不等价于完整爆炸伤害计算。可见爆炸特效、冲量输入和真实刚体运动在证据上分别说明,不把有特效当成完成了伤害系统。
恢复优先级当前为 90,属于工程槽位约定;它只表示此入口需要暂时压过普通移动,不是原机制通用优先级数字。恢复槽标签包含会话信息,完成时核对人物与会话,避免旧恢复过程清除后来占用该槽的任务。
物理反馈提供当前逻辑锚点
倒地后,骨盆和上身可能已经离开原路线位置。恢复处理读取骨盆世界位置,向下寻找带可行走语义且法线允许的地面,再通过原生表面投影确认可恢复位置。物理阶段的逻辑锚点根据该反馈更新,普通行走保持暂停,不能同时按旧路径推进坐标。
线速度判断使用骨盆速度与一半上脊柱速度的较大值,角速度读取骨盆。这个选择避免手脚约束的高频抖动让全身永远无法被认为稳定;它仍是当前身体与物理资产上的工程判据,不是对所有角色姿态都成立的物理定理。
若地面支撑不可用,当前检查不会据此强制恢复站立。站立位置还要经过表面与空间检查,身体和任务的位置交接因此依赖可验证的场景输入。真实刚体模拟中的滚动和末端抖动不服从普通行走的最大步长约束,不能拿物理位移与正常移动步长直接比较后宣布速度异常。
稳定、地面和站立净空共同决定恢复资格
当前恢复状态保存物理年龄、连续稳定时间、姿态混合进度、恢复位置及会话。物理阶段至少持续 1.3 秒,核心线速度不超过 35 cm/s,骨盆角速度不超过 90°/s,并在有支撑情况下连续稳定至少 0.35 秒,才进入站立空间判断。速度、角速度和时间使用不同单位,不能把单一超时当成身体已经安全。
settled = supported
&& finite(linearSpeed) && 0 <= linearSpeed <= 35 cm/s
&& finite(angularSpeed) && 0 <= angularSpeed <= 90 degrees/s
stableTime = settled ? stableTime + dt : 0
canLeaveRagdoll = age >= 1.3 s && stableTime >= 0.35 s
上式是当前判据的数学摘要,省略阶段与输入检查,不是直接可编译的 C++。任何一次失稳都重新累计连续稳定时间。稳定满足而站立空间不足,状态进入 WaitingForSpace,等待环境允许;它不会因为此前已经稳定足够久就越过空间检查。
站立检查从支撑地面构造身体胶囊,检查该体积是否被场景占用,还检查其他未退休行人的水平身体范围。当前逻辑邻人分支没有在这一检查中实现完整多楼层语义分类,图示不应被理解成复杂垂直城市的通用恢复解算。正式片段和补充片均在当前平坦场地内验收。
姿态恢复完成后,移动也不能直接沿旧路径继续
进入姿态过渡时,需要将刚体状态换成动画可以稳定评估的姿态输入。当前实现先保存骨骼的物理姿态快照和世界变换,在目标恢复位置重新建立显示组件基准,将快照根变换换算到新组件坐标系,然后关闭刚体模拟并混合回待机。
平滑过渡占位具体完成了什么
这条路径解决物理姿态与动画姿态的连续交接,持续时间为 0.8 秒。用归一化进度 a 计算平滑动画权重,剩余权重保留倒地快照:
a = clamp(blendTime / transitionDuration, 0, 1)
animationWeight = a² × (3 − 2a)
snapshotWeight = 1 − animationWeight
这种混合可以减少瞬间切姿态的跳变,但它没有选择真实起身动作,也没有从躺姿分析起身脚掌支撑和身体轨迹。倒地物理与起身动画依赖不同资源,需分别验收;这里的平滑混合仍属于起身占位。
快照还需要空间换算。物理期间骨骼组件可能已经相对原代理发生位移与旋转;若把这一姿态直接放回新的组件位置而不校正根变换,人物会产生额外空间偏移。当前在关闭物理和重新附着后计算旧组件到新组件的相对变换,将其纳入快照根;后续姿态混合读取校正后的快照。这个处理服务于当前骨架和资源,不宣称任意倒地姿态都具有完成的起身质量。
过渡期间仍然检查安全条件
进入 Blending 后,恢复不再只依赖最初一次地面和净空结果。支撑或空间变为不安全时,BlendTime 暂停增长,已经形成的姿态混合进度保留。这里没有自动重新打开物理模拟,也没有完整回滚到新的布娃娃状态;应称为冻结姿态过渡,而非重新进行一次物理倒地。
安全条件重新允许后,过渡继续推进。达到时长后将状态标为 Complete,清理姿态快照和匹配的恢复任务槽。它清除的是自己持有的约束,不替其他任务做取消或完成,也不向剧情承诺角色已经到达原目的地。
从身体实际位置请求新路径
恢复结束后,关系协调发现人物从暂停转回可执行状态,读取当前实体位置和朝向,重置动作状态,并对保留目标请求重规划。目的仍是先前选出的安全位置;旧查询代次和旧起点已经不适合继续使用。
这与“重新触发逃离”有不同结果。重触发可能再次选择终点、重置响应计数或恢复错误的日常任务;当前路径让安全响应父任务继续存在,移动叶根据实际位置更换执行数据。StartCount 保持 1,恢复后的查询代次可以改变,正好分别说明业务意图与执行恢复。
原任务若已取消,重规划入口不会将其恢复为 Walking;身体仍可完成安全交接,人物则保持取消后的静止状态。任务已结束和身体尚待恢复可以同时成立。把两者统一标成一个“完成”会让取消时机无法解释,也可能在物理恢复回调中误装回旧指令。
历史补片检查了不能直接恢复的情况
补充片:倒地期间取消任务检查原目的已取消之后,身体恢复不会重新激活该任务。对应历史记录中,恢复后的观察持续约 2 秒,根位置漂移为 0 cm;这是该次工程边界检查,不能推广为所有任务取消接口都已通过同样验收。
补充片:站立空间被阻挡用实际碰撞横梁检查等待,清障后才允许恢复。记录中站立空间受阻等待约 2 秒,随后对仍有效的原目标重新规划。清障动作由场景脚本发出,不是恢复任务自动移除障碍。
这两段保留此前构建的原始证据,不与四段主片混称为同一次最终构建录制。两段补片分别补充取消与站立空间约束的反例。它们也使用已标注的姿态过渡占位,未提供新的真实起身资源。
目的地不可达时,失败怎样交回上层
最后一个主片展示不可达后的处理。通路被围挡封闭后,角色在有界重试后返回失败,再按上层新指令前往备用车。最终到达时,原围挡仍保留。
视频 4:不可达后的上层改派,18.40 秒。本例配置三次重规划重试,耗尽后下层明确 Failed,父任务记录一次失败并等待决策;另一次剧情指令指定备用车。围挡始终保留,不展示自动清障或无限自动选择其他目标。
局部等待、有界重规划与明确失败分别发生
前方一步被挡住时,局部运动可以先等待并请求重新评价方向。当前独立行走入口累计连续阻塞时间,也观察到拐点距离是否有实际改善。连续阻塞严格超过 1.5 秒,或无进展严格超过 2 秒时,可以进入重规划;处于转向阶段时保留相应计时例外。
重规划增加重试计数,未超上限时撤回旧路径,增加代次并重新请求。当前计数超过 3 后进入 Failed。片中“三次”对应本次项目策略,并不是参考引擎在所有不可达场景中的固定次数。查询入口若直接得到无完整路径、资源容量不足或其他明确失败,也可能提前失败,不必先重复三次局部阻塞。
因此,要区分“局部短步失败”和“移动任务已经失败”。前者还有本任务允许的等待与重试,后者需要向父任务返回结果。对一条持续封闭的通路,如果一直只记局部阻挡、不结束执行,上层就无法依据失败结果更换目标;如果每次身体轻微受阻都直接结束父任务,又会丢失执行层能够处理的恢复机会。
父任务只消费一次当前失败
指令父任务在活动过程中发现移动 Failed,保存失败原因、增加 FailureCount,并进入 AwaitingDecision。之后即使移动层仍保持 Failed,也不逐更新再次增加计数。判断依赖父任务已进入等待决策状态;这一去重依赖当前父任务状态机,不覆盖任意消息重发或分布式幂等处理。
if child.status == Failed and parent.status != AwaitingDecision:
parent.failureCount += 1
parent.lastFailure = child.reason
parent.status = AwaitingDecision
这段摘要假定前面的来源、会话和移动控制资格检查均已通过。缺少这些检查,只凭一个 Failed 值去修改父任务,可能把其他任务或旧执行的失败消费到当前会话里。具体拥有关系依照本入口的匹配条件,不在这段短伪代码里重复。
进入 AwaitingDecision 后,父任务继续保存业务来源和原目标,等待上层选择。它不创建备用车、不删除障碍、不由移动层清除失败状态并重新启用旧路径。这里保留的是失败后的决策上下文,不代表原移动仍有执行资格。
明确改派仍然沿同一指令会话执行
上层决定改去备用车时,Redirect 仍通过父任务的控制资格检查,取新的对象变换快照,取消旧请求并建立新的移动代次。父任务从 AwaitingDecision 回到 Travelling,保留原有失败计数,直到备用执行也实际完成或再次失败。
主片记录中父任务会话保持为 1,指令计数为 2,失败反馈为 1。等待决策约 1.4667 秒后,上层输入明确备用指令,新查询代次形成,最终在车 2 旁停稳。这个等待时间由场景发令计划形成,不是父任务倒计时到期就自动从一张备用清单选择下一项。
备用目标如果也不可达,下层仍能明确失败;本文没有用它证明完整任务重试树或多级派遣策略。当前例子只建立一条可核查的链:执行层返回不能完成,上层持有失败事实并改派,新的执行重新接受路径和运动检查。
参数要连同消费者、阶段和单位一起阅读
下面汇总公式和状态处理中使用的参数及其消费者。相同数值出现在不同接口,并不保证同样语义;相同“等待”又可能由秒、命令或任务关系解除。表中数值属于本次交付分支,不是对参考机制统一常量的声明。
| 参数或记录 | 本次值或条件 | 消费者与含义 |
|---|---|---|
| 场景对象接近偏移 | (-200, 0, 0) cm |
发令时对象局部坐标,经对象变换得到目标 |
| 路径末点接受 | 各坐标分量差不超过 15 cm | AcceptPath 使用的 FVector::Equals,不同于球形距离 |
| 最终到达范围 | 三维距离不超过 15 cm | 路径耗尽后的实际位置检查,启用动作时还需停步完成 |
| 动作曲线采样 | 120 区间、121 样本 | 导入时数据提取,不是运行帧率 |
| 奔跑参考速度 | 约 594.324 cm/s | 资源距离曲线与时长之比,用于采样率和循环距离匹配 |
| 逃离期望速度 | 330 cm/s | 当前角色的原生运动预算,非全人口统一速度 |
| 逃离安全距离 | 650 cm | 主片场景配置,候选与最终结果在各自接口另有余量 |
| 逃离候选投影容差 | 水平距离不超过 50 cm | 防止候选被投影到明显不同的位置 |
| 候选路径搜索上限 | 2048 多边形扩展 | 每个候选预检的有界工作,非正式请求服务的通用预算 |
| 逃离请求优先级 | 10 | 本入口向请求服务提交的项目优先级,非原版统一索引 |
| 运动子步上限 | 0.05 s | 实际运动拆步与场景检查,不代表逻辑、物理、视频同帧率 |
| 恢复移动槽优先级 | 90 | 当前独立恢复叶的关系约定 |
| 物理最短时间 | 1.3 s | 与稳定、支撑、净空共同判断,不是固定到点起身 |
| 连续稳定时间 | 至少 0.35 s | 失稳重置,物理反馈通过后才累计 |
| 核心速度/角速度 | 不超过 35 cm/s、90°/s | 物理稳定判据,线速度与角速度不能混用 |
| 姿态过渡时长 | 0.8 s | 已标注起身占位的混合时间,安全条件失效时冻结 |
| 当前重规划上限 | 允许三次,超过后失败 | 本次独立移动策略,非所有任务共用的参考规则 |
运行状态与资源配置也需要分别保存。每个角色保存动作进度、查询和恢复会话,资源资产保存可复用的曲线与片段引用。父任务来源、事件来源和恢复会话也不能合并成一个“当前控制者”,否则旧事件、迟到路径和恢复完成都可能误改新任务。
本篇没有通过接口命名推导未执行能力。比如脚本命令能够携带来源,不代表已有完整脚本 VM;任务槽可以表达 Pending 与 Active,不代表任意多态树都已移植;存在动作曲线和真实布娃娃,也不代表已完成完整运动网络和所有起身动画。范围说明保留在每个消费环节,避免读者将表中字段当作更大系统已经验收的证据。
验证应沿任务身份和实际执行分别检查
现有交付记录为构建成功,158 项回归中 73 项成功、85 项带既有警告成功,0 失败、0 未运行。告警仍保留,不能改写为“158 项全无警告”。测试项数也不表示视频场景数或原系统覆盖率。
原城市六角色独立运行验收通过,记录为 1494 帧、49.80 秒模拟时间,最大正常移动步长约 4.333 cm、最小间距约 63.386 cm,近/远转向检查工作数为 41/8。这是对应独立验收条件下的结果,不是四段主片里每次运动采样的统一上限,也不是游戏实时帧率与大人群性能。
录像负责可见过程,记录负责身份和门控
主片均为连续真实 PIE 录制,未剪接、变速或合成人物;发布编码为 1280×720、15 fps。字幕概括阶段,不能当成与状态改变完全同一时刻的事件日志。源码、状态采样和实际画面需要分层使用,才能解释“看起来停住”是否属于命令等待,“站起来了”是否意味着原意图仍有效。
| 主片 | 发布片长 | 对应记录中的主要结果 | 不能据此推出什么 |
|---|---|---|---|
| 前往接应对象 | 约 11.53 s | 同一人物,父任务会话 1,完成后停稳,失败反馈 0 | 不含上车和移动对象追踪 |
| 等待、继续与改道 | 15.40 s | 等待漂移 0 cm,继续保持代次,改道使旧查询失效,会话仍为 1 | 外部约 3.5 秒等待不等于运行时自动计时 |
| 逃离与倒地恢复 | 约 11.53 s | 运行时选点,StartCount=1,真实物理倒地,恢复后移动并安全结束 | 不含完整感知、伤害、完整逃离算法和真实起身动作 |
| 不可达后改派 | 18.40 s | 失败反馈 1,父任务等待新决定,明确改派后完成,围挡保持 | 不含自动清障和无限备用策略 |
判断一段接续是否正确,需要同时检查原人物句柄、父任务来源与会话、目标、查询代次、身体阶段、实际位置和完成结果。人物恢复移动只证明发生了运动,可能仍是错误任务;父任务计数正确也不能替代碰撞和场景画面。主片中这些观察分别保存,避免用一个总成功计数覆盖整个过程。
自动检查还包含持续等待无自动恢复、旧路径结果拒绝、失败只上报一次、外部任务槽保护、有限逃离候选及恢复期间父任务保持等反例。它们帮助核对接口语义;真实物理、角色可见性和姿态占位再通过对应场景记录说明。不同证据各自负责一层,不能把同一段源码、一次回归和一张截图累计成更强的“完整还原”结论。
原机制、工程适配与未覆盖范围
机制阅读提供了上层意图持有子任务、等待与取消分开、路径请求身份、失败由上层消费,以及倒地恢复后接续移动的依据。当前工程用原生 C++ 状态和任务槽承接这些关系,UE 提供场景对象、碰撞世界、骨骼动画和物理表现。本文的类名作中性化说明,具体原始源码锚点留在配套审稿材料中。
本次实现仍是有界入口:指令父任务只覆盖当前命令关系,逃离只搜索有限候选;威胁和冲击由脚本触发;起身是已说明的姿态过渡;地面与动作适配限于当前平坦场地。完整场景行为、脚本资源与任务克隆、复杂人群、网络复制、持续移动目标追踪和正式伤害链需要各自实现与验收。
四段主片使用当前交付的源码与构建指纹,两段历史补充片保留原录制版本。交付记录注明本轮未保存原城地图修改,并保留地图指纹供回查。本文整理只读材料并检查素材,没有再次构建项目、运行整套回归或录制新演示;文中测试数字均引用对应交付记录。
从一次接应继续扩展角色行为
当前实现将持续任务意图与可替换的执行状态分开保存。等待由明确指令解除,改道更换查询但保留业务会话;物理倒地暂时接管身体,恢复后只让仍有效的意图继续;下层无法完成时,父任务收到失败并等待决策。
这些关系为后续行为提供了接入位置。上层可以换成更完整的剧情或事件决策,导航服务可以继续扩展表面与特殊连接,表现层也可以替换真实起身动作。每次替换都需要重新检查交接边界:新实现是否保留当前来源,取消后还有谁可能回写,失败怎样结束,实际运动和姿态怎样对齐。
上述扩展仍待实现。当前交付限于可通过场景观察和源码核查的任务执行子集。玩家能看到接应、等待、逃离和恢复,工程侧则能沿来源、会话、查询、位置与结果解释这些过程。
AI 协作复盘
本次写作使用开发交付中的冻结源码、功能合同、事件与逐帧记录,先按场景梳理职责,再补调用顺序、公式和失败路径。AI 负责组织材料、制作机制图并核对媒体引用;已完成的工程功能没有在写作时重新修改,也没有重录演示。
材料整理中需要特别处理历史信息。安全响应底层接口仍能接收外部指定终点,部分旧注释也沿用这个说法;当前主片调用的是先由有限策略选点、再进入通用响应的入口。因此正文分别说明策略与事件接口,没有依据旧注释把主片写成“脚本给定安全点”,也没有反过来宣称所有响应入口都自动选点。
另一个容易混淆的地方是“动画只表现”。动画播放器确实不写逻辑位移,但原生执行层会消费从动作提取的根距离与转角曲线。正文把资源数据、原生预算、物理检查和姿态评估分开,保留这项关系;动作曲线对运动预算的约束,是解释起步、行进停步与立即停止所必需的实现细节。
核查时分别使用对应证据:同一个父任务可以使用新查询,真实倒地可以配合占位起身,源代码可以证明调用关系而不能单独证明画面质量。图中公式是当前消费者的说明,场景截图和视频是实际录制,二者分别标注,便于后续审稿按具体分支核对。












