
1. 项目概述刚体驱动的角色移动在Unity里做角色移动很多新手上来就想着直接改Transform的position这确实简单直接但做出来的感觉往往轻飘飘的没有重量感也处理不好碰撞。想让角色动得真实、有物理反馈比如走起来有惯性、跳起来有重力、撞到墙会停下来那就必须请出物理系统的核心——**刚体Rigidbody**组件。这个项目要做的就是彻底抛弃直接操控Transform的“土办法”完全拥抱物理系统用刚体组件来实现一套包含移动、跳跃、冲刺的基础角色控制器。这不仅仅是换种写法而是从“动画播放”思维切换到“物理模拟”思维。你会直接对刚体施加力Force和冲量Impulse让Unity的物理引擎PhysX来计算最终的运动结果。这样做的好处是显而易见的移动自带加速度和减速度手感更真实与环境的交互碰撞、斜坡、台阶全部由物理引擎自动处理省心又准确而且这套逻辑稍加修改就能套用到各种需要物理反馈的物体上比如被踢飞的箱子、滚动的球。无论你是想做一款平台跳跃游戏、俯视角射击游戏还是任何需要角色与场景有扎实物理交互的项目掌握刚体移动都是绕不开的基本功。接下来我会带你从零开始拆解每一个环节的原理和实现细节避开我当年踩过的那些坑。2. 核心思路与方案选型为什么是刚体而不是Transform在动手写代码之前我们必须搞清楚一个根本问题用刚体Rigidbody和直接用Transform控制移动到底有什么区别这决定了我们整个项目的架构。2.1 Transform移动的局限性直接修改Transform.position相当于在说“无视一切物理规则立刻把物体放到这个坐标上。” 这种做法简单粗暴但问题很多无碰撞反馈物体会直接“穿”过其他碰撞体除非你手动写复杂的射线检测逻辑。手感虚假移动是瞬间完成的没有加速、减速的过程感觉像在滑动没有重量。物理失灵如果你同时给一个物体加了刚体又直接改Transform物理引擎会懵掉导致物体抽搐、穿模等不可预测的bug。Unity官方手册也明确警告不要同时操控同一个GameObject的Rigidbody和Transform。2.2 刚体移动的物理优势刚体组件让游戏对象受物理引擎控制。我们通过脚本向刚体施加力AddForce或直接设置速度velocity然后物理引擎会综合质量、阻力、重力、碰撞等因素计算出每一帧物体应有的位置和旋转再更新Transform。真实交互自动处理与场景中其他碰撞体的交互产生逼真的碰撞、反弹效果。惯性手感移动有加速和减速过程物体有“质量”感。简化逻辑复杂的斜坡行走、台阶边缘检测、物体被推动等效果物理引擎都帮你做好了。2.3 方案选型Force-Based 还是 Velocity-Based用刚体移动主要有两种思路选择哪种取决于你想要的手感1. 力驱动Force-Based这是最物理、最真实的方式。通过Rigidbody.AddForce()施加力让物理引擎根据F ma公式计算加速度。角色的速度是力持续作用的结果。优点物理正确手感真实自然加速曲线平滑。缺点响应有延迟因为需要加速时间在需要快速、精确响应的操作如平台跳跃的微操时可能感觉“有点肉”。适用场景写实风格的3D游戏如《战神》、《黑暗之魂》、赛车游戏、需要真实物理反馈的物体。2. 速度驱动Velocity-Based直接设置刚体的速度向量Rigidbody.velocity。这相当于直接告诉物理引擎“我不管过程现在立刻以这个速度运动。”优点响应即时操控精准手感干脆。是2D平台跳跃和快节奏游戏的常见选择。缺点略显“不物理”因为忽略了加速度过程。如果处理不好可能会与某些物理效果如某些力的叠加产生预期外的冲突。适用场景2D平台跳跃游戏如《空洞骑士》、《蔚蓝》、俯视角射击游戏、需要极强操控感的游戏。我的选择与理由对于标题中“移动、跳跃、冲刺”这个组合尤其是“冲刺”这个需要瞬时响应的动作速度驱动Velocity-Based方案在实现和手感调优上更直接、更容易控制。因此本项目将主要采用直接修改Rigidbody.velocity的方式来实现水平移动和冲刺而跳跃则使用AddForce来获得更自然的抛物线轨迹。这是一种混合方案兼顾了响应速度和部分物理真实性。3. 环境准备与组件配置理论清楚了我们开始搭建战场。正确的组件配置是后续一切操作的基础这里错一步后面全是坑。3.1 创建角色与添加组件在场景中创建一个Cube或导入你的角色模型命名为Player。选中Player在Inspector面板点击Add Component搜索并添加Rigidbody。这是我们的核心动力源。继续添加Capsule Collider或Box Collider。刚体必须配合碰撞体才能与场景交互。对于人形角色胶囊碰撞体Capsule Collider是首选因为它没有棱角在斜坡和边缘移动时更顺滑不易卡住。3.2 关键刚体参数详解与配置添加刚体后你会看到一堆属性。别慌我们重点关注这几个Mass质量默认1。表示物体的重量。质量越大需要越大的力才能推动它被撞击时运动状态改变也越小。对于主角通常保持1或一个较小的值如0.5即可避免感觉过于笨重。Drag阻力默认0。影响物体因力移动时的空气阻力。值为0时物体一旦动起来就几乎不会停下太空环境。增大该值物体会更快减速。对于地面移动的角色通常需要设置一个较小的Drag如1-5这样在你停止输入时角色会自然滑行一小段距离后停下手感更佳。Angular Drag角阻力默认0.05。影响物体旋转的阻力。对于角色控制器我们通常不希望它胡乱旋转。可以将其调高如5或直接冻结旋转。Use Gravity使用重力必须勾选。否则你的角色会飘在天上。Is Kinematic是否为运动学刚体千万不要勾选。勾选后刚体将不受物理力影响只能通过Transform移动这就违背了我们使用刚体的初衷。Constraints约束这里非常关键为了做出标准的、不会翻倒的角色移动我们需要冻结不必要的旋转和轴向移动。Freeze Rotation冻结旋转把X, Y, Z全部勾选。这能防止角色在碰撞或受力时东倒西歪保持直立。Freeze Position冻结位置根据你的游戏类型决定。对于2D游戏或锁定Y轴移动的3D游戏可以冻结Y轴防止角色意外浮空或下沉。但通常我们通过代码和碰撞检测来控制Y轴跳跃所以这里可以先不冻结。我的标准配置建议Mass: 1 Drag: 3 Angular Drag: 5 Use Gravity: True Is Kinematic: False Constraints: Freeze Rotation X, Y, Z3.3 创建地面与基础场景创建一个Plane或Cube缩放作为地面并为其添加一个Box Collider默认就有。确保你的Player对象站在这个地面上。运行游戏你应该看到角色受重力影响稳稳站在地面上。如果掉下去了检查两者的碰撞体是否正常且角色刚体的Use Gravity已开启。4. 核心模块实现移动、跳跃与冲刺现在进入核心的脚本部分。我们将创建一个名为PlayerMovement.cs的脚本挂载到Player对象上。4.1 基础移动读取输入与速度控制首先获取刚体引用并处理玩家输入。using UnityEngine; public class PlayerMovement : MonoBehaviour { [Header(移动参数)] public float moveSpeed 8f; // 水平移动速度 public float groundDrag 3f; // 在地面时的阻力 [Header(组件引用)] private Rigidbody rb; private float horizontalInput; private float verticalInput; void Start() { rb GetComponentRigidbody(); // 确保刚体旋转被冻结防止摔倒 rb.freezeRotation true; } void Update() { // 在Update中获取输入更及时 MyInput(); // 控制Drag实现停止输入后的滑行效果 ControlDrag(); } void FixedUpdate() { // 在FixedUpdate中执行物理操作保证与物理引擎步调一致 MovePlayer(); } private void MyInput() { horizontalInput Input.GetAxisRaw(Horizontal); // A/D 或 左右箭头 verticalInput Input.GetAxisRaw(Vertical); // W/S 或 上下箭头 } private void MovePlayer() { // 计算移动方向。注意这里基于世界坐标如果要做摄像机相对移动需要转换。 Vector3 moveDirection new Vector3(horizontalInput, 0, verticalInput).normalized; // 核心直接给刚体一个水平方向的速度 rb.velocity new Vector3(moveDirection.x * moveSpeed, rb.velocity.y, moveDirection.z * moveSpeed); } private void ControlDrag() { // 简单判断如果在地面上就应用地面阻力否则应用很小的空中阻力或0 if (IsGrounded()) { rb.drag groundDrag; } else { rb.drag 0; // 空中几乎无阻力保持跳跃惯性 } } // 一个简单的地面检测方法后续会完善 private bool IsGrounded() { // 暂时返回true下一节实现 return true; } }代码解析UpdatevsFixedUpdate输入检测放在Update里因为它每帧执行响应快。物理操作如修改velocity必须放在FixedUpdate里因为物理引擎以固定的时间步长默认0.02秒更新在这里操作能保证物理计算的稳定性和一致性。GetAxisRaw返回没有平滑滤波的原始输入-1 0 1响应更直接适合快节奏操作。rb.velocity我们直接设置了X和Z方向的速度但保留了Y轴原有的速度rb.velocity.y。这是为了不影响重力下落和跳跃的垂直速度。ControlDrag这是一个重要技巧。在地面时给予一定阻力让角色能自然停下。在空中时取消阻力确保跳跃和下落轨迹只受重力影响不会被意外减速。4.2 实现跳跃力与状态的配合跳跃不能简单给一个向上的速度那样会像火箭一样匀速上升。我们需要一个瞬间的、向上的力然后让重力把它拉下来形成抛物线。public class PlayerMovement : MonoBehaviour { [Header(跳跃参数)] public float jumpForce 10f; // 跳跃力度 public float airMultiplier 0.5f; // 空中移动效能通常比地面低 public LayerMask groundLayer; // 用于射线检测的地面层 [Header(状态)] private bool isGrounded; void Update() { MyInput(); ControlDrag(); // 跳跃检测放在Update确保不错过按键 if (Input.GetKeyDown(KeyCode.Space) isGrounded) { Jump(); } } void FixedUpdate() { // 在物理更新前检查地面 GroundCheck(); MovePlayer(); } private void GroundCheck() { // 方法1从角色底部发射一条短射线检测地面 float checkDistance 0.2f; // 检测距离略大于皮肤宽度 Vector3 rayStart transform.position Vector3.up * 0.1f; // 从脚部稍上方开始 bool hitGround Physics.Raycast(rayStart, Vector3.down, checkDistance, groundLayer); // 方法2更稳定使用球形检测覆盖脚底区域 // float radius 0.3f; // isGrounded Physics.CheckSphere(transform.position - Vector3.up * 0.1f, radius, groundLayer); isGrounded hitGround; } private void Jump() { // 关键首先将垂直速度归零消除下落惯性确保每次跳跃高度一致 rb.velocity new Vector3(rb.velocity.x, 0f, rb.velocity.z); // 使用Impulse力模式施加一个瞬间的力 rb.AddForce(transform.up * jumpForce, ForceMode.Impulse); // 跳跃后立即更新状态防止空中连跳 isGrounded false; } private void MovePlayer() { Vector3 moveDirection new Vector3(horizontalInput, 0, verticalInput).normalized; float currentSpeed moveSpeed; // 如果在空中移动速度打折扣 if (!isGrounded) { currentSpeed * airMultiplier; } // 计算目标速度 Vector3 targetVelocity moveDirection * currentSpeed; // 只覆盖水平速度保留垂直速度重力/跳跃 rb.velocity new Vector3(targetVelocity.x, rb.velocity.y, targetVelocity.z); } }关键点与避坑指南地面检测这是跳跃系统的基石。Physics.Raycast简单但可能漏检比如一只脚在悬崖外。Physics.CheckSphere或Physics.BoxCast更可靠。记得在场景中为地面物体设置正确的Layer并在groundLayer变量中指定避免检测到角色自身。跳跃前重置Y速度rb.velocity new Vector3(rb.velocity.x, 0f, rb.velocity.z);这行至关重要。如果不重置当你从空中快速下落时按下跳跃向上的力会先抵消向下的速度导致跳跃高度变矮手感不一致。力模式ForceMode.ImpulseAddForce的第二个参数。Impulse表示施加一个瞬间的冲量力×时间适合跳跃、爆炸等瞬时效果。Force则表示施加一个持续的力每物理帧适合火箭推进。空中控制通过airMultiplier如0.5限制空中的移动能力这是很多平台游戏的做法防止玩家在空中过于灵活增加操作难度和真实性。4.3 实现冲刺瞬时加速与冷却冲刺Dash通常是一个短时间、高速度的爆发移动并伴有冷却时间。public class PlayerMovement : MonoBehaviour { [Header(冲刺参数)] public float dashSpeed 20f; // 冲刺速度 public float dashDuration 0.2f; // 冲刺持续时间秒 public float dashCooldown 1f; // 冲刺冷却时间 public KeyCode dashKey KeyCode.LeftShift; // 冲刺按键 [Header(冲刺状态)] private bool isDashing false; private bool canDash true; private float dashTimeLeft; // 冲刺剩余时间 private Vector3 dashDirection; // 冲刺方向 void Update() { MyInput(); ControlDrag(); if (Input.GetKeyDown(KeyCode.Space) isGrounded) { Jump(); } // 冲刺输入检测 if (Input.GetKeyDown(dashKey) canDash) { StartDash(); } // 冲刺计时逻辑 if (isDashing) { Dash(); } } void FixedUpdate() { GroundCheck(); // 如果不是冲刺状态才执行普通移动 if (!isDashing) { MovePlayer(); } } private void StartDash() { if (!canDash) return; isDashing true; canDash false; dashTimeLeft dashDuration; // 确定冲刺方向如果有输入按输入方向如果没输入按角色面朝方向 Vector3 inputDir new Vector3(horizontalInput, 0, verticalInput); if (inputDir ! Vector3.zero) { dashDirection inputDir.normalized; } else { dashDirection transform.forward; // 默认向前冲 } // 冲刺开始时取消重力影响防止下坠 rb.useGravity false; // 立即赋予一个很高的水平速度 rb.velocity dashDirection * dashSpeed; // 将垂直速度设为0确保水平冲刺 rb.velocity new Vector3(rb.velocity.x, 0f, rb.velocity.z); } private void Dash() { if (dashTimeLeft 0) { // 在冲刺持续期间保持速度。也可以在这里加入粒子特效、屏幕后处理等 rb.velocity dashDirection * dashSpeed; dashTimeLeft - Time.deltaTime; } else { EndDash(); } } private void EndDash() { isDashing false; rb.useGravity true; // 恢复重力 // 冲刺结束后将速度重置为当前输入方向的速度避免突然停止 Vector3 moveDir new Vector3(horizontalInput, 0, verticalInput).normalized; rb.velocity moveDir * moveSpeed; // 开始冷却计时 Invoke(nameof(ResetDash), dashCooldown); } private void ResetDash() { canDash true; } }冲刺实现要点状态管理使用isDashing和canDash两个布尔值精确控制冲刺的激活与冷却防止连续无限制冲刺破坏游戏平衡。方向处理冲刺方向需要智能判断。优先使用玩家输入的方向如果玩家没有输入比如只按了冲刺键则使用角色当前面朝方向transform.forward。重力控制在冲刺期间rb.useGravity false是一个常见技巧。这能确保冲刺是纯粹的水平运动不会因为重力而下坠手感更干脆。但结束后务必记得恢复冷却与重置使用Invoke函数进行简单的冷却计时。在EndDash中不是将速度归零而是平滑过渡到当前的移动输入速度避免顿挫感。与普通移动的互斥在FixedUpdate中通过if (!isDashing)来确保冲刺时不会执行普通的MovePlayer逻辑防止速度被覆盖。5. 高级调优与问题排查基础功能都有了但要让手感变得“爽”还需要一些精细的调优和问题处理。5.1 摄像机跟随与相对移动目前我们的移动是基于世界坐标的按W就是向世界Z轴走。在3D游戏中这通常不符合直觉。我们需要让移动方向相对于摄像机。public class PlayerMovement : MonoBehaviour { public Transform orientation; // 一个代表角色面朝方向的空物体 public Camera playerCam; void Start() { // 如果没指定取主摄像机 if (playerCam null) playerCam Camera.main; // 初始化方向参考 if (orientation null) { orientation new GameObject(Orientation).transform; orientation.SetParent(transform); orientation.localPosition Vector3.zero; } UpdateOrientation(); } void Update() { // 每帧根据摄像机更新面朝方向 UpdateOrientation(); // ... 其他输入检测 } private void UpdateOrientation() { // 获取摄像机的前向和右向向量但忽略Y轴垂直分量 Vector3 viewDir playerCam.transform.forward; viewDir.y 0; viewDir.Normalize(); Vector3 rightDir playerCam.transform.right; rightDir.y 0; rightDir.Normalize(); // 让orientation物体面朝摄像机方向 orientation.forward viewDir; // 也可以让角色模型旋转这里我们只影响移动方向计算 } private void MovePlayer() { // 使用orientation的方向来计算移动 Vector3 moveDirection orientation.forward * verticalInput orientation.right * horizontalInput; moveDirection.Normalize(); // ... 后续速度计算不变 rb.velocity new Vector3(moveDirection.x * currentSpeed, rb.velocity.y, moveDirection.z * currentSpeed); } }这样无论摄像机如何旋转按W永远是向前屏幕上方移动符合第三人称或第一人称游戏的操作习惯。5.2 斜坡处理与地面检测优化默认的射线检测在斜坡上容易出问题。角色可能会被判定为“未接地”而无法跳跃或者在斜坡上移动打滑。优化方案使用球形检测并考虑法线private void GroundCheck() { float radius 0.3f; Vector3 spherePosition transform.position - Vector3.up * 0.1f; // 检测球在脚底略下方 isGrounded Physics.CheckSphere(spherePosition, radius, groundLayer); // 进阶获取地面法线判断是否是可行走的斜坡例如角度小于45度 if (Physics.SphereCast(transform.position Vector3.up * 0.5f, radius, Vector3.down, out RaycastHit hit, 1f, groundLayer)) { float slopeAngle Vector3.Angle(hit.normal, Vector3.up); if (slopeAngle maxSlopeAngle) // 比如maxSlopeAngle 45 { // 斜坡太陡视为不可行走可能触发滑落逻辑 isGrounded false; } else { isGrounded true; // 可以在斜坡上调整移动方向使其沿斜坡表面移动避免“爬坡”感 } } else { isGrounded false; } }5.3 常见问题与解决方案速查表问题现象可能原因解决方案角色移动像在冰上滑行停不下来Drag值设置过低或MovePlayer中持续给速度没有在无输入时归零水平速度。适当增加Drag如3-5。在MovePlayer中当输入为0时将水平速度渐变为0rb.velocity new Vector3(rb.velocity.x * 0.9f, rb.velocity.y, rb.velocity.z * 0.9f);跳跃高度不一致有时跳不高跳跃前没有重置Y轴速度。在下落时跳跃向上的力需要先抵消向下的动量。在Jump()函数中跳跃前执行rb.velocity new Vector3(rb.velocity.x, 0f, rb.velocity.z);角色卡在斜坡边缘或轻微离地地面检测射线/球体的位置或半径设置不当。调整检测点的位置rayStart或spherePosition和检测半径radius确保能覆盖角色底部的整个区域。使用Debug.DrawRay或Gizmos可视化检测范围。冲刺后角色浮空不下落冲刺结束时忘记恢复重力rb.useGravity true。在EndDash()函数中确保将rb.useGravity设回true。移动方向不受摄像机控制移动计算使用的是世界坐标方向没有与摄像机方向关联。实现“摄像机相对移动”参考5.1节使用摄像机的前向和右向向量计算移动方向。角色移动时抖动或抽搐1. 同时在Update和FixedUpdate中修改Transform或Rigidbody。2. 碰撞体之间有轻微重叠或穿透。1. 确保所有对Rigidbodyvelocity,AddForce的操作都在FixedUpdate中。2. 检查角色和地面的碰撞体确保没有初始穿透。可以尝试微调碰撞体位置或尺寸或为刚体设置一个很小的Sleep Threshold。快速移动时穿墙刚体的Collision Detection模式设置为Discrete离散检测在高速下可能错过碰撞帧。将高速移动物体如冲刺时的角色的刚体Collision Detection属性改为Continuous Dynamic连续动态检测。注意这会增加性能开销仅对必要对象使用。5.4 性能与手感终极调优建议固定时间步长Fixed Timestep在Edit - Project Settings - Time中Fixed Timestep默认是0.02秒50次/秒。降低这个值如0.016666即60次/秒会使物理更新更频繁手感更顺滑但CPU负担加重。对于大多数游戏0.02是平衡点。插值Interpolation如果发现刚体运动有抖动可以尝试将刚体的Interpolation属性从None改为Interpolate。这会让物体的渲染位置在两帧物理更新之间平滑过渡使运动看起来更流畅尤其适用于跟随摄像机追踪的主角。合理使用碰撞体对于复杂模型不要使用其高面数网格作为碰撞体。应使用简化的几何体如胶囊、立方体或组合碰撞体Compound Collider来近似形状能极大提升物理性能。参数不要死记硬背moveSpeed、jumpForce、drag这些参数没有“标准值”。你需要根据你的游戏风格写实、卡通、快节奏和关卡设计在Play模式下反复调试找到最合适的手感。记住好的手感是调出来的不是算出来的。这套基于刚体的移动系统虽然比直接修改Transform复杂但它为你打下了坚实的物理交互基础。从这里出发你可以轻松地扩展出二段跳、蹬墙跳、游泳、飞行等更复杂的移动能力因为它们都建立在同一套稳定、可靠的物理规则之上。