机器人程序化动画(三):大型蜘蛛机器人的动画状态与战斗行为协调

光束连射(Focus)的请求发来时,巨型机器人还在走,六只脚里只有三只接地。协调器先下达停止指令,等足部接触和机体速度满足条件,再把请求交给执行器。开场实录里有一次请求在等待中就被取消,另一次则顺利开火。两次结束后,机器人都接着走了。后面的靶场实录里,它也会因为目标失效而停止连射,并留在原地。

文章目录
巨型机器人站定攻击的 UE 实录
本地 UE 实录。画面展示行走、站定和攻击之间的切换。是否已站稳由接触和速度记录确定。
站定攻击实录

前两篇讲了腿怎样支撑机体,以及动作怎样写到骨骼上。这一篇接着看开场实录里的光束连射。什么时候能开始攻击、头部怎么瞄准、发出去的光束是否命中,这三件事各有一套判定,也各有一份运行记录。

一次攻击如何从请求走到发射

行为协调器先判断能否接下攻击请求。接下后,它等机器人站稳,并在等待期间拒绝新的攻击请求。下文把请求从被接受到完成、取消或被更高优先级动作接管的这段时间称为「攻击占用」。站稳后,攻击执行器负责安排什么时候准备、什么时候开火、什么时候结束。动画姿态层则读取当前阶段,更新头部和武器等部件的骨骼姿态。

模块 负责什么
移动控制器 执行移动与转向指令,提供机体速度和足部接触状态
行为协调器 接受或拒绝请求,等待站稳,启动攻击并处理退出后的移动
目标观测 提供目标身份、生命状态、距离和相对朝向
攻击执行器 推进攻击阶段、触发发射,处理取消
动画姿态层 保留移动系统给出的机体和腿部姿态,再更新头部、武器等部件的骨骼

还在等站稳就取消时,执行实例尚未创建,撤掉待启动的请求即可。连射中取消则要让正在运行的执行器停下来,否则即使画面已经切回待机,后续发射仍可能照常触发。

动作状态、骨骼姿态和战斗结果

第二篇演示了核心展开,做法是采样 UE 动画序列(AnimSequence)OpenCore,取得核心骨骼相对头部的平移。这一篇的光束连射会根据目标方向实时计算头部旋转。两种做法写入的局部变换不同,写完后都要沿头部的骨骼层级往下合成,才能得到核心和武器在组件空间里的姿态。

以连射中的一帧为例,移动系统用机体变换和实测足底位置重建腿部姿态,攻击姿态层再在这个结果上更新头部。发射和伤害另有各自的事件。调试时可以分别看机体位置、骨骼姿态和伤害计数,它们可能在不同的更新点写入。

设机体在组件空间中的变换为 C_body,头部相对父骨的变换为 L_head。先做头部的局部变换,再通过父骨换算到组件空间,结果记作 C_body ∘ L_head。核心也用同样的顺序,接上自己的局部变换。机体在对齐目标时还会转动,所以每帧都要沿当前的父骨重算。如果沿用上一帧核心的组件变换,画面上可能出现机体已经转过去、核心却还留在原方向的情况。这里的公式说明父子骨骼如何合成,瞄准或后坐具体写在哪一步,要对照执行器和姿态层的实现。

机体父链、头部和核心的局部变换
左图是 Focus 连射原片选帧,引线标出机体、头部与武器、支撑腿。右图拆开三段骨骼变换,便于看清哪里写入局部旋转、哪里随父链更新。

在低头、抬头或炮管偏转时,武器指向和机体朝向可以不同。协调器看的是机体朝向,用它判断还要不要先转身。实际发射方向由发射器决定,需要结合发射采样和弹道记录来看。比如机体已经进入前方 45° 的启动范围,武器仍可能被前方障碍物挡住。反过来,炮口看着已经对准,协调器也可能因为距离或目标生命状态而拒绝启动。

同一帧可以记录的值 对应空间或时钟 该值能解释的问题
机体前向、足底接触、身体线速度和角速度 物理读回,世界空间 底座是否还在移动,能否交给攻击
Head 或 Core 的局部骨骼姿态 姿态评估,父骨局部空间 当前覆盖改变了哪一段骨骼链
当前攻击阶段 执行器时钟 动作在准备、发射还是收束
实际发射事件、伤害应用 发射会话和目标结果 哪一次发射发生,谁收到伤害

请求进入等待时,攻击入口已经被占用,头部还可以维持原来的姿态。攻击完成后,画面也可能留有一段混合过渡。查攻击为何没启动,先看请求和执行器日志。查姿态接缝,则看攻击阶段、父骨变换和混合权重。

攻击请求如何排队,结束后如何继续移动

从移动请求到攻击执行的交接路径
站定攻击实录中的交接路径。等待中的请求已经占用攻击入口,调用执行器后才进入攻击阶段。

协调器接受攻击请求时,会先记下当前的行走速度设定,再让移动控制器停下。实录里的设定值是 100 cm/s。等待中取消、攻击正常完成、执行中取消,三次都恢复了这个设定,随后也测到了机体重新移动。

源码还处理了攻击期间收到的新指令。收到行走(Walk),就把待恢复的速度改成配置的行走速度。收到停止(Stop),就把待恢复的速度设为零。机器人在攻击期间继续保持停止,等请求结束后才采用更新后的设定。站定录像里没有中途改令。

等待时为什么也会拒绝新的攻击

6.03 秒收到光束连射请求时,机器人还在减速。6.33 秒再请求榴弹发射,协调器以「已有攻击占用」(attack_owned)拒绝。11.33 秒重复请求光束连射时,同样被拒绝。两次拒绝后,原来的请求都还在,等待计时也继续。

