1. 从一个滑动小球开始为什么它是理解Unity运动系统的钥匙很多人刚接触Unity的时候总觉得运动系统是个特别庞大的话题——刚体、碰撞体、物理材质、关节、角色控制器光看名字就头大。但如果你问我有没有一个最小的案例能把Unity里运动这件事的核心逻辑串起来我会毫不犹豫地说做一个滑动小球。这个案例看起来简单到有点不起眼但它其实是一个绝佳的切入点。为什么这么说因为一个小球在场景里滑动背后牵扯到的东西远比表面多你要决定它是用物理引擎驱动还是用代码直接改位置你要处理它和地面的摩擦你要考虑它撞到墙之后怎么反弹你还要想清楚输入怎么映射到运动上。这些问题恰恰是后面做角色移动、载具控制、甚至复杂物理交互时都会反复遇到的。我自己刚开始学Unity那会儿就是被到底该用Rigidbody还是Transform.Translate这个问题卡了很久。网上答案五花八门有人说用刚体有人说直接改位置但很少有人把为什么讲清楚。所以这篇内容我想借滑动小球这个载体把Unity运动系统里最基础也最容易踩坑的几个点掰开揉碎讲一遍。不管你是刚打开Unity的新手还是已经做过几个Demo但总觉得运动手感不对劲的开发者应该都能从中找到对自己有用的东西。整篇内容会围绕一个可运行的小球滑动场景展开从场景搭建、物理配置、输入处理到运动方式的选择、摩擦与阻尼的调参、边界处理再到常见问题的排查一步步把细节补全。你跟着做下来能得到一个手感可控、逻辑清晰的小球运动Demo更重要的是你能理解每一步背后的道理以后遇到别的运动需求也能自己推导。2. 场景搭建与物理组件配置先把地基打对2.1 地面、小球与相机的三件套布置动手写代码之前场景本身得先搭对。很多人急着写脚本结果场景里连个像样的地面都没有小球一放上去就往下掉然后开始怀疑人生。我建议按下面的顺序来每一步都确认没问题再往下走。先建地面。在Hierarchy面板右键选择3D Object里的Plane这就是最省事的地面。默认的Plane是10x10单位大小对于一个小球滑动的场景来说够用了。如果你想要更大的活动范围可以把它Scale调大比如X和Z都设成3那就是30x30的区域。地面建好之后位置保持在原点就行。然后建小球。同样右键3D Object里选Sphere。默认半径是0.5单位也就是直径1单位。这个尺寸和地面的比例是协调的先不用改。把小球的位置往上抬一点比如Y轴设成0.5这样它刚好贴着地面。如果你把小球放在Y0它会和地面重叠物理引擎一启动就会把它弹开或者卡住这是个很常见的初始位置坑。相机这块默认的Main Camera位置是(0, 1, -10)朝向正前方。对于俯视或者斜视小球滑动来说这个角度其实还行但如果你想看得更清楚可以把相机往上抬一点、往前倾一点。我一般会把相机位置设成(0, 8, -8)旋转设成(45, 0, 0)这样是一个斜向下的视角既能看到小球的运动轨迹又能看清它和地面的接触关系。当然这个纯看个人习惯你觉得怎么舒服怎么来。灯光默认的Directional Light就够用不用动。到这里场景的静态部分就搭好了。2.2 Rigidbody和Collider谁负责动谁负责撞接下来是核心部分——给小球加物理组件。选中Sphere在Inspector里点Add Component搜索Rigidbody加上去。这个组件是Unity物理系统的入口加了它小球才会受到重力、才会和别的碰撞体发生物理交互。Rigidbody加上的瞬间你会发现Mass默认是1Drag是0Angular Drag是0.05Use Gravity是勾选的。这些默认值先别急着改我们后面调参的时候再细说。这里要强调的是Rigidbody决定了小球怎么动而Collider决定了小球怎么撞。Sphere默认自带一个Sphere Collider半径0.5和它的Mesh匹配。这个Collider就是小球的碰撞形状。地面Plane也自带一个Mesh Collider不过这里有个细节要注意Plane的Mesh Collider默认不是Convex的对于静态地面来说这没问题因为静态碰撞体不需要Convex。但如果你的地面是个复杂的模型最好换成Box Collider或者确保Mesh Collider的配置正确否则可能出现小球穿透地面的情况。还有一个容易被忽略的点如果两个物体都要参与物理碰撞至少其中一个必须有Rigidbody。地面是静态的不需要Rigidbody小球有Rigidbody这样它们之间的碰撞就能正常计算。如果你给地面也加了Rigidbody记得把它的Is Kinematic勾上否则地面会被小球的碰撞推着跑那就闹笑话了。2.3 物理材质让小球滑起来而不是黏在地上组件都加好之后运行一下你会发现小球掉到地面上就一动不动了。这很正常因为默认的物理材质摩擦系数比较大小球没有外力就不会自己滑。但我们要做的是滑动小球所以得给它一个合适的物理材质。在Project面板右键Create里的Physic Material命名成BallMaterial。选中它Inspector里有两个关键参数Dynamic Friction和Static Friction。Dynamic Friction是动摩擦影响物体已经在滑动时的阻力Static Friction是静摩擦影响物体从静止到开始滑动需要克服的阻力。默认值都是0.6这个值偏大小球滑起来会很快停下。我的经验是想要顺滑的滑动效果把Dynamic Friction调到0.1到0.2之间Static Friction调到0.1左右。如果你想要更冰面的感觉可以调到0.05。但注意别调到0完全无摩擦的话小球会一直滑下去停不下来除非撞墙。还有一个参数是Bounciness弹性。默认0小球撞墙不会弹。如果你想要一点反弹效果可以调到0.3到0.5。但弹性太高会让运动变得难以控制建议先从0.2试起。把这个物理材质拖到小球的Sphere Collider的Material槽里。注意是拖到Collider上不是Rigidbody上这个槽位在Collider组件里。拖完之后再运行如果你给小球一个初始速度它就能滑起来了。3. 让小球动起来的三种方式选错了后面全是坑3.1 Transform直接位移简单但会绕过物理系统最直观的方式就是直接改Transform的position。写起来大概是这样void Update() { transform.position new Vector3(Input.GetAxis(Horizontal), 0, Input.GetAxis(Vertical)) * speed * Time.deltaTime; }这段代码能让小球跟着方向键动看起来没问题。但它有个根本性的缺陷它绕过了物理系统。也就是说小球虽然动了但Rigidbody并不知道它是怎么动的碰撞检测会变得不可靠。你可能会看到小球穿过墙壁或者撞到东西之后行为诡异。这种方式适合什么场景适合那些不需要物理交互的纯视觉移动比如UI元素的位移或者你完全自己写碰撞逻辑的情况。但对于滑动小球这种需要和地面、墙壁交互的场景我不推荐用这种方式。3.2 Rigidbody的velocity物理驱动的主流做法正确的方式是操作Rigidbody的速度。代码大概长这样private Rigidbody rb; void Start() { rb GetComponentRigidbody(); } void FixedUpdate() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 movement new Vector3(h, 0, v); rb.velocity movement * speed; }这里有几个关键点。第一用FixedUpdate而不是Update。因为物理计算是在固定时间步长里进行的FixedUpdate和物理系统的节奏对齐在这里改velocity最稳定。如果你在Update里改帧率波动的时候运动就会抖。第二直接赋值velocity会覆盖掉重力在Y轴上的分量。你看代码里movement的Y是0赋值之后小球的垂直速度就变成0了它就不会往下掉。但这样小球也没法跳起来。如果你想要重力正常作用应该只改水平方向的速度Vector3 velocity rb.velocity; velocity.x h * speed; velocity.z v * speed; rb.velocity velocity;这样Y轴的速度由物理引擎自己管小球该掉还是掉该弹还是弹。3.3 AddForce更物理但更难控制还有一种方式是施加力rb.AddForce(movement * forceAmount);这种方式最符合真实物理力产生加速度加速度改变速度。但问题是它很难精确控制。你按一下方向键小球会加速松开之后它不会立刻停而是因为惯性继续滑。如果你想要那种推一下滑一段的手感AddForce很合适。但如果你想要按着走、松开停的即时响应AddForce会让你觉得小球不听话。我个人的经验是做滑动小球这种需要精确控制的场景用velocity直接赋值更省心。AddForce更适合做那种有惯性、有重量感的运动比如推箱子或者开车。3.4 三种方式的对比与选择依据把三种方式放在一起对比一下方式物理交互控制精度适用场景Transform位移差绕过物理高纯视觉移动、UIRigidbody.velocity好高角色移动、精确控制AddForce最好低惯性运动、推拉物体选择的核心依据是你需要多精确的控制以及你希望物理系统参与多少。滑动小球这个场景我推荐velocity方式兼顾了物理正确性和操作手感。4. 摩擦、阻尼与速度曲线手感调优的细节4.1 Drag参数到底在做什么Rigidbody上有个Drag参数默认0。它的作用是模拟空气阻力每帧都会让速度衰减。具体来说Unity内部会按这个公式衰减速度velocity乘以(1 - Drag * Time.fixedDeltaTime)。Drag越大衰减越快。对于滑动小球如果你用velocity直接赋值的方式Drag其实影响不大因为你每帧都在覆盖速度。但如果你用AddForceDrag就很重要了它决定了小球松开力之后多久停下来。我一般会把Drag设在0.5到2之间具体看想要多滑。有个坑要注意Drag设太大小球会像在糖浆里动一样很黏设太小又会滑得停不下来。建议从1开始试然后根据手感微调。4.2 物理材质的摩擦组合效果前面说了物理材质的摩擦系数但实际效果是两个接触物体的摩擦组合决定的。Unity默认用平均法最终摩擦 (材质A摩擦 材质B摩擦) / 2。所以如果你只调了小球的材质地面的默认材质摩擦是0.6平均下来还是不小。想要真正顺滑地面的材质也得调。给地面也建一个物理材质摩擦设低一点比如0.1。这样两边都低小球滑起来才顺畅。或者你可以用Combine模式在物理材质里把Friction Combine设成Minimum这样取两者中较小的那个效果更直接。4.3 用代码实现速度渐变而不是瞬间启停直接赋值velocity有个问题按下方向键的瞬间小球速度从0直接跳到目标速度松开又瞬间归零。这种硬切的手感很生硬。想要更自然可以用Lerp做插值Vector3 targetVelocity new Vector3(h, 0, v) * speed; Vector3 currentVelocity rb.velocity; Vector3 newVelocity Vector3.Lerp( new Vector3(currentVelocity.x, 0, currentVelocity.z), targetVelocity, acceleration * Time.fixedDeltaTime ); rb.velocity new Vector3(newVelocity.x, currentVelocity.y, newVelocity.z);这里的acceleration控制加速的快慢值越大越接近瞬间启停值越小越肉。我一般设在5到15之间看想要多灵敏的手感。这种方式能让小球有一个平滑的加速和减速过程操作起来舒服很多。4.4 一个容易忽略的点FixedUpdate的时间步长所有物理相关的代码都跑在FixedUpdate里而FixedUpdate的调用频率由Time.fixedDeltaTime决定默认是0.02秒也就是每秒50次。这个值可以在Edit菜单的Project Settings里的Time面板改。为什么不建议随便改因为改小了物理计算更频繁性能开销大改大了物理计算变粗糙快速运动的物体可能穿透。对于滑动小球这种场景默认的0.02就挺好。但如果你发现小球高速运动时偶尔穿过墙壁可以适当调小这个值比如0.01代价是性能。5. 边界处理与碰撞响应让小球撞墙之后合理反弹5.1 用碰撞体围出活动区域小球滑起来之后你得给它一个边界不然它滑出地面就掉下去了。最简单的做法是在地面四周加四面墙。用Cube就行拉长拉扁围成一个框。墙的Collider用Box Collider不需要Rigidbody因为它们是静态的。墙的高度不用太高0.5到1就够了反正小球也跳不了多高。位置要贴着地面的边缘别留缝隙否则小球可能卡进去或者钻出去。这里有个细节墙和地面之间如果有微小的缝隙小球高速撞上去的时候可能从缝里穿过去。解决办法是让墙稍微嵌入地面一点或者把墙的Collider做得比视觉模型大一圈。5.2 碰撞反弹的物理参数调节小球撞墙之后怎么弹取决于两个东西小球的物理材质Bounciness以及碰撞时的速度。Bounciness是0到1之间的值0是不弹1是完全弹性碰撞理论上会以相同速度弹回。实际调的时候0.3到0.6是比较自然的值。太高了小球会像弹珠一样乱飞太低了撞墙就贴住不动。另外墙的物理材质也会影响反弹如果墙的Bounciness是0那即使小球是1组合之后反弹也会减弱。Unity默认用平均值所以两边都要考虑。5.3 用代码监听碰撞事件做自定义响应有时候光靠物理反弹不够你可能想在撞墙的时候做点别的事比如播放音效、扣血、改变颜色。这就需要监听碰撞事件。给小球挂一个脚本void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag(Wall)) { // 撞墙了做点什么 Debug.Log(撞到墙了); } }注意墙的Tag要设成Wall或者你自己定义一个Tag。OnCollisionEnter只在碰撞开始的瞬间调用一次适合做一次性响应。如果你想要碰撞持续期间一直有反馈用OnCollisionStay。还有个容易混淆的是OnTriggerEnter那是触发器用的。触发器不产生物理反弹只是检测重叠。如果你把墙的Collider的Is Trigger勾上小球就会直接穿过去只是会触发事件。这个别搞混了。5.4 防止小球卡在角落或者抖动小球在角落里可能会卡住或者抖动这是物理引擎在处理多个接触点时的常见现象。解决办法有几个一是把物理材质的摩擦调低减少接触点的阻力二是给小球加一点点Bounciness让它能从角落弹出来三是在代码里检测到小球速度极低且位置在角落时给它一个小的推力。还有一种情况是小球在地面上微微抖动这通常是因为重力把它压进地面然后碰撞解算又把它推出来来回震荡。可以在Rigidbody的Collision Detection里选Continuous Dynamic让碰撞检测更精确。或者把地面的Collider稍微抬高一点点给小球留一点余量。6. 输入映射与相机跟随把操作体验做完整6.1 键盘输入到运动方向的映射Unity默认的Input Manager里Horizontal和Vertical已经绑定了方向键和WASD。用Input.GetAxis拿到的值是-1到1之间的平滑值适合做运动输入。如果你想要更即时的响应可以用Input.GetAxisRaw它只返回-1、0、1三个值没有平滑过渡。对于滑动小球我建议用GetAxisRaw因为滑动需要精确控制平滑过渡反而会让操作感觉延迟。当然这也看个人喜好你可以两种都试试看哪种手感好。如果你想要支持手柄或者自定义按键可以在Input Manager里配置。不过对于入门来说默认的键盘输入就够了。6.2 相机跟随的两种实现思路小球滑来滑去相机得跟着不然它滑出视野你就看不见了。最简单的做法是把相机作为小球的子物体这样小球动相机就动。但这样相机会跟着小球旋转如果你不希望相机转就别用这种方式。另一种是写脚本让相机跟随public Transform target; public Vector3 offset new Vector3(0, 8, -8); void LateUpdate() { transform.position target.position offset; }用LateUpdate是为了确保在小球移动之后再更新相机位置避免相机抖动。offset就是相机相对小球的偏移量你可以根据视角需求调整。如果你想要更平滑的跟随可以用Lerptransform.position Vector3.Lerp(transform.position, target.position offset, followSpeed * Time.deltaTime);followSpeed控制跟随的快慢值越大跟得越紧值越小越拖沓。我一般设在5到10之间。6.3 输入平滑与响应速度的平衡前面提到用Lerp做速度插值其实输入本身也可以做平滑。比如你可以对Input.GetAxis的返回值再做一次平滑处理让方向变化不那么突兀。但这会引入延迟对于需要快速响应的滑动操作来说可能得不偿失。我的建议是输入用GetAxisRaw保持即时性速度用Lerp做平滑这样既有快速响应又有自然的加速减速。两者结合手感最好。7. 常见问题排查小球不听话的时候怎么办7.1 小球直接掉下去穿过地面这是最常见的问题。原因通常有几个地面没有Collider或者Collider配置不对小球的Collider太小或者位置偏了小球的运动速度太快物理引擎来不及检测碰撞。排查顺序先看地面有没有ColliderMesh Collider的话确认Mesh赋值了再看小球的Sphere Collider半径是不是和视觉模型匹配如果都正常把Rigidbody的Collision Detection改成Continuous Dynamic或者调小Fixed Timestep。7.2 小球滑不动或者滑一下就停滑不动通常是摩擦太大。检查小球和地面的物理材质把Dynamic Friction和Static Friction都调低。如果还是不行看看是不是Drag设太大了或者代码里有没有意外地把速度归零。滑一下就停除了摩擦还可能是Drag太大或者你用了AddForce但力太小。用velocity方式的话检查一下是不是每帧都在赋值有没有被别的代码覆盖。7.3 小球抖动或者卡在墙里抖动一般是物理解算的震荡解决办法前面说了调低摩擦、加一点弹性、用Continuous Dynamic。卡在墙里通常是因为碰撞体之间有重叠或者小球被挤进了墙的Collider内部。检查墙的位置确保它们和地面之间没有缝隙也没有和小球的初始位置重叠。如果小球高速撞墙后卡进去可以把墙的Collider做厚一点或者用射线检测提前预判碰撞。7.4 不同帧率下运动表现不一致这个问题通常是因为在Update里做了物理相关的操作。记住所有和Rigidbody相关的代码都放FixedUpdate所有和Transform直接相关的放Update。另外乘以Time.deltaTime或Time.fixedDeltaTime来补偿帧率差异这样不同帧率下运动速度才一致。如果你发现高帧率下小球滑得远低帧率下滑得近八成是哪里漏乘了deltaTime或者物理和渲染的更新节奏没对齐。8. 从滑动小球延伸出去还能怎么玩把基础版本跑通之后其实可以在这个框架上加很多东西。比如加个跳跃检测到空格键就给Y轴速度赋一个值比如加个加速带小球进入某个区域就自动加速比如加个终点线小球碰到就显示通关。再往深了做可以换成Character Controller来做更精确的角色移动或者用NavMesh做自动寻路。但那些都是后话了滑动小球这个案例的价值在于它用最小的复杂度把Unity运动系统的核心逻辑展示清楚了物理组件怎么配、运动方式怎么选、参数怎么调、问题怎么排查。我自己在做更复杂的运动系统时经常会回到这个最简单的场景来验证想法。因为变量少出了问题容易定位。如果你能把滑动小球的每一个参数都调明白后面做角色、载具、飞行器无非是在这个基础上加维度而已。最后分享一个我踩过的坑刚开始做的时候我总想着一步到位把所有功能都加上去结果出了问题根本不知道是哪个部分导致的。后来学乖了先做一个能滑的小球确认没问题再加一个功能再确认。这种增量式的做法看起来慢其实最快。
阅读完成 · 觉得有帮助?