一刀之后,角色系统发生了什么:深入拆解《消逝的光芒 2》断肢管线
一刀之后,角色系统发生了什么:深入拆解《消逝的光芒 2》断肢管线
砍掉一只手,看起来只需要把手部模型隐藏,再生成一个掉在地上的模型。但只要让角色动起来,这个方案就会接连出问题:手臂弯曲时断口裂开,掉落的手突然跳回默认姿态,袖子仍然连着两端,角色还在用已经消失的手发动攻击。
《消逝的光芒 2》把这件事拆成了几层:身体模型负责描述“砍的是哪里”,伤害规则决定“能不能砍断”,表现定义决定“隐藏什么、补上什么”,脱落体负责自己的骨架与物理,动画轨道则继续控制角色的挣扎、倒地和碰撞反馈。
下面沿着一次感染者左前臂被切断的过程,讲清楚命中选择、断口生成、骨架交接和动画适配,再反推美术应该交付哪些资产。伪代码保留脚本和反汇编中的关键条件;自行实现的部分标作“实现示例”。

本文三维配图为 AI 生成的丧尸技术示意图;橙色表示断点或补片,青色表示骨架与空间辅助信息。
1. 身体模型:一次命中怎样变成“切掉左前臂”
1.1 身体区域、骨骼和损伤操作是三层数据
角色身上有很多骨骼,但战斗逻辑不需要对每根手指骨单独设计一套受伤规则。游戏先将身体分成 Head、Torso、ArmLeft、ArmRight、LegLeft、LegRight 等区域,再在区域里定义具体骨骼支持的操作。
damageable_body_models.scr 的左臂定义,简化后可以写成:
1 | // 身体模型脚本展开:区域内的骨骼 → 对应损伤表现 |
这里有两个容易忽略的细节。
首先,切断与骨折没有共用一个表现入口。前臂切断走 left_forearm_cut,整条左臂的骨折走 left_arm_break。前者要产生脱落体,后者可以让手臂仍然挂在身体上,只是失去主动支撑。
其次,l_forearm 只是语义定位点,不等于最终断面必须贴着这根骨骼的原点。具体补口位置还会引用网格中的辅助元素,左前臂使用 patch_elem_arm_left_b。这样,骨骼负责动作,辅助元素负责伤口摆放,两者不必强行挤在同一个坐标上。
1.2 破洞也不一定需要增加动画骨骼
左臂还定义了 hole_forearm_left,类型是 VirtualBone,父级为 l_forearm,关联辅助元素 helper_forearm_left,最后执行 Hole 类操作。
1 | // 虚拟损伤位置:借用已有骨骼的空间关系 |
这类位置用于表达“前臂表面的一处破损”,不要求动画师为每个伤口制作一根参与动作的骨骼。它把损伤定位从动画层级里抽离出来,避免角色骨架随着伤口数量不断膨胀。
1.3 损伤关系图与动画骨架必须分开
Parent 容易被误读成建模软件里的骨骼父级。身体模型却给 neck 同时写了 l_clavicle 和 r_clavicle 两个父节点:
1 | // 游戏逻辑:身体模型中的关系 |
动画骨架是一棵树,一个骨骼通过唯一父级计算变换;这里组织的则是损伤节点之间的关系图。同一套骨骼名称可以用于两套系统,但不能把这份表直接拿去重建 Skeleton。否则脖子应该跟随哪一个锁骨,就已经说不通了。
这个区别也解释了断肢系统为什么需要额外的身体状态:动画描述骨骼怎样运动,损伤图描述部位之间的关联。手不再可见以后,动画轨道仍然可以保留手骨的变换数据。
1.4 腰斩还会筛选攻击平面的方向
大切口也不只是“命中了躯干”。pelvis 对应的 horizontal_cut 使用间接损伤条件,指定 helper_waist_0,将辅助区域沿三个局部轴放大为 [3.0, 2.5, 2.5],允许的攻击平面偏差为 30°。左右纵向切断使用胸部辅助区域,允许偏差为 45°。
把这几项输入展开:
1 | // 游戏逻辑:腰斩候选的条件与操作 |
这些参数让美术预制的切口与攻击方向发生联系。辅助区域决定在哪里接受候选,平面偏差限制什么方向的挥砍适合这个断面,优先级用于候选选择。它并不要求刀刃走到哪里,就实时沿着那条轨迹重建一个任意形状的切口。
1.5 先匹配身体语义,再交给规则层
把关系串起来,数据流是:
%%{init: {"theme":"base","themeVariables":{"primaryColor":"#25201d","primaryTextColor":"#f1ece4","primaryBorderColor":"#ef8950","lineColor":"#79cbd5"}}}%%
flowchart LR
A[武器命中] --> B[左臂区域]
B --> C[前臂骨骼]
C --> D[切断候选]
D --> E[伤害条件与概率]
E --> F[左前臂切断表现]
F --> G[身体隐藏与补口]
F --> H[脱落体与物理]
因此,命中检测不应该直接负责生成碎块。它提供受击位置和上下文,由身体模型解释这个位置,再让规则层决定最终效果。
2. 伤害决策:砍中、砍断和死亡为什么必须分开
2.1 75 表示生命比例条件,不是断肢概率
damageable_body_presets.scr 的 Biter 预设,将左臂的 Cut 和 Break 都配置为最大身体生命百分比 75.0,同时标记为 NonLethal。
用伪代码表达这组规则:
1 | // Biter 左臂规则展开;healthPercent 是规则求值时使用的身体生命比例 |
NonLethal 的含义是这次身体损伤操作本身不强制杀死角色,而不是免除武器本身的伤害。一次攻击仍可能先把生命值打空,然后同时产生断肢。
同一个 Biter 预设里,头部切断的生命比例条件是 50.0,另外还配置 ProbabilityMul("Cut", 0.5)。这两个数控制不同的事情:前者限制是否进入候选,后者调整概率权重。不能把它们相加,也不能因为身体已经低血量,就默认每次命中都必然断头。
2.2 受击状态会继续修改断肢概率
damage_rules.scr 中有一个很具体的规则:ragdoll_during_bleeding_increases_limb_cut_prob。它要求这次伤害带有 Ragdoll 反应标记,并且目标正在流血,然后调用 AddLimbCutProb(0.1)。
1 | // 伤害规则脚本展开 |
这个判断检查的是伤害携带的反应标记,不是笼统地判断“敌人已经躺在地上”。例如其他规则可以给一次命中加上布娃娃反应,从而影响后续伤害处理。
这一条规则给当前概率加上 0.1;头部的 ProbabilityMul 则属于部位倍率。实现自己的规则系统时,要明确两者在哪个阶段求值,否则“先加再乘”和“先乘再加”会产生不同的手感。
2.3 处决与环境撞击走不同的伤害预设
sync_action_damage_presets.scr 中的 finisher_limb_cut 组合了以下设置:
1 | // 处决伤害预设展开 |
处决不是普通挥刀简单增加一点伤害,而是把“致死”和“身体损伤要求”一起写进预设。表现层仍要找到目标可用的损伤操作和资源;概率设成 1.0 不会替美术生成一套不存在的断面。
环境碰撞也能进入同一套身体损伤系统。ai_damage_presets.scr 中的 wall_hit_ragdoll 使用 AI_WallHitRagdoll,配置 BodyDamageEnforced、切断概率 1.0 和破坏概率 0.6,同时引用墙面碰撞的反应预设。
两者的共同点,是把不同来源的攻击统一转成“伤害定义、身体损伤参数、反应参数”。这样刀刃、处决和环境碰撞可以复用后面的断肢资产,不必各做一套生成碎块的逻辑。
3. 外观分离:原身体怎么消失,断面怎么出现
3.1 一个切断操作包含两个端口
left_forearm_cut 并不是一个单独的模型文件。它是一份表现配方,组织了原身体和脱落体两侧的资源:
1 | // left_forearm_cut 的表现配置展开,省略各体型的椭球数值 |
这里的 BodyPatch 填住身体剩下的那一端,Gib.Patch 填住掉落前臂的另一端。两端必须各自封口,否则无论从身体还是地面碎块看过去,都可能看到空壳内部。
补片选择还检查 DamageSize。尺寸为 slim 时使用细版,普通尺寸使用默认版。把这个局部选择展开就是:
1 | SelectForearmPatch(characterProperties) { |
这比“所有感染者都用一块伤口模型,再统一缩放”更稳妥。瘦手臂、厚外套和特殊感染者的断面轮廓不同,单纯缩放常常只能匹配直径,无法同时匹配轮廓和厚度。
3.2 隐藏外壳为什么能比切三角形更省事
HideEllipsoid 描述了一种体积隐藏需求:在角色表面限定一个区域,让这一带的外层不再显示。后面的补片负责填充可见内部。
这类算法不必首先把每个相交三角形切成两半。用一个椭球判断表面是否处于隐藏范围,就可以在外观上形成缺口。下面用最小着色实现说明这个计算:
1 | // 实现示例:所有输入先转换到同一个参考空间 |
椭球本质上是一个经过旋转和非均匀缩放的球。把位置逆变换回单位球空间后,只需检查距离平方是否小于 1。

外壳负责角色的连续表面,补片负责损伤后露出的内部;图中的灰色和橙色是辅助材质。
这样的“外层隐藏 + 内部补片”有公开的工业先例。Valve 在 Rendering Wounds in Left 4 Dead 2 中使用姿态空间椭球、投影纹理与独立伤口模型,并强调阴影和深度 Pass 也要应用裁剪。技术展开:GDC 2010 演讲
实际落地时,坐标空间比距离公式更重要。伤口如果固定在世界空间,角色一转身,它就会在皮肤上滑动;补片如果跟骨骼走、隐藏区域却不跟,二者又会错开。隐藏区域和补片必须遵守同一套附着与形变约定。
3.3 大切口需要能够变形的补片
普通前臂切口使用 Simple 类型。躯干纵向切断则使用 Skinned 补片,身体端和脱落端分别引用胸部补片模型;脱落体还配置 ShowEllipsoid,物理改用 sim_ragdoll.lite。
这组差异对应了两个制作问题:
- 小型端口的局部形状比较稳定,可以围绕一个附着位置组织。
- 胸部切口跨越更大的形变范围,角色弯腰或旋转时,内部模型也要随骨架变形。
因此,美术交付不能只有一张“断面贴图”。小补片、蒙皮补片、身体端与脱落端各有职责;哪种资产用于哪种断点,要由表现配方明确指定。
3.4 骨架类型与断面尺寸是两次独立选择
前臂配方里有一组按 man、woman、banshee 等名字索引的隐藏体积,同时又有一组按 DamageSize 选择的补片。两组选择解决不同问题。
damageable_body_skeleton_groups.scr 把 man_zmb_skeleton.msh、man_zmb_light_skeleton.msh、man_srv_skinybiter_skeleton.msh 等骨架都映射到 man。因此,某个瘦感染者可以使用 man 的隐藏参数,同时使用 slim 的补片模型:
1 | // 游戏数据关系展开:两个索引不能合并成一个体型枚举 |
对制作管线而言,这意味着调整骨架比例时要复查隐藏区域,调整局部粗细时要复查补片轮廓。只把模型整体缩放一遍,并不能自动完成这两层适配。
4. 美术管线:怎样制作一套真正能用的断肢资产
4.1 从断点表开始制作
以左前臂为一个完整制作单元,交付物应围绕运行时实际需要的输入组织:
1 | 左前臂断点 |
先把这些关系定好,再去雕刻伤口。否则高模做得再精细,进入引擎后也可能发现补片没有可靠的附着点,或者袖口压根不在隐藏范围里。
4.2 完整角色和损伤内部可以分开生产
基础角色仍然按照常规流程完成高模、低模、UV、烘焙、材质和蒙皮。额外的损伤资产主要集中在允许露出的区域:局部断面、破洞内部、衣物破边,以及必要的体型变体。
不必为了三个前臂断点就给整个人制作完整的可解剖内部。能被镜头看到的部分才需要真实几何;永远被外壳挡住的区域没有必要支付建模、贴图和运行时成本。
制作补片时,先让低模轮廓与外壳开口吻合,再增加内部层次。伤口外沿需要适度覆盖原表面,用来隐藏微小误差;覆盖过宽会形成凸起,表面过近则会闪烁。贴图要留足边缘扩张,避免缩小采样后出现一圈亮边。
4.3 袖子必须和皮肤响应同一个断点
“皮肤断了,袖子还连着”并不是材质问题,而是部件归属没有定义完整。袖子、皮肤可以是不同网格,却必须知道同一个断点会移走哪些部分。
手套一般跟随手,护腕可能跟随前臂,挂在腰间的道具则留在身体。长外套、披挂与布料还要检查连接关系:不能让一块属于脱落体的布料继续被身体端的约束拉住。
游戏的 human_gib 组件里有 CoMeshPartClothSet,说明碎块对象本身预留了处理衣物部件的职责。它不会替代美术对衣服边界和归属的制作。
4.4 蒙皮不是沿着断点一刀切成硬权重

左侧展示边界定位,中间展示骨骼影响的连续分布,右侧展示独立封口资产。预分段路线需要匹配边界顶点;体积隐藏路线则重点检查补片与遮罩的重合。
正常手肘需要多骨骼混合才能自然弯曲。不能为了方便断肢,就把断点两侧所有顶点分别硬绑定到一根骨骼。
线性混合蒙皮的核心关系是:
顶点最终位置取决于所有影响骨骼的变换。如果一圈前臂顶点仍受上臂影响,生成脱落体时就必须为这部分影响安排姿态来源,或者使用提前处理好权重的脱落版本。直接删除上臂骨骼,再归一化剩余权重,很容易让断口瞬间收缩或偏移。
制作时可以把检查写成一条清楚的约束:
1 | // 资产验收逻辑 |
不同 LOD 也要满足同样的约束。低 LOD 可以减少细节,但不能删掉补片依赖的骨骼,却不提供替代映射。
4.5 给美术的交付表应该具体到一个断点
以前臂为例,可以将制作和导出约定落实成下面这张表。前两列对应前面配方实际消费的输入,最后一列是制作时的检查方法。
| 交付内容 | 接入位置 | 制作与验收要点 |
|---|---|---|
| 前臂断口定位元素 | patch_elem_arm_left_b |
记录局部轴方向;身体端、碎块端用各自的安装变换测试,不能只看模型原点重合 |
| 各骨架组的隐藏范围 | HideEllipsoid(group, …) |
叠加显示隐藏边界与补片;检查伸肘、弯肘、前臂扭转 |
| 默认和细尺寸补片 | 两个 chr_dmg_cut_patch_default_a 变体 |
外圈盖住开口,内面从身体侧和碎块侧分别可见;检查法线、UV 和材质 |
| 衣物部件与附件归属 | 身体及 human_gib 的网格、布料部件 |
袖子和皮肤一起处理,护腕随前臂,跨断面的布料约束需要解除 |
| 蒙皮与绑定姿态 | CoSkinnedMesh、CoSkeleton |
保留每个非零权重需要的姿态来源;不要把物理代理骨数量当成蒙皮骨数量 |
| 胶囊体和关节 | sim_cut_limbs |
用碰撞调试视图检查接缝;关闭粒子也应能正常分离和落地 |
| 远距离版本 | 网格、补片及骨骼 LOD | 在断口已出现时切换 LOD,检查补片消失、穿插和残余袖口 |
这些检查适合做成 DCC 导出前的校验,再配一个引擎内预览动作集。检测缺失名称、权重引用和资源引用可以自动完成;补片贴合与动作观感仍要在运动中检查。
5. 骨架交接:身体继续动,掉下来的前臂由谁控制
5.1 动画骨架与物理骨架不是一回事
动画层级里可能有很多手指骨,物理系统却只用一个胶囊体代表整只手。动画需要保留局部动作细节,物理需要少量稳定、便宜的碰撞代理,两者服务的目标不同。
所以,“前臂不再显示”不意味着必须从原角色的 Skeleton 中删除前臂骨。保留骨架索引可以继续使用已有动画;可见性、碰撞和能力状态再分别决定哪些结果仍然有效。
这一原则解决的是兼容性。角色缺了手之后是否仍然播放双手动作,是下一层行为选择的问题,不能指望删几根骨骼就自动解决。
5.2 脱落体拥有自己的骨架和物理组件
human_gib.prefab 把碎块组织成一个完整对象:
1 | // 预制体组件与连接的简化表达 |
最后两行很关键:碎块被再次击中时,伤害输入能够继续转成物理冲量。它不是生成后只负责播放一个下落动画的摆设。
CoSkinnedMesh 和 CoSkeleton 则解释了为什么不能一概用静态网格替代脱落体。若需要前臂、手腕继续有相对运动,仍然需要由物理结果驱动骨骼,再由骨骼驱动表面。
前臂的配方还显式请求了 PhysicsAdditionalBone("l_upperarm")。因此,不能按“名字叫前臂”就把它需要的骨骼集合写死成 l_forearm 以下的子树。**可见几何的范围、蒙皮所需的姿态范围、物理需要的骨骼范围,是三个不同集合。**这项额外物理骨配置也不能代替蒙皮权重检查。
5.3 新骨架必须继承切断瞬间的世界姿态

交接的第一步是保持位置和姿态连续,再让身体与脱落体各自继续运动。
这里展开姿态交接所需的坐标计算。设原角色的模型到世界矩阵为 ,骨骼当前模型空间矩阵为 ,脱落体自己的模型到世界矩阵为 。希望新骨骼出现在同一个世界位置,就应满足:
相应算法可以写成:
1 | // 姿态转换算法示意:要求双方骨骼映射与绑定姿态空间已经对齐 |
这不是把动画文件再播放一遍。它复制的是当前这一刻已经求出的姿态,所以无论角色正弯肘、挥手还是跌倒,脱落体都能从正确形状开始。
还要保证根变换和骨骼姿态来自同一个时刻。拿上一帧的手臂姿态搭配这一帧的角色位置,会导致高速运动中的肢体突然“弹”出一截。
碰撞体还多一层偏移。骨骼原点通常放在关节,胶囊体原点却可能位于前臂中段。设 是碰撞体相对骨骼的安装矩阵,那么初始刚体世界矩阵应当是:
物理求解完成后,再通过 写回新骨架。少乘或少除这层安装矩阵,就会出现“网格出生位置正确,一开启物理就错位”的问题。
5.4 骨折为什么不会直接掉下来
left_arm_break 引用的 partial_ragdoll_left_arm,把 spine3 设置为运动学刚体,左上臂、前臂和手设置为动态刚体。
1 | // 局部布娃娃配置展开 |
运动学胸部跟随角色动作,动态手臂接受物理求解;连接它们的关节约束仍然存在,所以手臂会垂落和摆动,却不会成为独立碎块。
肩、肘、腕使用 Dof6Spring2 约束。三个平移轴的上下限都设为零,表示关节连接点不能自由拉开;旋转方向则给不同范围。肘部示例还设置 frame_b_rotation_xyz([0, 0, -90]),用来调整约束坐标系。
1 | // 肘部约束参数展开:角度定义在约束局部坐标系中 |
不能把这些角度直接当成世界坐标里的抬手角度。约束坐标系放错了,即使角度数值相同,手肘也可能朝相反方向弯曲。
5.5 脱落体使用另一套约束参数
sim_cut_limbs 里,左上臂、前臂、手分别对应槽位 3、5、14,三者都是动态胶囊体;肘连接 3 → 5,腕连接 5 → 14。这套模板覆盖全身可分离部件,并不表示每次切下前臂都创建整个人的物理体。
两套配置的左肘限制就有所不同:
| 左肘参数 | 挂在身体上的断臂 | 脱落体模板 |
|---|---|---|
| 角度下限 | [10, -80, 90] |
[10, -40, 90] |
| 角度上限 | [125, 10, 90] |
[120, 30, 90] |
| 平移上下限 | 均为 [0, 0, 0] |
均为 [0, 0, 0] |
| B 端坐标系旋转 | [0, 0, -90] |
[0, 0, -90] |
所以复用骨骼不等于复用全部物理参数。悬挂手臂要跟随角色运动,落地碎块要避免夸张扭转,两者可以共享骨名和胶囊体结构,再分别调整关节活动范围。
6. 动画适配:断肢之后的挣扎,是一条带事件的时间轴
6.1 专用反应动作不只是换一个动画名字
感染者动画资源中存在断臂、断腿和断头的专用片段。比如 biter.scr 的 m_bit_death_arm_cutoff_sl,除了动作片段,还配置了一系列事件。
将其中最重要的部分按轨道时间展开:
1 | // m_bit_death_arm_cutoff_sl 的事件脚本展开 |
这一条时间轴承担了三件事。
**第一,控制碰撞伤害窗口。**动作开始时开启,到了指定事件关闭。角色正在踉跄、倒下还是已经稳定落地,适用的碰撞反馈并不相同。
**第二,让声音和落地位置对齐。**身体砸地时,事件既播放 biter_body_falling,也以 pelvis 为位置关联表面交互。音效和地面反馈不是随便延迟一个固定秒数后触发,而是放在动作的合适位置。
第三,控制什么时候允许离开动作。USER_CAN_BLEND_OUT 表示开放混出窗口,不等于立即删除角色或强制切成全身布娃娃。具体接下一个状态什么动作,由上层控制器决定。
6.2 不同动作的交接窗口并不相同
断腿轨道 m_bit_death_leg_cutoff_sr 的对应事件是:在 0 开启碰撞伤害,63 关闭,83 允许混出,87 触发身体落地音效和骨盆表面交互。
这意味着不能写一个所有断肢通用的“受击后等待两秒再倒地”。每条动作的重心变化、迈步和落地节奏不同,事件也要跟着动作制作。
1 | // 动画事件消费逻辑示意:跳帧时处理跨过的事件,而非只判断相等 |
这里的消费函数展示实现要点:如果一帧跨过了某个时间点,事件仍然需要执行。单纯写 currentFrame == 81,低帧率时可能漏掉关闭事件,导致碰撞伤害状态一直残留。
6.3 活着时还要适配攻击、装备和 IK
专用死亡反应解决的是一部分情况。前面 Biter 的左臂损伤允许非致命,因此活体角色还需要处理能力变化。
这层可以按依赖关系来组织:缺少手部时,依赖该手的握持目标和攻击判定失效;武器明确留在身体、随肢体脱落还是单独掉落;使用双手的动作需要退出或切换。下面是用于自建控制器的适配示意:
1 | RefreshCapabilities(bodyState) { |
不要用“骨骼还存在”作为手仍然可用的判断条件。骨架可以为了动画兼容而保留,能力状态必须依据身体损伤结果计算。
7. 物理反馈与特效:让切断具有速度、重量和持续性
7.1 脱落体需要继承运动
掉下来的前臂原本就在运动。若创建时速度清零,它会像突然被刹住一样垂直掉落。对于一个刚体参考,其空间点速度可以写为:
其中后半项就是旋转带来的速度。对于动画驱动的手臂,还需要从连续世界姿态估计局部运动,不能只复制角色根节点的奔跑速度。
游戏的 DefaultGibPhysicsImpulse 进一步为碎块提供表现性调节:
1 | // 默认碎块运动参数展开;这是参数,不是完整求解公式 |
设计师可以放大攻击方向上的运动,再叠加向上速度和旋转。整段躯干切断则还使用沿切面法线的附加速度,使分离方向更清楚。
这些范围展示了物理反馈的调参入口,不应把它们当成真实人体质量或严格守恒的实验结果。游戏表现追求的是动作读起来清楚,且运动能与碰撞稳定衔接。
7.2 隐藏网格不会自动移除碰撞
身体上已经消失的部分不能继续作为原角色的有效受击表面;脱落体也不能继续被跨越断面的旧约束拉回去。这要求视觉和物理对同一个身体状态作出响应。
同时,碎块与身体相互碰撞是允许的:普通切断和全身切断的相关开关都设置为 true。因此生成时的碰撞代理和初始姿态必须足够准确,否则两者严重穿插,求解器可能把碎块突然推出很远。
检查问题时,应先画出胶囊体和约束。看到网格位置正确,不代表物理代理也正确;视觉网格和碰撞骨骼的原点偏差,往往比冲量参数更值得先排查。
7.3 两端出血与表面血迹分别更新
左前臂配方分别指定身体端和碎块端效果。身体端效果跟着残余角色运动,碎块端效果跟着新骨架运动,落地血泊则由碎块自己的组件管理。
此外,blood_mask_default 使用 Incremental 模式和 12 个等级,值域配置为 0 到 51。这是另一层持续外观状态,不等于每次切断都新建一组粒子。
1 | // 表现配置的职责划分 |
瞬时飞溅负责命中的那一下,端口效果负责位置连续性,表面血迹负责保留变化。把它们分开,才可以分别调整时长和成本,而不是通过无限增加粒子来掩盖错误的几何。
8. 状态与回收:断过的身体必须记得自己缺了什么
8.1 一张状态表不够
游戏程序的身体损伤状态字段里,同时存在 bones_states、body_elements_states、body_masks_values、命中计数和最近命中信息。保留字段名,用不限定底层类型的伪代码表达如下:
1 | DamageableBodyReplicatedState { |
它们解决的问题不同:骨骼级状态对应具体损伤位置,区域级状态对应整条手臂等更粗的语义,遮罩状态保存血迹等持续外观,命中计数服务于累计条件,最近命中信息则为反应提供位置和方向。
把这些都塞进一个 isDismembered 布尔值,会马上失去表达能力:一个角色可能手已经没了,上臂还在;左臂失能,右臂仍能攻击;全身有血迹,但没有任何部位脱落。
联机时,应先让各端得到一致的部位状态,再更新外观。碎块飞到哪里可以采用较便宜的表现同步,但“这只手是否还在、还能否攻击”必须保持一致。
8.2 候选检查:先排除重复状态,再选择一个槽
DamageableBodyRuntime 把状态掩码和四个活动槽分开维护。下面将候选检查与执行入口的选槽部分合并成一个函数,保留实际判断顺序。两个状态掩码沿用中性名称 stateMaskA、stateMaskB;函数名为了阅读重新命名。
1 | // 游戏逻辑:掩码为 64 位,返回执行入口选择的槽位 |
开头检查的是候选要求的位是否已经全部被覆盖。例如当前状态是 0011,候选为 0010,相与后仍是 0010,所以拒绝;候选为 0110 时只重叠了一部分,这一条不会拒绝它。
槽位扫描则按顺序返回第一个符合条件的项。空槽可以用,已有项与候选的关系掩码相交也可以用。它没有先扫描所有槽寻找关联项、再回头找空槽;先遇到哪个就返回哪个。
occupiedSlotsMask 是占用位图。例如 0101 表示槽 0 和槽 2 被占用,数值是 5,但活动项只有两个。不能拿这个数直接与槽数比较。
8.3 从身体状态到渲染:把矩阵写进蒙皮网格
选好槽并处理完关联节点以后,执行路径会保存候选、设置占用位,然后调用引擎的 CoSkinnedMesh::SetMeshAttributeMtx。传入的属性索引是 3 * slot + 1,参数类型是 mtx34,也就是一个 3×4 矩阵。
1 | // 游戏逻辑:执行路径中的槽位提交部分 |
这一步把“角色发生了某个损伤”接到了“蒙皮网格收到一份空间参数”。美术配置的隐藏体积与补片位置,最终必须进入这样的运行时通道,才能影响可见结果。
这里的固定容量属于这条矩阵属性通道,不代表整个游戏只能存在四个伤口或四块碎块。第 3 节的椭球距离判断给出一种着色实现;这条调用已经明确的是矩阵如何送到网格,完整 Shader 内部仍需要另外解析。
移除路径也会主动复位网格属性,而不只是把指针清空。它使用一个固定矩阵:旋转部分为单位矩阵,平移为 [0, -1024, 0]。
1 | // 游戏逻辑:对应槽位的网格属性复位 |
linkMask 是移除入口使用的另一份关联掩码,与选槽时的 relationMask 分开。这里最实用的经验是:**逻辑状态、槽位占用和网格属性必须一起更新。**只清掉逻辑状态,旧矩阵仍可能继续影响外观;只清掉矩阵,重复命中检查又可能认为损伤仍然存在。
8.4 重复切断还要处理上下游关系
先切掉手,再切掉前臂,第二次生成的前臂不能长出一只新手。因此,碎块生成应消费当前部位状态,而不是每次都取完整身体模板。
1 | // 基于部件关系的生成算法示意 |
对于体积隐藏路线,同样的关系可以直接写成遮罩组合。假设 H 表示手部隐藏体积,F 表示前臂隐藏体积,两者都在角色绑定姿态空间求值。先切手时,身体显示 1-H,手部碎块显示 H;随后切前臂,新碎块显示的是 F × (1-H),身体显示 (1-F) × (1-H)。如果新碎块只使用 F,完整模型里的手就会重新出现。
1 | // 实现示例:从完整网格复制姿态,用遮罩保留正确的碎块表面 |
补片迁移与遮罩继承必须一起做。只复制遮罩,新前臂的手腕会留下一个空洞;只复制补片,补片外面又可能长出完整的手。这里迁移的是部件与附着关系,不是把旧补片当成一个世界坐标固定的物体搬过去。
随后,身体端移除的是这批仍然存在的部分,脱落端生成的也是同一批。重复事件不能再生成第二份;角色已经被回收时,迟到的资源回调也不能把旧碎块重新放回场景。
8.5 创建距离与保留距离要分开
parts_manager_params.scr 将镜头前方的创建距离设置为 25、保留距离设置为 30;后方分别是 15 和 20。按这组距离策略,可以写出一个容易理解的管理算法:
1 | // 距离策略示意;前后判定细节由相机系统提供 |
假设镜头前方有一个已经生成的碎块,玩家退到距离 27 时,它仍然保留;但在同样位置发生的新断肢,不必继续生成同等级碎块。这中间的距离差形成了滞回区,可以避免镜头在阈值附近移动时反复创建和删除。
管理器还将更新间隔、移除检查间隔和最大寿命分开配置。它们属于对象生命周期策略,不能当成刚体求解频率。数量多起来后,真正需要预算的是活动刚体、约束、蒙皮与特效,不只是碎块的三角形数。
9. 把一条断肢链真正做通
回到最开始的左前臂。假设身体规则求值时,Biter 的生命比例是 60%,目标位置是 l_forearm,本次切断概率判定也已通过。那么后面的输入和输出应当具体到这些内容:
| 阶段 | 本次例子的结果 |
|---|---|
| 身体模型 | ArmLeft → l_forearm → left_forearm_cut |
| 规则 | 满足 ≤ 75% 的条件;切断操作本身非强制致死 |
| 定位 | patch_elem_arm_left_b |
| 外观 | 按骨架组选择隐藏体积,按 DamageSize 选择双侧补片 |
| 运行时槽 | 检查状态掩码,选择槽,将矩阵写入蒙皮网格属性 |
| 脱落体 | human_gib,自己的骨架、蒙皮网格、布料与物理组件 |
| 物理 | sim_cut_limbs,额外请求 l_upperarm,应用碎块运动参数 |
| 反馈 | 身体端出血、碎块端出血、碎块血泊;角色按生死状态选择后续反应 |
把这些接口组装成自己的系统时,最容易犯的错误,是先把身体状态标成“前臂不存在”,再去读取要生成的前臂。第二步读到的已经是修改后的状态,可能漏掉部件,或退回完整模板把之前切掉的手重新带回来。
因此,集成顺序应当先取快照、准备对象,再提交状态。下面是一个实现示例:
1 | // 实现示例:快照中同时保存当前部件、姿态和运动 |
这个例子专门处理产生碎块的切断路径,破洞、骨折分别进入自己的表现分支。实际接入时,可以在角色生成阶段就预载常用断口资源,避免第一次挥刀才开始加载;如果必须延后创建,则需要保存命中请求,资源就绪后重新检查目标和身体状态。
姿态交接和状态提交应当发生在明确的更新阶段:动画先得到这一帧的姿态,身体与碎块使用同一份快照,物理从初始化好的碰撞体开始接管。这样才能同时避免缺口先出现、碎块晚出现,以及碰撞体出生在旧位置的问题。
做原型时,先用一个调试按钮触发左前臂切断,把这条链跑通,再接真实武器命中。依次检查四组情况:
| 检查 | 要回答的问题 |
|---|---|
| 静止、弯肘与挥臂时切断 | 断口是否贴合,碎块有没有姿态跳变? |
| 先断手再断前臂 | 会不会重复生成或重新长出部件? |
| 不同衣服和远近 LOD | 袖口、补片与骨骼映射是否仍然正确? |
| 倒地、贴墙与连续受击 | 约束、碰撞窗口和动画事件是否协同? |
把一条手臂上的这些问题解决,再扩展到全身,才会得到可维护的断肢系统。角色美术交付的是一组能够分离、补口和继续变形的资产;运行时则负责让这些资产在正确时刻,由正确的动画或物理系统接管。