日志里,attack_queued 表示请求已经入队,attack_started 表示站稳检查通过,攻击已交给执行器。第一次光束连射在等待期间取消。第二次完成了攻击,之后的榴弹发射则在启动约 0.60 秒后取消。前一种情况只需撤回请求,后两种还涉及执行器。

这几次恢复使用的都是原来的 100 cm/s 行走指令。第一次收到攻击请求时,机体实测速度约为 121.29 cm/s。站稳后速度已接近零。记录速度设定和实测速度,才能看清协调器恢复了什么,以及机体是否真的重新走起来。

等待取消、活动取消、完成和目标失效的退出策略
三个恢复 100 cm/s 的出口来自站定攻击案例。目标案例的进入请求和退出请求均为零。

等待请求和执行实例分别保存什么

协调器分别保存待启动的请求和正在执行的攻击。接单时,它记下动作名称、入队时刻、原有移动速度,以及启用目标检查时的目标身份。站稳后,待启动的请求转为执行中,实际的光束或榴弹实例仍由原有动作入口创建。准备到了第几帧、已经发了几次,这些由执行实例记录,协调器只需要知道哪一次攻击还占着入口。

这里的「排队」一次只等一条已经接受的请求。6.33 秒来的榴弹请求当场被拒,光束取消后也没有补发榴弹。11.33 秒又收到一次光束请求,原来的入队时刻照样保留。如果每次收到重复指令都重新计时,连续按同一个攻击键就可能让站稳计时一再从头算起。

协调器还会检查执行器是否已经在运行。即便协调器自己没有等待项,已有的光束、榴弹或激光执行实例也会挡住新的请求。这样能覆盖从其他入口已经启动攻击的情况。判断有没有空位时,先看谁在等待,再看谁已经在执行。只查一个队列长度会漏掉后者。

需要查的状态 谁记录 调试时能判断什么
请求何时入队 协调器 等待从什么时候算,后来的物理采样能否用于这次请求
目前能否接另一条攻击 协调器与已有执行实例 有请求在等待,或已有攻击正在运行
当前攻击到了哪一阶段 执行实例 处于准备、连射、收束中的哪一步
退出后给移动层什么值 协调器 续接原速度、采用新指令,还是保持停止

站稳后怎样启动攻击

站稳后,协调器先把请求标为执行中,再调用动作入口。这个入口也接收外部命令,因此调用期间要临时跳过协调器的入队检查,避免同一条命令又排一次队。调用返回后,如果对应的光束或榴弹执行实例没有创建,就记录启动失败、释放占用并保持停止。如果实例成功创建,则由执行器推进。正常完成时,协调器等实例结束后再恢复保留的行走速度。

攻击期间收到停止指令或取消

判断攻击期间发出的停止指令有没有生效,要对照发令时间和退出时给移动控制器的速度设定。单看最后速度为零,也可能是等待超时、目标失效,或者机器人本来就没有走。

站定案例里的榴弹攻击在 26.10 秒启动,26.70 秒取消。这 0.60 秒是从启动到取消的间隔。核对取消时,先查发射日志里还有没有新增发射,再分别追踪已发射物、已结算伤害和仍在播放的特效。现有记录没有逐一跟踪这些对象的清理时刻。

哪些退出会恢复行走

普通完成时,协调器把预先保存的行走速度交回移动控制器。主动取消通常也走这条路。如果取消时已有优先级更高的动作接管,则保持停止。其他零速度出口还包括重置、等待超时、派发失败和目标失效。这里描述的是源码中的退出规则。站定实录直接覆盖的是等待中取消、正常完成和执行中取消,目标实录直接覆盖的是越距与死亡。

退出时先记下这次结束的是哪种攻击,然后清除等待项、活动项、站稳计时和保存的目标。转向指令也归零,最后才写入退出后应采用的移动速度。下一次请求即使还瞄着同一个靶,也会用新采样重新等站稳。这个顺序能防止上一轮留下的计时或目标身份被下一轮误用。

主动取消时,已有动作入口负责收尾。目标失效也会走这个入口,区别在于协调器先记录目标为什么失效,再释放请求并发出取消。这里能用发射日志检查的是取消之后有没有新增发射。场上已经出现的弹丸和特效各自还要更新多久,需要到相应系统里看它们何时清理。

攻击前如何判定机体已站稳

机器人收到停止指令后,机体可能还在滑动或转动,摆动中的腿也需要时间落地。因此,协调器用物理模拟测得的足部接触状态和机体速度来决定何时启动攻击。

启动要求六足全部接地,机体线速度低于 0.1 cm/s,角速度低于 0.005 rad/s。移动控制器还必须报告当前姿态允许结束移动,将控制交给攻击。协调器只使用从攻击请求进入队列到本次检查之间采集的数据(含两端),距采样不得超过 0.10 秒。

上述条件须连续满足 0.25 秒。期间只要有一项不再满足,计时就重新开始。普通攻击请求从入队起最多等待 12 秒。

第二次光束连射排队期间的机体速度和接触足数
横轴表示协调器检查站稳条件的时刻。它检查的是物理模拟在此前采集的数据。

第一次收到光束连射请求时,只有三只脚接地,机体仍以约 121.29 cm/s 移动。此时协调器已发出停止指令。第二次请求的曲线显示,六足先全部接地,机体线速度随后才降到阈值以下。

下图记录了同一场测试中的三次攻击请求。两个红点分别表示榴弹发射请求被拒,以及重复的光束连射请求被拒。原来的请求继续等待。

移动中请求攻击的事件时间轴

