Unity中使用FixedUpdate导致角色跳跃卡顿的原因与解决方法
Unity第三人称角色移动帧率相关问题原理解析与规范实现
核心问题根因
- 全逻辑放在Update中低帧率下跳跃高度缩水的原因
重力效果是连续加速度过程,代码里逐帧累加重力、执行位移的写法本质是用一阶欧拉积分近似模拟连续物理效果。这种积分方式的误差和单帧时间间隔正相关:帧率越低,Time.deltaTime数值越大,重力累加的误差越高,计算出的下落速度会比真实物理值偏大,角色还没到达预期跳跃高度就被更快拉向地面,最终表现为低帧跳不高。 - 垂直逻辑迁移到FixedUpdate后空中卡顿的原因
卡顿的核心诱因就是Update和FixedUpdate的执行节奏不同步。CharacterController不是刚体驱动组件,Move方法的执行效果完全和调用时机绑定,和物理帧没有强制同步关系:水平移动逻辑放在Update中时,每渲染帧都会更新水平位移,和屏幕刷新节奏完全匹配;但垂直位移放在固定时间步执行的FixedUpdate中时,单个渲染帧周期内可能执行0次、1次甚至多次FixedUpdate,垂直位移的更新节奏和水平位移、渲染节奏错位,地面状态下垂直速度接近0,错位感知极弱,但空中阶段垂直位移占比高,视觉上就会出现明显的卡顿顿挫。 - 两次半帧重力计算方案的生效原理
这个写法本质是将原本的一阶欧拉积分替换为二阶精度的半隐式辛欧拉积分:把单帧内一次性累加整帧重力的操作,拆成位移执行前累加半帧重力、用更新后的速度完成整帧位移、位移执行后再累加剩下半帧重力。这种写法把速度的采样点从帧起始位置挪到了帧中点,大幅降低了大时间步下的积分误差,无论Time.deltaTime取值多大,最终计算出的垂直位移都能和真实重力加速度下的运动结果高度吻合,因此不需要依赖FixedUpdate就能实现帧率无关的稳定跳跃高度。
规范实现方案
使用CharacterController开发角色移动时,不需要把任何移动逻辑拆分到FixedUpdate中。CharacterController不受Unity物理帧循环的同步约束,所有输入采集、位移计算、Move调用统一放在Update生命周期中执行,配合半帧重力积分法就能保证全帧率下的移动、跳跃一致性。
参考实现代码:
using UnityEngine; public class ThirdPersonMovement : MonoBehaviour { [Header("基础移动参数")] public float moveSpeed = 5.5f; public float jumpHeight = 1.8f; public float gravity = -20f; // 重力值可根据手感微调,取值比-9.81*2略高可抵消少量积分误差 public float groundCheckRadius = 0.25f; public Transform groundCheckPos; public LayerMask groundLayer; private CharacterController _controller; private Vector3 _velocity; private bool _isGrounded; private void Awake() { _controller = GetComponent<CharacterController>(); } private void Update() { // 地面检测 _isGrounded = Physics.CheckSphere(groundCheckPos.position, groundCheckRadius, groundLayer); // 落地时重置垂直速度,避免重力无限累加 if (_isGrounded && _velocity.y < 0) { _velocity.y = -2f; // 保留微小负速度保证贴地,避免下坡、走台阶时悬空 } // 采集输入 计算水平方向位移 float hInput = Input.GetAxisRaw("Horizontal"); float vInput = Input.GetAxisRaw("Vertical"); Vector3 moveDir = new Vector3(hInput, 0f, vInput).normalized; // 可在此处追加相机朝向对齐逻辑,实现相机相对方向移动 if (moveDir.magnitude >= 0.1f) { _velocity.x = moveDir.x * moveSpeed; _velocity.z = moveDir.z * moveSpeed; } else { _velocity.x = Mathf.Lerp(_velocity.x, 0f, Time.deltaTime * 10f); _velocity.z = Mathf.Lerp(_velocity.z, 0f, Time.deltaTime * 10f); } // 跳跃触发 if (Input.GetButtonDown("Jump") && _isGrounded) { // 用物理公式推导跳跃初速度,调整重力、跳跃高度参数时无需反复调试初速度 _velocity.y = Mathf.Sqrt(-2f * gravity * jumpHeight); } // 位移前累加半帧重力 _velocity.y += gravity * 0.5f * Time.deltaTime; // 执行整帧位移 _controller.Move(_velocity * Time.deltaTime); // 位移后累加剩余半帧重力 _velocity.y += gravity * 0.5f * Time.deltaTime; } }
实现注意事项:
- 不要直接依赖CharacterController自带的
isGrounded属性,该属性存在一帧左右的检测延迟,容易引发连跳失败、落地滑步、斜坡浮空等问题,自行用CheckSphere/CheckCapsule做地面检测稳定性更高- 水平速度归零用Lerp做平滑过渡,比直接设0的手感更自然
- 如果需要追加冲刺、攀爬等位移逻辑,直接在Update中追加对应速度计算即可,不需要拆分到其他生命周期
内容的提问来源于stack exchange,提问作者John Andrew
相关产品推荐
相关产品推荐

