Unity物理引擎实战:刚体抛物线运动与旋转控制实现

发布时间:2026/7/28 9:29:01
Unity物理引擎实战:刚体抛物线运动与旋转控制实现 1. 项目概述与核心思路最近在做一个物理反馈比较强的动作游戏里面有个需求挺有意思当角色用重击打飞一个箱子或者罐子时这个物体不能只是简单地飞出去然后掉地上那样太“呆”了。策划想要的效果是物体被撞飞的瞬间会一边沿着一个漂亮的抛物线轨迹运动一边自身持续地、不规则地旋转就像电影里被爆炸气浪掀飞的碎片那样充满动感和随机性。这个效果在Unity里实现说简单也简单说复杂也复杂。简单在于核心就是刚体Rigidbody的力施加和旋转控制复杂在于如何让“抛物线”和“旋转”这两个运动自然耦合并且效果可控、不穿模、性能友好。今天我就把自己趟过的路、踩过的坑以及最终稳定可用的方案从头到尾拆解一遍。这个需求本质上拆解为两个独立但又需要协同工作的物理模拟抛射体运动Projectile Motion和持续旋转Continuous Rotation。在Unity的物理引擎PhysX框架下我们不能直接去修改物体的Transform位置和旋转来模拟抛物线那样会与物理引擎冲突导致诡异的碰撞检测问题。正确的思路是将物体完全交给物理引擎驱动我们通过脚本在恰当的时机以恰当的方式施加力和扭矩引导物理引擎计算出我们想要的效果。听起来有点绕但理解了这个原则后面的一切就顺理成章了。2. 核心组件与物理原理剖析2.1 刚体Rigidbody一切物理模拟的基石在Unity中任何想要受物理引擎控制的物体都必须挂载Rigidbody组件。它是连接我们的逻辑代码与底层PhysX物理模拟的桥梁。对于“被撞飞”的物体Rigidbody的配置至关重要质量Mass决定了物体有多“重”。质量越大需要越大的力才能产生相同的加速度。一个木箱和铁箱被同样力撞飞效果肯定不同。这里需要根据游戏内物体的“感觉”来设定通常0.5到5之间是常见范围。阻力Drag和角阻力Angular Drag这两个参数模拟了空气阻力。Drag影响直线运动速度的衰减Angular Drag影响旋转速度的衰减。如果你想物体飞得远、转得久就把这两个值设小比如0.01如果想物体很快停下就设大比如1.0。对于我们的“被撞飞”效果通常希望有一定衰减但不要太快所以可以设置在0.05到0.2之间。碰撞检测模式Collision Detection对于高速运动的物体比如被大力撞飞默认的Discrete离散检测可能导致穿模。强烈建议设置为Continuous连续或Continuous Dynamic连续动态虽然更耗性能但能有效避免物体从其他碰撞体中“穿过”的Bug。冻结旋转Freeze Rotation这个要特别注意默认是全部不冻结。如果我们想完全由脚本控制旋转可以冻结所有轴的旋转然后通过rigidbody.angularVelocity来施加旋转。但更常见的做法是不冻结让物理引擎也参与旋转计算我们只施加一个初始的旋转扭矩。注意不要尝试在FixedUpdate中同时用rigidbody.AddForce和直接修改transform.position也不要同时用AddTorque和修改transform.rotation。这种混合操作会引发不可预测的行为是物理模拟的大忌。坚持“力驱动”原则。2.2 抛物线运动的物理本质与实现抛物线运动在忽略空气阻力或阻力很小的理想情况下可以分解为两个方向的运动水平方向匀速直线运动因为水平方向不受力假设无阻力。垂直方向匀加速直线运动受恒定重力加速度影响。在Unity中实现它有三种主流思路各有优劣方案一计算初速度向量使用 AddForce(force, ForceMode.VelocityChange)这是最符合物理直觉、效果最自然的方法。我们需要计算一个初始速度向量。优点完全由物理引擎模拟碰撞反应真实与场景中其他物理物体交互性好。缺点需要根据想要的抛物线高度和距离来反推初始速度和角度计算稍复杂。如何计算假设我们知道目标落点targetPos和想要达到的最大高度peakHeight。我们可以将运动分解。首先垂直方向的运动时间由重力加速度g和高度决定。从最高点下落到地面或起始高度的时间t_down sqrt(2 * peakHeight / g)。由于上升和下降时间对称起点和终点高度相同总飞行时间t_total 2 * t_down。那么水平初速度vx (targetPos.x - startPos.x) / t_total。垂直初速度vy sqrt(2 * g * peakHeight)。最终的初速度向量就是(vx, vy, vz)其中vz用同样的水平方向逻辑计算。然后用rigidbody.AddForce(initialVelocity, ForceMode.VelocityChange)施加这个模式会直接改变速度忽略质量非常适合用来设定一个初始“击飞”速度。方案二计算初始力向量使用 AddForce(force, ForceMode.Impulse)Impulse模式是施加一个瞬间的冲量力乘以时间它会根据物体的质量来改变速度。公式是冲量 目标速度 * 质量。所以如果你已经算好了方案一中的initialVelocity那么需要的力就是initialVelocity * rigidbody.mass。优点考虑了物体质量不同质量的物体用同样的“力”击飞效果不同更真实。缺点需要获取刚体的质量并且如果质量动态变化可能会有点小问题。方案三持续施加力不推荐用于纯击飞效果在FixedUpdate里持续给一个水平方向的力同时物体受重力下落。这种方法更适合像飞机、火箭这种有持续动力源的情况。对于一次性的撞击用前两种更合适。我个人在实际项目中更常用方案一VelocityChange因为它不依赖于质量更容易控制不同物体被击飞后速度的一致性计算也更直接。方案二Impulse在需要体现“重量感”时更好。2.3 持续旋转的控制策略旋转的控制相对独立。目标是让物体在空中翻滚而不是稳定地绕某个轴转。方法一施加随机扭矩AddTorque在物体被撞飞的瞬间施加一个随机的扭矩。rigidbody.AddTorque(new Vector3(Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f)) * torqueStrength, ForceMode.Impulse);优点实现简单效果随机自然符合“被撞飞”的混乱感。缺点旋转轴和速度完全随机可能不够“好看”或者在某些情况下旋转太快/太慢。方法二设置随机角速度angularVelocity直接给刚体一个随机的旋转速度。rigidbody.angularVelocity new Vector3(Random.Range(-5f, 5f), Random.Range(-5f, 5f), Random.Range(-5f, 5f));优点更直接可以精确控制旋转速度的大小。缺点和扭矩一样完全随机。方法三施加一个带有主导方向的扭矩这是为了追求更好视觉效果的方法。比如我们希望物体主要绕着一个水平方向被击飞方向的侧向旋转。我们可以计算击飞方向的侧向向量通过叉乘然后以此为主要轴再混合一点随机性。Vector3 hitDirection ... // 被撞击的方向 Vector3 up Vector3.up; Vector3 dominantRotationAxis Vector3.Cross(hitDirection, up).normalized; // 得到一个垂直于击飞方向和朝上的轴 Vector3 randomAxis new Vector3(Random.Range(-0.3f, 0.3f), Random.Range(-0.3f, 0.3f), Random.Range(-0.3f, 0.3f)); Vector3 finalTorqueAxis (dominantRotationAxis randomAxis).normalized; rigidbody.AddTorque(finalTorqueAxis * torqueStrength, ForceMode.Impulse);优点旋转有主要趋势看起来更符合“被侧面击中而翻滚”的直觉视觉效果更可控、更专业。缺点计算稍复杂。对于大多数情况方法一或方法三是更好的选择。方法三尤其推荐用于需要表现打击感的游戏。3. 完整实现步骤与代码详解下面我将结合一个具体的场景来实现玩家控制的角色带有一个碰撞体撞击一个放在地上的箱子箱子被撞飞并旋转。3.1 场景与物体准备地面创建一个Plane或Cube缩放作为地面挂载BoxCollider。玩家创建一个胶囊体Capsule作为角色挂载Rigidbody设置质量为50Drag为2防止太飘和CapsuleCollider。为它编写一个简单的移动脚本这里不展开。被击飞物体箱子创建一个Cube缩放成箱子大小。挂载Rigidbody设置Mass为2Drag为0.1Angular Drag为0.05Collision Detection为Continuous Dynamic。再挂载BoxCollider。创建脚本在箱子上创建一个C#脚本命名为FlyAwayOnHit。3.2 脚本编写FlyAwayOnHit.cs这个脚本是核心负责检测撞击、计算抛物线、施加旋转。using UnityEngine; [RequireComponent(typeof(Rigidbody), typeof(Collider))] public class FlyAwayOnHit : MonoBehaviour { [Header(击飞参数)] public float maxPeakHeight 5.0f; // 抛物线最大高度 public float baseHorizontalDistance 8.0f; // 基础水平飞行距离 public float forceMultiplier 1.0f; // 力乘数用于微调 [Header(旋转参数)] public float minTorqueStrength 30.0f; public float maxTorqueStrength 80.0f; public bool useDominantRotation true; // 是否使用主导旋转轴 [Range(0f, 1f)] public float randomAxisWeight 0.3f; // 随机轴的影响权重 [Header(状态)] public bool hasBeenHit false; // 防止被多次击飞 private Rigidbody rb; private Vector3 startPosition; void Start() { rb GetComponentRigidbody(); startPosition transform.position; } // 使用碰撞检测更符合物理直觉 void OnCollisionEnter(Collision collision) { // 确保只被触发一次并且撞击者有一定速度避免轻轻碰到就飞 if (hasBeenHit || rb.isKinematic) return; // 可以在这里加标签判断比如只有被Player或Bullet撞才飞 // if (!collision.gameObject.CompareTag(Player)) return; // 获取撞击点和撞击方向从撞击体指向自己 ContactPoint contact collision.contacts[0]; Vector3 hitPoint contact.point; Vector3 hitDirection (transform.position - hitPoint).normalized; // 也可以直接用碰撞体的速度方向作为参考 // Vector3 hitDirection collision.relativeVelocity.normalized; // 计算击飞的目标点基于击打方向 // 这里简化处理将击打方向在XZ平面投影并归一化乘以基础距离 Vector3 horizontalHitDir new Vector3(hitDirection.x, 0, hitDirection.z).normalized; if (horizontalHitDir.magnitude 0.1f) // 如果垂直撞击给个默认方向 horizontalHitDir transform.forward; Vector3 targetPos startPosition horizontalHitDir * baseHorizontalDistance; targetPos.y startPosition.y; // 假设落点高度和起点一样地面 // 执行击飞 LaunchToTarget(targetPos, hitDirection); hasBeenHit true; // 可以在这里触发音效、粒子特效等 // SoundManager.Instance.PlayHitSound(); // ParticleSystem burst GetComponentParticleSystem(); // if(burst ! null) burst.Play(); } /// summary /// 核心方法将物体以抛物线形式发射到目标点 /// /summary /// param nametarget目标位置/param /// param namehitDir撞击方向用于计算旋转/param public void LaunchToTarget(Vector3 target, Vector3 hitDir) { if (rb null) rb GetComponentRigidbody(); // 1. 计算初始速度 Vector3 velocity CalculateLaunchVelocity(transform.position, target, maxPeakHeight); velocity * forceMultiplier; // 应用力度乘数 // 2. 施加力改变速度 rb.AddForce(velocity, ForceMode.VelocityChange); // 3. 施加旋转扭矩 ApplyRotationTorque(hitDir); } /// summary /// 计算从start到end达到指定峰值高度所需的初速度 /// /summary private Vector3 CalculateLaunchVelocity(Vector3 start, Vector3 end, float peakHeight) { // 确保峰值高度大于起点和终点的高度差 float maxY Mathf.Max(start.y, end.y) peakHeight; float apexHeight maxY - start.y; // 计算飞行时间 (t sqrt(2h/g) * 2) float g Mathf.Abs(Physics.gravity.y); // Unity中重力是负的取绝对值 float timeToApex Mathf.Sqrt(2 * apexHeight / g); float totalFlightTime timeToApex * 2; // 上升下降 // 如果时间过短或为NaN比如peakHeight设成0给个默认值 if (float.IsNaN(totalFlightTime) || totalFlightTime 0.01f) totalFlightTime 1.0f; // 计算水平速度 Vector3 horizontalDelta new Vector3(end.x - start.x, 0, end.z - start.z); Vector3 horizontalVelocity horizontalDelta / totalFlightTime; // 计算垂直速度 float verticalVelocity Mathf.Sqrt(2 * g * apexHeight); // 合成速度向量 Vector3 launchVelocity horizontalVelocity Vector3.up * verticalVelocity; return launchVelocity; } /// summary /// 施加旋转扭矩 /// /summary private void ApplyRotationTorque(Vector3 hitDirection) { Vector3 torqueAxis; float torqueStrength Random.Range(minTorqueStrength, maxTorqueStrength); if (useDominantRotation) { // 方法三主导轴 随机扰动 Vector3 horizontalHitDir new Vector3(hitDirection.x, 0, hitDirection.z).normalized; if (horizontalHitDir.magnitude 0.1f) horizontalHitDir Vector3.right; // 备用方向 Vector3 dominantAxis Vector3.Cross(horizontalHitDir, Vector3.up).normalized; Vector3 randomPart new Vector3(Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f)) * randomAxisWeight; torqueAxis (dominantAxis randomPart).normalized; } else { // 方法一完全随机 torqueAxis new Vector3(Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f)).normalized; } // 使用Impulse模式施加一个瞬间扭矩 rb.AddTorque(torqueAxis * torqueStrength, ForceMode.Impulse); } // 可选提供一个调试用的方法在编辑器里点击按钮测试 [ContextMenu(测试击飞)] void TestLaunch() { hasBeenHit false; rb.velocity Vector3.zero; rb.angularVelocity Vector3.zero; transform.position startPosition; Vector3 randomTarget startPosition new Vector3(Random.Range(-5f, 5f), 0, Random.Range(5f, 10f)); LaunchToTarget(randomTarget, new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f)).normalized); } }3.3 参数调节与效果微调脚本挂上去后在Inspector里你会看到一堆参数。游戏物理效果的“手感”很大程度上靠调这些参数maxPeakHeight(峰值高度) 控制物体能飞多高。值越大抛物线越陡峭。对于重物这个值应该小一些对于轻的碎片可以大一些。baseHorizontalDistance(基础水平距离) 配合击打方向决定物体大概飞多远。注意这个距离是理想无阻力情况下的实际因为有Drag会飞得近一些。forceMultiplier(力乘数) 一个全局调节器。如果你觉得整体力度不够或太猛调这个最方便不用动其他复杂计算。min/maxTorqueStrength(扭矩强度范围) 控制旋转的“力道”。太小转不起来太大会像陀螺一样疯转。建议从30开始试。useDominantRotation(使用主导旋转) 勾选后物体会倾向于绕一个与击打方向垂直的轴旋转视觉效果更符合直觉。randomAxisWeight(随机轴权重) 当使用主导旋转时这个值控制随机翻滚的程度。0.3意味着随机部分占30%能让每次旋转都有些许不同避免单调。调节流程建议先把maxPeakHeight和baseHorizontalDistance调到一个你觉得大概合适的范围。运行游戏撞击物体。观察飞行轨迹是否自然落地位置是否合理。调整Drag和Angular Drag。如果物体飞得太远、转得太久就适当加大这两个值。微调forceMultiplier和扭矩强度直到击飞的“力度感”和旋转的“混乱感”让你满意。可以尝试开关useDominantRotation感受两种旋转风格的区别。4. 进阶优化与常见问题排查4.1 性能与稳定性优化刚体休眠Sleeping物理引擎为了性能会对静止的刚体进入休眠状态。我们的物体飞出去最终停在地上后应该让它休眠。确保Rigidbody的Sleep Mode是默认的Sleep When Below Threshold即可。你也可以在物体落地后一段时间手动调用rb.Sleep()但通常没必要。碰撞体优化对于复杂的模型不要直接用Mesh Collider尽量使用简单的Compound Collider组合碰撞体比如用多个Box或Capsule来近似形状。Mesh Collider非常耗性能尤其是网格面数高的时候。脚本执行顺序所有对Rigidbody的操作AddForce,AddTorque, 修改velocity都必须放在FixedUpdate中或者由FixedUpdate内的事件如OnCollisionEnter触发。因为物理引擎在FixedUpdate中更新在其他地方修改力或速度可能导致帧间不一致。对象池Object Pooling如果你的游戏中有大量物体需要被击飞比如打碎一堆瓶子频繁的Instantiate和Destroy会引发GC垃圾回收卡顿。一定要使用对象池技术。物体被击飞、落地、停止运动后不是Destroy它而是将其重置速度归零、位置复位、hasBeenHit设为false并放回池中下次需要时再取出。4.2 常见问题与解决方案这里列一个表格快速排查你可能遇到的问题问题现象可能原因解决方案物体直接穿过了地面或其他碰撞体1. 刚体速度过快。2. 碰撞检测模式为Discrete。1. 检查计算出的初速度是否过大适当减小forceMultiplier或飞行距离。2.将Rigidbody的Collision Detection改为Continuous或Continuous Dynamic。物体被撞飞后旋转非常慢或不旋转1. 扭矩强度torqueStrength设置太小。2. 物体的Angular Drag角阻力设置太大。3. 物体的质量Mass太大。1. 增大minTorqueStrength和maxTorqueStrength。2. 减小Angular Drag尝试0.01-0.05。3. 检查物体质量是否合理对于箱子2-5比较合适。抛物线轨迹很奇怪比如飞得很平或直接往上窜1.maxPeakHeight设置不合理太高或为0。2. 计算初速度时起点和终点的Y值高度差处理有误。1. 确保maxPeakHeight是一个合理的正值如2-10。2. 调试打印CalculateLaunchVelocity函数计算出的horizontalVelocity和verticalVelocity检查数值。确保g取的是正数Mathf.Abs(Physics.gravity.y)。物体在空中旋转轴很单一总是绕同一个轴转使用了主导旋转轴但随机权重randomAxisWeight太小或为0。增大randomAxisWeight的值比如调到0.5增加随机性。或者关闭useDominantRotation。撞击后物体有时会疯狂抖动或抽搐1. 碰撞体之间有轻微重叠。2. 在Update中错误地持续施加力或修改Transform。3. 多个脚本同时操作同一个刚体。1. 检查碰撞体确保初始状态下没有相互嵌入。调整位置或碰撞体大小。2.确保所有物理操作都在FixedUpdate或物理事件回调中。3. 检查是否有其他脚本如AI脚本、动画脚本也在控制该物体。物体被击飞后碰到墙会粘住或反弹很怪物理材质Physic Material的问题。默认材质弹性和摩擦力为0.6/0.4。为被击飞物体和墙壁的碰撞体创建合适的物理材质。如果想减少反弹降低Bounciness弹性如果想减少滑动调整Dynamic Friction动摩擦和Static Friction静摩擦。4.3 效果增强技巧结合动画系统如果被击飞的是角色或带有骨骼的物体可以在被击飞瞬间触发一个受击动画比如短暂的僵直或翻滚动画同时结合根运动Root Motion或脚本控制让物理和动画混合效果更佳。加入屏幕抖动Camera Shake在撞击发生的瞬间触发一个相机抖动效果能极大增强打击感。时间缩放Time Scale特效在撞击命中的那一帧使用Time.timeScale做一个短暂的“子弹时间”效果比如瞬间降到0.1再快速恢复可以突出打击的力度和瞬间感。运动模糊Motion Blur如果项目支持为高速运动的被击飞物体启用运动模糊可以增加速度感和动态模糊的真实感。音效与粒子跟随为物体添加一个子物体上面挂载AudioSource和ParticleSystem。在被击飞时播放撞击声和飞溅的粒子如灰尘、火花并让这个子物体在物体运动过程中持续播放风声或旋转的音效。注意处理好3D音效的空间衰减。实现这个“被撞飞并旋转”的效果最关键的还是理解Unity物理引擎的工作方式——用“力”和“扭矩”去引导而不是直接“操控”。把参数调节的过程当作是在给游戏世界定义“物理法则”多试几次你就能找到那种既符合物理直觉又充满游戏张力的完美手感了。