从动量守恒到恢复系数:射击游戏碰撞分类与物理引擎调参实战
2026/9/23 4:11:01 网站建设 项目流程

作为一个既教物理又折腾独立游戏的人,我经常在FPS游戏里被朋友问:"这游戏的枪为什么打人会有击退效果?""为什么我跳起来撞到墙角会被卡住?"其实这些看似细枝末节的游戏手感,背后全是物理里碰撞分类的基本功。你平时在教科书上看到的是"弹性碰撞、非弹性碰撞"几个干巴巴的名词,但它在射击游戏里却直接决定了子弹打中敌人时是"噗"一下嵌入、还是"砰"一声弹开、还是整个人被掀飞出去。

这篇文章我不打算讲枯燥的公式推导,而是用尽量生活化的类比,把这三种碰撞类型(弹性碰撞、完全非弹性碰撞、一般非弹性碰撞)彻底讲透,再带着你把它们套到射击游戏的枪械反馈、物理子弹、爆炸冲击、人物碰撞体这些真实场景里,看看游戏引擎是怎么用这些物理规则营造手感的。不管你是中学物理还没忘完的普通玩家,还是正在做自己的射击游戏Demo的开发新手,这篇文章都应该能让你收获一点"原来如此"的感觉。

1. 先搞懂碰撞的分类底层逻辑

1.1 碰撞到底是什么——从动量守恒说起

要理解碰撞分类,第一个绕不开的概念是动量。物理课本里说的动量是物体质量和速度的乘积,公式写成 (p = mv),它描述的是一个物体"运动的强度"。两个物体碰到一起,如果不考虑地面摩擦力、空气阻力这些外部干扰,那么碰撞前后它们的总动量是守恒的,这就是动量守恒定律。

这个公式本身很简单,但它是个很可怕的约束——不管两个物体撞成什么样、碎成几块,只要没有外力参与,碰撞前后的总动量必须一模一样。这就是碰撞分类的根基:动量守恒是所有碰撞都必须遵守的底线,而碰撞分类则取决于在满足这个底线之外,还有没有额外多守住一条"动能守恒"。

很多人把"动能"和"动量"搞混,这里用一个生活化类比说明:动量像一个奔跑中的大胖子,动能像他那股"撞上去能把人怼飞"的冲击力。两个物体相撞,动量必然守恒,这是物理世界不允许讨论的规则;但动能可不一定——如果碰撞过程中有声音、有热量、有形变,那动能就有一部分被"偷走"变成了内能或声能。碰撞分类,本质上就是看"动能有没有被偷走、偷走了多少"。

1.2 为什么碰撞分类应该看成一条连续谱

教科书里的分类是:弹性碰撞、非弹性碰撞、完全非弹性碰撞。但真正做游戏物理、或者做物理题的时候,我更喜欢把它理解成一条连续谱,而不是三个互不相干的抽屉。

这条谱的两端是理想状态。一端是弹性碰撞,动能完全守恒,碰撞前后总动能一样,现实里几乎不存在,只是某些情况下非常接近——比如两颗台球对撞、牛顿摆里的钢球互撞。另一端是完全非弹性碰撞,两个物体碰完直接粘在一起,动能损失最大,比如一颗子弹射进沙袋里卡住不动。

而现实世界里绝大多数碰撞都落在两端之间,叫一般非弹性碰撞,动能有损失但没损失到"粘在一起"的程度。比如你玩碰碰车,两车撞完会弹开一些,但不会有台球那种干脆利落的弹性——一部分动能被车的形变、轮胎摩擦和碰撞时"砰"的噪音吃掉了。

物理学里有个概念叫恢复系数,公式是 (e = \frac{v_2' - v_1'}{v_1 - v_2})。看不懂没关系,你可以粗暴理解成"碰完之后分开的猛烈程度"。(e=1) 是理想弹性碰撞,(e=0) 是完全非弹性碰撞,游戏物理引擎里的Bounciness参数本质就是干这个的。

2. 弹性碰撞与非弹性碰撞的物理细节拆解

2.1 弹性碰撞:能量守恒的完美世界

弹性碰撞最神的性质不是动量守恒,而是它额外守住了动能守恒。以"一个运动小球撞另一个静止小球"这个最简单场景为例,设运动小球质量为 (m_1),速度为 (v_1),静止小球质量为 (m_2),碰后速度分别为 (v_1') 和 (v_2')。

动量守恒给出一个方程:

[ m_1 v_1 = m_1 v_1' + m_2 v_2' ]

动能守恒再给出一个方程:

[ \frac{1}{2} m_1 v_1^2 = \frac{1}{2} m_1 v_1'^2 + \frac{1}{2} m_2 v_2'^2 ]

两个方程联立求解,就能得到碰后的速度:

[ v_1' = \frac{(m_1 - m_2)}{m_1 + m_2} v_1 ]

[ v_2' = \frac{2 m_1}{m_1 + m_2} v_1 ]

这两个公式看起来吓人,但背后藏着特别反直觉的结论:如果 (m_1 = m_2),也就是两个球质量相等,你会发现碰后运动球的速度完全变成0,原来静止的球则完全获得了碰撞前的速度——这就是台球桌上经常看到的"定球"效果,也是牛顿摆最让人着迷的地方。质量差距越大,轻球越容易被弹飞,重球几乎纹丝不动。

2.2 非弹性碰撞:现实世界的主旋律

如果是非弹性碰撞,那动力学方程里就得损失掉一部分动能。现实里这种损失主要是三块:形变消耗(物体凹进去再弹回来但回不到原样)、热能消耗(摩擦生热)和声能消耗(碰撞发出的响声)。

因为动能不守恒,我们不能再写动能守恒方程,只能靠恢复系数 (e) 来做补充。碰后相对速度等于原相对速度乘以 (e),所以 (e=0.6) 就意味着"碰完以后互相分开的速度只有撞之前靠近速度的60%"。

一般非弹性碰撞算起来很麻烦,但搞懂完全非弹性碰撞就够了。完全非弹性碰撞的定义极端简单:碰完两个物体以同一个速度一起运动。既然粘在一起了,恢复系数就是0。拿子弹射沙包举例,设子弹质量 (m)、速度 (v),沙包质量 (M)、初始静止,碰撞后黏在一起的速度 (V) 可以直接算:

[ m v = (m + M) V ]

[ V = \frac{m}{m + M} v ]

子弹质量越小、沙包质量越大,最后共同速度就越小。这一个公式,其实就是射击游戏里"击退效果"的物理原型。

2.3 三个碰撞类型快速对比表

碰撞类型恢复系数e动量守恒动能守恒现实/游戏典型例子
弹性碰撞e = 1守恒完全守恒台球对撞、牛顿摆、理想化粒子
一般非弹性碰撞0 < e < 1守恒部分损失篮球落地弹起、汽车追尾、Ragdoll撞墙
完全非弹性碰撞e = 0守恒损失最大子弹射入沙包、两个泥团相撞、黏弹粘合

这张表看起来简单,但它背后有个特别值得新手记住的点:在物理引擎里头,所有碰撞类型的代码"底子"都是一样的——先保证动量守恒,再用恢复系数 (e) 去调整分离速度。你肉眼看到的"弹开"还是"黏住",其实只是同一个公式在不同参数下的表现。

3. 射击游戏引擎里碰撞是怎么跑的

3.1 从物理公式到游戏引擎:碰撞体的思想

游戏引擎当然不会像物理课本那样解方程,但原理是一脉相承的。在Unity、Unreal这些引擎里,一个可碰撞的对象通常由三件套组成:碰撞体(Collider,定义物体的形状范围)、刚体(Rigidbody,决定物体是否受物理影响)和物理材质(Physics Material,提供摩擦力与弹力系数)。

这里面物理材质的Bounciness参数,就是物理世界恢复系数 (e) 在主流的游戏引擎里的直译。调成1,物体撞啥都像打了鸡血一样弹个没完;调成0,碰撞后基本就"卸力"了。我们在FPS里踩墙跳、手雷弹墙、敌人中弹后飞出去,背后全靠这个数值。

一个常见的误区是:很多人觉得游戏里的碰撞就应该100%还原现实物理。真那样做,FPS根本没法玩——真实子弹动能一枪能掀翻成年人吗?游戏需要的是"看起来像那么回事、打起来很爽"的伪物理。所以引擎里几乎所有碰撞都是经过艺术化调参后的结果。

3.2 子弹命中目标:典型的完全非弹性碰撞案例

先说最核心的应用:子弹击中人体。现实里子弹射进肉体,绝大多数能量被组织吸收、子弹留在体内或穿出,这是典型的完全非弹性碰撞。在游戏里,这种"非弹性"体现在两个地方:一是伤害结算,二是受击反馈。

伤害结算用物理公式其实是"杀鸡用牛刀",游戏一般直接按命中部位和距离查表。但受击反馈就纯粹靠物理了。当子弹命中角色时,客户端会计算一个冲击力,施加到角色的Rigidbody上,让角色产生轻微后仰或位移。这个冲击力可以直接从动量定理推出来:子弹动量的变化量乘以一个修正系数,再除以受击角色质量,就能估算出角色被推倒的速度。

我在自己的FPS Demo里写过这样一段子弹命中后的受击逻辑:

Vector3 bulletVelocity = bulletRigidbody.velocity; float bulletMass = bulletRigidbody.mass; // 子弹命中瞬间的动量变化,模拟完全非弹性碰撞 Vector3 impactImpulse = bulletVelocity * bulletMass * hitReactionScale; // 把冲击力加到目标刚体上 targetRigidbody.AddForceAtPosition(impactImpulse, hitPoint, ForceMode.Impulse); // 同时给一个极短时间的HitStop,让枪感更清脆 StartCoroutine(FreezeFrames(hitStopDuration));

这里有个细节很多人不知道:为什么要用AddForceAtPosition而不是直接在Rigidbody上AddForce?因为冲击力施加位置不同,角色受力后的运动状态完全不同——打在胸口,角色是整体后退;打在头部,角色会向后仰甚至旋转倒地。AddForceAtPosition能精确控制"撞击点",实现出更自然的受击动画。

3.3 人物移动和跳跃:弹性碰撞的"偷懒"版本

人物与场景的碰撞看起来和"弹性"不太沾边,因为我们不希望角色被墙壁弹得乱飞。但跳床和弹跳板的设计就要反过来用它。

正常角色控制器(CharacterController)里的碰撞其实是"挡住就停"的逻辑,几乎没有弹性恢复,这相当于把地面和墙壁的恢复系数强行压到很低的水平。但如果你做一张跳床,或者想让玩家从高处落地时有一个"缓冲弹起"的手感,就要单独给那块碰撞面设置一个高Bounciness的物理材质,再配合速度衰减公式:

[ v_{bounce} = e \cdot v_{impact} ]

假如你从高处落地,撞击瞬间速度为20米/秒,地面物理材质的Bounciness设为0.5,那弹起速度就是10米/秒。这个公式和你在物理课上学过的恢复系数完全是一回事,但游戏里的地面往往还会给弹起方向加一个操控系数,让玩家能微调落点。

这里有一个无数人踩过的坑:如果你把角色整个当成普通刚体放到跳床上,碰上Bounciness=1的材质,它会无限反弹、越弹越高,玩家完全没法操控,撞两次摄像头都能晃晕。正确的做法是给角色加一个"最高弹跳速度"限制,或者在角色落地弹起瞬间覆盖恢复系数。

3.4 爆炸冲击波与Ragdoll:真实物理在游戏中的艺术化

爆炸伤害是射击游戏里最有画面感的物理时刻。手雷爆开时,会对爆炸半径内的所有刚体施加径向冲量,这个冲量的大小遵循类似平方反比的衰减公式:

[ F = \frac{k}{r^2} \cdot m ]

其中 (r) 是物体到爆炸中心的距离,(k) 是爆炸强度系数。距离越远,受力越小,这是从点爆炸能量扩散规律简化来的。而物理引擎里实际做的时候,会用球形范围检测加Falloff曲线来逼近,不会真的做一次平方反比——因为在每秒几十帧的游戏里,平方反比意味着近距离冲量无穷大,很容易把物体炸出物理边界。

Ragdoll(布娃娃物理)系统则是另一个经典应用。角色死亡后,骨骼切换成一个由多个刚体关节组成的布娃娃模型,这些刚体之间的碰撞就是典型的非弹性碰撞——身体部位碰到地面会反弹一点点,但因为肌肉张力消失,几乎不会产生真正的回弹。这个"刚刚好"的效果,就是靠把每个关节的物理材质Bounciness压到0.1到0.2之间实现的。

4. 游戏物理中的碰撞参数调优与实战避坑

4.1 物理材质Bounciness设置的实战经验

在FPS项目里,给不同表面设置Bounciness是门手艺活。我试过几套参数,直接贴出来供参考:

表面类型BouncinessFriction典型用途
混凝土/地面0.11.0角色落地、手雷触地不弹起
金属墙壁0.30.2手雷反弹、子弹弹射
跳床/弹性板0.8-1.00弹跳玩法
尸体/布娃娃0.10.5死亡动画自然倒地
冰块0.30滑行、打滑

这里有个很微妙的问题:手雷的Bounciness设计。真实手雷落地基本不弹,但游戏里如果Bounciness设成0,手雷落地就像泥巴一样瘫在地上,视觉效果非常死板。我一般设0.25到0.35,让它有明显但克制的两三次弹跳,给玩家留出反应时间,同时手感和真实感都能兼顾。

4.2 碰撞体和刚体常见问题

物理做多了,你会发现真正折磨人的不是公式,而是引擎里那些"物理发疯"的瞬间。我遇到的三个典型问题:

第一个是抖动(Jitter)。刚体物体紧贴地面时,会因为重力不断被压进碰撞体再弹出来,产生高频震动。解决办法,一个是把物体的Solver Iterations调高一点,另一个是干脆不要让它当纯刚体,用"运动学刚体+插值"来处理。

第二个是隧道效应(Tunneling)。子弹飞太快,一帧之内穿透了一面薄墙,这在子弹射击游戏里特别致命。引擎的解决办法是Continuous Collision Detection(连续碰撞检测),原理是把物体这一帧的运动轨迹当成一条"扫掠体"来做检测,而不是只检测终点位置。代价是性能开销大,所以一般只给子弹、高速导弹启用。

问题现象常见原因推荐排查方案
物体抖动/抖动漂移物理材质摩擦设置过高、刚体睡眠判定异常降低摩擦,调整SleepThreshold,开启刚体插值
高速子弹穿透薄墙默认离散碰撞检测只查末位置使用Continuous或ContinuousSpeculative模式
人物被碰撞体卡住碰撞体边缘过薄、多个碰撞体叠加给人物加Capsule碰撞体,避免使用复杂Mesh Collider
网络延迟导致位置跳变客户端预测误差,服务器回滚不一致用快照插值加延迟补偿,或在服务器端做HitValidation
手雷弹飞方向随机地面存在网格接缝,碰撞法线突变在爆炸点做射线检测,手工修正初速度方向

第三个是网络同步。这是射击游戏物理里最坑的地方。本地玩家丢一个手雷,手雷在客户端A有物理模拟,但在客户端B要看到同样的弹跳轨迹,就需要服务器协调。现在市面上主流的做法是"服务器权威+客户端预测"——物理计算在服务器跑,客户端本地预测路径,出现偏差时做平滑修正。物理参数稍微偏一点,两个端的手雷轨迹就会天差地别,所以打包时物理材质和初始速度必须做成可同步的参数。

4.3 性能优化思路

物理碰撞看着普通,实际是射击游戏性能的大坑。一个手雷爆炸要检测周围30个刚体,一发霰弹枪7颗子弹、每颗都要做碰撞检测,几十人混战的时候物理计算量能直接把CPU拖垮。我的优化思路有几条:

碰撞分层是必须的。把子弹、角色、场景做成不同的Layer,只让该碰撞的层做检测,比如子弹层只检测角色层和场景层,绝不检测其他子弹。引擎里每个物理每帧都在"广播"自己的位置,分层之后广播数量呈指数级下降。

另一个技巧是动态调整物理更新频率。远处的敌人和近处的敌人没有必要每帧都跑物理,可以分成两个或多个"物理调度等级":近距离每秒60次物理更新,中距离30次,远距离10次。这个方案在《绝地求生》这类大地图射击游戏里很常见,因为远处中弹的敌人根本没有必要做高精度的物理反馈——反正玩家也看不清。

还有一个容易被忽视的点是碰撞体的形状选择。很多初学者喜欢用Mesh Collider去精准拟合场景模型,但Mesh Collider的计算量是Box和Sphere的十几倍。射击游戏里的墙壁其实不需要那么精确的碰撞形状,用几个Box Collider拼出来,玩家完全感觉不到区别,但性能提升是肉眼可见的。

5. 我个人在处理碰撞时的几个心得

聊到这儿,基本的分类和游戏应用都过了一遍。最后分享几个我实际做项目中总结出来的经验,希望对你有直接的参考价值。

第一个心得是:做游戏物理前,先把物理公式写清楚,哪怕最后引擎一个公式都没用上。我在做碰撞系统时,把动量守恒、恢复系数、完全非弹性碰撞公式全部推导了一遍,结果写代码时发现所有逻辑都能对号入座——子弹击退用的是动量定理,手雷爆炸用的是类平方反比衰减,跳床弹跳用的就是恢复系数公式。公式是物理世界的地图,引擎只是交通工具,没有地图,你连该调哪个参数都不知道。

第二个心得是:精心调过的Bounciness参数一定要全项目统一保存。我见过太多项目里同一个地面,在这个场景Bounciness是0.1,换个场景又变成0.3,结果手雷弹跳手感完全不一致,玩家会觉得"这游戏物理有问题"。做一个物理参数总表,把所有表面材质统一管理,是一个成熟项目的基本功。

第三个心得是:物理手感不是越真实越好,而是越"符合预期"越好。射击游戏玩家不会去管真实子弹击中人体能不能把人击飞,他只关心"这一枪打中了、反馈清晰、玩起来爽"。所以我做受击反馈时,会刻意放大击退效果,把真实的冲击力乘以1.2到1.5的系数,让中弹后的位移更明显。这不算欺骗——这是游戏设计与物理的合理结合。

如果你想试着做自己的FPS物理玩法,建议从最简单的手雷弹跳开始:给一个球体设置刚体,调整Bounciness为0.3,丢出去观察两次弹跳衰减;然后把恢复系数改成0.8,观察"越弹越欢"的失控感。亲手把玩过这两个极端,你就能深刻理解恢复系数这条连续谱两端到底意味着什么,也就能明白射击游戏里的每一个"砰"背后,其实都是物理规则在替你讲故事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询