游戏时钟 请求与结果 后续移动
6.03 → 7.03 s 第一次光束连射仍在等待,期间拒绝榴弹请求,随后取消 恢复 100 cm/s 的行走指令
11.03 → 13.90 → 19.43 s 第二次光束连射等到机体站稳后开始,随后正常完成 恢复 100 cm/s 的行走指令
24.03 → 26.10 → 26.70 s 榴弹发射等到机体站稳后开始,随后取消 恢复 100 cm/s 的行走指令

39.9 秒的实录只在开头发出一次行走指令。三次攻击请求结束后,协调器都恢复了原来的行走速度设定,随后测得机体再次移动。100 cm/s 是指令值,实际速度由物理模拟决定。取消后发射计时器、临时组件和特效如何清理,要查执行器的日志。

六足接地后到攻击启动前的速度变化
第二次光束连射开始前,六足已经接地,机体仍在减速。红点和绿点分别表示本次检查未通过和通过。

0.25 秒从什么时候开始算

站定攻击主实录中,协调器在约 13.63333 秒首次确认全部条件满足,在 13.90000 秒启动第二次光束连射。启动时使用的物理数据采集于约 13.86667 秒。这样可以分清采样、判定和启动各自发生的时间。

全部条件首次满足 ≈ 13.63333 s
攻击启动         ≈ 13.90000 s
条件持续满足     ≈  0.26667 s ≥ 0.25 s

本轮约每 1/30 秒检查一次。0.25 秒相当于 7.5 个检查间隔,所以到第八个间隔时,首次达到持续时间要求。若更新频率改变,所需检查次数会随之变化。

13.60 秒时六足已经接地,机体线速度仍约为 1.01 cm/s,是启动阈值的约十倍。角速度也单独检查。在后面的目标门控实录中,攻击启动时机体角速度约为 0.00329 rad/s(约 0.189°/s),已经低于 0.005 rad/s 的阈值,仍有少量转动。

检查项 调试时看什么
足部接地与移动控制状态 六足是否同时接地,移动控制器何时允许攻击开始
机体速度 发出停止指令后,机体是否还在滑动或转动
物理采样时间 是否使用请求入队后采集的数据,数据是否仍在 0.10 秒内
持续时间 是否有任何一次检查失败,让 0.25 秒重新计时

如果协调器反复读到同一份物理数据,这份数据在 0.10 秒后就会失效,无法单独满足 0.25 秒的持续时间要求。排查攻击迟迟未启动时,可以先确认采样时间是否在推进,再检查接地与速度。

停止指令、物理采样和持续计时

拿启动那一帧来算,13.90000 秒减去样本的 13.86667 秒,约为 0.03333 秒。这是单份物理样本从采集到检查经过的时间,用来和 0.10 秒比较。13.90000 秒减去首次通过检查的 13.63333 秒,约为 0.26667 秒。这是协调器连续观察到条件满足的时间,用来和 0.25 秒比较。同一条启动记录里两个差值都用到了当前时刻,但各自减去的是不同事件。调试时若只保存一个时间,就很难看出是物理数据更新太慢,还是站稳条件刚刚满足。

线速度和角速度取自物理模拟的机体速度向量,比较的是各自的长度。机体即使没有明显向前走,也可能横向滑动。角速度也可能来自不止一个轴。按向量长度检查,可以把这些运动纳入同一次判定。

用于启动的样本还要满足两个时间条件。采样时刻要在请求入队之后或恰好同一时刻,并且不能晚于当前检查。相对于当前检查,它最多只能早 0.10 秒。第一条排除上一段动作留下的站稳数据,第二条排除读数长期不更新的情况。即使采样里六足都接地,两种过期方式都足以让启动条件失败。

0.25 秒从第一次同时通过这些检查的时刻开始算。后续某一次如果丢了足部接触,或速度又升到阈值以上,先前的时间会清零。等下一次全部通过,再记新的起点。启用目标检查时,机体朝向也要先进入启动范围。六足已经接地、线速度已经降下来,仍可能因为正在转向而继续等待。

站稳条件持续满足 0.25 秒后,协调器会调用原动作入口。只有光束或榴弹实例实际创建,日志才记「攻击已启动」。如果一直没过站稳检查,就回看足部接触和速度。如果物理条件已经通过,却仍没有启动事件,就查动作入口有没有创建实例。两种故障在画面上可能一样,机器人都停在那里没开火。

光束连射(Focus)与榴弹发射(Rain)

Focus 是对单个目标的光束连射。 机器人先用头部武器跟住目标方向,完成准备后按间隔发射多次光束。在完整演示里发了八次。在注册靶案例中,目标在第 5 发后失效,剩余发射随即取消。

Rain 是由榴弹发射器执行的攻击。 机器人先抬起发射器,保持抬起姿态完成发射,再放下发射器。本文采用三发请求演示。视频用于观察炮架动作和发射过程,当前记录没有量化榴弹的爆炸范围或伤害。

下面的光束连射实录先演示一次主动取消,第二次请求才完整走完准备、连射与收束。

光束连射完整实录(Focus)
Focus 准备阶段 UE 原图
准备阶段,累计 0 发。同一机位可观察机体和支撑腿。
Focus 连射阶段 UE 原图
进入连射,累计 1 发。左侧蓝白束流是发射效果。
Focus 收束阶段 UE 原图
收束开始,累计 8 发。三张图均取自第二次完整 Focus。

八次发射的同轮读回

