
1. 项目概述为什么Unity物理系统是游戏开发的基石如果你正在用Unity做游戏无论是2D平台跳跃还是3D开放世界物理系统都是你绕不开的核心。它远不止是让一个方块掉下来那么简单。一个响应灵敏、手感扎实的角色移动一次精准的碰撞反馈一个符合直觉的物体破碎效果这些直接决定了玩家对你游戏的第一印象。很多新手开发者包括几年前的我自己都曾在这里踩过坑角色卡墙、移动飘忽、碰撞检测失灵或者性能莫名其妙地掉帧。这些问题根源往往在于对Unity物理引擎的理解不够深入只是停留在“拖个Rigidbody组件”的层面。这篇指南的目的就是帮你彻底搞懂Unity的物理系统从最底层的碰撞检测原理到最上层的角色移动控制实现形成一个完整的知识闭环。我们会聚焦于最核心、最实用的部分避开那些华而不实的理论直接告诉你“怎么做”以及“为什么这么做”。无论你是想实现《蔚蓝》那样精准的平台跳跃还是《艾尔登法环》里那种带有重量感的角色移动这里面的核心逻辑都是相通的。接下来我们就从物理系统的全局设计开始拆解。1.1 核心需求解析物理系统到底在解决什么问题在动手写代码之前我们必须想清楚我们需要物理系统来做什么对于大多数游戏而言物理需求可以归结为三类第一模拟真实感。这是最基础的需求。重力让物体下落摩擦力让物体减速弹力让物体反弹。Unity内置的NVIDIA PhysX引擎或Box2D for 2D已经为我们封装好了这些牛顿力学定律。我们通过Rigidbody刚体组件来赋予游戏对象这些物理属性。但“真实”不等于“好玩”游戏物理往往是“艺术化”的真实。第二实现交互与反馈。这是游戏性的核心。当玩家的剑砍中怪物碰撞检测怪物需要播放受击动画并扣血碰撞响应。当玩家按下跳跃键角色需要以一个合适的初速度向上运动运动控制。物理系统在这里充当了“裁判”和“执行者”的双重角色它检测交互事件并驱动游戏对象的状态变化。第三管理性能与稳定性。这是高级需求但至关重要。物理计算非常消耗CPU资源。一个场景里有100个动态刚体互相碰撞和只有10个性能开销是天壤之别。同时不合理的参数设置如过大的力、过小的碰撞体会导致物体穿透、剧烈抖动甚至直接“飞”出场景的诡异现象。因此理解如何优化物理更新、选择合适的碰撞检测精度是项目规模扩大后的必修课。理解了这些需求我们就能明白学习Unity物理系统本质上是在学习如何配置、监听和干预一个复杂的模拟系统让它为我们的游戏玩法服务而不是被它牵着鼻子走。2. 物理系统核心组件深度解析Unity的物理系统由几个核心组件构成它们各司其职共同协作。很多人用不好物理就是因为没搞清楚这些组件之间的关系。2.1 Rigidbody物理世界的“身份证”与“驱动器”Rigidbody刚体是物理系统的入场券。一个GameObject只有挂载了Rigidbody或Rigidbody2D物理引擎才会去计算它的运动。关键属性与实战意义Mass质量影响物体受力和碰撞后的运动状态。但游戏中的质量单位是“相对”的。通常将主角质量设为1作为参考基准。一个箱子的质量可能是5一个气球的质量可能是0.1。质量差异过大会导致轻物体被撞飞时速度过快像炮弹一样需要谨慎设置。Drag / Angular Drag阻力/角阻力这是在非真空环境下的“空气阻力”。对于角色移动适当的线性阻力可以让角色在停止输入后自然滑行一段距离停下而不是瞬间“钉”在原地这能增加手感。角阻力则影响旋转的停止速度。Use Gravity使用重力勾选后受全局重力影响。对于飞行角色、太空游戏或特定状态如被击飞的角色可能需要动态关闭此选项。Is Kinematic是否为运动学刚体这是最重要的属性之一。勾选后该刚体不再受物理引擎的力驱动但可以通过Transform直接移动它并且它仍然能与其他非运动学刚体发生碰撞并影响对方。常用于移动平台平台按固定路径移动并可以承载站在上面的玩家。玩家控制的角色在某些架构下为了获得更精确、响应更快的移动控制很多动作游戏会选择用代码直接控制Transform来移动角色即运动学刚体而碰撞检测则由其他方式处理如射线投射。这避免了物理引擎的“惯性”和“延迟”感。注意运动学刚体与普通刚体的碰撞是一个单向影响过程。运动学刚体撞飞普通刚体很容易但普通刚体无法撞动运动学刚体。2.2 Collider物理世界的“形状”与“边界”Collider碰撞体定义了物体的物理形状。物理引擎根据碰撞体的形状而非渲染的网格Mesh来进行碰撞计算。这是为了性能因为碰撞体几何通常比渲染网格简单得多。碰撞体类型选型指南基本原型碰撞体Box, Sphere, Capsule性能最优应作为首选。胶囊体Capsule尤其适合作为人形角色的碰撞体因为它能很好地处理斜坡和台阶边缘。Mesh Collider网格碰撞体使用3D网格作为碰撞形状能完美贴合复杂模型如一块岩石。但代价巨大性能开销高且默认不能与另一个Mesh Collider发生碰撞除非勾选Convex凸包选项。仅适用于静态的、形状复杂且不可移动的环境物体。凸包碰撞检测逻辑这是理解复杂碰撞的关键。一个Mesh Collider勾选Convex后物理引擎会计算该网格的凸包Convex Hull——即包裹原网格的最小凸多面体。凸包碰撞的计算效率远高于凹网格碰撞。所以对于需要移动的复杂形状物体如一辆坦克为其添加一个简化的凸包Mesh Collider远比使用原始网格要高效得多。Collider的Is Trigger属性勾选后该碰撞体变为“触发器”。物理引擎会检测到有其他碰撞体进入、停留或离开它但不会产生实际的物理阻挡和反弹效果。这用于实现检测区域如拾取物品的范围、陷阱的触发区域、关卡的检查点等。2.3 物理材质定义碰撞表面的“性格”Physics Material或Physics Material 2D可以附加在Collider上用于定义碰撞表面的摩擦力和弹性反弹系数。Dynamic Friction动摩擦 Static Friction静摩擦分别影响物体滑动时和开始滑动时的阻力。冰面的摩擦力接近0橡胶地面的摩擦力则很高。Bounciness弹性0为无弹性完全非弹性碰撞1为完全弹性能量无损失。想实现一个弹力球就把这个值设高。Friction Combine Bounce Combine摩擦/弹性组合模式当两个物体碰撞时它们的摩擦力和弹性如何计算是取平均值Average、取大Max、取小Min还是相乘Multiply这个设置在处理多种材质交互时非常有用。实操心得对于大多数角色移动我倾向于在角色脚下的碰撞体上使用一个低摩擦力、零弹性的物理材质。低摩擦力让转向和起跑更灵敏零弹性避免在斜坡上反复弹跳。而墙面则可以使用高摩擦力、零弹性的材质。3. 碰撞检测与响应的完整实现知道了组件我们来看看它们如何协作。碰撞检测与响应是物理系统的核心工作流。3.1 碰撞生命周期与对应函数当两个都带有非触发器Collider和Rigidbody的物体发生碰撞时会触发一系列事件对应不同的Monobehaviour函数OnCollisionEnter:碰撞发生的第一帧调用。这是播放碰撞音效、产生粒子特效、计算伤害的绝佳位置。OnCollisionStay:碰撞持续的每一帧调用。可用于实现持续性的效果比如站在火焰地板上持续扣血。注意性能避免在这里做复杂计算。OnCollisionExit:碰撞分离的第一帧调用。可以用来标记状态结束。对于触发器Is Trigger则有另一套函数OnTriggerEnter:进入触发器范围。OnTriggerStay:停留在触发器内。OnTriggerExit:离开触发器。关键区别OnCollisionXXX函数传递的是一个Collision对象其中包含碰撞点、法线、相对速度等详细信息。OnTriggerXXX函数只传递对方碰撞体的引用(Collider)。如果你需要知道碰撞的细节比如从哪个方向撞的必须用OnCollisionXXX。3.2 层碰撞矩阵管理“谁能碰到谁”你肯定不希望玩家的子弹能打到队友或者角色的脚部碰撞体触发拾取物品的逻辑。这时就需要Layer Collision Matrix层碰撞矩阵。在Edit - Project Settings - Physics中你可以看到一个矩阵表格。行和列都是所有的层Layer。取消勾选某个交叉点就意味着这两个层的物体不会发生物理碰撞。标准实践为玩家、敌人、子弹、可交互物品、环境等创建独立的层。在矩阵中取消“玩家层”和“玩家子弹层”的交叉勾选实现队友伤害免疫。取消“触发器层”和“触发器层”之间的勾选避免两个触发器区域相互干扰。将只用于射线检测的层如“地面层”、“可攀爬层”与环境碰撞层分开在矩阵中将其与其他所有层的碰撞都关闭仅用于代码查询。这是管理复杂物理交互最高效、最清晰的方式务必在项目早期就规划好层的使用。3.3 射线检测更精准的查询工具有时我们需要的不是等待碰撞发生而是主动去“询问”物理世界。这就是射线检测Raycasting。它就像从一点发射一束激光看它击中了什么。核心函数Physics.Raycast// 从当前位置向下发射一条长度为1.5单位的射线 RaycastHit hitInfo; if (Physics.Raycast(transform.position, Vector3.down, out hitInfo, 1.5f)) { // 如果射线击中了某个碰撞体 Debug.Log(“踩在了” hitInfo.collider.gameObject.name “上”); // 可以通过hitInfo.distance获取距离hitInfo.normal获取法线用于判断坡度 }应用场景角色接地检测比用OnCollisionStay更可靠、更高效。在角色脚底向下发射短射线判断是否接触地面。武器攻击判定对于枪械可以从枪口向准心方向发射射线对于近战武器可以在挥砍的帧间进行扇形或多次射线检测。环境查询判断前方是否有墙用于自动贴墙跑、判断鼠标点击了场景中的哪个物体。高级技巧你可以通过LayerMask参数指定射线只检测特定的层这能极大提升效率和准确性。例如接地检测只检测“Ground”层避免检测到敌人或物品。4. 角色移动控制的三种范式与实现这是物理系统应用中最具挑战也最有趣的部分。如何让角色按照我们的意愿移动主要有三种主流范式。4.1 范式一基于力的物理驱动移动这是最“物理”的方式。给角色的Rigidbody施加力AddForce或冲量AddImpulse来使其运动。实现方式public class PlayerMovementForce : MonoBehaviour { public float moveSpeed 10f; public float jumpForce 5f; private Rigidbody rb; private bool isGrounded; void Start() { rb GetComponentRigidbody(); } void Update() { // 接地检测通常用射线 // ... // 获取输入 float horizontal Input.GetAxis(“Horizontal”); float vertical Input.GetAxis(“Vertical”); // 计算移动方向相对于摄像机 Vector3 moveDirection (Camera.main.transform.forward * vertical Camera.main.transform.right * horizontal).normalized; moveDirection.y 0; // 确保不向上下施力 // 施加力 rb.AddForce(moveDirection * moveSpeed); // 跳跃 if (isGrounded Input.GetButtonDown(“Jump”)) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); // 使用冲量瞬间施加力 } } }优点运动效果非常自然带有惯性、滑动感适合模拟真实物理的角色如球类、车辆、布娃娃。缺点操控感有延迟难以实现即停即走、精准的平台跳跃。需要仔细调校质量、阻力等参数来获得“手感”。4.2 范式二基于速度的直接控制移动直接设置Rigidbody的速度velocity完全覆盖物理引擎的计算。这是最常用、手感最直接的方式。实现方式public class PlayerMovementVelocity : MonoBehaviour { public float moveSpeed 8f; public float jumpSpeed 10f; private Rigidbody rb; private bool isGrounded; void Start() { rb GetComponentRigidbody(); } void Update() { // 跳跃检测在Update中处理输入更及时 if (isGrounded Input.GetButtonDown(“Jump”)) { rb.velocity new Vector3(rb.velocity.x, jumpSpeed, rb.velocity.z); } } void FixedUpdate() // 物理操作务必放在FixedUpdate中 { // 接地检测... // 获取输入... // 计算目标水平速度 Vector3 targetHorizontalVelocity moveDirection * moveSpeed; // 保留当前的垂直速度重力或跳跃速度 Vector3 currentVelocity rb.velocity; Vector3 newVelocity new Vector3(targetHorizontalVelocity.x, currentVelocity.y, targetHorizontalVelocity.z); // 直接赋予速度 rb.velocity newVelocity; } }优点响应极其迅速操控精准是大多数平台跳跃、动作游戏的首选。可以轻松实现空中横向控制、蹬墙跳等复杂操作。缺点完全无视了物理惯性运动可能显得“假”。需要开发者自己处理所有运动逻辑如斜坡处理、与移动平台同步。4.3 范式三运动学刚体与Transform移动将Rigidbody设为Is Kinematic然后通过直接修改Transform.position来移动。碰撞检测完全依赖射线或Physics.SphereCast等查询方法。实现方式简化public class PlayerMovementKinematic : MonoBehaviour { public float moveSpeed 8f; public float groundCheckDistance 0.2f; public LayerMask groundLayer; private Rigidbody rb; private CapsuleCollider col; void Start() { rb GetComponentRigidbody(); rb.isKinematic true; // 关键 col GetComponentCapsuleCollider(); } void Update() { // 获取输入... Vector3 moveDelta moveDirection * moveSpeed * Time.deltaTime; // 进行碰撞检测例如在移动前朝移动方向发射一个胶囊体射线 if (!Physics.CapsuleCast(…, moveDelta.normalized, moveDelta.magnitude, groundLayer)) { // 如果没有障碍物则移动 transform.position moveDelta; } else { // 如果有障碍物可以进行滑墙、爬坡等更复杂的处理 } // 接地检测同样使用射线... } }优点拥有最高的控制精度和灵活性可以实现任何自定义的移动规则。是许多硬核动作游戏和竞技游戏如《雷神之锤》、《守望先锋》早期版本的底层选择。缺点实现复杂度最高你需要自己重新实现一套完整的碰撞检测和响应逻辑相当于自己写一个简化的物理引擎。对开发者要求高。如何选择追求真实物理模拟赛车、沙盒选范式一。追求爽快精准的操作平台跳跃、ARPG选范式二。这是目前Unity社区最主流、资源最多的方案。追求极限控制和自定义竞技FPS、特殊移动机制选范式三。5. 高级主题与性能优化实战当你的游戏对象多起来物理系统很容易成为性能瓶颈。以下是一些关键的优化策略和高级技巧。5.1 固定时间步长与物理更新在Edit - Project Settings - Time中有一个关键参数Fixed Timestep默认0.02s即50Hz。这是FixedUpdate()的调用间隔也是物理引擎的更新频率。调高如0.016667s即60Hz物理模拟更平滑尤其对高速运动的物体如子弹碰撞检测更精确但CPU开销更大。调低如0.033333s即30Hz节省性能但可能导致“卡顿”感高速物体可能穿透薄墙。Maximum Allowed Timestep用于限制一帧内用于补偿FixedUpdate的最大时间。如果游戏卡顿导致一帧真实时间很长物理引擎会用多个FixedUpdate来追赶直到用完这个最大时间。设置它如0.1s可以防止极端卡顿帧导致物理模拟“暴走”耗尽CPU。5.2 刚体休眠与碰撞器优化休眠Sleeping当一个刚体速度近乎为零且一段时间没有外力作用时物理引擎会将其置为“休眠”状态不再计算其物理直到它被唤醒。这是一个非常重要的自动优化。确保你的静态环境刚体如地形一开始就处于休眠状态。碰撞器优化简化形状永远用简单的碰撞体Box, Sphere, Capsule去近似复杂形状。一个人物可以用胶囊体几个小盒子用于攻击判定来组合。分层管理如前所述善用层碰撞矩阵减少不必要的碰撞计算对。合并静态碰撞体对于不会移动的复杂静态环境如一整面凹凸不平的墙可以考虑使用一个大的Mesh Collider不勾选Convex来代替成百上千个小盒子但这需要测试性能因为单个复杂网格碰撞计算量也很大。另一种方法是使用Physics.BakeMesh来预计算碰撞数据。5.3 实现完全弹性碰撞与自定义物理效果Unity内置的碰撞响应是符合能量损失的。如果你想实现“完全弹性碰撞”碰撞后动能无损失单纯调高Bounciness到1可能因为浮点精度问题无法实现理想效果。一种更可控的方法是在OnCollisionEnter中手动计算并赋予速度void OnCollisionEnter(Collision collision) { // 假设与一个质量无穷大的静态墙面碰撞 ContactPoint contact collision.contacts[0]; Vector3 incomingVelocity rb.velocity; // 计算反射向量V_reflect V_in - 2 * (V_in · N) * N Vector3 reflectVelocity Vector3.Reflect(incomingVelocity, contact.normal); // 可以在这里乘以一个系数来控制能量损失1.0为完全弹性 reflectVelocity * 1.0f; rb.velocity reflectVelocity; }通过这种方式你可以实现台球、弹珠等需要精确能量传递的效果。6. 常见问题排查与调试技巧即使理解了原理在实际开发中依然会遇到各种诡异问题。下面是一个快速排查清单。6.1 问题物体抖动或“抽搐”可能原因1两个刚体相互嵌入每帧物理引擎都试图将它们推开导致振荡。解决检查碰撞体大小确保它们初始位置没有重叠。增加Physics.defaultContactOffset一个很小的值默认0.01可能有助于提前解算碰撞。可能原因2在Update中修改Transform.position同时在FixedUpdate中修改Rigidbody的速度或施加力。两套系统在争夺控制权。解决统一在一个地方通常是FixedUpdate处理物理移动。如果必须用Transform移动如过场动画则将刚体设置为Kinematic。6.2 问题碰撞检测延迟或穿透可能原因1物体速度太快如子弹在一帧内就穿过了薄碰撞体。这就是所谓的“子弹穿透”问题。解决增加物理更新频率调小Fixed Timestep。使用Rigidbody.interpolation插值它会在渲染帧之间平滑物理状态使运动更流畅但对解决高速穿透帮助有限。最佳实践使用射线检测。对于子弹不要用碰撞体而是在发射的帧间使用Raycast或SphereCast有一定半径的射线并设置射线的长度为速度 * Time.deltaTime这样就能检测到路径上的所有碰撞。可能原因2碰撞体Is Trigger被错误勾选导致物体直接穿过。解决仔细检查碰撞体设置。6.3 问题角色在斜坡上打滑或跳不起来可能原因角色碰撞体如胶囊体与斜坡的接触法线判断问题或者角色移动逻辑没有考虑斜坡。解决在接地检测时不仅检测是否接触地面还检测地面法线与垂直方向的夹角Vector3.Angle(hit.normal, Vector3.up)。如果夹角小于某个值如45度才判定为“可站立”的地面。当在斜坡上移动时将移动方向投影到斜坡平面Vector3.ProjectOnPlane使角色沿着斜坡表面移动而不是水平移动导致“铲地”。// 假设groundNormal是射线检测到的地面法线 Vector3 moveDirection (Camera.main.transform.forward * vertical Camera.main.transform.right * horizontal).normalized; moveDirection Vector3.ProjectOnPlane(moveDirection, groundNormal).normalized; rb.velocity new Vector3(moveDirection.x * speed, rb.velocity.y, moveDirection.z * speed);6.4 调试工具Physics Debug Visualization在Game视图右上角点击Stats面板可以看到物理相关的信息如Rigidbody数量。更重要的是在Scene视图中你可以通过Gizmos菜单开启Colliders的显示或者通过代码Debug.DrawRay、Debug.DrawLine来可视化你发射的射线这是调试碰撞和移动问题不可或缺的手段。我个人在开发中会为关键的检测射线如接地检测、前方障碍检测常驻一个调试绘制只在编辑器下显示这样任何异常都能一目了然。物理系统的调试眼见为实。