公司动态

Unity刚体物理优化实战:从基础属性到高速碰撞解决方案

📅 2026/8/1 5:44:49
Unity刚体物理优化实战:从基础属性到高速碰撞解决方案
1. 项目概述为什么刚体是物理交互的基石在Unity里做游戏尤其是涉及到任何形式的物理交互——比如一个箱子被推倒、一个球被弹起、一个角色被爆炸冲击波掀飞——你几乎都绕不开一个组件Rigidbody也就是刚体。很多新手开发者包括几年前的我自己都曾把它简单地理解为一个“让物体掉下来的开关”。勾上Use Gravity物体就往下掉不勾它就飘着。这没错但这仅仅是它能力的冰山一角。这个组件真正的威力在于它赋予了游戏对象遵循牛顿力学的“灵魂”。一个没有刚体的物体在Unity的物理世界里就像幽灵可以穿透一切或者被其他物体穿透。而一旦挂上了刚体它就有了质量、速度、角速度会受重力影响会与其他带碰撞器的物体发生碰撞并产生真实的反馈。从《愤怒的小鸟》里抛物线飞出的鸟儿到《艾尔登法环》里被击飞的敌人其背后最核心的驱动逻辑都离不开刚体系统。然而随着项目复杂度的提升特别是当场景中同时存在大量高速运动的刚体比如弹幕射击游戏、赛车游戏里的碎片、物理解谜中高速下落的机关时问题就来了。你会发现物体经常“穿模”——明明应该撞上的子弹却穿过了目标高速移动的平台会把站在上面的角色甩飞或者物理计算突然变得极其卡顿帧率骤降。这些问题本质上都是对刚体属性理解不透彻、缺乏优化策略导致的。所以这次我们不谈空洞的理论直接切入实战。我会结合自己踩过的无数个坑从最基础的属性参数讲起一步步拆解到如何应对高速碰撞这种“老大难”问题。目标很明确让你不仅知道每个参数是干什么的更要知道在什么场景下该怎么调以及调了之后可能会带来什么副作用。最终你能胸有成竹地构建出既稳定又高效的物理交互。2. 刚体核心属性深度解析与实战调参刚体组件面板上那一排参数每一个都不是摆设。调错一个可能整个物理感觉就全变了。我们挨个拆开看重点讲“为什么”要这么设计以及“怎么调”才合适。2.1 质量、阻尼与物理材质构建运动手感的基础质量 (Mass)这是最容易被误解的参数之一。它的单位是“千克”但你在Unity里设置一个“1”并不代表现实中的1公斤。它是一个相对值。物理引擎关心的是物体之间的质量比。核心原则避免使用极端质量值。我见过有人把主角质量设为0.1把一堵墙的质量设为10000希望墙纹丝不动。这会导致严重的数值不稳定轻则抖动重则物体被“炸飞”。一个良好的实践是将场景中“标准”物体的质量设为1比如一个角色、一个标准大小的木箱其他物体以此为基准进行缩放。一个石头大概2-3一个纸箱0.5一辆车5-10。保持质量在0.1到10的范围内是稳定计算的黄金法则。线性阻尼 (Drag) 与 角阻尼 (Angular Drag)你可以把它们理解为“空气阻力”。Drag影响物体直线运动速度的衰减Angular Drag影响旋转速度的衰减。Drag 0: 物体在真空中运动除非碰撞或受力否则永不停止。适合太空射击游戏中的子弹。Drag 1: 物体会很快停下来。适合模拟水下或粘稠环境中的物体。Angular Drag 0.05 (默认): 物体旋转后会转很久。如果你希望一个被击中的物体旋转几圈就停下可以适当调高这个值比如调到0.5或1。实战技巧对于玩家控制的角色通常我们会通过脚本施加力来移动而不是直接设置速度。这时一个适中的Drag比如2-5可以让你在松开按键后角色能平滑地滑行一小段距离停下手感会好很多。如果Drag为0一松手角色就立刻定住感觉很生硬。物理材质 (Physic Material)这是定义碰撞表面特性的资产。它不挂在刚体上而是挂在物体碰撞器组件的Material槽里。它主要控制两个东西摩擦力 (Friction)和弹性 (Bounciness)。摩擦力又分为静态摩擦力Static Friction和动态摩擦力Dynamic Friction。简单理解静态摩擦力是让物体从静止到开始滑动需要的力动态摩擦力是滑动起来后持续的阻力。冰面的两者都接近0橡胶面的两者都接近1。弹性0表示完全非弹性碰撞像一团泥巴撞上后粘住1表示完全弹性碰撞理想状态能量无损失。通常设置在0到0.8之间。一个篮球的弹性可能在0.7左右。避坑指南不要滥用高弹性我曾在一个解谜游戏里把机关球的弹性设为0.9结果小球在密闭空间里疯狂弹跳根本停不下来CPU占用率飙升。对于需要可控的物理环境弹性最好控制在0.6以下。另外物理材质的“摩擦力结合模式”和“弹性结合模式”也至关重要。默认是Average即取两个碰撞物体材质的平均值。如果你希望某个表面永远是“光滑的”比如冰面无论撞上什么都很滑可以将其材质模式设为Multiply并将摩擦力设为极低如0.1这样最终摩擦力会是两者相乘结果更小。2.2 约束、插值与碰撞检测高级控制与视觉平滑约束 (Constraints)这是防止物体“乱动”的神器。勾选对应的Freeze Position或Freeze Rotation就能在对应的世界坐标轴X, Y, Z上锁定位置或旋转。典型应用12D游戏。在一个2D横版游戏中你肯定不希望角色或敌人有Z轴的位置移动或X、Y轴的旋转。这时就冻结Position Z和Rotation X, Rotation Y。典型应用2平台。一个只能左右移动的升降平台可以冻结其Y、Z轴的位置和所有旋转。典型应用3布娃娃系统。在配置人形角色的布娃娃时通常需要先冻结所有约束然后通过代码在特定时刻如死亡时解除冻结让物理接管这样角色才能自然倒下。插值 (Interpolation) 与 外推 (Extrapolation)物理计算和画面渲染是不同步的。物理以固定时间步长默认为0.02秒即50Hz运行而渲染帧率是波动的。这会导致高速运动的刚体在画面上看起来有“抖动”或“跳帧”。None (默认)不处理。在低速时没问题高速时抖动明显。Interpolate这是最常用、最推荐的模式。刚体会根据上一帧和当前帧的物理状态平滑地插值出渲染时的位置。能极大改善视觉平滑度尤其是对于摄像机跟随的玩家角色。Extrapolate根据上一帧的物理状态预测当前渲染帧的位置。在物理帧率稳定且物体运动规律时效果好但一旦发生意外碰撞预测错误会出现明显的修正跳跃感觉更糟。通常不建议使用。重要提醒插值会引入一个渲染帧的延迟。对于需要极度精确、即时反应的竞技游戏如格斗游戏中对打击帧的判断可能需要权衡。但对于绝大多数情况无脑为玩家角色和主要运动物体开启Interpolate是提升视觉品质成本最低的方式。碰撞检测模式 (Collision Detection)这是解决“穿模”问题的第一道关卡也是性能影响的关键。Discrete (离散默认)只在每个物理时间步长检测碰撞。这是性能最好的模式但也是高速物体穿模的元凶。如果物体在一帧内移动的距离超过了自身碰撞体的尺寸它就可能从另一个碰撞体的“一边”直接“穿越”到“另一边”引擎根本来不及检测。只适用于低速运动的物体。Continuous (连续)针对动态刚体受物理驱动的刚体与静态碰撞器Static Collider之间的碰撞进行连续检测。能有效防止高速物体穿过静态环境如墙壁、地面。性能开销中等是解决高速子弹打不中墙壁这类问题的首选。Continuous Dynamic (连续动态)在动态刚体之间也进行连续检测。这是最精确的模式能防止两个高速运动的物体比如两颗子弹相互穿过。但这也是性能开销最大的模式滥用会导致帧率暴跌。配置策略场景中静止的建筑物、地形碰撞器设为Static刚体用Discrete或者干脆不用刚体。玩家、敌人、可动物体刚体设为Continuous。这能保证它们不会穿过静态环境。高速运动的子弹、抛射体刚体必须设为Continuous Dynamic。同时它们的碰撞器形状应尽量简单用Sphere或Capsule避免复杂MeshCollider并且质量可以设得很小如0.01。大量低速运动的碎片、粒子使用Discrete以节省性能。3. 高速碰撞优化实战从理论到解决方案当物体的速度超过一定阈值这个阈值和物体大小、物理时间步长有关默认的离散检测就会失效。优化高速碰撞是一个系统工程需要从多个层面入手。3.1 连续碰撞检测的局限性与射线补偿法虽然Continuous Dynamic模式是解决穿模的官方方案但它有两个致命缺点性能代价高每个使用此模式的刚体都会显著增加物理计算量。超过几十个帧率就可能吃紧。对薄物体不友好对于非常薄的碰撞体比如一把剑的剑身即使开启了连续检测仍有小概率在特定角度下穿过。因此我们需要更经济的补充方案。“射线补偿法”是我在弹幕游戏中最常用的手法。原理在物体移动前不是只依赖物理引擎的碰撞检测而是主动向移动方向发射一条射线射线的长度等于本帧速度 * Time.deltaTime。如果射线击中了目标则在碰撞发生前就处理命中逻辑如扣血、播放特效并阻止物体继续移动。public class Projectile : MonoBehaviour { public float speed 50f; public LayerMask targetLayer; private Rigidbody rb; private Vector3 lastPosition; void Start() { rb GetComponentRigidbody(); rb.collisionDetectionMode CollisionDetectionMode.Discrete; // 可以用性能更低的模式 lastPosition transform.position; } void FixedUpdate() { Vector3 moveVector transform.forward * speed * Time.fixedDeltaTime; float moveDistance moveVector.magnitude; // 向前发射射线 if (Physics.Raycast(lastPosition, transform.forward, out RaycastHit hit, moveDistance, targetLayer)) { // 提前处理命中逻辑 OnHitTarget(hit.collider); // 可选将物体移动到碰撞点 transform.position hit.point; // 销毁或禁用物体 gameObject.SetActive(false); return; } // 如果射线没命中再用刚体移动或直接更新位置 rb.MovePosition(rb.position moveVector); lastPosition rb.position; } void OnHitTarget(Collider target) { // 你的命中逻辑造成伤害、播放音效等 Debug.Log($Hit: {target.name}); } }优势性能可控射线检测的成本远低于连续物理检测。你可以精确控制哪些层LayerMask需要进行这种高精度检测。绝对可靠只要射线长度覆盖了本帧最大移动距离就绝不会穿模。逻辑前置可以在物理碰撞发生前就处理游戏逻辑响应更快。3.2 增大碰撞体与动态调整时间步长增大碰撞体 (增大碰撞器尺寸)这是一个简单粗暴但极其有效的方法。既然穿模是因为物体一帧移动距离超过了自身尺寸那就把它的“感应范围”变大。做法为高速物体添加一个比其视觉模型稍大的简单碰撞体如Sphere Collider。例如一颗视觉上很细的子弹可以套一个半径大一些的球体碰撞器。注意这会导致碰撞检测“提前”发生物体在视觉上还没接触时就触发碰撞。你需要通过调试来找到一个平衡点让这个“提前量”在视觉上可以接受比如在特效上做文章。动态调整固定时间步长 (Fixed Timestep)在Edit - Project Settings - Time中Fixed Timestep默认是0.02秒50次/秒。这个值决定了物理更新的频率。降低它如改为0.01秒物理更新频率提高到100次/秒。这意味着每帧物理计算次数更多物体每步移动的距离更短穿模概率自然降低。但是这会直接让物理计算的CPU开销翻倍除非你的游戏物理对象很少否则慎用。实战策略不要全局修改。可以考虑为高速物体单独创建一个物理更新层。但Unity原生不支持。更可行的方案是对于极端高速的物体如激光放弃使用物理引擎进行移动和碰撞转而使用上面提到的射线法或手动检测并用自己的逻辑更新位置。3.3 分层管理与碰撞矩阵优化当场景中有成百上千个物理对象时无差别的碰撞检测是性能杀手。Unity的图层Layer和碰撞矩阵Collision Matrix是管理碰撞关系的核心工具。优化步骤定义清晰的图层不要所有物体都放在Default层。例如Player(玩家)Enemy(敌人)PlayerProjectile(玩家子弹)EnemyProjectile(敌人子弹)Environment(静态环境)PhysicsDebris(物理碎片低优先级碰撞)IgnoreRaycast(Unity内置用于忽略射线)配置碰撞矩阵进入Edit - Project Settings - Physics或Physics 2D。这里有一个矩阵定义了哪些层之间会进行碰撞检测。取消不必要的勾选这是性能提升的关键。比如PlayerProjectile层不需要和EnemyProjectile层碰撞子弹互相对穿在游戏中很常见也不需要和PhysicsDebris层碰撞。Environment层通常只需要和动态物体碰撞环境层之间不需要相互碰撞因为都是静态的。为高速物体层精简碰撞关系为PlayerProjectile和EnemyProjectile层只勾选它们需要命中的层如Enemy,Player,Environment其他全部取消。效果通过精细的图层管理物理引擎每帧需要检测的碰撞对数量会大幅下降尤其是在对象数量多的时候性能提升立竿见影。同时逻辑也更清晰避免了“子弹打中另一颗无关子弹”这种意外情况。4. 实战案例构建一个稳定高效的抛射物系统让我们综合运用以上所有知识设计一个用于太空射击游戏的抛射物系统。需求是子弹速度极快每秒200单位以上数量多同屏上百发必须保证100%命中检测且性能开销可控。4.1 系统架构设计我们放弃使用Continuous Dynamic模式因为它无法承受上百个对象的开销。采用“离散刚体 射线补偿 图层过滤”的混合方案。对象池管理使用Object Pool复用子弹对象避免频繁的Instantiate和Destroy这是性能基础。刚体配置Mass: 0.01 (极轻)Drag: 0 (太空无阻力)Collision Detection:Discrete(为了性能)Interpolate:Interpolate(保证平滑视觉)冻结所有旋转约束子弹不需要旋转碰撞器配置使用Sphere Collider半径比视觉模型略大10%-20%以补偿离散检测。图层配置子弹放在PlayerProjectile层。在碰撞矩阵中该层仅与Enemy、Environment、某些可破坏物体层碰撞。4.2 核心逻辑实现public class HighSpeedProjectile : MonoBehaviour { public float speed 250f; public float damage 10f; public LayerMask hitLayerMask; // 在Inspector中指定可击中的层 public GameObject hitEffectPrefab; private Rigidbody rb; private Vector3 previousPosition; private bool isActive false; void Awake() { rb GetComponentRigidbody(); rb.isKinematic false; // 我们仍用物理移动但检测靠自己 rb.collisionDetectionMode CollisionDetectionMode.Discrete; } public void Launch(Vector3 direction) { isActive true; rb.velocity direction.normalized * speed; previousPosition transform.position; gameObject.SetActive(true); } void FixedUpdate() { if (!isActive) return; Vector3 currentPosition transform.position; Vector3 moveDirection currentPosition - previousPosition; float moveDistance moveDirection.magnitude; if (moveDistance 0.001f) // 有移动才检测 { // 关键使用Raycast进行高精度检测 if (Physics.Raycast(previousPosition, moveDirection.normalized, out RaycastHit hit, moveDistance, hitLayerMask)) { // 命中处理 ProcessHit(hit); ReturnToPool(); return; // 提前返回不再执行物理更新 } } // 如果射线未命中则更新记录的位置让物理引擎继续工作处理可能与其他非目标层的碰撞如特效触发器 previousPosition currentPosition; } void ProcessHit(RaycastHit hit) { // 生成命中特效例如在hit.point位置朝向hit.normal if (hitEffectPrefab ! null) { Instantiate(hitEffectPrefab, hit.point, Quaternion.LookRotation(hit.normal)); } // 尝试造成伤害 EnemyHealth health hit.collider.GetComponentEnemyHealth(); if (health ! null) { health.TakeDamage(damage); } // 可以添加其他逻辑如播放音效、传递命中信息等 } void OnDisable() { // 被对象池回收时重置状态 isActive false; rb.velocity Vector3.zero; } void ReturnToPool() { // 这里调用你的对象池回收方法 // ObjectPoolManager.Instance.ReturnProjectile(this); gameObject.SetActive(false); } }4.3 性能与效果权衡这个方案的精髓在于性能刚体使用Discrete模式物理开销最小。昂贵的连续检测被替换为可控的、每帧一次的Raycast。通过LayerMask射线只检测需要命中的层进一步优化。精度射线检测基于上一帧和本帧的实际位置只要FixedUpdate频率稳定就能覆盖全部移动路径实现100%无穿模。兼容性我们仍然保留了刚体的物理移动和离散碰撞检测。这意味着子弹如果与一些我们不想用射线处理的物体比如放在IgnoreRaycast层的粒子系统触发器、或者用于播放环境音效的触发器发生碰撞物理引擎仍然可以正常处理OnTriggerEnter等事件保持了系统的灵活性。5. 常见疑难杂症与排查清单即使配置得当物理系统依然可能出现诡异的问题。下面是我总结的一些“坑”和排查思路。5.1 刚体莫名抖动或飞走可能原因1碰撞体重叠。在游戏开始时两个带有碰撞体的物体初始位置就重叠在一起物理引擎会试图用巨大的力将它们分开导致“爆炸”。检查确保场景中所有静态和动态物体的碰撞体在初始状态下没有交叉。解决调整初始位置或使用Physics.IgnoreCollision在开始时暂时忽略特定碰撞对。可能原因2极端质量比。如前所述一个质量0.01的物体撞上一个质量1000的物体计算容易不稳定。检查检查场景中所有刚体的Mass值确保它们在一个合理的范围内0.1-10是安全区。可能原因3每帧多次添加力。在Update中调用AddForce而Update的调用频率是不固定的会导致力被重复添加产生不可预测的行为。解决所有对刚体的物理操作AddForce, AddTorque, velocity赋值等都必须在FixedUpdate中进行这是铁律。5.2 碰撞检测有时失灵可能原因1高速移动。这是最常见原因物体使用了Discrete模式且速度过快。排查检查该刚体的Collision Detection模式。对于任何速度可能较快的物体至少使用Continuous。进阶排查计算物体的“每帧最大移动距离”速度 * Time.fixedDeltaTime并与其碰撞体的尺寸如Sphere的半径Box的最短边对比。如果移动距离大于尺寸穿模风险极高。可能原因2碰撞体被禁用或层级错误。检查确认碰撞器组件Collider的勾选是开启的。确认两个物体的图层在碰撞矩阵中是相互勾选的。可能原因3触发器模式。如果碰撞器勾选了Is Trigger它将不会产生物理碰撞反馈如弹开只会触发OnTriggerXXX事件。你可能误以为碰撞没发生。区分需要物理反馈力、阻挡用普通碰撞器只需要检测重叠事件用触发器。5.3 性能突然下降可能原因1大量刚体使用Continuous Dynamic。排查在Edit - Project Settings - Physics底部勾选Enable Debug然后在游戏运行时查看物理引擎的耗时。使用Continuous Dynamic的刚体会被重点标记。解决严格按照本章第2节和第3节的策略为物体分配合适的碰撞检测模式。能用Discrete就不用Continuous能用Continuous就不用Continuous Dynamic。可能原因2复杂网格碰撞器过多。MeshCollider是性能杀手尤其是高精度的网格。解决对于不需要完美贴合形状的物体用Box、Sphere、Capsule等基本碰撞器组合替代。对于复杂静态地形考虑使用MeshCollider但勾选Convex凸包简化或使用专门的地形碰撞系统。可能原因3物理更新频率过高。Fixed Timestep设置得太小。检查在Time设置中查看Fixed Timestep值。除非有特殊需求如高精度物理模拟否则不要低于0.01。监控在Stats面板中查看Fixed Update per Second是否异常高。5.4 物体旋转控制不精准可能原因直接修改Transform.rotation。如果你在Update中直接修改物体的旋转会与物理引擎计算的旋转冲突导致抽搐。黄金法则对于带有刚体且需要物理交互的物体所有位置和旋转的修改都应通过刚体进行。正确做法使用rb.MoveRotation(Quaternion targetRotation)来平滑旋转或者对刚体施加扭矩rb.AddTorque()来让物理引擎驱动旋转。如果必须瞬间设定旋转且希望物理引擎尊重这个新状态可以在修改后调用rb.velocity Vector3.zero; rb.angularVelocity Vector3.zero;来清除累积的物理速度。