下面用的是独立光束实录的游戏时钟。第二次请求约在 5.033 s 进入准备,8.033 s 打出第一发,9.433 s 打出第八发。中间七个发射间隔都约为 0.20 s,首发到末发共 1.40 s。执行器在约 9.533 s 进入收束,到了 10.533 s 已经不再运行。阶段时间取自执行器的状态,发射时间取自累计发数发生变化的那一帧。完整八发这段没有启用目标门控,后面的靶场实录才会在第五发后退出。

Focus 阶段实录时钟 执行器阶段 姿态层读回 累计发射
5.833 s 准备 准备 0
8.033 s 连射 准备 1
9.433 s 连射 连射 8
9.533 s 收束 连射 8

首发那一帧,执行器已经进入连射,姿态层的读数还是准备,下一次更新才跟上。到收束开始时也能看到同样的先后顺序。所以要核对哪一帧开火,应查执行器阶段和累计发数。想知道那一帧骨骼是用哪种状态算出来的,则要看姿态层自己的读数。

Focus 的程序化头部瞄准

同配置复跑的 Focus 运行时骨骼读回
独立同配置复跑。白字由 UE 场景内的 TextRenderActor 绘制。面板记录程序化目标瞄准、子树范围和 AnimSequence 采样次数。

姿态层先从物理输入重建头部世界位置,用目标世界位置减去头部位置,再用 Actor 旋转的逆变换将方向转回组件空间。头部偏航限幅为 ±65°,俯仰限幅为 −18° 至 12°。这组角度控制头部骨骼,行为层的 45° 和 60° 则用于检查机体朝向。

d = inverseRotate(ActorRotation, TargetWorld − HeadWorld)
yaw = clamp(degrees(atan2(d.y, d.x)), −65, 65)
pitch = clamp(degrees(atan2(d.z, length(d.xy))), −18, 12)
qHeadLocal = inverse(qParentComponent) × qAimComponent

换相时保存上一份局部姿态,用 0.35 s 的平滑窗从这份姿态过渡到当前输出。令 u 为换相后时间除以 0.35 s 并限制在 [0, 1],混合权重为 u²(3 − 2u)。平移与缩放做线性混合,旋转经半球修正后做归一化线性插值。最后按父骨顺序重算头部子树的 20 根骨骼,子树外 67 根骨骼逐帧与支撑输入一致。

构造时会载入 OpenCore 资源,但这次完整 Focus 的准备、连射和收束记录中,AnimSequence 采样调用均为零。Core_01 随父骨转动,其位置仍由本帧父链决定。Focus 没有应用第二篇展示的核心展开平移轨道,本次源码中的炮管后坐位移也为零。连射画面中的束流属于发射表现层。

头部旋转怎样传到核心和武器

机体、头部和腿部的输入变换都在骨骼网格的组件空间。为了只在头部写入一个新旋转,姿态层先把头部子树里的各个变换换回父骨局部空间。比如核心先表示成「相对头部在哪里」。然后,它在组件空间求出瞄准旋转,再用头部父骨旋转的逆变换把结果换回局部空间。这样替换头部旋转时,核心和武器仍保留各自相对父骨的变换,供后续重新合成。

输出时从父骨向下合成。头部用更新后的局部旋转重新求组件变换,核心和武器再用各自的局部变换跟上父骨。这样即使核心相对头部的位置没有变化,它在画面里也会随着头部移动。要确认是否真的播放了展开动画,应查核心相对头部的平移,以及动画序列的实际采样调用。单看核心在镜头里的位置会把父骨转动也算进去。

阶段切换时,过渡从上一帧已经算好的局部姿态开始。过渡的目标仍然每帧重算,因此头部在混合期间也能继续跟着目标移动。0.35 秒规定的是骨骼从旧姿态转到新姿态要多久,发射仍按执行器的阶段时钟推进。于是同一帧完全可以已经发出了第一束光,骨骼还在从准备姿态过渡。两份记录要按各自的更新时间来读。

本地姿态记录对比了合成前后两份骨骼数据。头部子树内有 20 根骨骼参与重算,子树外的 67 根则逐帧保持输入。另一份技术读回直接从 UE 骨骼组件读取头部局部旋转和核心局部高度。前一种比较检查覆盖范围,后一种确认运行中的骨骼组件拿到了预期数据。

从 UE 骨骼组件里实际读到了什么

在 UE 里核对这条链时,可以同时看计算结果和组件里已经生效的值。前者能告诉你程序准备写哪些骨骼,后者能确认骨骼网格最终收到的结果。尤其是核心,最好记录它相对头部的位置和它在组件空间的位置。只留后一项,事后会很难判断变化是自己发生的,还是从头部传下来的。表中的读数就是从组件按这两套坐标取出来的。

技术读回图来自一次独立复跑。8.233 秒时,执行器处于连射阶段,已经记录两发。此时 UE 从骨骼组件读出的头部局部四元数约为 (0.02391, 0.05198, −0.41717, 0.90702),分量顺序是 XYZW。机体骨骼在组件空间的位置为 (0, 0, 0) cm,这是网格自己的坐标原点。整台机器人在关卡中的位置还要看 Actor 变换。

骨骼 相对父骨的位置(cm) 在骨骼网格组件中的位置(cm)
头部 Head_01 (0, 0.007, 156.852) (0, 0.007, 156.852)
核心 Core_01 (27.330, 0.000, 100.034) (25.107, −29.285, 253.108)
光束武器 Weapon_Laser_01 (540.268, −2.228, 147.549) (358.579, −421.758, 241.709)

以核心为例,相对头部的高度是 100.034 cm,而换到组件空间后,高度是 253.108 cm。两列的原点和坐标轴都不同。头部转动会让核心局部 X、Y、Z 三个分量一起影响最终位置,因此不能只把头部和核心的两个 Z 值相加。光束武器离父骨更远,同样的转角通常也会在武器上产生更显眼的位置变化。

