资讯动态

Unity C#控制小球运动:从Transform到Rigidbody的完整指南

发布时间:2026/10/27 4:46:37 来源:尧图企业网站定制
刚接触Unity那会儿我以为让一个小球动起来是再简单不过的事——挂个脚本、写两行位移代码跑起来不就完了结果第一次动手就翻车球要么像钉子一样钉在原地纹丝不动要么一按方向键就飞出屏幕再也找不回来要么撞到墙之后开始疯狂抖动。后来带过几个新人发现大家踩的坑几乎一模一样。所以这篇东西我想把“用C#控制小球运动”这件事从头到尾拆开讲透不只是给你一段能跑的代码而是把背后的坐标系、物理系统、输入处理、帧率关系这些容易含糊的地方一次说清楚。不管你是刚打开Unity编辑器的纯新手还是写过一点脚本但对Rigidbody和Transform的区别仍然模糊的半熟手看完都能自己判断“这个场景到底该用哪种移动方式”而不是照抄一段代码然后祈祷它能跑。1. 先想清楚小球到底该怎么“动”很多人拿到需求就直接开写这是最容易返工的。控制小球运动这件事表面上只有“移动”两个字实际上要先回答三个问题用哪套坐标和组件来驱动、移动是“瞬移式”还是“受力式”、输入从哪来。这三个问题的答案不同代码结构会完全不一样。1.1 Transform位移与Rigidbody物理移动的本质区别Unity里让物体动起来最主流的两条路是改Transform.position和操作Rigidbody。这两者的差别用生活化的说法就是前者是“你直接把球从A点搬到B点”后者是“你推了球一把让它自己滚过去”。改Transform是直接覆盖物体的位置数据不经过物理引擎。它的好处是响应即时、代码简单、完全可控坏处是它绕过了碰撞检测的求解流程——当你把一个带Collider的球用Transform硬塞进墙里物理引擎只能在下一帧发现“穿模了”然后尝试把球弹出来表现出来就是抖动、卡住、甚至直接穿墙飞走。操作Rigidbody则是把意图交给物理引擎。你设置速度或者施加力引擎在FixedUpdate里统一积分、处理碰撞、计算摩擦和反弹。代价是有一帧左右的延迟感而且参数没调好会出现“刹不住车”的滑行。我的判断标准很直接球需要和场景里的其他物体产生真实的碰撞、反弹、堆叠就用Rigidbody球只是在一个固定轨道上做表现层的移动比如UI演示、路径动画就用Transform。这个判断能帮你省掉后面80%的诡异问题。1.2 三种主流移动方案的适用场景对比同样是让球动方案选型直接决定了手感和后续扩展成本。我把常见的三种做法列出来对比你可以对着自己的需求挑。方案核心API手感特点适合场景主要坑点直接改位置transform.position/Translate绝对精确、无惯性固定路径、演示动画、非物理场景穿模、碰撞失效设速度rb.velocity匀速、有碰撞、略滑平台跳跃、推箱子、大多数玩法需要自己处理加减速施加力rb.AddForce有惯性、真实但难控模拟真实物理、球类运动参数敏感、容易失控新手最容易犯的错是“既要又要”用Transform移动却期待碰撞反馈或者用AddForce却希望球指哪停哪。选型阶段把预期定死后面就顺了。我个人的默认选择是只要场景里有物理交互一律用Rigidbody设速度它在可控性和物理正确性之间平衡得最好。1.3 输入系统选型旧Input还是新Input System输入这块Unity现在有两套并存的方案。旧的Input.GetAxis/Input.GetKey胜在简单直接几行代码就能读到键盘输入适合快速验证和小项目。新的Input System功能强、支持多设备映射和动作绑定但需要额外安装包、配置Action Asset学习曲线陡一些。对于“控制小球运动”这种需求我的建议是先跑通再说。用旧Input把逻辑验证完等玩法稳定了再考虑迁移。不要一上来就折腾Input System的配置那会让你在还没看到球动起来之前就失去耐心。下面所有示例我都用旧Input写逻辑是通用的迁移时只需要换读取输入的那一两行。2. 动手前的环境与场景搭建代码写得再漂亮场景没搭对一样跑不起来。这一节把最小可运行环境的搭建过程走一遍顺便解释几个新手经常忽略的设置项。2.1 创建工程与场景基础对象打开Unity Hub新建一个3D工程模板选3D Core就行。进入编辑器后默认场景里有一个Main Camera和一个Directional Light够用了。接着创建球Hierarchy面板右键 → 3D Object → Sphere。我习惯把它重命名为PlayerBall方便在代码里按名字找。创建完检查一下它的TransformPosition归零或者放在(0, 0.5, 0)这种稍微抬高的位置——球半径默认0.5放在y0.5正好贴着地面避免开局就陷进地板。地面用一个Plane右键 → 3D Object → PlaneScale调到(5, 1, 5)左右给球留出足够的活动空间。相机角度建议调成俯视45度左右这样能同时看清球的水平移动和它离地的高度调试时判断问题会快很多。2.2 给小球挂上Rigidbody和Collider的正确姿势选中PlayerBall在Inspector里点Add Component添加Rigidbody。这里有几个参数值得当场确认Mass默认1控制小球够用了。质量影响的是受力后的加速度如果你用设速度的方式移动质量基本不影响手感。Drag默认0。这个值控制速度衰减设速度移动时它会让球慢慢停下来。想要“松手即停”就保持0想要一点滑行感就调到0.5到2之间。Use Gravity保持勾选。除非你做的是俯视角平面游戏否则关掉重力会让球飘在空中。Constraints如果做的是2D平面上的滚动比如只在XZ平面移动可以勾选Freeze Position Y和Freeze Rotation X、Z防止球被撞飞或者乱转。Collider方面Sphere创建时自带SphereCollider半径和球体匹配一般不用动。但要注意如果球和地面都有Collider碰撞才会生效如果只给球加了Rigidbody没加Collider球会直接穿过地面掉下去。这是新手最常见的“球不见了”原因之一。2.3 地面与相机的必要设置地面Plane自带MeshCollider但它是单面的——从下方穿过去不会碰撞。正常玩法没问题但如果你做的球会跳很高再落下注意别让它从边缘掉出去。相机这块我建议先手动摆好一个能看全场景的角度不要急着写跟随脚本。等球的移动逻辑稳定了再考虑加相机跟随。调试阶段固定视角反而更容易发现“球是不是往错误方向跑了”。提示场景搭好后先按一次Play确认球在重力作用下落到地面上静止不动。如果球直接往下掉出视野八成是地面Collider缺失或者球的位置在平面范围之外。这个前置检查能帮你排除掉后面一半的“代码不生效”误判。3. 核心实现从零写出可用的移动脚本环境就绪开始写代码。我会从最基础的版本开始逐步加上边界处理、加速手感、朝向等细节每一版都解释为什么这么改。3.1 最简版本键盘控制水平移动新建一个C#脚本命名为BallController挂到PlayerBall上。第一版只做一件事读方向键让球在水平面上移动。using UnityEngine; public class BallController : MonoBehaviour { public float moveSpeed 5f; private Rigidbody rb; void Start() { rb GetComponentRigidbody(); } void FixedUpdate() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move new Vector3(h, 0f, v).normalized * moveSpeed; rb.velocity new Vector3(move.x, rb.velocity.y, move.z); } }这段代码有几个关键点值得展开。第一Input.GetAxis(Horizontal)返回的是-1到1之间的平滑值按A/D或左右方向键都能读到而且自带一点渐变手感比GetKey的0/1突变要柔和。第二normalized的作用是防止斜向移动变快——如果不归一化同时按上和右向量长度是1.414球会以1.4倍速度斜着跑这是很多教程漏讲的细节。第三赋值时保留了rb.velocity.y这样重力对垂直方向的作用不会被覆盖球该下落还是下落。为什么放在FixedUpdate而不是Update因为这里操作的是Rigidbody的物理属性物理引擎按固定时间步长默认0.02秒运行在FixedUpdate里改速度才能和物理求解对齐。放在Update里改帧率波动时会出现速度抖动。3.2 加上边界限制别让球跑丢第一版跑起来后你会发现球能一直往外跑跑出地面就掉下去了。加个简单的边界钳制public float boundary 4.5f; void FixedUpdate() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move new Vector3(h, 0f, v).normalized * moveSpeed; rb.velocity new Vector3(move.x, rb.velocity.y, move.z); Vector3 pos rb.position; pos.x Mathf.Clamp(pos.x, -boundary, boundary); pos.z Mathf.Clamp(pos.z, -boundary, boundary); rb.position pos; }这里用rb.position而不是transform.position是因为在物理循环里直接改Transform可能和物理引擎的状态不同步用Rigidbody的position接口更稳妥。Mathf.Clamp把坐标限制在正负4.5之间配合Scale为5的Plane实际尺寸是50x50但视觉上边缘在25左右留了足够余量。注意边界值要根据你地面的实际尺寸来定不要照抄。地面Plane默认Scale为1时尺寸是10x10边缘在正负5我上面把Scale调到5边缘在正负25所以4.5这个值其实偏保守你可以按需放大。写死数值前先在Scene视图里量一下。3.3 让手感更顺加速度与阻尼的取舍直接设速度的缺点是“松手即停”没有任何惯性手感偏硬。想要更自然的加减速有两种做法一是自己维护一个当前速度变量做插值二是用AddForce配合Drag。先看插值方案它保留了设速度的可控性又加了过渡private Vector3 currentVelocity; void FixedUpdate() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 target new Vector3(h, 0f, v).normalized * moveSpeed; currentVelocity Vector3.Lerp(currentVelocity, target, 10f * Time.fixedDeltaTime); rb.velocity new Vector3(currentVelocity.x, rb.velocity.y, currentVelocity.z); }Vector3.Lerp的第三个参数是插值系数10f * Time.fixedDeltaTime在默认0.02秒步长下约等于0.2意味着每物理帧向目标速度靠近20%。这个系数越大越跟手越小越“肉”。我一般从10开始调觉得太滑就加到15觉得太钝就降到6。再看AddForce方案public float forceAmount 20f; void FixedUpdate() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 dir new Vector3(h, 0f, v).normalized; rb.AddForce(dir * forceAmount); }这个方案的真实感最强但问题也最明显球会一直加速直到Drag把它拉回来而且松手后还会滑很远。要控制住它得配合较高的Drag值比如3到5和速度上限钳制。我的经验是除非玩法明确需要“推球”的物理感否则插值方案更省心调参维度少行为可预测。3.4 球体滚动朝向让视觉反馈跟上移动球在移动但贴图不转看起来就像在冰面上滑行。要让球“滚”起来可以根据速度方向旋转球体。最直接的做法是用Rigidbody的angularVelocity或者手动按位移方向转Transform。public float rollMultiplier 1f; void Update() { Vector3 horizontalVel new Vector3(rb.velocity.x, 0f, rb.velocity.z); if (horizontalVel.sqrMagnitude 0.01f) { Vector3 axis Vector3.Cross(Vector3.up, horizontalVel.normalized); float speed horizontalVel.magnitude; transform.Rotate(axis, speed * rollMultiplier * Time.deltaTime * 57.3f, Space.World); } }这里用叉乘算出旋转轴——球往右滚轴就是垂直于移动方向和上方向的那个向量。乘57.3是把弧度转成角度180/π约等于57.3。rollMultiplier用来微调滚动视觉速度和实际位移匹配时看起来最自然。这段放在Update里是因为它只影响视觉表现不影响物理状态不需要和物理步长对齐。4. 调试与常见问题排查实录代码写完不等于结束实际跑起来的问题往往比写代码花的时间还多。这一节整理我踩过和见别人踩过的典型坑。4.1 球不动、球乱飞、球抖动的原因速查现象最可能原因排查动作按方向键球完全不动脚本没挂上/没启用Rigidbody缺失输入轴名拼错看Inspector脚本勾选状态确认组件存在检查Console报错球一按就飞出屏幕速度值过大没归一化导致斜向加速FixedUpdate里用了Time.deltaTime把moveSpeed降到5以内加normalized确认用fixedDeltaTime球撞墙后疯狂抖动用Transform移动导致穿模Collider重叠改用Rigidbody设速度检查初始位置是否嵌在墙里球移动一顿一顿逻辑写在Update里操作物理帧率过低物理相关代码移到FixedUpdate球穿过地面掉下去球或地面缺Collider地面是单面MeshCollider且球从下方穿过补Collider检查球初始高度这张表覆盖了九成以上的新手问题。我特别想强调“球一按就飞”这个很多人把moveSpeed设成50甚至100觉得数字大就是快结果一帧移动好几米直接穿过所有碰撞体。正常手感下5到10就够了配合相机距离微调。4.2 物理参数调优的实操心得调参这件事没有标准答案但有几个经验值可以当起点。Rigidbody的Drag做设速度移动时保持0到0.5做AddForce移动时用2到5。Mass在设速度方案里几乎不影响手感但在AddForce方案里质量越大加速越慢想要“重球感”就调高。Interpolate选项建议设为Interpolate它能让球在两次物理更新之间的渲染位置做插值视觉上更平滑尤其低帧率时效果明显。还有一个容易被忽略的点Project Settings里的Fixed Timestep默认是0.02秒也就是物理每秒更新50次。如果你把游戏逻辑大量塞进FixedUpdate低配设备上可能出现物理更新跟不上渲染的情况。一般不用改但如果发现移动有明显卡顿可以试着降到0.01看看是否改善。4.3 输入无响应与多设备适配的排查思路输入没反应先确认三件事Game视图有没有获得焦点点一下Game窗口、输入轴在Project Settings → Input Manager里是否存在Horizontal和Vertical是默认就有的、脚本里的字符串拼写是否完全一致大小写敏感。如果要做手柄或触屏适配旧Input的GetAxis对键盘和手柄是统一处理的但触屏需要额外逻辑。我的建议是把手柄支持放到玩法稳定之后再做前期用键盘把核心逻辑跑通避免在输入适配上过早消耗精力。5. 从能跑到好用几个值得加的扩展基础移动跑通后下面这些扩展能让你的小球控制上一个台阶而且每一个都是独立的小改动可以按需取用。5.1 相机跟随的平滑实现固定视角调试够用但实际玩起来相机得跟着球。最简单的跟随是每帧把相机位置设成球位置加一个偏移public Transform target; public Vector3 offset new Vector3(0f, 8f, -8f); public float followSpeed 5f; void LateUpdate() { Vector3 desired target.position offset; transform.position Vector3.Lerp(transform.position, desired, followSpeed * Time.deltaTime); transform.LookAt(target); }放在LateUpdate是因为它在该帧所有Update和物理更新之后执行能拿到球的最新位置避免相机抖动。Vector3.Lerp让相机平滑追赶而不是硬贴followSpeed控制跟随的紧密度。LookAt保证相机始终朝向球。5.2 跳跃与地面检测的配合如果玩法需要跳跃核心是“只有在地面上才能跳”。最轻量的地面检测是用射线public float jumpForce 5f; public float groundCheckDistance 0.6f; private bool isGrounded; void FixedUpdate() { isGrounded Physics.Raycast(transform.position, Vector3.down, groundCheckDistance); // ... 移动逻辑 ... if (isGrounded Input.GetKeyDown(KeyCode.Space)) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } }ForceMode.Impulse表示一次性施加冲量适合跳跃这种瞬时动作。射线长度0.6略大于球半径0.5留了一点容差。注意GetKeyDown要放在Update里读更准确这里为了集中展示放在FixedUpdate实际项目里建议把输入读取和物理操作分开。5.3 速度上限与移动状态的简单封装当移动逻辑变复杂把速度计算和状态判断封装成方法会让代码好维护很多。比如加一个速度上限private Vector3 ClampHorizontal(Vector3 vel, float maxSpeed) { Vector3 horizontal new Vector3(vel.x, 0f, vel.z); if (horizontal.magnitude maxSpeed) { horizontal horizontal.normalized * maxSpeed; } return new Vector3(horizontal.x, vel.y, horizontal.z); }这个封装在AddForce方案里尤其有用因为力会持续累积速度不设上限球会越跑越快。设速度方案里它也能防止插值过程中的瞬时超速。6. 写在最后的几句实在话这套东西我从第一次写到现在最大的体会是移动逻辑本身不难难的是想清楚“我要的是哪种移动”。选型定对了代码量其实很少选型错了后面加多少补丁都在和物理引擎打架。我见过太多人用Transform移动然后花几天时间研究怎么让碰撞不抖其实换成Rigidbody设速度问题当场就没了。另一个经验是调参一定要在真机或者接近目标帧率的环境下做。编辑器里跑得顺打包后帧率一变手感可能完全不同。速度、插值系数、相机跟随速度这几个值最好在目标平台上再确认一遍。最后分享一个小技巧调试移动时在Scene视图里打开Gizmos用Debug.DrawRay把速度方向和地面检测射线画出来比盯着数字猜要直观得多。球到底往哪走、有没有贴地一眼就能看出来。这个习惯帮我省掉了大量“为什么它不动”的困惑时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