1. 从一个滑动小球说起:为什么值得认真拆解
很多人第一次接触Unity的物理系统,都是从一个小球在平面上滑动开始的。这个场景看起来简单到几乎不值得写一篇文章,但我在带新人和自己做项目复盘时发现,恰恰是这种最基础的场景,藏着物理引擎使用中最容易踩的坑。滑动小球这个案例,本质上是在回答一个问题:如何让一个物体在Unity里按照你期望的方式动起来。它涉及刚体组件、碰撞体、物理材质、力的施加方式、速度控制、摩擦与阻尼等一系列核心概念。你把这个搞透了,后面做角色控制器、载具物理、弹球游戏、推箱子解谜,底层逻辑都是同一套东西。
这篇文章适合谁看?如果你刚开始学Unity,对Rigidbody和Collider的关系还模棱两可,或者你做过一些Demo但总觉得物体运动“不太对劲”——该停的时候不停、该滑的时候卡住、速度忽快忽慢——那这篇内容就是写给你的。我会从整体设计思路讲起,把每个关键参数的计算逻辑拆开,再给出一套可以直接复现的实操流程,最后把我自己踩过的坑和排查方法整理出来。全文基于Unity的通用物理实践,不依赖任何特定版本的新特性,你用什么版本都能对照着做。
需要提前说明的是,滑动小球这个场景在不同项目里的实现方式差异很大。有人用纯物理力驱动,有人直接改Transform,有人用速度控制。这三种方式没有绝对的好坏,但适用场景完全不同。我下面会逐一分析它们的取舍逻辑,你根据自己的需求选就行。
2. 整体设计与方案选型:三种运动驱动方式的取舍
2.1 为什么不能直接改Transform
新手最容易想到的做法是每帧给小球的位置加一个偏移量,也就是直接操作Transform。这种做法在纯视觉演示里没问题,但它绕过了物理引擎的整个计算管线。什么意思呢?Unity的物理系统在FixedUpdate里做碰撞检测和响应,如果你在Update里直接改Transform,物理引擎根本不知道小球“应该”在哪里,碰撞体会出现穿透、抖动、或者干脆检测不到接触。
我试过一个很典型的场景:用Transform位移让小球在平台上滑动,结果小球碰到墙壁时直接穿过去了,因为物理引擎在它自己的计算周期里,小球的位置还是上一帧的旧值。所以只要你的场景里存在任何碰撞交互需求,直接改Transform就是死路一条。它只适合那种完全不需要物理反馈的纯展示动画。
2.2 力驱动与速度驱动的核心差异
真正用物理系统驱动小球运动,有两条路:施加力(AddForce)和直接设置速度(velocity)。这两者的区别,用生活类比来说,力就像你用手推一个箱子,箱子不会立刻达到你的手速,而是有一个加速过程;速度就像你直接把箱子瞬移到某个运动状态,它立刻就以那个速度开始跑。
AddForce的好处是物理表现自然,加速和减速都有惯性感,适合模拟真实世界的推动效果。但它的缺点是控制精度低,你想让小球精确停在某个位置,单纯靠力很难做到,因为力会持续改变速度,你需要不断调整力的方向和大小来“刹车”。
velocity的好处是控制精确,你设多少就是多少,适合需要精准操控的场景,比如平台跳跃游戏里的角色移动。但它的缺点是失去了惯性感,如果你每帧都硬设速度,物体会显得很“硬”,没有重量感。
我的建议是:滑动小球这个场景,用velocity做基础运动控制,用AddForce做特殊效果。比如小球在平面上滑动时用velocity保持匀速,碰到弹簧板时用AddForce给一个爆发力。这样既有精确控制,又有物理反馈。
2.3 物理材质与摩擦力的角色
很多人会忽略物理材质(Physic Material)的作用。小球在平面上滑动,摩擦力决定了它能滑多远、减速多快。Unity默认的物理材质Dynamic Friction和Static Friction都是0.6,这个值对于“滑动”场景来说偏大,小球会很快停下来。
如果你想要那种冰面滑行的效果,需要创建一个新的物理材质,把Dynamic Friction调到0.05到0.1之间,Static Friction调到0.1左右,Friction Combine设为Minimum。这样小球在滑动时受到的阻力会小很多,能滑出很长的距离。
但这里有个坑:物理材质是挂在Collider上的,不是挂在Rigidbody上。很多人改了Rigidbody的参数发现没效果,就是因为改错了地方。另外,两个碰撞体接触时,最终的摩擦系数取决于双方的Friction Combine设置,默认是Average,也就是取平均值。如果你只改了小球的材质,地面的材质还是默认值,最终摩擦系数会被拉高。所以要么两边都改,要么把Combine模式设成Minimum,取两者中较小的那个。
3. 核心细节拆解:刚体参数与运动逻辑的实操要点
3.1 Rigidbody关键参数逐个说清楚
小球要动起来,必须挂Rigidbody组件。这个组件里有一堆参数,我挑最关键的几个讲。
Mass(质量):默认是1。质量影响的是碰撞时的动量交换,不影响自由落体速度(伽利略老爷子早就证明了)。但在滑动场景里,质量会影响摩擦力对速度的衰减速率。质量越大,同样的摩擦力下减速越慢。如果你想让小球滑得更远,除了调低摩擦系数,适当增加质量也有帮助。但别调太大,否则碰撞时会把其他物体撞飞。
Drag(阻力):这是空气阻力,默认0.0。它和摩擦力是两回事。Drag是物体在没有任何接触时也会受到的减速,比如小球飞在空中时速度会慢慢降低。滑动场景里,Drag一般设0就行,减速交给摩擦力处理。如果你发现小球在平面上滑行时减速太快,先检查Drag是不是被误设了值。
Angular Drag(角阻力):控制旋转的衰减。小球滚动时,角阻力太大会让它很快停止旋转,看起来像在“滑”而不是“滚”。默认0.05,一般够用。如果你想要小球滚很久,可以调到0.01。
Use Gravity(使用重力):滑动场景里,如果平面是水平的,重力方向垂直于运动方向,不影响水平滑动。但如果你把平面倾斜了,重力就会产生一个沿斜面的分力,小球会加速下滑。这个后面讲斜面场景时会展开。
Constraints(约束):这个很关键。如果你只想要小球在XZ平面上滑动,不希望它上下跳动或者翻滚,可以冻结Y轴位置和X、Z轴旋转。但注意,冻结旋转后小球就不会滚了,看起来像在“滑冰”。要不要冻结取决于你的视觉需求。
Interpolate(插值):强烈建议设为Interpolate。物理系统在FixedUpdate里以固定时间步长运行,默认0.02秒一次,而渲染帧率是不固定的。如果不插值,小球在视觉上会抖动。Interpolate会让渲染位置在两次物理计算之间平滑过渡,看起来顺滑很多。
Collision Detection(碰撞检测):默认Discrete。如果小球速度很快,可能会穿过薄墙壁。改成Continuous可以解决,但性能开销更大。滑动场景里速度一般不快,Discrete够用。
3.2 速度控制的代码逻辑与边界处理
用velocity控制小球运动,核心代码就一行:rb.velocity = new Vector3(speed, rb.velocity.y, rb.velocity.z)。但这一行里有很多细节。
首先,为什么要保留y和z分量?因为重力在持续影响y轴速度,如果你把y也覆盖了,小球就永远不掉下来。同理,如果场景里有其他方向的力在作用,你只应该覆盖你关心的那个轴。
其次,速度应该在FixedUpdate里设置,不是在Update里。因为物理引擎的更新周期是FixedUpdate,你在Update里设速度,物理引擎可能在同一帧内多次读取到不同的值,导致行为不一致。
第三,输入处理要平滑。如果你用键盘控制,直接读Input.GetAxisRaw会得到-1、0、1三个离散值,小球会瞬间从静止跳到最大速度。更好的做法是用Input.GetAxis配合一个加速系数,让速度逐渐逼近目标值。比如:
float targetSpeed = Input.GetAxis("Horizontal") * maxSpeed; float acceleration = 10f; rb.velocity = Vector3.MoveTowards(rb.velocity, new Vector3(targetSpeed, rb.velocity.y, rb.velocity.z), acceleration * Time.fixedDeltaTime);这样小球有一个加速过程,手感更自然。
3.3 碰撞体的选择与尺寸匹配
小球用SphereCollider,这个没悬念。但尺寸要注意:SphereCollider的Radius默认是0.5,对应Unity默认球体(Scale为1)的半径。如果你把小球Scale改成了2,Collider的Radius也要相应调整,否则碰撞体会比视觉模型小一圈,看起来像穿模。
更稳妥的做法是让Collider自动匹配Mesh。在Collider组件右上角有个齿轮图标,点开选“Reset”,它会根据当前Mesh的包围盒自动设置尺寸。但自动匹配不一定精确,特别是对于非均匀缩放的物体。手动调Radius和Center更可靠。
另外,如果小球需要滚动,Collider的Material要设成低摩擦的物理材质。如果小球需要滑动但不滚动,除了冻结旋转,还可以把Collider的Material设成高摩擦,让它“抓地”。
4. 完整实操流程:从零搭建一个滑动小球场景
4.1 场景搭建与基础配置
打开Unity,新建一个3D场景。默认场景里有一个Main Camera和一个Directional Light,保留就行。
第一步,创建地面。右键Hierarchy面板,选3D Object > Plane。把Scale设为(2, 1, 2),让它大一点。位置归零。给它挂一个BoxCollider?不用,Plane自带MeshCollider。但MeshCollider在物理计算上比BoxCollider开销大,而且对于平面来说没必要。我的习惯是删掉MeshCollider,换成一个BoxCollider,Size设为(20, 0.1, 20),Center设为(0, -0.05, 0)。这样地面就是一个扁平的盒子,物理计算更高效。
第二步,创建小球。右键选3D Object > Sphere。位置设为(0, 0.5, 0),让它刚好在地面上方。Scale保持1。给它挂Rigidbody组件。Mass设为1,Drag设为0,Angular Drag设为0.05,Use Gravity勾上,Interpolate设为Interpolate,Collision Detection设为Discrete。Constraints先不动。
第三步,创建物理材质。在Project面板右键,Create > Physic Material。命名为“IceMaterial”。Dynamic Friction设为0.06,Static Friction设为0.08,Bounciness设为0.2,Friction Combine设为Minimum,Bounce Combine设为Maximum。把这个材质拖到小球的SphereCollider的Material槽里。地面的BoxCollider也拖同一个材质,或者新建一个类似的。
第四步,写控制脚本。新建一个C#脚本,命名为“BallController”,挂到小球上。脚本内容如下:
using UnityEngine; public class BallController : MonoBehaviour { public float maxSpeed = 5f; public float acceleration = 15f; public float deceleration = 10f; private Rigidbody rb; private Vector3 inputDirection; void Start() { rb = GetComponent<Rigidbody>(); } void Update() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); inputDirection = new Vector3(h, 0, v).normalized; } void FixedUpdate() { Vector3 targetVelocity = inputDirection * maxSpeed; targetVelocity.y = rb.velocity.y; float accelRate = inputDirection.magnitude > 0.1f ? acceleration : deceleration; rb.velocity = Vector3.MoveTowards(rb.velocity, targetVelocity, accelRate * Time.fixedDeltaTime); } }这段代码的逻辑是:在Update里读取输入方向,在FixedUpdate里把当前速度向目标速度逼近。加速和减速用不同的速率,松开按键时减速更快,手感更跟手。
4.2 参数调优与手感打磨
代码跑起来后,你会发现小球能动了,但手感可能不太对。这时候需要调参。
maxSpeed:控制最大速度。5f在Unity里大约是每秒5个单位,视觉上相当于一个人小跑的速度。如果你想要更快的滑动,可以调到8到10。但太快的话,Discrete碰撞检测可能会漏检,需要改成Continuous。
acceleration:控制加速快慢。15f意味着从静止到最大速度大约需要0.33秒。如果你想要更灵敏的响应,调到30;想要更重的感觉,调到5。
deceleration:控制松手后的减速。10f意味着从最大速度到静止大约0.5秒。这个值配合物理材质的摩擦力一起作用。如果物理材质摩擦系数很低,deceleration可以设小一点,让小球滑更久。
物理材质的Bounciness:0.2意味着小球碰到墙壁会弹回20%的速度。如果你不想要弹跳,设为0。如果想要弹力球效果,调到0.8以上。
我自己的经验是,先调物理材质,再调脚本参数。因为物理材质影响的是所有碰撞交互,脚本参数只影响输入响应。先把“滑”的感觉调对,再调“控”的手感。
4.3 边界与墙壁的碰撞处理
小球滑到地面边缘会掉下去,这通常不是我们想要的。加四面墙把地面围起来。
创建四个Cube,分别放在地面的四条边。每个Cube的Scale设为(20, 2, 0.5)或者(0.5, 2, 20),位置贴边。给它们挂BoxCollider(Cube自带),物理材质用和地面一样的低摩擦材质。
但这里有个细节:墙壁的物理材质如果摩擦系数太低,小球撞墙后不会减速,会沿着墙滑很久。如果你想要小球撞墙后快速停下,墙壁的材质应该用高摩擦。但同一个物理材质不能同时满足地面和墙壁的需求。解决办法是创建两个材质:地面用低摩擦,墙壁用高摩擦(Dynamic Friction 0.6,Static Friction 0.6,Friction Combine Average)。
另外,墙壁的高度要足够,防止小球弹跳时飞出去。2个单位高度对于半径0.5的小球来说够了,但如果你把Bounciness调得很高,建议加到3。
4.4 视觉反馈与调试辅助
调试物理运动时,光看Game视图有时候不够直观。我习惯在Scene视图里打开Gizmos,能看到Collider的线框。还可以在脚本里加OnDrawGizmos,画出速度向量:
void OnDrawGizmos() { if (rb != null) { Gizmos.color = Color.red; Gizmos.DrawRay(transform.position, rb.velocity); } }这样在Scene视图里能看到一条红线,长度和方向代表速度。调参的时候非常有用,你能直观看到速度是快了还是慢了,方向对不对。
另外,可以在小球后面拖一个Trail Renderer,看运动轨迹。Trail的Time设为0.5秒,宽度曲线从0.2降到0。这样小球滑过的地方会留下一条渐隐的轨迹,能清楚看到它的运动路径和速度变化。
5. 常见问题与排查技巧实录
5.1 小球抖动或闪烁
这是最常见的问题。原因通常有三个:一是Interpolate没开,二是物理时间步长和渲染帧率不匹配,三是Collider尺寸和视觉模型不匹配。
排查顺序:先确认Rigidbody的Interpolate设为Interpolate。如果还抖,打开Edit > Project Settings > Time,把Fixed Timestep从0.02改成0.01,物理计算更频繁,抖动会减轻。但注意,Fixed Timestep太小会增加CPU开销,0.01是折中值。最后检查Collider的Radius是否和球体视觉半径一致,不一致的话碰撞响应会异常。
5.2 小球穿墙
速度太快或者墙壁太薄。解决方案:把Rigidbody的Collision Detection改成Continuous Dynamic,把墙壁的BoxCollider厚度加到0.5以上。如果还穿,检查墙壁有没有挂Collider,有时候新建的Cube被误删了Collider。
另一个容易被忽略的原因是:小球在FixedUpdate里被直接改了Transform。如果你在代码里用了transform.position = ...,物理引擎就失去了对小球的控制。确保所有位移都通过Rigidbody的velocity或AddForce来做。
5.3 小球滑不远
检查三个地方:物理材质的摩擦系数、Rigidbody的Drag、地面的物理材质。最常见的情况是只改了小球材质,地面还是默认的0.6摩擦。把两边的Friction Combine都设成Minimum,或者两边都用低摩擦材质。
还有一个隐藏因素:小球的Mass。质量太小的话,同样的摩擦力下减速更快。可以适当增加到2到5试试。
5.4 小球滚动不自然
如果小球看起来在“滑”而不是“滚”,检查Angular Drag是不是太大了。默认0.05其实还好,但如果你的物理材质摩擦系数很低,小球受到的扭矩不足以让它滚动,就会一直滑。解决办法是适当提高摩擦系数,或者手动给小球加一个扭矩。
如果小球滚动时左右摇摆,可能是Collider的Center没对准。SphereCollider的Center应该是(0,0,0),如果偏了,旋转轴就不在球心,滚动会偏心。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 小球抖动 | Interpolate未开 | 检查Rigidbody组件 | 设为Interpolate |
| 小球穿墙 | 速度过快/墙壁过薄 | 检查Collision Detection和墙壁厚度 | 改Continuous,加厚墙壁 |
| 滑不远 | 摩擦系数过高 | 检查两边物理材质 | Friction Combine设Minimum |
| 不滚动 | 角阻力过大/摩擦不足 | 检查Angular Drag和材质 | 降低角阻力,提高摩擦 |
| 撞墙不减速 | 墙壁摩擦过低 | 检查墙壁材质 | 墙壁用高摩擦材质 |
| 掉下边缘 | 没有围栏 | 检查场景边界 | 加四面墙 |
| 速度突变 | 在Update里设速度 | 检查代码位置 | 移到FixedUpdate |
| 碰撞无反应 | 没挂Collider | 检查物体组件 | 添加Collider |
6. 从滑动小球延伸出去:还能怎么玩
滑动小球搞明白之后,很多场景都可以在此基础上扩展。比如把地面改成斜面,小球会受重力分力影响自动加速下滑,这时候你需要考虑重力沿斜面的分量计算:g * sin(θ),其中θ是斜面倾角。如果斜面角度是30度,重力加速度9.81,那沿斜面的加速度就是4.9左右。这个计算在做滑坡或者弹珠台的时候会用到。
再比如,把小球换成胶囊体,加上跳跃功能,就变成了一个基础的角色控制器。核心逻辑是一样的:用velocity控制水平移动,用AddForce或者直接设y轴速度来控制跳跃。区别在于跳跃需要检测是否着地,这就要用到射线检测或者碰撞回调。
还可以加一个弹簧板,小球踩上去会被弹飞。弹簧板的实现是在OnCollisionEnter里给小球施加一个向上的力,力的大小等于mass * jumpForce。注意要在FixedUpdate里施加,或者用rb.AddForce的ForceMode.Impulse模式,这样是一次性爆发力。
如果你想让小球沿着预定路径滑动,可以用Waypoint系统。设置几个路径点,小球依次向每个点移动,到达后切换下一个点。这种在塔防或者巡逻AI里很常见。
最后,性能方面提醒一句:场景里的小球数量如果超过几十个,物理计算开销会明显上升。可以考虑用对象池管理小球,或者对远处的小球降低物理更新频率。Unity的Physics Manager里有Sleep Threshold,速度低于这个值的小球会进入休眠状态,不再消耗物理计算资源。默认是0.005,一般不用改,但如果你发现小球该休眠的时候还在动,可以适当调高。
我在实际项目里踩过最深的坑是物理材质和脚本速度控制的相互干扰。有一段时间小球滑到一半突然急停,排查了半天才发现是地面的物理材质在某个版本里被误设成了高摩擦,而脚本里的deceleration又设得很大,两者叠加导致减速过快。后来我养成了一个习惯:调物理运动参数时,一次只改一个变量,改完跑一遍看效果,确认没问题再改下一个。这个习惯帮我省了很多返工的时间。