
1. 为什么要用刚体做2D人物移动先说结论在做Unity 2D人物移动时直接改Transform.position是最省事的写法但它也是最容易把项目搞崩的写法。真正做项目尤其是后面要加碰撞、跳跃、平台、敌人交互的时候刚体Rigidbody2D几乎是唯一靠谱的选择。刚体移动的核心思路不是“命令角色走到哪”而是“给角色一个速度让物理引擎去处理位置变化”。这两者的本质区别在于前者绕过了物理系统所有碰撞检测、碰撞响应、触发器回调全部失效后者把移动权交给物理引擎碰撞、反弹、摩擦力、速度变化都由引擎统一管理。打个比方直接改Transform相当于你在人群里硬挤出一条路别人撞不撞你、你会不会撞倒摊位全凭你自己算用刚体移动则是你正常走路撞到东西自然停下被推了一下也会自然踉跄物理世界会替你处理这些交互。实操中最常见的误区是把Rigidbody2D的bodyType设置成Static然后通过Transform移动再挂一个Collider2D。结果就是角色穿过墙壁、触发器不触发、地面检测时好时坏代码查了半天发现是物理系统压根没参与运算。刚体移动适合的场景非常明确需要碰撞交互的玩家角色、NPC、敌人、可推动物体、以及任何需要和场景物理元素产生关系的物体。纯展示用的飘字、粒子、装饰物则完全不需要刚体直接用Transform操作即可。2. 2D人物移动的整体设计与实现思路2.1 核心设计目标可操控、可碰撞、手感好移动系统设计的本质是让角色的行为符合玩家的直觉预期。按下左键角色向左移动松开按键角色停止或滑行撞到墙就停下而不是卡进去跳到空中自然下落。这些逻辑听起来简单但每一项都对应一个独立的系统模块。我见过很多初学者把移动、跳跃、动画、地面检测全部堆在Update里写一个脚本几百行改一个参数牵一发而动全身。正确的设计思路是分层拆解输入层读取玩家的操作指令键盘、手柄、触摸屏移动层根据输入计算目标速度驱动刚体产生位移交互层处理碰撞响应地面检测触发器等物理交互表现层根据移动状态切换动画、朝向、音效输入层和移动层分离的好处是便于后续接入AI控制、联网同步、或动画驱动。角色不该关心按键是玩家按的还是脚本模拟的只需要接收一个输入向量然后决定怎么动。2.2 为什么速度要用物理属性而不用Transform的position做插值Transform.position插值移动的问题不只是碰撞失效。更隐蔽的问题是移动速度不稳定因为插值通常基于每帧耗时如果帧率波动明显角色会忽快忽慢。刚体移动通过FixedUpdate固定步长计算物理位置配合物理引擎的插值功能能保证在不同帧率下移动表现基本一致。刚体移动还有一个隐藏优势是力、冲量、速度可以叠加。比如角色跑步时有初速跳跃时有一个向上的冲量两者互不干扰如果用Transform手动插值这些力的叠加完全要自己实现而且很难做到物理模拟的真实感。2.3 初始化设置Rigidbody2D组件参数怎么配准备工作是在角色对象上添加Rigidbody2D组件和Collider2D组件。这里有几个参数需要特别注意bodyType设置为Dynamic表示受物理引擎控制gravityScale默认是1平台跳跃游戏通常调到2到4手感更脆linearDrag线性阻尼控制水平速度衰减0代表不衰减数值越大松手后滑得越短interpolation建议设置为Interpolate或Extrapolate不然低帧率下角色移动会有卡顿感collisionDetectionMode建议设置为Continuous防止角色快速移动时穿透薄墙实际项目里有人喜欢把gravityScale调到0然后手动写重力模拟。我建议新手尽量用引擎自带重力等需要精确控制手感再自己实现。物理引擎的重力经过了大量实际项目验证默认效果远比手写稳定。3. 基于刚体的2D人物移动核心代码实现3.1 基本移动代码框架从输入到速度我们从一个最简单的框架开始这个框架可以直接拿去做项目基础using UnityEngine; public class PlayerMovement2D : MonoBehaviour { [Header(组件引用)] public Rigidbody2D rb2D; [Header(移动参数)] public float moveSpeed 5f; private Vector2 moveInput; private Vector2 moveVelocity; void Awake() { if (rb2D null) rb2D GetComponentRigidbody2D(); } void Update() { // 读取输入 float horizontal Input.GetAxisRaw(Horizontal); float vertical Input.GetAxisRaw(Vertical); moveInput new Vector2(horizontal, vertical).normalized; } void FixedUpdate() { // 计算目标速度 moveVelocity moveInput * moveSpeed; // 这里不直接设置rb2D.velocity而是用专门的处理 rb2D.velocity new Vector2( moveVelocity.x, rb2D.velocity.y ); } }这个代码里面有个关键细节FixedUpdate中设置velocity时只覆盖x轴保留y轴。因为y轴可能承载着跳跃或下落的物理速度如果整体覆盖跳跃行为会被直接抹掉。Input.GetAxisRaw返回的是离散值只有-1、0、1三态键盘控制时手感更干脆。如果用Input.GetAxis会有平滑过渡的浮点值适合手柄或摇杆但键盘下会感觉角色拖泥带水。3.2 改进版本加速、减速与手感调整直接设置velocity的优点是响应快缺点是角色像在冰上滑行或者像在按开关一样瞬间启动瞬间停止缺少重量感。真实游戏里角色的运动是渐变的按键后速度逐渐增加松开后被阻力逐渐拉回零。实现渐变手感需要自己做加速和减速逻辑using UnityEngine; public class PlayerMovementAdvanced2D : MonoBehaviour { public Rigidbody2D rb2D; [Header(移动参数)] public float maxSpeed 8f; public float acceleration 60f; public float deceleration 80f; [Range(0f, 1f)] public float velocityPower 0.9f; private Vector2 moveInput; private float targetSpeed; private float speedDiff; private float accelRate; private float movement; void Update() { moveInput new Vector2(Input.GetAxisRaw(Horizontal), 0).normalized; } void FixedUpdate() { // 计算目标速度 targetSpeed moveInput.x * maxSpeed; // 计算当前速度与目标速度的差值 speedDiff targetSpeed - rb2D.velocity.x; // 根据是否需要加速还是减速选择加速度 accelRate (Mathf.Abs(targetSpeed) 0.01f) ? acceleration : deceleration; // 应用加速度 movement speedDiff * accelRate; // 添加最终速度 rb2D.AddForce(new Vector2(movement, 0), ForceMode2D.Force); } }这个方案使用AddForce而不是直接改velocity让物理引擎参与速度计算手感会柔和很多。velocityPower参数可以对加速度做非线性调整数值越接近1加速过程越有个爆发感我实际项目里通常调到0.85到1之间需要根据角色手感做微调。这里有个理解难点为什么不用rb2D.velocity ...而要用AddForce因为velocity直接赋值会丢失物理引擎累积的力比如平台移动带来的惯性、爆风冲击等而AddForce是在已有速度基础上叠加更贴合物理规律。3.3 跳跃系统地面检测与向上的力跳跃几乎是每个2D游戏的标配它的核心不只是给一个向上的力还包括只有在地面时才能跳、在空中时不能二段跳除非刻意设计、以及跳跃高度和空中控制的调校。先写一个基础的跳跃检测using UnityEngine; public class PlayerJump2D : MonoBehaviour { public Rigidbody2D rb2D; public Transform groundCheckPoint; public float checkRadius 0.1f; public LayerMask groundLayer; public float jumpForce 12f; private bool isGrounded; void Update() { // 地面检测 isGrounded Physics2D.OverlapCircle(groundCheckPoint.position, checkRadius, groundLayer); if (Input.GetKeyDown(KeyCode.Space) isGrounded) { rb2D.velocity new Vector2(rb2D.velocity.x, jumpForce); } } }地面检测是整个跳跃系统的命门。OverlapCircle检测的radius要配合角色尺寸太小会导致角色站在地面边缘跳不起来太大又会导致离地一小段还能跳。我一般建议radius设为角色Collider高度四分之一左右groundCheckPoint放在脚底中心稍微偏移一点。LayerMask的配置也容易踩坑。地面层要单独创建并设置给所有地面物体不然OverlapCircle会把敌人、道具甚至角色自己也检测为地面跳跃逻辑就会混乱。跳跃手感调整方面有个常用技巧就是“可变跳跃高度”按住跳跃键跳得高轻触跳跃键跳得低。实现思路是在上升阶段如果玩家松开跳跃键立刻衰减垂直速度if (rb2D.velocity.y 0 !Input.GetKey(KeyCode.Space)) { rb2D.velocity new Vector2(rb2D.velocity.x, rb2D.velocity.y * 0.5f); }这个衰减系数0.5可以直接调出不同手感数值越小轻点跳得越低跳跃响应越脆。3.4 原地转向与角色朝向2D人物移动还有一个见得多但容易忽略的细节角色朝向。横版游戏里按左键角色应面向左按右键面向右。最朴素的做法是改变transform.localScale的x方向if (moveInput.x 0) transform.localScale new Vector3(Mathf.Abs(transform.localScale.x), transform.localScale.y, transform.localScale.z); else if (moveInput.x 0) transform.localScale new Vector3(-Mathf.Abs(transform.localScale.x), transform.localScale.y, transform.localScale.z);这比直接设置localScale new Vector3(1,1,1)好因为如果角色初始缩放不是1或者有缩放动画直接写死数值会把已有缩放覆盖掉。更好的方案是使用Transform.right或spriteRenderer.flipX。spriteRenderer.flipX不改变碰撞体大小和方向适合纯视觉表现localScale翻转会影响所有子物体适合带攻击判定框的角色。取舍标准是看你的角色是否需要在翻面时把攻击判定框、盾牌判定框等也一起翻转。如果需要用localScale如果只是贴图翻转用flipX。4. 实战中的碰撞检测与物理交互细节4.1 Collider2D的选择对移动手感的影响刚体移动和碰撞是分不开的。最常见的Collider2D有BoxCollider2D、CircleCollider2D和CapsuleCollider2D。对于人形角色CapsuleCollider2D通常手感最好因为圆角部分在上坡或下坡时不容易卡住。BoxCollider2D的直角边缘贴在斜面上时会突然卡住CircleCollider2D滚起来不稳定CapsuleCollider2D在不规则地形上的表现最顺滑。如果角色是方块形态或机器人BoxCollider完全没问题如果角色是圆滚滚的角色Circle更合适。Collider2D的offset也很关键。默认Collider是包住Sprite的但角色的视觉中心通常不等于物理中心。比如一只占两格的怪物头部其实是视觉装饰碰撞体应该稍微下沉只覆盖身体部分。不调offset的后果是角色头部能撞到头顶的平台看起来很假。另一个容易漏掉的设置是PhysicsMaterial2D。如果角色的Collider没有设置PhysicsMaterial摩擦力默认是0.4这会导致角色从斜坡上滑下来时速度特别快或者跳起来落在平台上往左右滑动。合理设置friction摩擦力和bounciness弹性可以调整移动手感。我通常在角色Collider上挂一个friction为0的PhysicsMaterial2D防止角色卡在斜坡或墙边时产生额外摩擦地面物体则保持默认或设置较低的摩擦。实测下来friction设为0的角色在斜坡上的移动手感最顺滑不容易出现卡坡。4.2 斜坡处理刚体移动不卡坡的实现方式斜坡是2D物理移动最恶心的问题之一。直接用velocity移动的角色在上坡时会明显减速下坡时会加速因为刚体在和斜坡做物理交互时重力分量会影响水平移动。最常用的处理方案有两种第一种把移动力的方向从水平改为沿着地面方向。实现方式是做射线检测获取地面的碰撞法线然后以法线的切线方向施加移动力。这种方案效果最好但代码复杂度高需要处理法线平滑插值不然角色在坡顶和坡底交接处会抖动。第二种继续使用水平力但适当增加角色的linearDrag和mass。这种方法不能完全消除坡上减速但通过调整加速度可以让减速不那么明显实现起来省事适合坡道角度不大的场景。对于大多数独立游戏项目我建议先用第二种方案。等角色出现明显的爬坡费劲、下坡滑翔问题并且已经成为影响体验的核心痛点时再上第一种复杂方案。实际项目里很多游戏干脆设计成没有斜坡或者斜坡只做视觉层面的装饰碰撞体用多个BoxCollider拼成阶梯状。4.3 刚体穿透问题与Continuous碰撞检测快速移动的角色穿过薄墙是一个经典Bug。默认的碰撞检测模式是Discrete它只检测每一帧结束后物体的位置是否重叠如果物体在一帧内移动的距离超过了墙的厚度碰撞就会被跳过。刚体穿透的解决方案有两种一是把Rigidbody2D的collisionDetectionMode设置为Continuous。这种模式会对移动路径做连续扫描检测路径上的所有碰撞体避免穿透。代价是消耗更多性能不过2D游戏中物体数量不多时几乎可以忽略。二是减小Fixed Timestep。在Project Settings中把Fixed Timestep从默认的0.02改为0.01或更小物理检测频率翻倍也会降低穿透概率。但要注意减小Fixed Timestep会加重CPU负担而且会让物理模拟更精细的同时32个物理步长内的工作量同步增加。我实际项目中通常两种方案同时用主角设置Continuous其他快速移动的物体子弹、飞行的敌人如果仍穿透再调整Fixed Timestep。注意Continuous检测也不是万能的它对高速旋转的物体和复杂组合碰撞体仍有局限性。5. 移动模块的架构优化与性能调优5.1 用Input System代替旧的Input ManagerUnity的新Input SystemInput System Package比旧的Input Manager更灵活支持键盘、手柄、触屏的统一抽象而且性能更好。虽然旧APIInput.GetAxis写起来简单但它在后台仍然会分配很多字符串哈希查找移动端上会带来无谓的GC开销。新Input System建议用InputAction的Action Map方式组织输入。以移动为例创建一个PlayerControls的输入配置定义Move动作在代码中绑定回调using UnityEngine; using UnityEngine.InputSystem; public class PlayerMovementInput : MonoBehaviour { [SerializeField] private Rigidbody2D rb2D; [SerializeField] private float moveSpeed 8f; private Vector2 moveInput; void OnEnable() { // 假设已在Inspector中配置好InputActionAsset var playerInput GetComponentPlayerInput(); if (playerInput.actions ! null) { playerInput.actions[Move].performed OnMove; playerInput.actions[Move].canceled OnMoveCancel; } } void OnDisable() { var playerInput GetComponentPlayerInput(); if (playerInput.actions ! null) { playerInput.actions[Move].performed - OnMove; playerInput.actions[Move].canceled - OnMoveCancel; } } void OnMove(InputAction.CallbackContext ctx) { moveInput ctx.ReadValueVector2(); } void OnMoveCancel(InputAction.CallbackContext ctx) { moveInput Vector2.zero; } void FixedUpdate() { rb2D.velocity new Vector2(moveInput.x * moveSpeed, rb2D.velocity.y); } }新系统的事件回调机制能避免每帧轮询输入这在移动端低性能设备上有实际收益。当然如果只是做小原型或学习老API完全够用。5.2 避免每帧Transform操作导致GC峰值刚体移动方案本身已经避免了大量Transform操作但在实际代码中会有人在移动脚本里写transform.position ...来微调角色位置比如做角色的抖动、缩放、视觉错位。这是灾难性的物理引擎在FixedUpdate中更新刚体位置你在Update中改Transform位置两者每帧互相打架角色会剧烈抖动而且碰撞体位置也会漂移。如果确实需要做角色的视觉偏移比如受伤时抖动、攻击时前冲建议使用子物体做视觉偏移不要动带刚体和Collider的根节点。或者用rb2D.MovePosition来做位移补偿它会在下次物理更新时把刚体移动到目标位置不会打破物理同步。还有一个常见的性能坑在OnCollisionStay2D里频繁创建Vector2或GameObject的引用。Unity每次碰撞回调都会产生一定的内存分配如果回调里再new对象就会为GC垃圾回收制造额外压力。移动脚本中应尽量在Awake或Start阶段缓存所有引用避免运行时反复创建。5.3 移动状态机从简单移动到复杂行为当项目里角色不止有移动还有跑、跳、攻击、翻滚、受伤硬直时在同一个Update里堆大量if判断会被迅速摧毁。这时候需要把移动逻辑抽成状态机。状态机的核心是角色在任意时刻处于一种状态每种状态有独立的进入、更新、退出逻辑。比如Idle状态下角色不做任何移动Run状态下才读取输入并施加力Jump状态下可以有不同重力系数Attack状态下锁定移动方向但不能转身。我建议的这套状态机结构虽然初始写起来要花点时间但后续加新技能、新状态不需要重构移动代码只需要新增一个状态类public abstract class CharacterState { protected PlayerMovementController controller; public virtual void Enter() { } public virtual void Update() { } public virtual void FixedUpdate() { } public virtual void Exit() { } } public class GroundMoveState : CharacterState { public override void Enter() { // 设置地面移动时的参数 } public override void FixedUpdate() { // 读取输入计算移动 } }状态机和简单的bool开关isMoving、isJumping、isAttacking相比最大的好处是状态切换时能干净地清理旧状态不会出现攻击结束后还残留在空中移动状态里的bug。6. 常见问题与排查技巧实录6.1 角色抖动/抽搐刚体移动中角色抖动的原因多数是Update里改了Transform位置FixedUpdate里又用刚体force推了一次两个系统争抢同一个物体的位置。排查时可以先把Update里的所有Transform操作注释掉看抖动是否消失。如果是就把视觉偏移移到子物体上或使用rb2D.MovePosition。还有一种抖动来源是插值设置问题。Rigidbody2D的interpolation如果设置为None角色在低帧率的手机上会出现肉眼可见的抖动。把它改为Interpolate通常能解决。6.2 角色卡墙、穿越墙壁卡墙最常见的原因是Collider2D的bodyType设置不正确或碰撞体尺寸比视觉大了一圈。在Scene视图里把碰撞体显示打开对着墙壁看看碰撞体和Sprite的贴合程度。另一种是PhysicsMaterial2D的摩擦值过高角色紧贴墙壁时被粘住。把角色Collider上挂的PhysicsMaterial2D的friction降到0试试。穿越墙壁的原因前面说过优先检查collisionDetectionMode再考虑Fixed Timestep。还有一个冷门原因是角色所在层的碰撞矩阵被关掉了比如Player层和Ground层的碰撞被禁用物理引擎会完全不检测这两个层之间的碰撞。6.3 跳跃后落地不正常、角色被弹起跳跃后落地被弹起的元凶是Collider2D的bounciness弹性没归零。如果角色的PhysicsMaterial2D设置了bounciness落地时会根据下落速度产生反弹看起来就是跳完了还在上下弹不停。把bounciness调成0或干脆不挂PhysicsMaterial2D默认无弹性。另一种情况是落地时正好踩在Collider棱角上导致角色以极小角度滑开。这种问题一般通过调整地形Collider形状解决比如将尖锐角度地面改成圆角或用平台特效器PlatformEffector2D处理单向平台。6.4 刚体被推动时无法恢复移动控制有些交互场景下角色会被力推开比如被敌人击退但如果这个力过大或持续玩家按方向键没反应。原因是AddForce过的力传递到velocity而自定义移动逻辑在FixedUpdate中设置了velocity力还在但移动直接覆盖了速度。这种需求需要做移动权重决策如果角色处于受击状态暂时不读取输入只让力度控制角色受击结束后恢复移动控制。可以用一个状态变量来控制移动逻辑是否生效而不是单纯靠物理上的velocity计算。6.5 输入延迟与惯性过大用AddForce做移动时最容易出现的体验问题就是惯性过大松手后角色还在往前滑。这其实是因为linearDrag太小。在Rigidbody2D上把linearDrag从0调到3、4左右松手后滑行距离会明显缩短。注意这个参数是全局的会影响跳跃时水平摩擦力所以要配合跳跃手感一起调。另一种是输入响应慢按键后角色隔了一两帧才动起来。这通常是新Input System的事件回调时机和FixedUpdate不同步导致的。此时要么在Update里做输入记录、FixedUpdate里读取要么启用InputSystem.settings.updateMode InputSettings.UpdateMode.ProcessEventsInFixedUpdate。7. 移动手感调优的独家心得这部分主要聊聊我在自己项目里调移动手感的一些经验这些不是文档里能查到的东西更多是一点点试出来的。调移动手感有一个通用顺序先调速度再调加速度最后调线性阻力。速度决定角色的最大移动能力加速度决定反应快慢线性阻力决定松手后的滑行长度。很多人一上来就调线性阻力结果怎么调都别扭其实是因为最大速度还没配好。速度与角色世界的比例关系很重要。如果角色是一只有十几厘米的小兔子移动速度8就显得过快如果是两米高的巨型机器人移动速度8又会显得太慢。这里的核心参照是角色碰撞体的尺寸、场景的距离尺度、以及屏幕内可视范围。我习惯先粗略设定角色横穿屏幕大约需要1.5秒再根据屏幕宽度反推速度值这样出来的手感比较自然。跳跃手感有两个核心参数重力缩放gravityScale和跳跃初速。这两个参数需要联动调整。如果重力过大、初速也过大跳跃轨迹会特别尖像火箭一样直上直下如果重力小、初速也小跳跃会显得软像在太空漫步。理想的比例是跳跃高度大约是水平最大速度每秒移动距离的三分之一到二分之一这个比例能营造出足够爽快又不失真实的跳跃感。还有一个被很多人忽略的参数是Rigidbody2D的mass质量。默认质量是1如果调大AddForce的力道看起来会变小因为同样大小的力产生了更小的加速度。如果你使用AddForce方案移动调mass相当于同时调整所有外力的响应度。我一般习惯把mass保持在1只调加速度数值来改变手感这样别人接手代码时不会被一坨互相牵制的参数搞晕。实战中还有一个细节跳跃同时按住斜方向角色在空中的水平移动速度应该是地面速度的一定比例。默认情况下刚体移动在空中和地面的速度相同但实际操作中空中速度过大会让跳跃变成飞行过小则跳跃时很难调整落点。我会在空中把水平加速度降为地面的70%这样既保持了可操控性又让跳跃有明确的抛物线轨迹。最后强烈建议在制作移动系统时把参数全部用[Header]分组并且在Inspector里能直接拖拽。我见过太多人把参数硬编码在代码里每调一次手感改一次代码重新编译效率太低。项目做大了以后移动参数的调试频率远高于其他功能好的参数暴露方式能救你的命。8. 从示例项目到真实项目的扩展建议许多刚入门的开发者做完基础移动后下一步就开始堆功能冲刺、二段跳、攀爬、贴墙滑、蹲跳、后空翻。每加一个功能都意味着移动模块需要更灵活的设计。如果你的项目已经用刚体做移动扩展这些功能时最需要关注的一点是不要直接改velocity的全部分量。每加一个新移动能力实际上都在操纵x轴和y轴速度的某个分量。如果每个系统都全量覆盖velocity互相之间就会踩踏。建议定一个约定水平移动只改velocity.x垂直移动只改velocity.y外力通过AddForce叠加。另一个扩展方向是动画同步。刚体移动提供了位置和速度但动画系统需要的是移动状态Idle、Run、Jump、Fall和移动方向。建议在移动脚本中暴露一个只读的状态枚举和速度向量供Animator读取。不要在动画代码里反向引用刚体速度来计算是否在地面那会引入额外依赖导致地面检测逻辑重复定义。网络同步是多人游戏里更复杂的话题。如果用刚体做移动服务器端和客户端的物理差异会让位置同步非常麻烦。在做多人项目前需要确认移动方案是否要切换到客户端预测服务器回滚模式这个模式下刚体更多是表现层工具而不是逻辑层工具。当然这是后话先把单机手感调好再说。最后一点移动系统是玩法的基础层但不要过度设计。很多项目死在无限抽象的状态机和接口模式上。刚上手时先用最直接的velocity赋值方案完成功能等确实碰到扩展瓶颈再抽状态机。我见过太多人第一天写移动就在纠结用接口还是抽象类结果一周后游戏原型还没跑起来。移动系统的设计应该跟着游戏玩法走而不是跟着架构模式走。实际做一个2D横版游戏移动模块通常会占掉初期开发的很大比重。刚体移动这个方案是经过大量项目验证的稳定选择配合精心调校的参数能让角色在感受到快速反馈的同时保持物理可信度。如果刚看完这篇文章想动手试试我建议从最简单的velocity赋值版本开始跑通一整套移动碰撞跳跃闭环后再逐步往里面添加加速度、空中控制、状态机这些进阶内容。没有标准答案手感和项目适配度永远是唯一标准。