按这一版的变换实现,子骨骼先用父骨的缩放调整局部偏移,再将偏移按父骨旋转转到组件空间,最后加上父骨位置。旋转则按父骨旋转乘以局部旋转来合成。源码将这两步写成位置和旋转的合成。

子骨骼组件位置 = 父骨组件位置 + rotate(父骨旋转, 父骨缩放 ⊙ 子骨骼局部位置)
子骨骼组件旋转 = 父骨组件旋转 × 子骨骼局部旋转

其中 ⊙ 表示逐分量相乘。这个例子还提供了一种排查顺序。画面里核心移动了,先查动画序列是否实际被采样,再比较核心局部平移和父骨旋转。局部数据保持、父骨在转时,组件空间的位置本来就会变化。这样可以在运行日志里把程序化瞄准和第二篇的核心展开区分开。

榴弹发射器的抬升、保持与回落

以下是独立榴弹发射实录。前文 26.10–26.70 s 的活动取消发生在站定攻击主实录。

榴弹发射实录(Rain)
Rain 实录选帧
观察发射器抬起、可见发射效果与降低。
光束连射与榴弹发射在独立实录中的执行阶段
方框按阶段顺序排列,宽度不代表时长。

榴弹发射沿用前面的头部瞄准,同时采样发射器骨骼 Weapon_Launcher_01 的局部轨道。准备时,按阶段经过的时间采样抬炮序列 RaiseHindCannon_01。发射时保持该序列末帧,收束时再按时间采样落炮序列 LowerHindCannon_01。三个阶段仍走同一套局部混合和父骨合成,因此头部可以继续瞄准,发射器也能独立抬起和落下。独立录像采用三发请求,现有记录没有标出每发榴弹的时刻。

机器人何时能朝目标开火

本地目标射程与启动、继续执行的朝向扇区
两个扇区以实测机体前向为基准。紫色点表示对齐前的目标样本,距离约 25.67 m,朝向偏差约 50°。

靶场登记了一个目标。距离取机体与靶标在水平面上的间距。朝向偏差则从机体正前方量到靶标方向,保留正负号,方便决定向哪边转。下面用点积与二维叉积说明这个角度怎么得到。角度正负按实录的坐标约定读取。

deltaXY = targetXY − bodyXY
range = length(deltaXY)
toTarget = normalize(deltaXY)
headingError = atan2(cross2(forwardXY, toTarget),
                     dot(forwardXY, toTarget))

本地场景的射程是 60 m。目标在机体前方 ±45° 内才可开始攻击。攻击开始后,允许目标偏到 ±60°,给转动留出余量。射程一直是 60 m,目标离开射程或死亡都会中止攻击。录像验证了越距和死亡两种退出,±60° 来自配置。

偏差超出启动门限时,协调器把转向交给移动控制器。本地转向请求上限为 1.5°/s,启用目标检查时,整段排队共用 30 秒超时,起点仍是入队时刻。下图展示首次 Focus 从进入对齐到实际启动的过程。

首次目标对齐的实测朝向和转向请求
上栏是目标相对机体前向的实测偏差绝对值,下栏记录转向请求。进入启动扇区后,物理稳定仍需要时间。

朝向误差、转向指令与物理响应

靶场第一次接受请求时,目标已经在射程内,方位仍偏了约 −50°。图中画的是偏差的绝对值,判断是否超出 ±45° 用这个量就够了。转向还要知道正负。假设机体正朝 XY 平面的 +X,目标方向约为 (0.643, −0.766),那么点积约为 0.643,有符号的二维叉积约为 −0.766。用 atan2 得到约 −50°。如果改用 acos(dot),结果只剩 50°,两侧目标都会得到同一个角度。

转向指令在日志里用 rad/s 记录。0.02617994 rad/s 换算为 1.5°/s,是请求上限。机体究竟转了多少,还取决于足部支撑和物理求解。在目标验证实录中,对齐从 5.033 s 持续到 11.367 s,约 6.33 s。假定整段都按上限匀速转,会算出约 9.50°。记录中的约 6.30° 是启动前的实际转角,两段统计的终点不同,转向指令也可能已经收回。排查时,把指令是否发出和机体实际转了多少分开看。

11.37 秒时,目标与机体前方的夹角约为 44.99°,已进入可启动范围。实际攻击到 12.67 秒才开始,中间过了约 1.30 秒。0.25 秒从六足接地、速度和其他启动条件全部满足时才开始计。启动时转向指令已归零,角速度也降到阈值以下,夹角约为 43.69°。

以偏差 50° 为例。如果攻击还在等待,机体得先转进 45° 以内。如果攻击已经开始,50° 仍落在允许继续的范围内,还要看射程、生命值等条件。5.03 秒的实录属于前一种。后一种是根据配置举的例子,两次实测中止分别由越距和死亡触发。源码用的边界是启动允许等于 45°,执行中超过 60° 才因朝向退出,距离超过 60 m 才因越距失效。目标和机体重合等方向退化情况没有单独的场景实录。

注册目标身份与观测有效期

请求通过初检后,协调器保存这次要打的目标在场景中的对象路径。等待期间和攻击开始后,它都会重新读取目标。每次检查对象是否仍有效、是否活着、距离和角度是否为有限值,以及观测是否在 0 至 0.10 s 内。如果重新查到的是另一个对象,即使它活着、也在射程内,原来的攻击仍会结束。越距和死亡有实录可对照,换目标这一条来自源码。

为什么转向只需要进入可攻击范围

