如果你玩过《Totally Accurate Battle Simulator》(全面战争模拟器),大概率见过那些走路东倒西歪、身体仿佛没有骨头的小人物。玩家社区里一般把这种一推就倒、四肢乱甩、动作极其“放飞自我”的角色统称为软骨兵。单位本身不一定叫这个名字,但那个观感确实让人过目不忘。
这篇文章不打算只聊乐子。我会从物理模拟的角度拆解“软骨兵”到底为什么软,然后用 Unity 完整实现一个最简单的软骨兵原型,并给出可复制的 C# 脚本和参数调优思路。无论你是 Unity 初学者,还是想给自己的游戏加入“物理搞笑单位”的开发者,这篇文章都能给你一套可以直接上手的方案。
1. 背景与核心概念
1.1 TABS 与软骨兵的由来
TABS 是 Landfall 工作室推出的一款整活向物理模拟策略游戏。玩家在开局前购买不同单位,进入战斗后,单位会自动冲向敌方,真实战斗过程全部交给物理引擎计算。游戏里的角色有一个显著特点:走路摇摇晃晃,打架像喝醉了一样,经常出现“一拳打空然后自己摔倒”的名场面。
玩家口中的软骨兵,就是这类单位中的典型代表。它并不是某个特定兵种的官方名称,而是一类动作特征的集合,主要包括:
- 站立时身体不停晃动;
- 受到轻微冲击就会四肢瘫软;
- 被击倒后很难自己站起来;
- 走路时肢体大量乱甩,完全没有传统士兵的稳定性。
这种效果不是美术动画做出来的,而是由物理模拟自然涌现出来的。理解这一点,是复刻软骨兵的关键。
1.2 为什么传统动画做不出这种效果
传统游戏角色的移动通常采用“骨骼动画系统”。美术在建模软件里制作关键帧动画,引擎按照时间轴播放,角色的手臂和腿严格按照关键帧运动。这个过程稳定、可控,但一旦加入随机碰撞,效果就会变得很“僵硬”,因为动画系统并不知道自己撞到了墙还是被人踹了一脚。
TABS 的做法完全不同。它没有让动画系统直接控制角色四肢,而是把每个骨骼节点当作一个物理刚体,骨骼之间通过关节约束连接,程序只给角色一个“朝那个方向走”的意图,剩下的交给物理仿真。于是角色每一步都要靠物理引擎“现算”,一旦某个部位受力不均匀,整个身体就会歪向一边。
这就是软骨兵的本质:一个由物理意图驱动、而非关键帧驱动的程式化角色。
1.3 做“软骨兵”需要掌握什么
如果你想在 Unity 里自己做一只软骨兵,需要掌握的知识点至少包括:
- Rigidbody(刚体):让骨骼拥有质量、速度和受力能力。
- Collider(碰撞体):让骨骼可以被碰撞,也会倒地。
- CharacterJoint 或 HingeJoint(关节):约束骨骼之间的相对运动。
- 姿态控制:给关节施加扭矩,让角色有“努力站立”的意图。
- 噪声扰动:用 Perlin 噪声制造自然的随机晃动。
下面我们从环境准备开始,一步步搭建一个最小可运行的软骨兵。
2. 环境准备与版本说明
本教程使用 Unity 3D 物理系统完成,不依赖第三方插件。
| 项目 | 建议配置 |
|---|---|
| 操作系统 | Windows 10/11、macOS 均可 |
| Unity 版本 | 2021.3 LTS 或 2022.3 LTS |
| 开发语言 | C# |
| 脚本编辑器 | Visual Studio 2022、VS Code 或 Rider |
| 物理系统 | Unity 内置 3D 物理(PhysX) |
| 外部依赖 | 无 |
如果你的项目正在使用 Unity 2019 或 2020,绝大多数 API 也能正常工作,但个别菜单入口可能不同。本文以较稳定的 LTS 版本为例,重点讲解实现思路。
示例项目结构如下:
SoftBoneSoldierDemo/ ├── Assets/ │ ├── Scenes/ │ │ └── Demo.unity │ ├── Scripts/ │ │ ├── EditMode/ │ │ │ └── SoftRagdollBuilder.cs │ │ ├── JellyController.cs │ │ └── SoftStandIntent.cs │ └── Prefabs/ │ └── SoftSoldier.prefab在实际开发中,骨架模型通常由美术提供,但这里为了把逻辑讲清楚,我全部使用 Unity 内置的 Cube 搭一个简单人体模型。
3. 软骨兵背后的核心原理
3.1 布娃娃系统的工作原理
布娃娃(Ragdoll)是角色动画和物理系统结合的经典方案。一般流程是:
- 把角色的骨骼分成多个节点,比如头部、脊椎、左右手臂、左右腿。
- 每个节点挂一个 Rigidbody,节点之间用 CharacterJoint 连接。
- 给每个节点添加合适的 Collider。
- 物理引擎负责计算所有刚体之间的约束、碰撞和运动。
当角色正常活动时,动画系统仍可能占主导;当角色死亡或者受到巨大冲击时,系统会关闭动画控制,让角色完全按物理规律倒下。你可以在很多游戏中看到“尸体从楼梯滚下去”的效果,这就是布娃娃系统。
标准布娃娃强调的是“被动受力”。软骨兵则更进一步,连日常走路也很依赖物理驱动,因此关节参数会被调得特别软,外力稍微一变,身体马上变形。
3.2 为什么 TABS 单位看起来像没有骨头
这里引入一个概念:物理驱动的程序化动画。
在 TABS 中,单位身体并不是由预设关键帧驱动,而是由“目标姿态 + 物理约束”共同作用。程序不断计算当前姿态和目标姿态之间的差异,然后通过关节马达输出一个纠正扭矩。如果纠正扭矩太小,身体就会被重力压垮;如果摆动自由度太大,身体就会显得松散。
所以软骨兵的“软”通常来自三个层面的设置:
- 关节摆动角度大:四肢可以大幅度偏离初始角度,稍微受力就会扭曲。
- 关节回正力矩弱:即使有驱动扭矩,也不足以让身体稳定回正。
- 额外扰动持续存在:来自 AI 的移动指令、重力、碰撞、甚至轻微的噪声扭矩,都会让身体不断抖动。
这三者组合在一起,角色就会呈现出站立不稳、肢体乱甩的搞笑状态。
3.3 Unity 中实现软骨兵的三大要素
如果你在 Unity 里实现软骨兵,可以把上面的三点对应成具体组件:
- 每个骨骼节点:挂 Rigidbody + Collider。
- 骨骼之间的关系:挂 CharacterJoint。
- 姿态回正能力:写一个脚本,不断把骨骼拉回初始旋转。
- 随机扰动:用 Perlin 噪声生成微小扭矩,模拟“肌肉痉挛”和“空气阻力”。
其中最难拿捏的是“努力站立但又站不稳”的平衡点。扭矩给大了就是普通士兵,扭矩给小了就是一摊泥。实战部分会给出初始参数,你可以在 Inspector 里实时调整观察效果。
4. 完整实战案例:Unity 里做一只软骨兵
这一节会从零创建一个可以在 Unity 编辑器中直接运行的软骨兵原型。整个过程分为五步:
- 搭建人体骨架模型;
- 用脚本一键添加刚体、碰撞体和关节;
- 编写关节软化脚本;
- 编写站立意图脚本;
- 运行并调整参数。
4.1 创建场景与骨架模型
打开 Unity,新建一个 3D 项目,然后创建一个空物体,并命名为Soldier。
在Soldier下创建 11 个 Cube,分别代表身体部位,名称如下:
Soldier ├── Pelvis(骨盆) ├── Spine(脊椎) ├── Head(头部) ├── UpperArm_L(左上臂) ├── LowerArm_L(左下臂) ├── UpperArm_R(右上臂) ├── LowerArm_R(右下臂) ├── UpperLeg_L(左大腿) ├── LowerLeg_L(左小腿) ├── UpperLeg_R(右大腿) └── LowerLeg_R(右小腿)所有骨骼一律作为Soldier的平级子物体,不要嵌套父子关系。关节连接完全由后面的脚本处理,Transform 层级只管视觉摆放。
为了让模型看起来像人形,可以按下面的位置调整局部坐标:
| 骨骼名称 | Position (X, Y, Z) |
|---|---|
| Pelvis | (0, 1.3, 0) |
| Spine | (0, 1.9, 0) |
| Head | (0, 2.5, 0) |
| UpperArm_L | (-0.7, 2.0, 0) |
| LowerArm_L | (-0.7, 1.4, 0) |
| UpperArm_R | (0.7, 2.0, 0) |
| LowerArm_R | (0.7, 1.4, 0) |
| UpperLeg_L | (-0.25, 0.75, 0) |
| LowerLeg_L | (-0.25, 0.15, 0) |
| UpperLeg_R | (0.25, 0.75, 0) |
| LowerLeg_R | (0.25, 0.15, 0) |
4.2 一键生成物理骨骼
手动给 11 个物体添加刚体、碰撞体、关节很繁琐。下面是一个编辑器脚本,可以一键完成装配。
文件路径:Assets/Scripts/EditMode/SoftRagdollBuilder.cs
using System.Collections.Generic; using UnityEditor; using UnityEngine; public class SoftRagdollBuilder { [MenuItem("Tools/Build Soft Ragdoll")] public static void Build() { GameObject root = GameObject.Find("Soldier"); if (root == null) { Debug.LogError("场景中找不到名为 Soldier 的物体,请先创建骨架根节点。"); return; } string[] boneNames = { "Pelvis", "Spine", "Head", "UpperLeg_L", "LowerLeg_L", "UpperLeg_R", "LowerLeg_R", "UpperArm_L", "LowerArm_L", "UpperArm_R", "LowerArm_R" }; Dictionary<string, Transform> bones = new Dictionary<string, Transform>(); foreach (string name in boneNames) { Transform bone = root.transform.Find(name); if (bone == null) { Debug.LogWarning($"未找到骨骼:{name}"); continue; } bones[name] = bone; // 每个骨骼都是一个物理节点 if (bone.GetComponent<Rigidbody>() == null) { bone.gameObject.AddComponent<Rigidbody>(); } if (bone.GetComponent<Collider>() == null) { // 这里直接用 BoxCollider,方便匹配 Cube bone.gameObject.AddComponent<BoxCollider>(); } } // 定义父子连接关系 (string child, string parent)[] links = { ("Spine", "Pelvis"), ("Head", "Spine"), ("UpperLeg_L", "Pelvis"), ("LowerLeg_L", "UpperLeg_L"), ("UpperLeg_R", "Pelvis"), ("LowerLeg_R", "UpperLeg_R"), ("UpperArm_L", "Spine"), ("LowerArm_L", "UpperArm_L"), ("UpperArm_R", "Spine"), ("LowerArm_R", "UpperArm_R") }; foreach ((string child, string parent) link in links) { if (!bones.TryGetValue(link.child, out Transform child)) { continue; } if (!bones.TryGetValue(link.parent, out Transform parent)) { continue; } CharacterJoint joint = child.gameObject.AddComponent<CharacterJoint>(); joint.connectedBody = parent.GetComponent<Rigidbody>(); joint.autoConfigureConnectedAnchor = true; SoftJointLimit limit = new SoftJointLimit { limit = 45f }; joint.swing1Limit = limit; joint.swing2Limit = limit; } Debug.Log("软骨兵物理骨架装配完成!接下来可以挂上 JellyController 和 SoftStandIntent。"); } }这段脚本做了什么:
- 在
Soldier下按名字查找所有骨骼; - 为每个骨骼添加
Rigidbody和BoxCollider; - 按列表创建
CharacterJoint,把子骨骼连接到父骨骼的刚体上; - 设置摆动角度为 45 度,让骨骼具有一定的松弛感。
脚本不需要拖到场景中,只需要放在Assets目录下的Editor文件夹(如果不存在就新建一个),Unity 会自动识别编辑器脚本。然后点击顶部菜单Tools > Build Soft Ragdoll即可。
运行后,11 个骨骼都会带上物理组件,人物模型会直接从站立状态变成瘫软状态。
4.3 编写关节软化脚本
有了物理骨架,还需要让角色不断“软”下去。下面这个脚本会给每个刚体持续施加一个由 Perlin 噪声生成的随机扭矩,让身体始终处于不稳定的扭动状态。
文件路径:Assets/Scripts/JellyController.cs
using UnityEngine; public class JellyController : MonoBehaviour { [Header("刚体参数")] public float bodyMass = 0.5f; public float drag = 0.2f; public float angularDrag = 0.1f; [Header("关节参数")] public float swingLimit = 55f; [Header("随机扰动")] public float noiseStrength = 6f; public float noiseSpeed = 2f; private CharacterJoint[] _joints; private Rigidbody[] _bodies; private float _seed; private void Start() { _joints = GetComponentsInChildren<CharacterJoint>(); _bodies = GetComponentsInChildren<Rigidbody>(); _seed = Random.value * 100f; foreach (Rigidbody body in _bodies) { body.mass = bodyMass; body.drag = drag; body.angularDrag = angularDrag; body.solverIterations = 8; } foreach (CharacterJoint joint in _joints) { SoftJointLimit limit = new SoftJointLimit { limit = swingLimit }; joint.swing1Limit = limit; joint.swing2Limit = limit; joint.enableProjection = true; joint.projectionDistance = 0.05f; joint.projectionAngle = 10f; } } private void FixedUpdate() { int index = 0; foreach (Rigidbody body in _bodies) { float t = Time.fixedTime * noiseSpeed + _seed + index * 0.13f; Vector3 torque = new Vector3( Mathf.PerlinNoise(t, 0.31f) - 0.5f, Mathf.PerlinNoise(t + 1.7f, 0.57f) - 0.5f, Mathf.PerlinNoise(0.21f, t + 0.4f) - 0.5f ) * noiseStrength; body.AddTorque(torque, ForceMode.Force); index++; } } }关键点说明:
noiseStrength控制扰动强度。默认 6 代表一个比较柔和的“肌肉抽动”力度,数值过大会导致角色疯狂打转。swingLimit会在运行时覆盖关节的摆动角度,让身体更松弛。enableProjection是关节投影矫正,可以减少高变形时模型被拉伸撕裂的概率。- 扭矩在
FixedUpdate中施加,确保物理计算的稳定性。
把该脚本挂到Soldier物体上,点击 Play,你会看到模型开始向各个方向扭动,但此时它基本没有“站直”的意图,更像一摊被风吹动的软泥。
4.4 编写站立意图脚本
只有软还不够,软骨兵更准确的状态应该是“想站直却站不起来”。下面这个脚本记录每个骨骼的初始旋转,然后在物理帧中计算当前旋转与初始旋转的差值,并施加一个纠正扭矩。
文件路径:Assets/Scripts/SoftStandIntent.cs
using UnityEngine; public class SoftStandIntent : MonoBehaviour { [Header("恢复力度:越大越接近正常士兵")] public float torque = 8f; private struct BonePose { public Rigidbody body; public Quaternion initialRotation; } private BonePose[] _bones; private void Start() { Rigidbody[] bodies = GetComponentsInChildren<Rigidbody>(); _bones = new BonePose[bodies.Length]; for (int i = 0; i < bodies.Length; i++) { _bones[i] = new BonePose { body = bodies[i], initialRotation = bodies[i].transform.rotation }; } } private void FixedUpdate() { foreach (BonePose pose in _bones) { Quaternion delta = pose.initialRotation * Quaternion.Inverse(pose.body.transform.rotation); delta.ToAngleAxis(out float angle, out Vector3 axis); if (angle < 0.01f) { continue; } // 将角度差转换为矫正扭矩 Vector3 correctionTorque = axis.normalized * angle * Mathf.Deg2Rad * torque; pose.body.AddTorque(correctionTorque, ForceMode.Force); } } }原理拆解:
initialRotation记录骨骼生成时的世界旋转,因为是直立模型,所以这个旋转就是“默认站立姿态”。- 每物理帧计算当前旋转与初始旋转的差值,转成角坐标轴。
- 如果当前骨骼偏转 30 度,脚本就会产生一个把骨骼推回初始角度的扭矩。
torque越大,角色越硬;torque越小,角色越软。
把SoftStandIntent也挂到Soldier上,再次运行游戏。这时的效果应该是:身体在重力下倒下,但脚本会不断施加恢复力,导致角色像一条被电击的软体生物一样不停挣扎。
4.5 运行与验证
最终运行效果取决于参数组合。建议你在一开始就打开 Inspector,边运行边修改。
以下是推荐的一组初始值:
JellyController: bodyMass = 0.5 drag = 0.2 angularDrag = 0.1 swingLimit = 55 noiseStrength = 6 noiseSpeed = 2 SoftStandIntent: torque = 8使用这组参数,角色应该出现:
- 初始站立 0.5 秒左右后开始瘫软;
- 身体不停摆动、旋转;
- 偶尔有某个部位被拉回接近初始姿态;
- 整体视觉感受非常接近 TABS 里的搞笑单位。
如果角色完全站不住,或者倒地后没有任何挣扎,可以尝试调整:
| 现象 | 调整方式 |
|---|---|
| 倒地后完全不动 | 调大SoftStandIntent.torque,比如到 15 |
| 扭动太猛,像触电 | 降低noiseStrength,比如到 2 |
| 关节角度太硬 | 调大swingLimit到 70 |
| 整体漂移、下沉 | 调大bodyMass,或增加drag |
这一阶段的实验性很强,参数没有绝对标准,建议多试几组,观察不同参数组合产生的“性格”。
5. 常见问题与排查思路
5.1 角色抖动过于剧烈
角色疯狂抖动的常见原因有两个:
noiseStrength过大,导致每帧施加的扭矩远超关节承受范围。- 物理帧率不足,刚体之间的约束求解不够稳定。
优先把noiseStrength降低到 2 到 4 之间,同时检查 Time 面板中的Fixed Timestep。默认值是 0.02,约每秒 50 次物理更新。如果你在 Update 中给刚体施力,很容易出现不稳定,所以务必在FixedUpdate中操作物理刚体。
5.2 角色倒地后无法做出“挣扎”动作
如果角色倒下后像死了一样,说明站立意图太弱。可以把SoftStandIntent.torque从 8 提高到 15 左右。
注意这里有一个技巧:不要一开始就调很大,否则角色会直接站成笔直的木头人。正确做法是观察“倒在地上”的最大挣扎幅度,再慢慢增加扭矩,找到一个介于“站不稳”和“站得稳”之间的区间。
5.3 模型关节被大幅拉伸
当骨骼扭转超过一定角度时,CharacterJoint 的投影矫正可能无法完全约束刚体,会出现模型被“拉丝”的效果。
解决办法:
joint.enableProjection = true; joint.projectionDistance = 0.05f; joint.projectionAngle = 10f;把projectionDistance和projectionAngle调低,可以更积极地拉回关节位置,代价是物理表现会稍微僵硬一些。
5.4 人物模型下沉或者穿透地面
多半是 Collider 配置问题。由于每个骨骼都只添加了一个 BoxCollider,它的尺寸固定为 1x1x1,如果你的 Cube 缩放不是 1,则碰撞体与模型就不匹配。
你可以通过代码在生成时自动设置:
BoxCollider collider = bone.GetComponent<BoxCollider>(); collider.size = Vector3.one;再配合Physics.autoSyncTransforms和Physics.ContactOffset调整穿透问题。
5.5 同时存在多个角色时游戏变卡
每增加一个软骨兵,就会增加大量刚体和关节,物理引擎需要处理的海量约束也随之上升。
优化方向是:
- 角色数量不要超过 10 到 20 个;
- 每个骨骼尽量使用简单 Collider,比如基础几何体;
- 对于远处角色,可以关闭刚体模拟,只保留动画;
- 考虑使用 Unity Job System 和 Burst 做自定义物理模拟。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 抖动剧烈 | 噪声扭矩过大 | 降低 noiseStrength |
| 倒地后不挣扎 | 恢复扭矩过小 | 调大 SoftStandIntent.torque |
| 模型撕裂 | 关节投影矫正过弱 | 调小 projectionDistance |
| 穿透地面 | Collider 尺寸不匹配 | 重建 Collider 或调 ContactOffset |
| 多角色卡顿 | 物理约束过多 | 降低角色数量、简化 Collider |
6. 最佳实践与工程建议
6.1 把参数做成可配置资产
在 TABS 风格的项目里,每个单位都应该有自己的“软硬度”,而不是一套参数走天下。推荐把关节参数封装成 ScriptableObject,这样策划和美术可以直接在编辑器里配置单位类型。
[CreateAssetMenu(fileName = "SoftBoneConfig", menuName = "TABS/SoftBoneConfig")] public class SoftBoneConfig : ScriptableObject { public float mass = 0.5f; public float drag = 0.2f; public float swingLimit = 55f; public float standTorque = 8f; public float noiseStrength = 6f; }把配置对象传入JellyController和SoftStandIntent,运行时读取,方便批量调整。
6.2 用意图层和物理层分离的方式设计 AI
TABS 的精髓在于,AI 不直接控制角色骨骼,而是输出“意图”。你可以把意图理解为:
- 攻击目标位置;
- 移动方向;
- 需要保持的基本姿态。
物理层根据意图,结合当前身体状态,逐步施加扭矩。这样设计的好处是,开发人员不需要手动编排摔倒动画,意外碰撞也会自动产生真实反馈。
6.3 不要用纯随机数做扰动
纯随机力的表现往往是高频抖动,看起来非常假。用 Perlin 噪声代替随机数,能营造更平滑的“肌肉抽搐”感。如果希望不同角色表现不同,可以在初始化时给每个角色生成一个随机种子。
6.4 性能与稳定性的平衡
物理模拟的稳定性和性能通常是一对矛盾。每增加一个关节,都会消耗额外的物理求解时间。对于移动端项目,需要注意:
- 控制单场景布娃娃数量;
- 降低关节数量,比如省略手指、脚趾等不必要骨骼;
- 非战斗状态时关闭布娃娃模拟;
- 使用
Physics.autoSimulation = false做手动帧率管理。
6.5 调试物理效果的两个小技巧
第一,使用Debug.DrawRay画出扭矩方向,可以直观看到每个骨骼的受力情况:
Debug.DrawRay(pose.body.transform.position, velocity, Color.green);第二,在 Inspector 中实时调整参数时,不要在运行态频繁切换“暂停/继续”,否则物理引擎的插值可能会产生异常速度,导致角色突然飞出去。遇到这种情况,直接停止运行再重新开始即可。
7. 总结与学习路线
通过这篇文章,你应该能掌握以下内容:
- 理解软骨兵的本质是物理驱动动画,而不是传统骨骼动画;
- 知道 TABS 式搞笑单位所需的三大要素:松散关节、弱回正、持续扰动;
- 能够在 Unity 中自己搭建一套完整的布娃娃骨架;
- 会使用
CharacterJoint、Rigidbody和噪声扭矩组合出“软体单位”; - 遇到抖动、穿透、撕裂等问题时,知道从哪里排查。
如果想继续深入,可以尝试以下几个方向:
- 研究
CharacterJoint的LowAngularXLimit、HighAngularXLimit、swingAxis等参数,做出不对称肢体行为; - 学习程序化动画中的 IK(反向运动学),结合物理模拟实现更自然的攻击动作;
- 研究 TABS 单兵预算和招募机制,把它引到自己的游戏数值设计中;
- 尝试用 Unity DOTS 做大量软骨兵同屏模拟。
做物理驱动角色最有趣的地方,在于你永远无法完全预测角色的下一秒动作。把关节调松一点,把扭矩调小一点,你会发现角色本身就在替你“整活”。希望这套实现思路能帮你在自己的游戏里做出更多有趣的奇奇怪怪的软骨兵。