
很多刚接触Unity的朋友第一次写角色移动时最大的困惑往往不是“代码不会写”而是“为什么同一个移动网上能找到七八种写法每种写法还都不一样”。有人用transform.Translate有人用CharacterController.Move还有人直接给Rigidbody加力。这几种方法到底有什么区别新手到底该学哪种这篇文章就围绕Unity中角色移动的几种常见实现方式展开从C#脚本的基础逻辑说起逐个拆解每种方法的原理、代码、适用场景最后再说说我在实际项目里踩过的坑和总结出的选型经验。全文面向零基础新手但老手也可以当一份查漏补缺的备忘录。1. 先搞懂移动角色的本质是每帧把坐标变一点在开始写代码之前我建议所有人都先想清楚一件事游戏里的“移动”到底是在动什么答案其实非常简单就是每一帧改变了物体的位置。Unity的场景里任何一个物体都有一个Transform组件里面存着它的position位置、rotation旋转、scale缩放。物体的移动本质上就是把position这个三维坐标在每一帧朝着某个方向改一点点。这样说可能有点抽象你可以把游戏运行理解成在翻一本动画书。每一帧就是翻一页页与页之间角色位置发生了变化连起来看就“动”了。帧率越高一秒钟翻的页数越多动起来就越流畅。那么问题来了既然本质就是改坐标为什么Unity里会有好几种改法直接写transform.position 新位置不就行了理论上可以但实际项目里没有人会全程这么干。因为直接改position有一个致命问题它完全无视物理碰撞。你写一行代码把角色往墙的方向挪角色会直接穿进去因为transform.position只负责“把物体放到这个坐标”根本不检查路上有没有障碍物。所以Unity的移动方案本质上分成了三条路线路线核心操作是否检测碰撞是否受物理影响典型用途直接改Transformtransform.Translate否否特效、演示、简易敌人CharacterControllercontroller.Move()是仅自己实现传统第一/第三人称角色Rigidbodyrb.MovePosition()/rb.AddForce()是是物理交互、推箱子、载具这三条路线没有绝对的好坏只有合不合适。下面我会分章节把每一种的实现方法、代码、原理都讲透并且把新手最容易踩的坑一个一个复盘给你看。1.1 移动的本质每帧把角色往目标方向推一点在C#脚本里移动代码一般会写在两个地方Update()或者FixedUpdate()。这两个方法的区别我会在下一章详细展开这里先记住一个结论直接改变坐标这种操作通常放在Update()里。如果你要写一个简单的“物体朝前走”最直白的代码是这样的using UnityEngine; public class SimpleMove : MonoBehaviour { public float speed 5f; void Update() { Vector3 direction Vector3.forward; // (0, 0, 1)世界坐标Z轴正方向 transform.position direction * speed * Time.deltaTime; } }把这段代码挂到任意物体上运行后物体会朝Z轴方向一直往前飞。注意这里我乘了一个Time.deltaTime它表示“上一帧到这一帧所经过的时间”单位是秒。这个值非常关键先按下不表下一章专门讲。1.2 直接改坐标、专用控制器、物理引擎三者本质是三种取舍我把三条路线放在一起对比是想先给你建立一个整体认知直接改Transform简单但控制不了碰撞适合做背景里的装饰物、粒子、剧情演示或者那些压根不需要跟场景交互的“移动”。CharacterController是Unity官方为了让开发者做“人形角色”专门设计的一个组件它封装了胶囊碰撞体、自动坡度检测等能力。用它的Move()方法移动角色角色不会穿墙也不会被物理引擎莫名弹飞但重力得自己写。Rigidbody是物理引擎的入口。一旦物体挂上Rigidbody并调用物理API它就会受到重力、碰撞反弹、摩擦力等真实物理效果的影响。适合做载具、箱子、摆锤这些需要“被物理世界干扰”的东西。在继续阅读之前只要记住这三种方式背后对应的是三种不同的场景需求不要混用。混用往往就是各种灵异Bug的起点。2. 移动的计时器Update、FixedUpdate 和 deltaTime 的账要算清楚新手写移动代码时最容易忽略、也最容易翻车的一个环节就是对“帧”的理解。Unity里的游戏循环很直观游戏运行期间每一帧都会执行脚本中的Update()方法然后渲染出这一帧的画面。但是这里有一个关键问题不同电脑的帧率是不一样的。好一点的电脑游戏跑120帧差一点的跑30帧都在正常范围。假设你不乘Time.deltaTime直接写transform.position new Vector3(0.1f, 0, 0);那么在120帧的电脑上一秒执行120次移动了12米在30帧的电脑上一秒执行30次移动了3米同一个游戏换个电脑角色速度差了四倍这显然是不行的。为了消除帧率带来的差异我们需要让“每次移动的距离”跟“距离上一帧的时间”挂钩。Time.deltaTime就是Unity提供给我们的上一帧耗时单位是秒。一般来说60帧下它大约是0.016秒30帧下大约是0.033秒。把移动距离乘以Time.deltaTime后公式就变成了这一帧移动距离 速度 * 每帧时间帧率越高每帧时间越短单帧移动距离越小但每秒总移动距离保持不变。这就是为什么所有移动代码里都会出现Time.deltaTime——它是保证不同设备上速度一致的基石。2.1 Update 和 FixedUpdate 分别该干什么Unity脚本里有两个高频更新方法Update()和FixedUpdate()。Update()每一帧调用一次调用频率不固定跟机器性能有关。适合处理输入检测、非物理的动画状态切换、UI更新等。FixedUpdate()按固定时间步长调用默认是每秒50次也就是0.02秒一次这个间隔是固定的跟帧率无关。Unity的物理引擎在内部也是按这个固定步长来运算的。所以所有涉及Rigidbody的物理操作加力、设速度等都应该放在FixedUpdate()里。可以这样理解Update()是画面更新FixedUpdate()是物理世界的时钟。物理世界的时钟不会因为画面卡了而放慢依然稳如老狗地每秒跑50次。新手最常见的错误就是把物理操作写在Update()里。在帧率低的时候Update()调用次数变少给刚体加力的次数也变少刚体的运动就会变得一卡一卡甚至完全不受控。2.2 移动代码里不写 deltaTime 会怎样我见过很多新手在学了Time.deltaTime之后还是忍不住把它漏掉因为“不乘也看不出来问题”。在项目早期确实很难看出来因为场景简单、帧率稳定。但一旦场景复杂度上去帧率波动变大问题就开始暴露了角色在某一帧突然卡顿一下之后位置却像“瞬移”了一样。如果你在角色移动代码里漏掉了Time.deltaTime本质上你就把移动速度和帧率绑在了一起——这会让你的角色速度在不同设备上表现完全不一致。所以我的建议是从第一行移动代码开始就把Time.deltaTime写进去养成肌肉记忆。3. 方式一Transform.Translate —— 五分钟学会但别拿它做角色控制Transform.Translate是Unity里最直观、最好上手的移动方法。它的代码如下using UnityEngine; public class TranslateMove : MonoBehaviour { public float speed 5f; void Update() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 direction new Vector3(horizontal, 0, vertical); transform.Translate(direction * speed * Time.deltaTime); } }把这段代码挂到角色上按方向键或者WASD角色就能在场景里走来走去了。对于完全没接触过Unity的新手来说第一次看到自己的角色动起来这种成就感无可替代。但我要在这里泼一盆冷水Transform.Translate只适合学习演示和剧情走位不适合做真正的主角控制。为什么因为Translate的操作绕过物理系统角色会无视一切碰撞体。你的角色会穿墙、穿箱子、穿地上的一切障碍。在官方文档里它的定位就是“移动这个变换对象”完全不管物理规则。如果你只是做一个场景里飘来飘去的粒子、一个用来演示的箭头、一个待机转圈的装饰物那用Translate非常合适代码简洁性能开销也低。3.1 Translate的坐标系参数Self和World别搞混了Transform.Translate还有一个重载让我想单独拿出来讲transform.Translate(Vector3.forward * speed * Time.deltaTime, Space.Self);第二个参数有两个选项Space.Self和Space.World。默认是Space.Self。Space.Self是相对自身坐标系的Z轴移动也就是说无论物体旋转到什么方向它都朝自己的前方移动。Space.World是相对世界坐标系的Z轴移动不管物体怎么转它都沿着世界坐标的固定轴移动。举个现实例子一辆车朝东开车头也朝东。这时你把车头转向南边旋转了如果移动方向用Space.Self车会朝南继续开如果用Space.World它依然朝东平移。新手最容易困惑的是我想让角色“面对哪个方向就走哪个方向”这里到底该用哪个答案是Space.Self。但这里有一个隐藏陷阱如果你的角色模型还没有旋转到正确角度用Space.Self移动可能会让角色“横着走”。这个问题的根源不在移动代码而在模型资源的轴心和默认朝向设置。3.2 斜向移动会变快这是新手一定会踩的坑用Transform.Translate做方向键控制时还有一个非常经典的问题斜着走比直着走快。正常情况下你按“W”朝前走速度是5。按“W”和“D”同时按理论上应该也是速度5但实际算出来方向向量是(1, 0, 1)它的长度是√(1²1²) ≈ 1.414。如果不处理角色斜着走的速度就是5×1.414≈7.07。对角线方向凭空多了41%的速度。解决方案很简单把方向向量归一化Normalize让它长度变成1Vector3 direction new Vector3(horizontal, 0, vertical); if (direction.magnitude 1f) { direction.Normalize(); } transform.Translate(direction * speed * Time.deltaTime);如果不想写if也可以直接用direction.normalized它只保留方向长度强制为1。但要注意如果direction本身就是零向量normalized会返回零向量没有副作用所以多数情况可以直接用。3.3 Translate在什么场景下值得用讲了一堆坏话还是得给Translate一个公道它并非一无是处。在我的实际项目里Translate常用于非玩家角色的简单巡逻比如守卫来回走直线子弹、箭矢这类高速飞行物很多情况下它们不需要和所有物体发生碰撞命中靠射线检测剧情演算时的镜头运动菜单里的装饰元素飘动它的优势是干净利落、性能好。缺点是不受物理规则约束。所以我的建议是记住它的存在但用它之前先问问自己这个物体需要撞到别的东西停住吗4. 方式二CharacterController —— 正经的角色移动应该从这一套开始如果你的项目是传统的第一人称或第三人称控制角色我的建议是直接学CharacterController。它是Unity专门为“人形角色移动”设计的一套组件内置胶囊碰撞体、自动坡度识别、自动边界约束你可以把它理解成一个“自带碰撞盒并且可以被角色控制移动”的组件。先看一个完整的移动代码模板using UnityEngine; public class CharacterMove : MonoBehaviour { public float moveSpeed 5f; public float gravity -9.81f; private CharacterController controller; private Vector3 velocity; void Start() { controller GetComponentCharacterController(); } void Update() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); // 1. 输入方向转为世界空间的前进方向 Vector3 move transform.right * horizontal transform.forward * vertical; // 2. 控制器的Move方法自带碰撞检测 controller.Move(move * moveSpeed * Time.deltaTime); // 3. 手动加重力 velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }这段代码至少能实现角色朝输入方向移动、不穿墙、收重力、能上坡下坡。对一个新手玩家角色来说已经比Translate版本像样多了。4.1 为什么有了Rigidbody还要用CharacterController这是很多人拒绝CharacterController的原因“Unity不是有物理引擎吗我刚体照样能移动角色干嘛学一套新API”刚体确实能做角色移动但做起来非常别扭。角色不是箱子它需要快速响应输入、精确到达指定位置、又不被物理引擎莫名弹飞。刚体在物理引擎里会被碰撞、弹力、摩擦力等参数影响一旦场景里有其他物体推了一下角色角色就会歪、抖你得花大量时间调质心、调Drag、锁定旋转约束才能让它看起来像一个“站着的人”。CharacterController的设计目标就是解决这个问题。它内部也有碰撞检测胶囊形但不会给角色附加物理力不会因为被撞就东倒西歪也不会受摩擦力影响。它的运动方式更接近“角色自己走”而不是“被物理系统推着走”。对绝大多数角色控制需求这是比Rigidbody更省心的方案。4.2 Move方法里为什么必须自己写重力CharacterController的使用里新手最懵的一句话是“为什么角色加了重力脚本还是一直飘在空中”原因是CharacterController.Move()不会自动施加重力。它只负责“把角色移动到你指定的位置”不会替你计算重力加速度。这是设计者有意为之因为不同游戏对重力的需求差别很大有的游戏跳跃高度高、下落弧线夸张有的游戏重力接近现实落体很快。如果把重力写死在组件里反而成了限制。所以你需要自己维护一个垂直方向的速度变量我上面的代码里就是velocity每帧把这个速度累加gravity * Time.deltaTime再连同水平移动一起传给controller.Move()。有一个细节我要提醒如果角色跟地面接触了velocity.y还在不断累加负数那角色会一直受到不断增大的“向下速度”干扰看起来就是“吸在地面上”但偶尔抖动。解决办法是检测角色是否落地如果落地就把velocity.y归零或者给一个很小的负值if (controller.isGrounded velocity.y 0f) { velocity.y -2f; // 给一个轻微的下压力保证贴着地面 }这样处理之后角色不会一碰到地面就疯狂堆积下落速度后续做跳跃功能也会顺畅很多。4.3 CharacterController的SimpleMove和Move到底用哪个CharacterController提供了两个移动方法Move()和SimpleMove()。一字之差坑也不一样。Move(Vector3 motion)参数是“本帧要移动的位移”单位是米不受重力影响。你要自己算好重力并传给Move。SimpleMove(Vector3 speed)参数是“每秒的速度”单位是米/秒它会自动应用重力并且返回值是一个布尔值表示角色是否落在地面上isGrounded。听起来SimpleMove是更省事的那个但它有一个硬伤SimpleMove会自动应用内置重力而且这个重力不可配置。如果你需要精确控制跳跃手感、下落加速度SimpleMove根本做不到。另外SimpleMove内部把垂直速度完全接管了你想处理“角色站在斜坡上微微下滑”这种效果时就没法通过垂直速度手动干预。所以我的经验是前期学SimpleMove可以快速看到效果但从做一个像样的角色控制器的角度直接学Move 手写重力是更正确的起点。别怕多写两行这会让你的角色控制代码更加自由。5. 方式三Rigidbody —— 让物理引擎帮你算移动也把麻烦交给它如果说CharacterController是为“人”设计的那Rigidbody就是为“物体”设计的。物体挂了Rigidbody后Unity的物理引擎就会接管它受重力、受碰撞、受摩擦力、受各种力。你要移动它不再建议直接改transform.position因为物理引擎每帧会根据速度、受力、碰撞重新计算位置你直接改的坐标会被覆盖而且很容易和物理运算冲突导致物体抖动或者穿模。Rigidbody移动有两种典型写法MovePosition和AddForce。它们的心态完全不同。5.1 MovePosition告诉刚体“我要到这个点”using UnityEngine; public class RbMovePosition : MonoBehaviour { public float speed 5f; private Rigidbody rb; void Start() { rb GetComponentRigidbody(); } void FixedUpdate() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 input new Vector3(horizontal, 0, vertical).normalized; Vector3 targetPos rb.position input * speed * Time.fixedDeltaTime; rb.MovePosition(targetPos); } }注意这里用Time.fixedDeltaTime因为物理相关的API必须在FixedUpdate()里调用。FixedUpdate的步长默认是0.02秒所以Time.fixedDeltaTime对应的就是0.02秒。MovePosition的含义是让刚体经过物理计算后移动到目标位置。它依旧会被墙壁挡下来但移动过程是“物理引擎认可的”不会穿墙。这跟transform.position 的直接赋值有什么区别区别在于MovePosition会参与物理系统完整的碰撞检测流程而直接赋值本质上就是瞬移。就像你开车MovePosition是正常驶入停车场并停在车位上直接赋值是你打开车库门后连人带车瞬移到墙里面了。5.2 AddForce让物理引擎“推着”角色走using UnityEngine; public class RbAddForce : MonoBehaviour { public float force 10f; private Rigidbody rb; void Start() { rb GetComponentRigidbody(); rb.freezeRotation true; // 防止力把角色推倒 } void FixedUpdate() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 input new Vector3(horizontal, 0, vertical).normalized; rb.AddForce(input * force, ForceMode.Acceleration); } }AddForce和MovePosition的思路完全不同MovePosition是“把这个物体放到那个位置”AddForce是“给物体一个作用力让它在物理规则下自己动起来”。用AddForce移动角色最直观的感受就是“有惯性”你按下按键角色不会立刻全速前进而是慢慢加速松开按键它还会滑行一段距离才停下。这种手感在赛车、球类、物理益智游戏里非常真实、非常带感但在传统平台跳跃游戏里就很痛苦了因为角色“发飘”跳起来之后还能被外力改变方向。所以AddForce更适合需要大量物理交互的场景比如推箱子、击飞、角色被爆炸冲击波推开这类“力”的传播而精确操作的角色控制还是交给CharacterController更合适。5.3 Update 和 FixedUpdate 分不清刚体角色抖动到你怀疑人生如果你用Rigidbody做移动却把它写在了Update()里画面会怎样我直说会时不时抽搐、穿透、甚至飞起来。因为物理引擎是在FixedUpdate的固定时间步长里推进的它有自己的运行节奏。你在Update()不固定帧率里改动刚体位置或加速度物理引擎下个固定步长计算时又会基于它自己计算的结果覆盖或融合两套节奏相互打架物体的运动自然就不平滑。这个问题最大的迷惑点在于它不是一个每次都必现的问题而是偶发的、由帧率和物理步长的相位差导致的。所以可能你在自己电脑上试了半天都没问题换个配置低一点的电脑测试角色就开始抖了。写Rigidbody物理操作时老老实实放进FixedUpdate()里。6. 一张表把选型说清楚什么项目用哪种移动方式前面几章已经分别把三种方式讲清了这一节我来做一个总结性的选型对照。需求场景推荐方案理由新手练习、剧情走位、纯视觉装饰Transform.Translate简单直接不需要物理参与传统第一/第三人称角色控制CharacterController.Move自带碰撞检测精确控制受物理干扰小平台跳跃、需要精准落点的角色CharacterController.Move跳跃高度可控重力可调推箱子、载具、被爆炸冲飞的物体Rigidbody.AddForce天然模拟惯性、碰撞反弹和摩擦需要移动但又要被墙壁挡住的NPCCharacterController 或 Rigidbody.MovePosition根据是否参与物理进一步选择高速子弹、非物理飞行特效Transform.Translate性能好配合射线检测实现命中这里给新手的建议是第一遍学习时三种方法都写一遍感受差异。但做正式项目时先问自己一句“我的角色需要被物理世界支配吗”如果不需要优先CharacterController如果需要再考虑Rigidbody。有一种情况很常见角色走着走着被地上的小箱子绊了一下或者被敌人打飞出去。这种需求不能简单用CharacterController一套方案解决因为在物理交互时CharacterController不会自动响应外力。成熟的做法是平时移动用CharacterController在角色被击飞时临时切换为Rigidbody模式或者用代码给角色一个临时的AddForce并关闭角色控制。这是一个进阶话题但你要知道移动方案的选型不是一锤子买卖而是可以动态切换的。7. 新手最容易翻车的四个现场以及我的排查顺序最后这个部分我把自己当年踩过、也看无数新手踩过的坑集中复盘一遍。每一个坑都对应一段真实经历希望能帮你节省大量排查时间。7.1 加了CharacterController角色还是穿墙问题描述角色身上明明挂着CharacterController组件移动时还是穿过了墙壁。这是新手的“经典迷惑”。排查之后你会发现移动的代码里用的是transform.Translate而不是controller.Move。CharacterController的碰撞检测只作用于它的Move方法如果你用transform.Translate或直接改transform.position就相当于绕过了CharacterController物理世界谁都拦不住你。所以排查第一个要看的就是移动操作到底走的是什么通道。CharacterController组件存在并不能自动约束Transform的位移必须通过它的Move或SimpleMove方法移动才有碰撞效果。7.2 角色一直往下掉或者根本不落地问题描述用CharacterController控制角色角色要么像在月球上飘要么一落地就疯狂往下掉。这个问题有两个常见原因第一忘了实现重力。CharacterController不会自动加重力如果Move的位移里垂直分量永远是0角色就会悬浮在空中。解决办法是维护一个竖向速度变量每帧累加gravity * Time.deltaTime再传给Move。第二重力实现里没有判断落地点。我在4.2节里提过角色落地后isGrounded为true如果不把竖向速度归零速度会无限累加下一帧Move时角色会被巨大的竖向速度“按”进地面然后被物理系统弹开看起来就像在地上疯狂抖动。排查时把velocity.y的更新逻辑单独列出来逐帧看它是怎么变的问题基本就清楚了。7.3 用Rigidbody角色抖动、穿模、速度不稳定问题描述Rigidbody角色在移动中会抖动有时还会穿到墙面里。这个坑十有八九是因为移动代码写在Update()里或者用了transform.position直接改位置。物理引擎在固定步长里计算刚体属性你在渲染层按帧率改位置两者的时序不一致最终结果就是“画面在动但物理系统觉得你根本没动”于是下一帧物理系统又把你拉回它计算的位置视觉上就是抖动。排查顺序先确认代码在哪个方法里执行确认是不是FixedUpdate再确认移动是不是通过Rigidbody.MovePosition或AddForce完成最后把你的Rigidbody组件上的Interpolate设置为Interpolate或Extrapolate这在低帧率情况下能改善视觉平滑度。7.4 斜着走速度明显快一截这个问题我在3.2节详细讲了原因是方向向量没归一化。排查思路也很直接把输入向量打出来看看斜向移动时它的长度是不是√2左右。解决办法就是direction.normalized。别看它代码简单很多正式项目里都能见到这个Bug因为症状并不明显你不拿秒表测根本感觉不到。7.5 推荐的新手自查顺序综合来看我建议你遇到移动相关Bug时按下面顺序排查看先看移动代码在哪个生命周期里执行Update还是FixedUpdate。再看移动代码使用的是哪个API直接改transform、controller.Move、还是物理接口。再看移动向量是否归一化有没有乘以Time.deltaTime或Time.fixedDeltaTime。最后看组件是否齐全CharacterController/Rigidbody都挂了吗物理材质、碰撞层设置没错吗这套顺序看起来朴素但能解决我见过的绝大多数新手移动问题。回到我自己的经验上如果你只打算做角色移动我建议从CharacterController开始学它最接近“游戏角色”的直觉Transform.Translate了解一下原理知道它为什么不适合主角Rigidbody留到后面需要物理交互时再深入。移动只是Unity游戏开发的冰山一角但把这一块吃透你就能理解Unity里“直接改坐标”和“让系统帮自己改坐标”这两种思维方式的根本区别而这种区别会在后面做任何功能时都帮到你。