协调器计算的是目标相对机体在水平面上的方位。它取目标和机体之间的 XY 距离,从机体四元数提取水平朝向,再把目标方位减去机体朝向,将角差折回 [−180°, 180°)。这一折返会影响跨边界时的选向。例如机体朝向 179°,目标方位 −179°,直接相减是 −358°,折返后是 +2°。移动控制器因此能收到正确的转向方向。这个例子用于说明角度运算,实录里的起始误差约为 −50°。

等待阶段的策略也很直接。角差绝对值超过 45° 时,协调器按角差的符号发出转向指令,速率为配置的 1.5°/s。进入 ±45° 后,协调器停止请求转向,继续等机体稳定。攻击开始后,朝向上限改为 ±60°。19.30 秒那次退出的原因是目标死亡。这样即使目标还在可继续执行的朝向范围内,生命状态仍可以结束攻击。

机体进入范围以后,头部还可以在自己的限幅内继续瞄准。前者解决的是机体能否在当前朝向下启动,后者决定头部怎么转。把机体也强制转到误差接近零,会让这一版已经允许开始的请求继续等待。反过来,即便头部从画面上已经对着靶标,协调器仍按机体朝向来做启动检查。

射程、存活与视线

2.03 秒,目标活着、夹角也小,但相距约 92.12 m,超出射程约 32.12 m。5.03 秒,目标回到约 25.67 m,机体需要先转向。13.17 秒,第一轮攻击已经启动,目标再度离开射程,攻击随即结束。19.30 秒,第二轮的目标仍在射程内,却因生命值归零而中止。把距离、角度、生命值和退出原因分别记录下来,可以直接查到每次是哪个条件触发了退出。

朝向扇区只看目标相对机体的位置。射线是否通畅,要看从哪里发起碰撞查询、使用什么通道,以及先撞到了哪个对象。这里的靶场可以确认注册靶会收到命中,通用遮挡行为还需要带障碍物的实录。越距后,本次攻击取消,机器人保持停止。如果要改成自动追击,还要另外决定追谁、向哪走,以及什么时候重新发起攻击。

目标失效如何中止连射

单目标门控的 UE 实录
本地目标验证场。近景玩家负责观察,脚本另行布置并移动注册目标。
目标门控与实测伤害
目标失效前的 UE 原片选帧
19.233 s,日志显示累计 4 发,注册目标剩 20 HP。画面左侧小人是注册目标,右侧近景角色是观察玩家。
目标失效后的 UE 原片选帧
19.333 s,同一机位。此时注册目标 HP 已归零,当前攻击已退出。血量和退出原因来自同轮日志。
两次目标失效与实际伤害结果
全局图用于对照两轮请求、活动区间和 HP 端点。第五发附近的逐发与退出时刻见下方放大图。
目标门控实录时钟 输入或观测 当时的结果
2.03 s 目标约在 92.12 m 外 请求直接被拒
5.03 s 目标回到约 25.67 m,朝向仍超出启动门限 排队并开始对齐
11.37 s 进入启动扇区 等待机体站稳
12.67 s 站稳检查通过 第一轮启动
13.17 s 目标再次离开射程 第一轮取消,零发
15.17 → 15.47 s 目标返回,重新请求并通过站稳检查 第二轮启动
19.267 → 19.300 s 第五发让靶标生命值归零,协调器在下一次更新读到死亡 取消余下射击
20.30 s 对同一目标再发请求 拒绝,原因为目标已死

两次光束连射为什么结束

第一轮在 12.67 秒开始准备。约半秒后,目标被移到约 92.12 m 外,触发越距退出(out_of_range)。目标当时还活着,发射计数仍为零。

15.17 秒重新请求后,协调器再次检查目标和机体。15.43 秒采到六足接地,机体线速度接近零,角速度约 0.00029 rad/s。目标距离约 25.67 m,朝向偏差约 43.7°,因此这次可以直接等站稳。两次从入队到启动分别用了约 7.63 秒和 0.30 秒。执行器的准备时间从攻击启动后另算。

第二轮的第 5 发在约 19.267 s 命中,UE 的游戏能力系统(Gameplay Ability System,GAS)将目标生命值从 20 扣至 0。协调器在约 19.300 s 的下一次更新读到死亡,记录 dead 并结束攻击。此时目标对象仍然有效。20.30 s 再次请求时,入口处的存活检查也拒绝了同一目标。

目标归零与退出的逐发时间线
前四发分别在 18.467、18.667、18.867、19.067 s,第 5 发在 19.267 s。片尾读回仍为 5 发。退出后活动计数清零,会话记录仍保留最终发射总数。

源码在目标失效时以零速度释放占用,并调用现有取消入口清理会话。目标案例进入时保留的速度也为零,因此录像验证的是退出和停止结果。站定案例的 100 cm/s 自动恢复属于显式取消与正常完成。

第五发之后,哪一步真正停止连射

这段实录里的两次攻击适合放在一起查。第一轮在 12.67 秒已经启动,约半秒后因越距结束,最终零发。第二轮同样进入了执行器,后来累计五发,直到靶标死亡才停止。如果只看最后都没有继续开火,两个出口很像。把启动事件、最终发数和退出原因摆在一起,就能区分「来不及发射」和「连射中途取消」。

靶场里,19.233 秒还只打了四发,目标剩 20 HP。19.267 秒第五发命中,生命值从 20 降到 0。19.300 秒,协调器读到目标死亡,记录退出原因并发出取消指令。到 19.333 秒,执行实例已经退出。这里能查出先后顺序,是因为扣血、协调器发令和执行实例退出各有记录。

