1. 项目概述为什么Unity物理系统是项目成败的关键做Unity开发这些年我踩过最大的坑往往不是那些炫酷的渲染效果或者复杂的业务逻辑而是物理系统。一个角色莫名其妙地穿墙而过一个箱子在空中抖动不止或者两个物体碰撞时爆发出诡异的能量——这些看似不起眼的小问题轻则影响游戏体验重则直接导致项目延期甚至回炉重造。物理系统尤其是刚体Rigidbody和碰撞Collision相关的部分是游戏世界真实感与稳定性的基石但它就像一座冰山表面参数简单水下却藏着无数需要精确调校的细节。这篇指南就是基于我过去几年在多个商业项目中与Unity物理引擎“搏斗”积累下来的实战经验。它不是官方文档的复述而是聚焦于那些文档里一笔带过、但实际开发中会让你头疼不已的“坑点”。我们将深入刚体碰撞的底层逻辑拆解物理材质Physics Material中每一个参数的真实含义并结合2022年及之后版本Unity的一些变化提供一套可落地、可排查的解决方案。无论你是在开发一款需要精确物理反馈的模拟游戏还是一个要求角色移动丝滑的ARPG理解并驾驭好这套系统都能让你的项目稳定性提升一个量级。2. 刚体Rigidbody核心参数深度解析与避坑刚体组件是物理系统的灵魂它让一个GameObject受物理引擎驱动。但很多开发者只是简单地勾选“Use Gravity”和“Is Kinematic”就了事殊不知下面每一个参数都关联着性能与表现。2.1 质量Mass、阻力Drag/Angular Drag与重力Use Gravity的协同作用质量是物理计算的基础单位但它不是重量。在Unity的默认物理环境下地球重力加速度9.81一个质量为1的物体其“重量感”大致相当于1公斤。但这里第一个坑就来了不要随意设置过小或过大的质量值。我曾在一个项目中将子弹的质量设为0.01结果发现它几乎无法撞动任何物体而将一个巨型BOSS的质量设为10000导致它与其他物体碰撞时计算极其不稳定产生剧烈抖动。一个实用的经验法则是将你场景中主要交互物体的质量范围控制在0.5到10之间并保持它们之间有合理的比例关系比如一个箱子质量是2角色质量是1汽车质量是5。阻力Drag和角阻力Angular Drag控制物体在移动和旋转中受到的“空气阻力”。它们对于模拟真实感至关重要。Drag为0时物体会在无外力作用下永远运动太空环境设为5以上物体会快速停下适合模拟在水中或泥泞中的运动。角阻力同理控制旋转停止的快慢。常见的误区是忽略角阻力导致物体碰撞后像陀螺一样旋转不停。我的建议是对于大多数地面移动的物体如角色、车辆将角阻力设置为比线性阻力稍大例如Drag1, Angular Drag2可以更快地稳定旋转状态避免滑稽的“打转”现象。“Use Gravity”这个勾选框看似简单但结合“Is Kinematic”会产生关键影响。一个动态刚体非Kinematic勾选重力会受重力下落。而一个运动学刚体Kinematic勾选重力是无效的因为它不受物理力驱动。这里有个高级技巧你可以通过脚本在运行时动态切换“Is Kinematic”状态。比如一个被玩家拾起的物体你可以先将其设为Kinematic跟随玩家手部移动避免物理计算当玩家扔出时再取消Kinematic并施加一个力让它进入自然的抛物线运动。这比单纯用Translate移动然后切换要稳定得多。2.2 插值Interpolation与碰撞检测Collision Detection模式的选择这是影响视觉平滑度和物理稳定性的关键设置也是新手最容易配置错误的地方。插值Interpolation物理计算是按固定时间步长Fixed Timestep进行的而渲染帧率Update是波动的。这会导致基于物理运动的物体在渲染时出现“卡顿”或“抖动”。插值就是为了解决这个问题。它有“None”、“Interpolate”和“Extrapolate”三种模式。None无插值。物理更新直接反映在渲染帧在帧率波动时必然抖动。仅推荐用于几乎静止或对平滑度无要求的背景物体。Interpolate插值基于上一帧和当前帧的物理状态进行平滑。这是最常用、最稳定的选择尤其适用于主角摄像机跟随的物体如玩家角色、主要载具。它能提供丝滑的视觉运动。Extrapolate外推基于当前帧和物理计算的速度预测下一帧的位置。这能减少输入延迟感觉更“跟手”但有个致命缺点当物体发生意外碰撞如突然撞墙时由于是预测位置可能会发生短暂的穿透然后再被物理引擎纠正视觉上会有“抖动”或“回弹”。因此Extrapolate仅推荐用于高速运动且碰撞简单的物体比如射击游戏中的子弹。碰撞检测Collision Detection决定了物理引擎如何检测碰撞。模式选择错误是导致“穿模”的罪魁祸首。Discrete离散默认模式。只在每个物理时间步长检测碰撞。如果物体速度非常快比如子弹它可能在一个时间步内就从墙的一侧运动到了另一侧引擎会认为它没有与墙发生接触从而“穿墙而过”。仅适用于低速运动的物体。Continuous连续对动态刚体非Kinematic进行连续的碰撞检测能有效防止高速穿透。但这是以巨大的性能开销为代价的。绝对不要给场景中大量物体设置此模式会导致帧率骤降。Continuous Dynamic连续动态这是最需要理解的模式。它本身是“Continuous”的一种优化。一个设置为Continuous Dynamic的刚体在与任何碰撞体交互时都会进行连续检测。而一个设置为Continuous的刚体只在与其他Continuous或Continuous Dynamic刚体交互时才会触发连续检测。最佳实践是给你场景中少数高速且重要的运动物体如玩家发射的炮弹、高速移动的玩家角色设置为Continuous Dynamic给可能被这些高速物体撞击的静态或重要物体如墙壁、Boss设置为Continuous其他绝大多数物体保持Discrete。这样能在性能和效果间取得最佳平衡。避坑提示永远不要给静态碰撞体Static Collider即没有刚体的物体或运动学刚体Kinematic Rigidbody设置Continuous或Continuous Dynamic模式这没有任何效果且浪费资源。连续检测只对非运动学的动态刚体有意义。2.3 约束Constraints与休眠Sleeping机制的应用场景约束用于冻结刚体在某个或某些轴上的运动或旋转这是实现特定物理行为的神器。冻结位置Freeze Position比如一个2D横版游戏的玩家你通常需要冻结其Z轴位置防止它意外“浮”出屏幕平面。再比如一个只能左右移动的滑块就冻结Y和Z轴。冻结旋转Freeze Rotation这是解决许多诡异问题的关键。一个在地面上滑行的箱子如果不想它因为地面不平而东倒西歪就冻结其X和Z轴旋转在3D中只允许绕垂直轴Y轴旋转如果需要的话。对于第一人称射击游戏的枪械模型你可能需要冻结所有旋转让它始终跟随摄像机方向而不受物理碰撞影响。休眠Sleeping机制是物理引擎重要的性能优化手段。当一个移动的物体速度降到几乎为零低于某个睡眠阈值且一段时间内没有受到外力物理引擎会将其置为“休眠”状态不再计算它的物理更新直到有新的力或碰撞唤醒它。这能极大减少CPU开销。问题在于有时我们不希望物体休眠。例如需要持续检测的触发器如果一个物体上有触发器Trigger且需要持续检测玩家进入一旦它休眠触发器检测也会停止。这时需要勾选刚体上的“Sleeping Mode”为“Never Sleep”。微小的持续运动比如一个受风力轻微晃动的旗帜其速度可能永远达不到唤醒阈值导致看起来“僵住”。此时也需要禁止休眠或者通过脚本定期施加一个微小的唤醒力。3. 碰撞体Collider配置与层级矩阵Layer Collision Matrix精讲刚体定义了物体的物理属性而碰撞体Collider则定义了它的形状和边界。两者的配合决定了碰撞如何发生。3.1 基础碰撞体类型选型与性能考量Unity提供了多种原生碰撞体选择不当会影响性能和效果。Box/Sphere/Capsule Collider盒/球/胶囊体这些是原始碰撞体Primitive Colliders。它们的计算效率最高应作为首选。胶囊体特别适合用于角色控制器因为它能很好地处理斜坡和台阶且没有方形的棱角导致的卡顿感。Mesh Collider网格碰撞体使用模型的网格数据作为碰撞形状。这是最精确的但也是性能开销最大的。它应该仅用于场景中复杂且静态的几何体如复杂的地形、岩石并且务必勾选“Convex”凸包选项对于非静态物体则必须勾选。一个复杂的凹面Mesh Collider在计算碰撞时会极其昂贵。Terrain Collider地形碰撞体专为地形系统优化性能很好用于大地形。Wheel Collider车轮碰撞体专为车辆模拟设计内部有复杂的悬架和摩擦力模型不要试图用其他碰撞体模拟车轮。重要原则能用简单碰撞体组合Compound Colliders解决的绝不用Mesh Collider。例如一个桌子可以用一个Box Collider做桌面四个细长的Box Collider做桌腿来组合实现。这比使用一个完整的Mesh Collider高效得多。3.2 触发器Is Trigger与碰撞消息的收发机制勾选“Is Trigger”后碰撞体将不再产生物理阻挡效果而是变成一个“感应区域”用于触发游戏逻辑。这是实现技能范围、拾取物品、事件区域的核心。这里的关键在于理解不同的消息MessageOnTriggerEnter/Stay/Exit当另一个碰撞体进入/停留/离开触发器时在触发器所属的GameObject上调用。这是最常用的。OnCollisionEnter/Stay/Exit当两个非触发器碰撞体发生物理碰撞时在双方GameObject上调用。一个常见的坑是混淆这两者。记住只要有一方是触发器就会触发OnTriggerXXX系列函数只有双方都不是触发器才会触发OnCollisionXXX系列函数。消息接收依赖于GameObject上有相应的脚本组件。如果消息没有被处理它不会报错只是静默消失。因此确保你的逻辑脚本挂载在正确的对象上并且函数名拼写完全正确包括大小写。3.3 图层与碰撞矩阵Layer Collision Matrix的工程级管理策略这是管理大型项目物理交互的基石但很多团队初期忽视它后期就会陷入“屎山”代码和莫名其妙的碰撞失效中。Unity的图层Layer系统不仅用于渲染更用于物理过滤。你可以在Edit - Project Settings - Physics (或 Physics 2D)中打开碰撞矩阵Collision Matrix。这是一个NxN的表格定义了哪些图层之间的物体会发生碰撞/触发检测。最佳实践规划专属物理图层不要使用默认的“Default”层处理一切。至少创建以下专用层Player玩家角色。Enemy敌人。PlayerProjectile玩家发射物。EnemyProjectile敌人发射物。Environment静态环境墙壁、地面。Interactable可交互物品宝箱、开关。IgnoreRaycast这个内置层常用于不需要射线检测的物体。在碰撞矩阵中精细配置根据游戏逻辑有选择地启用或禁用碰撞。例如Player层和Enemy层碰撞发生物理阻挡。Player层和EnemyProjectile层碰撞子弹击中玩家。PlayerProjectile层和Enemy层碰撞。PlayerProjectile层和Player层不碰撞避免误伤自己。PlayerProjectile层和EnemyProjectile层不碰撞子弹对撞的逻辑如果需要可以单独用触发器处理。Environment层和所有其他层都碰撞除了可能需要忽略的如IgnoreRaycast。通过矩阵配置你可以完全用数据驱动复杂的碰撞关系无需在代码中写一堆if判断逻辑清晰且性能最优。当出现碰撞异常时首先检查碰撞矩阵和物体图层设置能解决80%的问题。4. 物理材质Physics Material参数全解与实战调校物理材质用于定义碰撞体表面的摩擦力和弹性反弹效果。它看似只有两个主要参数但微小的调整会带来天差地别的感觉。4.1 动态/静态摩擦力Dynamic/Static Friction与摩擦组合模式静态摩擦力Static Friction物体从静止到开始运动所需要克服的摩擦力。值越大越难推动。动态摩擦力Dynamic Friction物体在运动过程中受到的持续摩擦力。值越大运动减速越快。Unity使用一个“摩擦系数”的概念。当两个表面接触时会基于两者各自的物理材质通过一个“摩擦组合器”Friction Combine模式来计算最终的摩擦系数。模式有四种Average平均取两个摩擦值的平均值。这是最常用、行为最可预测的模式。Minimum最小取两个摩擦值中较小的一个。这会让表面感觉更滑。比如冰面低摩擦和任何物体接触最终摩擦都很小。Maximum最大取两个摩擦值中较大的一个。会让表面感觉更涩。Multiply相乘将两个摩擦值相乘。这会导致摩擦值急剧变化通常用于模拟非常特殊的交互不推荐常规使用。实战调校心得对于大多数游戏地面我会将静态摩擦力设为0.4-0.6动态摩擦力设为0.2-0.4组合模式用Average。这能提供一种“略有重量感但移动顺畅”的感觉。如果你想模拟冰面可以将两个摩擦力都设为0.05或更低。如果想模拟沙地或泥泞可以将动态摩擦力设得比静态摩擦力还高例如 Static0.3, Dynamic0.6这样物体会很容易开始移动但一旦动起来就会很快减速。4.2 弹力Bounciness与弹力组合模式弹力决定了碰撞后的反弹程度0为无反弹完全非弹性碰撞1为完全弹性碰撞能量无损失。和摩擦力一样弹力也有组合模式Bounce Combine选项和意义与摩擦组合模式类似。这里有一个巨大的坑不要轻易将弹力设置为1或接近1在理想情况下弹力为1意味着碰撞后动能毫无损失。但在离散的物理模拟中这极易导致数值不稳定。物体会在两个表面之间以极高的频率来回碰撞速度在几个物理帧内就可能因为计算误差飙升到一个极大的值“爆炸”或者陷入高频抖动。这就是为什么你有时看到物体碰撞后“鬼畜”地飞向太空。安全实践对于需要弹跳效果的物体如篮球、弹力球将弹力设置在0.6到0.9之间并配合使用Average或Minimum组合模式。对于绝大多数环境物体和角色弹力直接设为0。如果你确实需要模拟近乎完美的弹性碰撞比如台球游戏除了设置高弹力如0.98务必同时增加刚体的角阻力Angular Drag和线性阻力Drag来吸收那些因计算误差产生的额外能量防止系统失控。也可以考虑在速度超过某个阈值时手动钳制速度。4.3 物理材质的高级应用摩擦方向2与自定义效果在3D物理材质中还有一个“Friction Direction 2”和相关的“Dynamic/Static Friction 2”参数。这用于实现各向异性摩擦即物体在不同方向上的摩擦力不同。一个经典的例子是滑雪板沿着板长方向方向1摩擦力很小便于滑行垂直于板长方向方向2摩擦力很大便于控制转向。你需要通过“Friction Direction 2”这个向量来指定第二个摩擦力的方向。虽然原生参数不多但物理材质是连接物理模拟和游戏逻辑的桥梁。你可以通过代码在运行时动态更换物体的物理材质来实现状态变化。例如角色踩到冰面将其脚下的碰撞体物理材质切换为低摩擦材质。车辆启动ABS防抱死系统临时将轮胎的物理材质动态摩擦力调低。物体被施加“油腻”效果为其更换一个弹力极低、摩擦力也极低的特殊材质。5. 常见物理问题诊断与性能优化清单即使理解了所有参数在实际开发中还是会遇到各种光怪陆离的问题。下面是我整理的一个常见问题排查清单。5.1 物体抖动、穿透与异常飞行的根源与修复症状物体尤其是堆叠的物体轻微抖动或嗡嗡作响。诊断这是“震颤”Jitter现象通常源于两个碰撞体在接触点反复进行微小的碰撞检测和反作用力计算。修复增加物理迭代次数在Project Settings - Physics中适当增加Default Solver Iterations默认求解器迭代次数尝试从6提高到10-15和Default Solver Velocity Iterations速度迭代次数。这给了物理引擎更多计算时间来稳定接触点。调整物理材质降低弹力Bounciness增加摩擦力。避免极端质量比确保相互接触的物体质量不要相差过于悬殊如1000:1。可以适当调整质量。使用更简单的碰撞体用Box/Sphere代替复杂的Mesh Collider。症状高速运动的物体如子弹穿过了碰撞体。诊断碰撞检测模式设置为Discrete物体速度过快。修复将高速物体的刚体Collision Detection模式改为Continuous Dynamic并将其可能击中的静态障碍物的碰撞检测模式改为Continuous。症状物体被碰撞后以不可思议的速度飞向天际。诊断“爆炸”现象。通常由以下原因引起多个碰撞体在单帧内相互嵌入过深物理引擎试图用巨大的力将它们分开。弹力Bounciness设置过高在多次快速碰撞中能量累积。时间步长Fixed Timestep不稳定或过小。修复确保场景初始化时物体没有相互嵌入。可以在编辑模式下仔细检查或写一个启动脚本来检测并分离。遵循4.2节的建议严格控制弹力值。确保Time.fixedDeltaTime固定时间步长是一个稳定的值默认0.02。不要在运行时频繁修改它。在代码中可以为刚体的速度设置一个最大钳制值rigidbody.velocity Vector3.ClampMagnitude(rigidbody.velocity, maxSpeed);5.2 性能瓶颈分析与Profiler工具使用指南物理计算是CPU密集型任务。当游戏卡顿时学会使用Unity Profiler (Window - Analysis - Profiler) 是必备技能。定位物理开销在Profiler的CPU使用率图表中找到Physics.Processing或Physics.Simulate这样的条目它直接显示了物理引擎消耗的时间。分析开销来源过多的动态刚体这是最常见的瓶颈。每个动态刚体每帧都需要计算。尽可能将静止的物体设为Static静态无刚体或运动学刚体Is Kinematic。复杂的碰撞体检查是否有大量未勾选Convex的复杂Mesh Collider。在Profiler的Physics模块详情中可以看到具体的碰撞体数量和处理时间。昂贵的碰撞检测检查是否有过多物体使用了Continuous或Continuous Dynamic检测模式。过小的Fixed TimestepTime.fixedDeltaTime值越小物理更新频率越高每帧CPU开销越大。除非有极高的物理精度要求如赛车模拟否则不要轻易调低默认的0.02即每秒50次物理更新。优化策略对象池与休眠对频繁生成/销毁的物理物体如子弹、特效碎片使用对象池并利用好休眠机制。简化碰撞用基本碰撞体组合替代复杂网格碰撞体。分层管理利用碰撞矩阵彻底禁用不可能发生交互的图层之间的检测。降低更新频率对于远处或不重要的物理物体可以考虑通过脚本降低其刚体更新的频率或者将其设为运动学并用更简单的方式模拟。5.3 脚本与物理交互的时序陷阱与最佳实践Unity中Update,FixedUpdate,LateUpdate的执行顺序是物理问题的另一个常见来源。FixedUpdatevsUpdateFixedUpdate按固定时间间隔Time.fixedDeltaTime调用与物理更新同步。所有读取或修改刚体状态如AddForce, 读取velocity的代码必须放在FixedUpdate中。如果在Update里施加力由于Update帧率不固定会导致力的施加不均匀物理表现诡异。Update每渲染帧调用一次。处理输入、游戏逻辑、非物理相关的动画等。LateUpdate在所有Update执行完毕后调用。常用于摄像机跟随确保摄像机基于物体在当前帧最终的位置进行移动。最佳实践代码模式void Update() { // 1. 在Update中获取玩家输入 float moveHorizontal Input.GetAxis(Horizontal); float moveVertical Input.GetAxis(Vertical); // 将输入存储到类变量中供FixedUpdate使用 m_MovementInput new Vector3(moveHorizontal, 0.0f, moveVertical).normalized; } void FixedUpdate() { // 2. 在FixedUpdate中基于存储的输入施加物理力 if (m_MovementInput.sqrMagnitude 0.01f) // 避免施加微小力 { m_Rigidbody.AddForce(m_MovementInput * moveSpeed, ForceMode.Acceleration); } // 3. 也可以在FixedUpdate中读取物理状态确保数据一致性 // float currentSpeed m_Rigidbody.velocity.magnitude; }绝对要避免的反模式在Update中直接使用Transform.Translate或Transform.position来移动带有刚体的物体。这会与物理引擎的计算产生冲突导致不可预测的行为如抖动、碰撞失效。如果你需要完全控制一个物理物体的运动如平台移动请将其刚体设为Is Kinematic然后在FixedUpdate中修改其Rigidbody.MovePosition。