1. 项目概述:为什么Unity物理系统值得深挖?
如果你在Unity里做过游戏,尤其是带点动作、解谜或者模拟元素的,那你肯定跟物理系统打过交道。可能你只是给一个方块加了个Rigidbody,让它自由下落,或者给角色加了个Character Controller,让它能跳起来。但很多时候,我们只是停留在“能用”的阶段——物体掉下去了,撞到东西弹开了,感觉“物理”生效了,任务完成。然而,当项目稍微复杂一点,比如要做一辆手感真实的赛车、一个布娃娃系统(Ragdoll)的敌人,或者一个需要大量物体交互的沙盒场景时,各种诡异的问题就冒出来了:物体穿模、性能骤降、运动卡顿、碰撞检测失灵……这时候你才会意识到,Unity的物理系统远不止“加个组件”那么简单。
我自己在项目里踩过不少坑。最早做一个小球滚动的游戏,觉得物理嘛,不就是重力加碰撞?结果在斜坡上,小球时不时会鬼畜抖动甚至飞出去;后来做一个FPS游戏,手雷的抛物线投掷和爆炸冲击,怎么调都觉得“假”;再后来接触手机平台,面对成百上千个可交互的碎片,帧率直接崩盘。这些问题逼着我去深挖Unity物理引擎的底层逻辑、参数意义和最佳实践。我发现,很多问题其实官方文档里都有提及,但散落在各处,缺乏一个从原理到实战、从入门到调优的系统性梳理。
所以,这篇内容就是把我这些年折腾Unity物理系统积累的经验、踩过的坑和验证过的方案,做一个集中的输出。它不仅仅是一份参数说明书,更是一个实战指南。我会从最基础的组件讲起,拆解它们是如何协同工作的,然后深入到物理材质、关节、射线检测等高级话题,最后聚焦在性能优化和那些“教科书上不会写”的疑难杂症排查上。无论你是刚接触Unity不久的新手,还是已经有一定经验但被物理问题困扰的开发者,我相信这里面的内容都能给你带来实实在在的帮助。我们的目标很明确:不仅要让物体“动起来”,更要让它们“动得对”、“动得好”、“动得高效”。
2. 物理系统核心架构与组件深度解析
Unity的物理系统不是一个黑盒魔法,它是一套由多个核心组件和后台引擎(如PhysX)共同构成的模拟框架。理解这套架构,是解决一切高级问题和进行性能优化的前提。
2.1 两大基石:碰撞体(Collider)与刚体(Rigidbody)
这是物理交互的起点。很多新手会混淆两者的职责,导致设置错误。
碰撞体(Collider):它只定义了一个物体的“形状边界”,用于物理引擎计算碰撞。它本身没有质量,不会运动,只是一个“区域”。你可以把它想象成一个隐形的、不可穿透的力场外壳。Unity提供了多种基本碰撞体(Box, Sphere, Capsule, Mesh等)和复合碰撞体(由多个基本体组成)。
注意:对于复杂的模型,切忌直接使用Mesh Collider并勾选“Convex”以外的选项。非凸(Convex)的Mesh Collider性能开销极大,且只能用于静态物体(即不带Rigidbody的物体)。对于动态物体,应该用多个基本碰撞体(Box, Capsule)去近似拟合其形状,或者确保Mesh是凸的。
刚体(Rigidbody):这是让物体“活”起来的关键。它为物体赋予了物理属性:质量(Mass)、阻力(Drag)、角阻力(Angular Drag),并使其受到重力(Gravity)等力的影响。物理引擎会计算作用在Rigidbody上的所有力和扭矩,然后每帧更新它的位置和旋转。
核心关系:一个物体要参与动态物理模拟(比如被力推开、自由落体),它必须同时拥有Rigidbody和至少一个Collider。只有Collider没有Rigidbody的物体,我们称之为“静态碰撞体”(Static Collider),比如地面、墙壁,它们不会移动,但其他动态物体可以撞到它。只有Rigidbody没有Collider的物体……它就是个不受碰撞影响的质点,基本没有实用价值。
一个常见的误解:Is Kinematic选项。勾选后,物体不再受物理引擎的力驱动,但你仍然可以通过脚本直接修改其Transform.position来移动它,并且它依然能与其他动态刚体发生碰撞(作为碰撞的主动方)。这常用于玩家控制的角色(配合Character Controller)、移动的平台或者需要被脚本精确控制的物体。关键点:Kinematic刚体移动时,它会“推开”其他普通动态刚体,但自身的运动完全由你掌控。
2.2 物理材质(Physic Material):定义碰撞的“性格”
物理材质决定了两个碰撞体接触时的表面行为。它就像给碰撞体表面涂了一层有特定属性的涂料。
- 动态摩擦力(Dynamic Friction):物体滑动时的摩擦力。值越大,停下来越快。想象冰面(低摩擦力)和砂纸(高摩擦力)的区别。
- 静态摩擦力(Static Friction):物体从静止到开始滑动所需克服的摩擦力。通常略大于动态摩擦力。
- 弹力(Bounciness):碰撞后能量保留的系数。0表示完全非弹性碰撞(像橡皮泥粘住),1表示完全弹性碰撞(理想情况,无能量损失)。实际应用中,大于1的值会导致能量增加,物体越弹越高,通常用于特殊效果,但需谨慎。
- 摩擦力合并模式 & 弹力合并模式:当两个拥有不同物理材质的物体碰撞时,如何计算最终的摩擦力和弹力?通常使用“Average”(平均值)或“Multiply”(相乘)就能满足大多数情况。“Min”和“Max”在某些特定模拟中可能有用。
实操心得:不要忽视物理材质。一个常见的“滑冰感”问题,即角色或物体在平面上停不下来,往往就是因为摩擦力设置过低。给地面和角色脚底的碰撞体分配一个合适的物理材质,能立刻改善操作手感。对于弹球游戏,通过调整弹力可以轻松控制球的活跃程度。
2.3 物理引擎的后台:PhysX与更新周期
Unity默认的3D物理引擎是NVIDIA的PhysX。我们不需要直接操作它,但需要理解它的工作节奏。
物理模拟是离散的,发生在固定的时间步长(Fixed Timestep)里,而不是每一帧渲染的时间。这保证了模拟的稳定性,不受帧率波动影响。你可以在Project Settings -> Time中找到Fixed Timestep,默认是0.02秒(即每秒50次物理更新)。
这意味着什么?如果你的游戏逻辑帧率(Update)是100FPS,那么每帧之间物理可能更新了2次(FixedUpdate)。如果帧率降到25FPS,那么每帧之间物理会更新2次以上来“追上”真实时间。这就是为什么你的物理代码(比如施加力)应该放在FixedUpdate而不是Update里——为了和物理引擎的步调保持一致,避免因帧率不同导致力的施加频率变化,从而影响物理效果。
一个性能陷阱:Maximum Allowed Timestep。当游戏卡顿时,一帧的耗时可能远超Fixed Timestep。这个参数限制了物理引擎在一帧内用于“追赶”真实时间的最大计算时间。设置得太低,物理会显得“变慢”;设置得太高,则可能在卡顿时导致单帧物理计算过长,加剧卡顿。通常保持默认(0.333秒)即可,但对于强调物理稳定性的游戏,可能需要微调。
3. 碰撞与触发检测的实战应用
检测到物体之间的接触是游戏逻辑交互的基础。Unity提供了两套机制:碰撞(Collision)和触发(Trigger)。
3.1 碰撞(Collision) vs. 触发(Trigger)
这是根本性的区别:
- 碰撞:两个碰撞体发生物理接触,会产生物理反馈(力,阻挡)。双方必须都有非Trigger的Collider,且至少一方有Rigidbody。
- 触发:两个碰撞体发生体积重叠,但不产生物理反馈。只需要将Collider的
Is Trigger属性勾选即可。常用于检测区域,如拾取物品、进入关卡、伤害区域。
如何选择?一个简单的判断标准:是否需要物理引擎来处理接触后的运动?如果需要物体被挡住、弹开,用碰撞;如果只需要知道“它们碰到了”这个事件,然后由游戏逻辑(如加分、扣血、播放音效)来处理,用触发。触发由于不计算物理响应,性能开销通常更低。
3.2 消息函数与获取信息
当碰撞或触发事件发生时,Unity会调用对应GameObject上脚本中的特定消息函数。这是你编写交互逻辑的地方。
对于碰撞:
OnCollisionEnter(Collision collisionInfo):碰撞开始的第一帧调用。OnCollisionStay(Collision collisionInfo):碰撞持续的每一帧调用。OnCollisionExit(Collision collisionInfo):碰撞结束的第一帧调用。
对于触发:
OnTriggerEnter(Collider other):进入触发区域的第一帧。OnTriggerStay(Collider other):停留在触发区域的每一帧。OnTriggerExit(Collider other):离开触发区域的第一帧。
关键对象:Collision和Collider参数包含了丰富的信息。
collisionInfo.gameObject/other.gameObject:与我发生交互的对方物体。collisionInfo.contacts:一个ContactPoint数组,包含了碰撞点的位置、法线等信息。这对于计算碰撞后的特效位置(如火花、凹痕)或者精确的反弹方向至关重要。collisionInfo.relativeVelocity:碰撞时双方的相对速度。可以用来计算碰撞的剧烈程度,决定播放多大的撞击声效或是否造成破坏。
一个实战技巧:在OnCollisionEnter中,利用relativeVelocity.magnitude来判断碰撞强度。例如:
void OnCollisionEnter(Collision collision) { float impactStrength = collision.relativeVelocity.magnitude; if (impactStrength > 5f) { PlaySound(bigCrashSound, impactStrength); // 根据强度调整音量 SpawnParticles(collision.contacts[0].point); // 在碰撞点生成特效 } }3.3 层级碰撞矩阵(Layer Collision Matrix)
这是管理复杂场景中“谁和谁能碰撞”的核心工具。位于Edit -> Project Settings -> Physics(或 Physics 2D)。
为什么需要它?想象一个游戏,有玩家、敌人、子弹、道具、墙壁、仅装饰用的树叶。你肯定不希望子弹打中队友、玩家捡不到道具(因为被碰撞体挡住了)、或者角色被一片树叶卡住。通过给不同的物体类型分配不同的Layer,并在碰撞矩阵中精确配置,你可以完全控制碰撞关系。
配置策略:
- 创建清晰的Layer:不要都用默认的“Default”。创建如“Player”、“Enemy”、“Bullet”、“Pickup”、“Wall”、“Ground”、“Decoration”等。
- 在矩阵中取消勾选:例如,“Bullet”层不与“Bullet”层碰撞(避免子弹互撞),“Player”层不与“Pickup”层碰撞(但Pickup的Collider是Trigger,用于检测),“Decoration”层不与任何动态物体碰撞。
- 对性能的影响:减少不必要的碰撞检测对性能提升显著。一个“Decoration”层的物体,如果它只与“Nothing”层碰撞,那么物理引擎几乎会完全忽略它与其他层的碰撞计算。
常见问题排查:“我的物体明明设置了碰撞体,为什么穿过去了?” 首先检查的就是Layer Collision Matrix,确保这两个物体所在的层之间的复选框是勾选状态。
4. 射线检测(Raycasting)与形状投射(Shape Casting)
这是物理查询的利器,用于“主动探测”而非“被动等待碰撞事件”。它们不依赖持续的碰撞检测,而是在你需要的时候,发射一条射线或一个形状去探测场景。
4.1 射线检测(Physics.Raycast)
这是最常用、最高效的查询方式。想象你用手电筒照向黑暗,看到光斑落在哪里。
基本用法:
Ray ray = new Ray(originPosition, direction); float maxDistance = 100f; LayerMask groundLayer = LayerMask.GetMask("Ground"); RaycastHit hitInfo; // 用于存储击中信息 if (Physics.Raycast(ray, out hitInfo, maxDistance, groundLayer)) { // 击中了“Ground”层的物体 Vector3 hitPoint = hitInfo.point; // 击中点 GameObject hitObject = hitInfo.collider.gameObject; // 被击中的物体 float distance = hitInfo.distance; // 击中距离 }参数解析:
origin,direction: 定义射线。maxDistance: 射线最大长度,避免无谓的远距离计算。layerMask:极其重要!通过位掩码指定只检测哪些层。使用LayerMask.GetMask(“Layer1”, “Layer2”)或~(1 << layerIndex)来排除某一层。不指定则会检测所有层,是性能浪费和逻辑错误的常见根源。queryTriggerInteraction: 指定是否检测Trigger碰撞体。通常使用QueryTriggerInteraction.Ignore来忽略触发器,除非你明确需要检测它们。
高级用法与变体:
Physics.RaycastAll: 返回射线路径上所有击中的物体,按距离排序。用于霰弹枪、激光穿透等效果。Physics.SphereCast/CapsuleCast/BoxCast: 不是发射一条线,而是发射一个形状体。这对于检测一个有体积的物体(如角色)前方是否有空间移动非常有用,避免了角色“嵌进”墙里才发现碰撞。
4.2 形状投射在角色控制器中的应用
这是实现一个自定义、手感好的角色移动的关键。CharacterController组件内部就大量使用了CapsuleCast。
为什么不用简单的Raycast?因为角色有体积。一条射线只能检测一个点,如果射线从角色中心发出,即使射线没碰到墙,角色的肩膀可能已经撞上去了。CapsuleCast用一个胶囊体去探测,能更准确地反映角色的实际碰撞体积。
模拟一个简单的角色移动碰撞检测:
public CapsuleCollider playerCollider; public float skinWidth = 0.1f; // 一层薄薄的“皮肤”,用于防止数值误差导致的抖动 bool CanMove(Vector3 direction, float distance) { Vector3 point1 = playerCollider.transform.position + playerCollider.center + Vector3.up * (playerCollider.height/2 - playerCollider.radius - skinWidth); Vector3 point2 = playerCollider.transform.position + playerCollider.center - Vector3.up * (playerCollider.height/2 - playerCollider.radius - skinWidth); float radius = playerCollider.radius - skinWidth; if (Physics.CapsuleCast(point1, point2, radius, direction, out RaycastHit hit, distance)) { // 如果检测到碰撞,可以在这里处理滑动(例如,沿墙面滑动) return false; } return true; }这段代码计算了胶囊体的上下半球心点,并考虑了skinWidth来避免在非常接近障碍物时发生高频的“碰撞-分离”抖动。
实操心得:对于复杂的移动逻辑(如平台游戏),可能需要组合使用多种检测。例如,用Raycast向下检测地面,用CapsuleCast向前检测墙壁,用SphereCast向下检测悬崖边缘。每一帧根据检测结果来决定角色的速度、是否可跳跃、是否处于爬墙状态等。
5. 力、关节与高级物理模拟
让物体动起来,除了直接设置Transform,更符合物理规律的方式是施加力(Force)和扭矩(Torque)。而关节(Joint)则用于模拟物体之间的连接关系。
5.1 力的施加方式
通过Rigidbody.AddForce()和AddTorque()来施加。
- ForceMode参数是关键:
Force: 添加一个持续的力,与质量有关。这是最常用的,模拟像火箭发动机、风力这样的持续推力。Impulse: 添加一个瞬间的冲量,与质量有关。模拟像子弹击中、爆炸冲击、跳跃起跳瞬间的力。Acceleration: 添加一个持续的加速度,忽略质量。所有物体获得相同的速度变化。VelocityChange: 添加一个瞬间的速度变化,忽略质量。
如何选择?对于需要根据物体质量产生不同效果的应用(重的物体更难推动),用Force或Impulse。对于希望所有物体行为一致的应用(比如游戏中的一阵风,吹动所有纸片和石头),用Acceleration或VelocityChange。AddForce通常放在FixedUpdate中,以保持力的施加频率稳定。
5.2 常用关节(Joint)详解
关节用于约束两个刚体之间的运动关系。Unity内置了多种关节,适用于不同场景。
- 固定关节(Fixed Joint):将两个物体牢固地绑定在一起,像焊接一样。适用于组合物体(如剑和剑柄),当受力超过
Break Force阈值时,关节会断裂。 - 铰链关节(Hinge Joint):模拟门、钟摆、杠杆的旋转。你可以设置旋转轴、角度限制、弹簧和马达。一个常见应用是制作可开关的门:
- 将门(带Rigidbody和Collider)通过Hinge Joint连接到门框(一个静态Collider)。
- 设置旋转轴(如Y轴)。
- 设置角度限制(如-90到90度)。
- 可以启用弹簧(Spring)让它自动回中,或者启用马达(Motor)让它自动旋转。
- 弹簧关节(Spring Joint):用弹簧连接两个物体。可以设置弹簧的刚度(Stiffness)和阻尼(Damping)。适合做绳索、链条的初步模拟,或者两个物体之间柔软的连接。注意,原生Spring Joint性能一般,对于复杂的绳索模拟,可能需要专门的插件或自己用多个刚体和关节来模拟。
- 可配置关节(Configurable Joint):这是最强大、最复杂的关节。它几乎可以模拟任何类型的连接,通过分别约束线性(位置)和角度(旋转)的自由度。你可以用它来制作自定义的车辆悬挂、布娃娃的肢体连接等。它的学习曲线较陡,需要仔细理解每个自由度的含义(X Motion, Y Motion, Angular X Motion等)。
关节使用注意事项:
- 关节连接的两个物体通常都需要Rigidbody。
- 关节的
Connected Body属性如果为空,则默认连接到世界空间(World Space)。 - 滥用关节,尤其是可配置关节,会对性能造成较大影响。在移动平台要格外小心。
- 关节的断裂(Break Force)事件可以通过
OnJointBreak(float breakForce)函数来捕获,用于播放断裂音效或特效。
5.3 布娃娃系统(Ragdoll)的实现要点
布娃娃本质就是一个人形骨骼的每个部位(头、躯干、四肢)都是一个带碰撞体的刚体,然后用关节(主要是Character Joint,它是简化版的可配置关节)把它们连接起来。
创建流程(使用Unity内置工具):
- 准备好带人形动画(Humanoid Avatar)的模型。
- 在模型根节点上,添加
Ragdoll组件(旧版Unity在GameObject -> 3D Object -> Ragdoll...)。 - 在弹出的向导中,将场景中模型骨骼的各个Transform拖到对应的槽位(如“Left Hips”拖左髋部的骨骼)。
- 调整每个关节的旋转限制(Twist/ Swing Limit),使其符合人体生理结构(比如胳膊不能向后弯)。
- 点击“Create”。Unity会自动为每个部位生成碰撞体、刚体和关节。
核心控制:
- 激活/禁用:布娃娃激活时,物理关节控制骨骼,动画失效。禁用时,动画重新控制骨骼。通常通过设置所有刚体的
isKinematic来实现切换。isKinematic = true时,物体不受物理影响,可由动画驱动;isKinematic = false时,物体受物理控制。 - 切换时机:角色死亡时,将
isKinematic设为false,并通常给某个部位(如骨盆或胸部)施加一个力,来产生倒下的初速度。角色复活时,将所有刚体的isKinematic设回true,并将骨骼位置对齐到动画姿势。
性能与稳定性:
- 一个完整的布娃娃通常包含15-20个刚体,对CPU是不小的负担。在移动端或需要大量敌人的场景中,需考虑对象池和延迟激活(例如,只在玩家能看到的地方激活布娃娃)。
- 关节限制设置不当,在高速碰撞下可能导致肢体扭曲成夸张的角度(关节“断裂”感)。需要仔细调试
Break Force和关节的Projection Mode(投影模式,用于在物理误差过大时强行将关节拉回正确位置)。
6. 物理性能优化深度指南
物理计算是CPU密集型任务,尤其是在移动设备或拥有大量动态物理物体的场景中。优化物理性能往往是项目后期的重要攻坚点。
6.1 性能瓶颈分析与工具
首先,你需要知道问题出在哪里。Unity Profiler是你的第一道工具。
- 打开
Window -> Analysis -> Profiler。 - 在CPU Usage区域,关注
Physics.Processing和Physics.Simulate这两项。如果它们占用的时间很高(比如每帧超过5-10ms),就说明物理是瓶颈。 - 使用Physics Debugger(
Window -> Analysis -> Physics Debugger, 旧版可能在Physics菜单下)。它可以可视化地显示:- 碰撞体的线框。
- 睡眠状态(Sleeping)的物体(显示为蓝色)。睡眠的物体物理引擎会跳过计算,直到被唤醒。
- 每帧进行的碰撞检测对(Pairs)。
- 射线和形状投射。
通过Physics Debugger,你可以一眼看出场景中哪些碰撞体过于复杂、哪些物体在不必要地保持活动状态。
6.2 静态与动态碰撞体优化
这是最有效的优化手段之一。
- 静态碰撞体(Static Collider):没有Rigidbody的碰撞体。物理引擎会在场景启动时为所有静态碰撞体构建一个静态碰撞空间数据结构(如BVH树)。这个构建过程可能较慢,但构建完成后,与静态碰撞体的检测效率极高。关键点:一旦构建完成,静态碰撞体绝对不能移动!如果移动了,整个静态数据结构需要重建,会造成严重的性能卡顿。对于需要移动的环境物体(如电梯、移动平台),必须给它加上Rigidbody,并勾选
Is Kinematic,这样它就被视为动态的,但运动由你控制。 - 动态碰撞体(Dynamic Collider):带有Rigidbody的碰撞体。它们之间的碰撞检测开销远高于与静态碰撞体的检测。因此,要尽量减少同时活动的动态碰撞体数量。
6.3 刚体睡眠(Sleeping)与唤醒
物理引擎的智能优化。当一个动态刚体的速度低于某个阈值(Sleep Threshold)并持续一段时间后,它会进入睡眠状态。睡眠的刚体几乎不消耗CPU资源。当有足够的力作用其上或与其他物体碰撞时,它会被唤醒。
如何利用?
- 确保你的刚体在静止时能顺利入睡。检查
Sleep Threshold(默认0.005),过高的值会导致物体难以入睡。 - 避免每帧都用
AddForce或直接修改velocity去扰动一个本应静止的物体。这会导致它无法入睡。 - 对于一堆堆叠的物体(如一堆箱子),最下面的箱子承重静止后应该入睡。但如果上面的箱子还在微小的晃动,可能会阻止下面的箱子入睡。适当增加
Sleep Threshold或调整物理材质摩擦力可能有帮助。
6.4 碰撞体复杂度管理
碰撞体的形状复杂度直接影响性能开销。
- 优先使用基本碰撞体:Box, Sphere, Capsule。它们的计算速度最快。
- 谨慎使用Mesh Collider:
- 对于静态环境(山体、复杂建筑),可以使用非凸的Mesh Collider,因为它只在初始化时被烘焙进静态数据结构。
- 对于动态物体,必须勾选
Convex。非凸的Mesh Collider不能用于动态物体间的碰撞。 - 即使勾选了
Convex,顶点数也不要太多。使用模型的简化版(Low Poly)网格来生成碰撞体,而不是用渲染网格。可以在3D建模软件中制作一个简化的“碰撞体模型”,或者在Unity中通过Mesh Collider的Cooking Options尝试简化。
- 使用复合碰撞体(Compound Collider):用多个基本碰撞体(子物体)组合成一个复杂形状,远比用一个高面数的凸Mesh Collider高效。例如,一个椅子可以用一个盒体做座位,四个圆柱做腿。
6.5 其他关键优化技巧
- 减少不必要的碰撞检测:如前所述,善用Layer Collision Matrix。这是免费的午餐,能立即剔除大量无用的碰撞计算。
- 调整物理更新频率:在
Project Settings -> Time中,增大Fixed Timestep(例如从0.02s改为0.04s)会降低物理更新的频率,从而减轻CPU负担,但会降低物理模拟的精度和流畅度。这对于节奏较慢或物理精度要求不高的游戏(如卡牌、策略)可能是一个可行的权衡。注意:这会影响所有基于FixedUpdate的逻辑。 - 控制物理迭代次数:在
Project Settings -> Physics中,有Default Solver Iterations和Default Solver Velocity Iterations。求解器迭代次数越高,物理约束(如关节、穿透纠正)计算越精确,但开销越大。对于简单场景,可以尝试适当降低(例如从默认的6降到4)。对于有复杂关节或堆叠的场景,降低迭代次数可能导致不稳定。 - 使用空间划分与兴趣管理(Interest Management):对于超大规模物理场景(如成千上万个可交互碎片),仅靠上述优化不够。需要设计逻辑,只对玩家周围一定范围内的物体启用物理(
Rigidbody设置为Kinematic或直接禁用Collider),远处的物体冻结或使用更简单的模拟。这属于游戏逻辑层面的优化。
7. 常见疑难问题与实战排查技巧
即使理解了原理,实际开发中还是会遇到各种奇怪的问题。下面是我总结的一些典型“坑”及其解决方案。
7.1 物体抖动或“颤动”(Jitter)
现象:两个碰撞体接触时,尤其是堆叠时,出现高频的微小抖动。原因:
- 缩放(Scale)非均匀:碰撞体的局部或全局缩放不是(1,1,1)。物理引擎对非均匀缩放的处理并不完美,可能导致碰撞检测异常。
- 碰撞体穿透:在某一帧,两个物体因为速度过快或其它原因,发生了深度穿透。下一帧物理引擎试图将它们推开,可能用力过猛导致反向穿透,如此循环,形成抖动。
- 质量比过于悬殊:一个质量极轻的物体放在一个质量极重的物体上,轻物体会因为微小的计算误差而不停抖动。
解决方案:
- 确保所有参与物理的物体,其缩放最终为(1,1,1)。如果模型需要缩放,考虑在父级空物体上缩放,而保持带碰撞体和刚体的子物体缩放为1。
- 增加
Fixed Timestep的频率(减小数值,如从0.02到0.016667对应60FPS)可以提高模拟精度,减少穿透,但会增加CPU开销。 - 为静态碰撞体或质量很大的刚体勾选
Rigidbody上的Is Kinematic,或者将其质量设置为无限大(实际上通过不添加Rigidbody或设为Kinematic实现)。 - 使用连续碰撞检测(Continuous Collision Detection, CCD)。在高速运动的物体(如子弹)的Rigidbody上,将
Collision Detection从Discrete(离散)改为Continuous或Continuous Dynamic。这会追踪物体在两帧之间的运动轨迹,防止因速度过快而穿过薄墙。注意:CCD开销很大,只对少数高速物体使用。
7.2 物体“穿模”(Passing Through)
现象:物体直接穿过了另一个碰撞体,没有触发任何碰撞事件。原因:
- 速度过快:这是最常见原因。在离散检测中,如果物体一帧移动的距离超过其自身尺寸(或对方碰撞体的厚度),就可能从“墙前”直接跳到“墙后”,错过了检测。
- 碰撞体层级设置错误:Layer Collision Matrix中,两个物体的层没有勾选碰撞。
- 一方是Trigger:如果一方是Trigger,不会发生物理阻挡,物体会直接穿过。
- 刚体被设置为Kinematic:Kinematic刚体不会因碰撞而被物理引擎自动推开,除非你在脚本中处理。如果对方是普通动态刚体,它会被Kinematic刚体推开,但Kinematic刚体自己会“穿”过去。
解决方案:
- 对高速物体启用CCD(如上所述)。
- 检查并修正Layer Collision Matrix。
- 明确设计意图:是需要物理阻挡(Collision)还是仅需检测(Trigger)?
- 对于Kinematic刚体(如玩家控制器),需要在脚本中手动处理碰撞响应。通常使用
CharacterController.Move()或自己用Rigidbody.MovePosition()配合碰撞检测来实现。
7.3 性能突然下降(Spikes)
现象:游戏运行流畅,但偶尔会卡顿一下。原因:
- 静态碰撞体被移动:如前所述,移动一个静态碰撞体会触发昂贵的静态物理数据结构重建。
- 大量刚体同时被唤醒:例如,一场爆炸唤醒了几十个睡眠的刚体,导致单帧物理计算量激增。
- 复杂的Mesh Collider生成或销毁。
- 关节断裂(Break Force):如果有很多用关节连接的物体,且关节同时断裂,会产生大量新的动态物体,增加计算负担。
排查与解决:
- 使用Profiler定位卡顿帧,查看是否是
Physics.Processing激增。 - 确保场景中所有不该移动的环境物体都不要挂Rigidbody(成为静态碰撞体)。
- 对于可移动的环境物体,确保其Rigidbody是Kinematic。
- 对于爆炸等会影响大量物体的效果,考虑分帧处理唤醒或施加力。
- 使用对象池管理可破碎物体或带关节的物体,避免运行时Instantiate和Destroy。
7.4 射线检测(Raycast)相关问题
问题1:检测不到物体
- 检查发射点和方向是否正确。可以用
Debug.DrawRay在Scene视图中可视化射线。 - 检查Layer Mask:这是最常出错的地方。确认你传入的Layer Mask包含了目标物体所在的层。
- 检查目标物体的碰撞体是否被禁用(
collider.enabled = false)。 - 检查射线的最大距离是否足够。
问题2:检测到错误的物体(如检测到自己)
- 在角色自身发射射线检测地面或前方时,射线很可能从角色内部的碰撞体发出,第一击中的就是自己。
- 解决方案:使用
Raycast的重载函数,传入一个RaycastHit[]数组,然后遍历结果,跳过hit.collider.transform.IsChildOf(myTransform)的物体。或者更优雅地,使用Physics.Raycast的layerMask参数,将角色自身的层排除在外:int mask = ~(1 << gameObject.layer);。
物理系统的调试是一个需要耐心和逻辑分析的过程。遇到问题时,系统地检查组件设置、层级关系、更新函数和性能数据,利用好Debug绘制工具,大部分问题都能被定位和解决。记住,物理模拟是确定性的(在相同输入下产生相同结果),这为复现和调试问题提供了基础。