前五发间隔约为 0.20 秒。照这个间隔推算,下一发原本会落在约 19.467 秒。这个时间只用于对照,并没有对应的发射日志。协调器在 19.300 秒就发出了取消指令,实例的最终记录也停在五发。八发的完整过程要看另一段实录,那一段没有启用目标门控。

本次攻击结束后,活动面板不再保留连射计数。核验总发数时,要看这一实例的最终记录。活动面板适合回答「此刻还在执行吗」,实例记录才能回答「这一轮一共发了几次」。退出后再请求同一个靶标时,入口直接以目标已死亡拒绝。这也说明同一个对象可以仍留在场景里,生命状态却已经不满足下一次攻击请求。

命中查询如何找到扣血对象

本地 GAS 测试组件负责扣血。画面左侧的小人是靶标,右侧近景角色是观察玩家,两者是不同的 UE Actor。日志分别记下他们的初始生命值、受伤次数和最终生命值,用来确认伤害确实结算给靶标。

本地靶场把测试查询标识 raw6 映射到 UE 的 Visibility 碰撞通道。靶标组件在这个通道上的响应从重叠(ECR_OVERLAP)改为阻挡(ECR_BLOCK),这样射线就能把靶标作为阻挡命中返回。目标注册和碰撞响应需要分别配置。只登记目标,可以让协调器检查它的距离和朝向。射线能否命中,仍由靶标组件对该通道的响应决定。这里使用的是测试场的配置,证据也限于这次查询。

这轮抗性为零,物理伤害和弱点倍率均为 1。注册靶的实际结算为 100 − 5 × 20 = 0,观察者没有收到伤害应用。换一轮输入时,应按碰撞和接收结果计算扣血,发射次数与伤害应用次数分别统计。

记录 注册目标 观察玩家 判断
初始 HP 100 100 两个对象使用相同起始生命
伤害应用次数 5 0 伤害归属随命中接收者记录
最终 HP 0 100 目标被判定死亡,观察者未受击
接收端证据 本地 UE GAS 本地 UE GAS 复核这轮结果时以对象和应用计数对应

要查某一发是否造成伤害,可以按攻击实例和发射序号找到命中查询,再对照实际受击对象和扣血事件。目标死亡是协调器在下一次更新中读到的结果。

一发光束如何从命中走到扣血

拿其中一发来看,执行器先记下发射,射线查询再返回命中的对象,伤害组件最后更新受击者的生命值。靶场里发了五次,也扣了五次血,两份记录能逐发对上。这里有预先布置的靶标和碰撞通道。换到有遮挡的场景,发射次数还可以照常增加,伤害次数则取决于射线实际命中了谁。

工程的命中记录还区分了单次伤害与持续效果。这个本地演示会对一次确认的光束命中结算一次已配置的 20 点伤害,然后立即取消演示用的周期效果,避免后续更新重复结算。第五发日志既保留了结算前后的 20 与 0,也记录了周期句柄在下次世界更新前已取消。这个做法服务于本地测试。它能验证连续命中和生命值退出的关系,原版服务器的伤害判定另需证据。

第五发之后有两条不同的取消路径。伤害组件在单次结算后取消周期效果,避免同一次命中继续扣血。行为协调器读到目标死亡后取消攻击,避免再发下一束光。第五发的伤害日志和协调器退出日志相差约 0.033 秒,伤害日志只确认周期效果在下次世界更新前已经取消。遇到多扣一次血,要回查伤害组件。遇到目标死后还在开火,则要查协调器和执行器之间的取消有没有生效。

受击、倒地与重置

本地受击、倒地与重置实录

这段独立演示记录了两次受击测试输入和一次重置,录像状态包括空闲、倒地过渡和死亡。受击与重置发生在机器人本身。前一节的 100 HP 至 0 发生在注册靶上,两段记录各有独立输入。

独立受击、倒地与重置实录选帧
同一轮本地状态演示的选帧。先观察机器人本体的受击效果和倒地,再看重置后的姿态。

机器人本体的受击路径

独立实录记录了机器人本体从待机(idle)到倒地前停稳(parking_death),再到死亡(dead)的过程。前一节则是机器人作为攻击方,扣掉靶标的生命值。两段录像分别观察机器人的倒地表现和靶标的伤害结算。

重置后,先核对机器人恢复到可操作的姿态,再发一条新的行走指令,确认它还能移动。开场的站定实录则是在攻击结束后自动恢复原来的行走指令。

移动、攻击与重置的连续演示

连续演示先让机器人起步、停下,接着执行光束连射和榴弹发射,再安排取消、两次本地受击和重置。片尾由脚本重新发出行走指令,看机器人能否再次起步。开场实录只在最初发过一次行走指令,后来是协调器在攻击退出后恢复了速度设定。

连续演示中各类指令的时间关系
图中为脚本下发时刻。命令时间可以定位段落,具体画面变化和状态进入由各自的记录核对。
重置后重新接收行走指令的 UE 原片选帧
连续演示后段的原片选帧。重置后发出新的行走指令,机器人再次起步。

连续实录把两种攻击、受击和重置放在同一次运行中。独立实录更适合逐帧查看准备、发射和收束。

录像 主要观察 同轮记录的用途
站定攻击,39.9 s 移动、收足、取消、恢复 用等待采样和事件区分未启动与已启动
单目标,约 23.17 s 目标摆位、机体转向、两次攻击 用朝向、会话合计与目标 HP 校对结果
Focus 阶段实录,16.9 s 准备、连射、收束、取消后重发 逐帧读回可定位八次发射和姿态层状态
独立 Rain,17.9 s 抬起、发射、降低 同轮收据确认三发请求与观察阶段
本地受击与重置,17.9 s 机器人本体的受击、倒地与重置 区分测试指令、可见表现与终止状态
连续演示,47.9 s 走停、两种攻击、受击、重置、再走 核对同一运行中的状态交接以及重置后的行走指令

