Unity Term Book
脚本与生命周期

Update & Lifecycle

在 Unity 主游戏循环中,Update 每帧运行,FixedUpdate 按固定 Physics 时间步运行,LateUpdate 则在所有 Update 之后执行。为角色移动、相机跟随与物理交互选对钩子,能避免画面抖动与逻辑错序;这是每个 MonoBehaviour 作者必须掌握的帧时序基础。

想象一下...

Update 是跟着 FPS 走的模拟时钟——适合眼睛看到的一切:移动、输入、AI。FixedUpdate 是节拍稳定(0.02s)的数字时钟——用于物理,避免运动抖动。LateUpdate 最后运行——Camera 等所有东西都移动完再跟随,避免一帧延迟。

概念详解

Update() 每帧运行——频率跟随 FPS (30、60、120…)。移动/速度务必乘以 Time.deltaTime,才能在不同帧率下保持一致。

FixedUpdate() 按 Time.fixedDeltaTime 运行(默认 0.02s = 每秒 50 次)。与 FPS 无关。用于 Rigidbody.AddForce()、Rigidbody.velocity、Physics 查询。一帧渲染可能对应 0、1 或多次 FixedUpdate。

LateUpdate() 在该帧所有脚本的 Update() 之后运行。 适合 Camera 跟随(玩家已移动)、IK,以及任何必须“读取” Update 之后结果的逻辑。

OnGUI() 已过时。 Update 与 Coroutine:一次性或延迟逻辑 → 优先用 Coroutine,而不是在 Update 里每帧轮询。

示意图:一帧内的顺序

⏰ Physics Timestep Accumulation

FixedUpdate() × N

N = 上一帧累积的 timestep — 通常 0 或 1,卡顿时可能更多

↓
📥 Input Processing
↓

Update() — 所有脚本

Input、AI、游戏逻辑、动画触发

↓

LateUpdate() — 所有脚本

Camera follow、IK、Update 后逻辑

↓
🎨 Rendering (Culling → Drawing → PostFX)

动手步骤

1

在 Update 中始终乘以 Time.deltaTime

transform.position += speed * Time.deltaTime * direction —— 单位/秒,与 FPS 无关。

2

物理力 → FixedUpdate

把 AddForce() 和 velocity 放在 FixedUpdate。在 Update 读 Input,在 FixedUpdate 施加。

3

Camera 跟随 → LateUpdate

Camera 脚本用 LateUpdate,确保玩家已经移动——没有一帧延迟。

4

删除未使用的 Update

即使空的 Update 也有开销。不需要每帧时,优先用 Events 或 Coroutines。

交互模拟器

按 Run 逐步查看帧循环。可调整 FPS 与 Physics 频率。

FixedUpdate

0

calls

Update

0

calls

LateUpdate

0

calls

Render

0

frames

Current step: Idle
Frame: 0|Time: 0.00s|Phase: Stopped

代码示例

基础

把逻辑正确拆分到 Update、FixedUpdate 和 LateUpdate。

public class PlayerMovement : MonoBehaviour
{
  [SerializeField] private float speed      = 5f;
  [SerializeField] private float jumpForce  = 8f;
  private Rigidbody _rb;
  private Vector3   _moveDir;
  private bool      _jumpRequest;

  void Awake() => _rb = GetComponent<Rigidbody>();

  void Update()
  {
      // Input — every frame, store for FixedUpdate
      float h = Input.GetAxisRaw("Horizontal");
      float v = Input.GetAxisRaw("Vertical");
      _moveDir = new Vector3(h, 0, v).normalized;

      if (Input.GetButtonDown("Jump")) _jumpRequest = true;
  }

  void FixedUpdate()
  {
      // Apply physics — do not use Time.deltaTime here
      _rb.velocity = new Vector3(_moveDir.x * speed, _rb.velocity.y, _moveDir.z * speed);

      if (_jumpRequest)
      {
          _rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
          _jumpRequest = false;
      }
  }
}

public class CameraFollow : MonoBehaviour
{
  [SerializeField] private Transform target;
  [SerializeField] private Vector3   offset = new Vector3(0, 5, -8);

  void LateUpdate()
  {
      // After target.Update() — target pose is final
      transform.position = Vector3.Lerp(transform.position,
          target.position + offset, Time.deltaTime * 5f);
  }
}

代码示例

进阶

对不必每帧运行的逻辑,优先用 Coroutine 而非 Update。

public class EnemyAI : MonoBehaviour
{
  private Transform _player;
  private float _attackCooldown = 2f;

  void Start()
  {
      _player = GameObject.FindWithTag("Player").transform;
      // AI tick every 0.2s instead of every frame → saves CPU
      StartCoroutine(AIUpdateLoop());
      StartCoroutine(AttackLoop());
  }

  // Run AI logic 5×/sec instead of 60×/sec
  private IEnumerator AIUpdateLoop()
  {
      var wait = new WaitForSeconds(0.2f);
      while (true)
      {
          UpdatePathfinding();
          yield return wait;
      }
  }

  private IEnumerator AttackLoop()
  {
      while (true)
      {
          yield return new WaitForSeconds(_attackCooldown);
          if (Vector3.Distance(transform.position, _player.position) < 3f)
              Attack();
      }
  }
}

📌 快速记忆

  • ▸Update 中所有移动都要 * Time.deltaTime
  • ▸物理 force/velocity → FixedUpdate(通常不需要 deltaTime)
  • ▸Camera 跟随 → LateUpdate(等玩家移动完)
  • ▸稀疏逻辑 → Coroutine + WaitForSeconds

⚠️ 常见错误

  • ❌ 在 FixedUpdate 里移动 transform 却不用 Time.fixedDeltaTime

    速度依赖物理频率——不一致

    ✅ 乘以 Time.fixedDeltaTime,或使用 MovePosition()

  • ❌ 在 FixedUpdate 里读 Input

    帧快时可能漏掉 GetKeyDown

    ✅ 在 Update 读 Input,存标志,在 FixedUpdate 施加