为什么倒地后不会继续覆盖头部

死亡后,机器人的姿态来自全身物理结果。姿态层选状态时先检查死亡,随后才检查正在准备的特殊动作、下砸或射击。一旦选中死亡,它就直接使用本帧的全身姿态,跳过头部瞄准和混合。否则,身体已经倒地,头部还可能被瞄准逻辑拉回靶标方向。

重置还涉及缓存。姿态层发现重置次数变化时,会丢弃上一轮保存的局部姿态、阶段状态以及发射和命中相关的计数缓存。重置后的头部从新的输入开始更新,避免把死亡前最后一帧当成新一轮过渡的起点。连续实录随后再发出一条行走指令,用于验证重置后的移动入口仍可用。这里观察到的是测试场里的状态恢复,攻击结束后的自动续走仍由另一段站定实录验证。

逻辑测试、UE 实录与验证边界

站定攻击的报告记录了 13 项逻辑测试,后续目标门控的报告记录了 22 项,覆盖范围不同。逻辑测试可以直接输入高速度、过期样本或无效目标,检查协调器如何处理。足部是否真的着地、机体能否转动、碰撞查询是否命中靶标,还要在 UE 场景中核对。

现有实现足以支撑单一注册目标、站定攻击、取消和移动接续的讨论。遮挡、追击导航、多目标选择、复杂地形战斗联合控制仍需独立输入和实测。受击、倒地和重置的画面来自脚本驱动的本地演示。原游戏的攻击资源、触发时序和完整行为调度需要分别对照。

机体不转,或发射后没有扣血

如果协调器持续请求转向,机体却一直转不进去,先确认目标还在启动范围外。进入范围后转向指令归零是正常的。若指令仍非零,再沿着移动控制器查到足部刚体,看有没有收到指令、有没有施力,以及下一帧读到的机体朝向有没有变化。交接记录中曾出现施加扭矩却没有有效转动的情况。通过验收的实录使用了受控的足部刚体设置。日志记录休眠设置从运行前的 1 改为运行中的 0,关闭后恢复为 1,也另有主动唤醒的记录。这些值说明本次测试怎样设置和还原场景,历史案例为何没有转动仍需单独排查。

如果画面已经发射,靶标却没掉血,先查执行器有没有记下这一发,再看碰撞查询命中的组件,最后查受击对象和 GAS 结算次数。目标通过射程检查以后,仍可能因为碰撞响应配置不对而打不中。此时去改动画时间,射线还是会碰到同一个错误对象,或者仍然没有命中。靶场保留了碰撞配置和伤害结果,方便把发射、命中、扣血逐步对照。

攻击结束时清理当前请求与执行实例。关闭测试场景时才恢复临时修改的休眠设置。日志里的主动唤醒次数和视频帧数是两项独立记录。

按症状定位执行中断

机器人停住却没开火,可以先找对应请求的 attack_started。没有这条事件,回查足部支撑、采样时间、角速度和目标对齐。已经有这条事件,就查执行器是否仍在准备,或是否已经提前退出。开场的第一次光束连射在等待中取消,靶场的第一轮则已开始准备,随后因目标越距而中止。画面上两次都没有开火,日志里的处理位置不同。

机器人在攻击后仍停着也有两种可区分的原因。第一种是退出记录没有给移动层恢复意图,目标案例的零速度就是预期结果。第二种是恢复请求已经发出,但底座仍无法移动,例如支撑或转向控制未能产生有效的物理响应。站定主片用后续位移确认了第一步之后确实有运动。运行时诊断最好同时显示请求速度和物理速度,否则两条线在停步附近都接近零,切换问题只剩一幅看上去没动的画面。

遇到拒绝事件,先看被拒的是哪一条请求。开场实录拒绝了后来发出的指令,原请求继续等待。靶场 20.30 秒那次则是原攻击早已退出,新请求因为靶标已死而没有通过检查。前一种查入口留给了谁,后一种查新请求指向谁,以及当时读到的目标状态。

复查的问题 最少需要对齐的观测 本篇可对照的材料
为什么画面已经停下仍未攻击? 等待请求、最近物理样本、是否已进朝向扇区、稳定时长 站定窗口放大、目标对齐记录
为什么只发了部分弹? 当前活动实例、退出原因、按实例统计的发射结果 目标案例的两次活动会话
为什么发射了但没看到扣血? 发射提交、查询命中的组件、目标身份、伤害接收结果 本地接收者配置和伤害结果
为什么攻击退出后机器人仍静止? 退出原因、恢复速度请求、实际位移 站定主片与目标主片分别对照
为什么画面回到了常态却又出现攻击效果? 阶段与姿态读回、退出后有无新发射、表现资源的寿命 现有录像可定位阶段,逐资源清理需额外日志

核对一次攻击的日志

先用请求和攻击实例找到入队、启动与退出,再对照每次检查使用的物理采样时刻。若要核对命中,就沿发射序号找到碰撞结果和受击对象。例如目标实录的第 5 发,可依次查到发射、靶标生命值归零、协调器结束攻击三个事件。

前两篇分别解释足部支撑和局部姿态的实现。这里把它们接到一次攻击上:先确认机体站稳,再按目标状态推进攻击,最后根据退出原因决定是否恢复行走。实录覆盖这台机器人的本地单目标场景。

发表评论