C# Unity 脚本编程:MonoBehaviour 与游戏循环

FreeGuideOnline 最新 2026-06-17

C# Unity 脚本编程:MonoBehaviour 与游戏循环

Unity 中的脚本本质上都是组件,而驱动这些组件运转的核心就是 MonoBehaviour 类。理解 MonoBehaviour 的生命周期与 Unity 的游戏循环,是写出稳定、高效 Unity 程序的第一步。本教程将从零为你拆解这些概念,提供大量可直接上手的代码示例。


什么是 MonoBehaviour?

MonoBehaviour 是 Unity 引擎提供的基类,所有挂载到游戏对象上的 C# 脚本都必须继承自它。它不仅仅是一个普通的类,更是一张“与引擎对话的入场券”——当你继承 MonoBehaviour 时,你的脚本就自动获得了:

  • 被 Unity 游戏循环自动调用的能力(UpdateStart 等)。
  • 直接访问该对象身上的 TransformGameObject 等组件的快捷属性。
  • 响应物理碰撞、触发器等事件的方法。
  • 在编辑器中使用 [SerializeField] 等特性暴露变量到 Inspector 面板的能力。

一个最简脚本示例:

using UnityEngine;

public class MyFirstScript : MonoBehaviour
{
    // 脚本启用时调用一次
    void Start()
    {
        Debug.Log("Hello, Unity!");
    }

    // 每一帧调用一次
    void Update()
    {
        // 每帧的逻辑,比如检测输入
    }
}

MonoBehaviour 生命周期:游戏循环的基石

Unity 的游戏循环并不是一个单独的 while(true) 循环,而是由引擎内部维护的一个事件驱动框架。MonoBehaviour 的各个消息方法会按照固定顺序被引擎调用,这就是所谓的“生命周期”。掌握它们的执行时机和频率,是编写正确逻辑的关键。

下图展示了最常用的生命周期方法调用顺序(从上到下):

Awake() → OnEnable() → Start() → 物理更新循环(FixedUpdate) 
→ 输入事件(OnMouseDown等) → 游戏逻辑循环(Update) 
→ 渲染前回调(LateUpdate) → 渲染帧 → OnDisable() → OnDestroy()

下面逐个讲解这些核心方法。

Awake()

  • 调用时机:脚本实例被加载时立即调用,在 Start 之前,且无论脚本是否激活(enabled)都会被调用。
  • 调用次数:一次。
  • 典型用途:组件间引用初始化、设置单例模式、初始化不依赖于其他脚本顺序的数据。
  • 注意:所有对象的 Awake 都在 Start 之前执行完毕,但同一场景中不同对象之间的 Awake 调用顺序是不确定的。如需保证顺序,应在 Awake 中获取引用,在 Start 中使用它们。
private Rigidbody rb;

private void Awake()
{
    // 获取自身组件,比在 Start 中更早
    rb = GetComponent<Rigidbody>();
}

OnEnable()

  • 调用时机:对象变为激活且脚本启用时(例如 SetActive(true) 或脚本组件被勾选)。
  • 典型用途:订阅事件、重置状态。因为对象池回收再利用时 Awake 不再执行,OnEnable 就承担了“重新初始化”的角色。
private void OnEnable()
{
    // 例如重新订阅一个全局事件
    GameManager.OnGamePaused += HandlePause;
}

Start()

  • 调用时机:在 Awake 之后,第一次 Update 之前,且仅在脚本启用时调用。
  • 典型用途:需要依赖其他对象已完成 Awake 的初始化操作,比如从另一个脚本获取数据。
  • 注意:如果脚本一开始就是启用的,Start 会在第一帧开始前执行;如果是中途激活,则在当前帧的 Update 之前执行。
private PlayerHealth playerHealth;

private void Start()
{
    // 此时其他对象的 Awake 已全部完成,可以安全获取外部引用
    playerHealth = FindObjectOfType<PlayerHealth>();
}

FixedUpdate()

  • 调用时机:以固定的时间步长调用,默认 0.02 秒(50次/秒),可以在 Project Settings → Time → Fixed Timestep 中调整。
  • 典型用途:所有涉及物理的计算,如施加力、速度调整、Rigidbody 的运动。因为它与物理引擎的更新频率同步,能保证物理模拟的稳定性。
  • 重要原则:不要在 FixedUpdate 里使用 Input 获取普通按键(会漏帧),应使用 Input.GetAxis 等连续输入,或使用 Update 获取输入后在 FixedUpdate 中应用。
public float moveForce = 10f;
private Rigidbody rb;

private void FixedUpdate()
{
    float horizontal = Input.GetAxis("Horizontal");
    float vertical = Input.GetAxis("Vertical");
    Vector3 force = new Vector3(horizontal, 0, vertical) * moveForce;
    rb.AddForce(force);
}

Update()

  • 调用时机:每一帧调用一次。帧率不固定,取决于硬件性能和渲染负载。
  • 典型用途:绝大部分的游戏逻辑,如移动、计时、输入处理、动画状态检测。所有与帧相关的操作都应放在此方法。
  • 重要:使用 Time.deltaTime 让运动与帧率无关。
public float speed = 5f;

private void Update()
{
    // 帧率无关的移动
    float step = speed * Time.deltaTime;
    transform.Translate(Vector3.forward * step);
}

LateUpdate()

  • 调用时机:在所有 Update 执行完毕后调用,同样每帧一次。
  • 典型用途:摄像机跟随、动画后处理、需要依赖其他对象已在 Update 中完成移动的操作。
  • 示例:第三人称摄像机在 LateUpdate 中定位到角色位置,确保角色已经在 Update 中移动完毕。
public Transform target;
public Vector3 offset;

private void LateUpdate()
{
    // 目标已经在 Update 里更新了位置,此时跟随最平滑
    transform.position = target.position + offset;
}

OnDisable()

  • 调用时机:对象禁用或脚本组件取消勾选时。
  • 典型用途:清理事件订阅、取消注册、停止协程。与 OnEnable 成对使用以避免内存泄漏。
private void OnDisable()
{
    GameManager.OnGamePaused -= HandlePause;
}

OnDestroy()

  • 调用时机:对象被销毁时(比如场景切换、调用 Destroy())。
  • 典型用途:最终清理,如释放非托管资源、移除静态引用等。

游戏循环的执行顺序深度解析

理解执行顺序不仅能帮你调试疑难 Bug,更能让你写出高效、可预测的代码。

  1. 同一帧内的顺序

    • 所有 Active 对象的 Awake → 所有 OnEnable → 所有 Start
    • 物理循环FixedUpdate 可能在一帧内执行 0 次、1 次或多次(当物理步长小于渲染帧时间时会多次执行)。
    • 输入事件:如 OnMouseDown 等会在 Update 之前由引擎调用。
    • UpdateLateUpdate
    • 渲染完毕后可能还有 OnWillRenderObject 等回调。
  2. 不同对象之间的顺序

    • 同一场景内,不同脚本的 Awake / Start 执行顺序是非确定性的(除非使用 Script Execution Order 在项目设置中手动指定顺序)。
    • 一个对象上多个脚本的先后顺序也可以手动调整(Inspector 右上角齿轮 → Execution Order)。
  3. 协程与生命周期

    • 协程的 yield return null 会在下一帧的 Update 之后执行。
    • yield return new WaitForFixedUpdate() 则会在下一个 FixedUpdate 后继续。

最佳实践

  • 获取组件(GetComponent)放在 Awake 中。
  • 跨脚本引用初始化放在 Start 中。
  • OnDisable 中取消所有注册,永远与 OnEnable 配对。

实战示例:控制角色移动的三件套脚本

下面用一个简单的角色移动示例,展示 UpdateFixedUpdateLateUpdate 的配合。

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float speed = 8f;
    public float jumpForce = 12f;
    private Rigidbody rb;
    private bool isGrounded;
    private Vector3 inputDirection;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        // 收集输入(每帧敏感)
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        inputDirection = new Vector3(h, 0, v).normalized;

        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            // 跳跃指令传递给 FixedUpdate
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
            isGrounded = false;
        }
    }

    private void FixedUpdate()
    {
        // 应用移动(物理相关)
        Vector3 velocity = inputDirection * speed;
        velocity.y = rb.velocity.y; // 保持原有的 y 轴速度(重力影响)
        rb.velocity = velocity;
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
            isGrounded = true;
    }
}

// 简单的第三人称摄像机跟随
public class ThirdPersonCamera : MonoBehaviour
{
    public Transform target;
    public Vector3 offset = new Vector3(0, 5, -10);
    public float smoothSpeed = 0.125f;

    private void LateUpdate()
    {
        if (target == null) return;
        Vector3 desiredPosition = target.position + offset;
        Vector3 smoothedPosition = Vector3.Lerp(transform.position, desiredPosition, smoothSpeed);
        transform.position = smoothedPosition;
        transform.LookAt(target.position + Vector3.up * 1.5f);
    }
}

性能优化与常见陷阱

优化建议:

  • Update 方法是一个微小的开销:Unity 即使 Update 为空,也会因为 C++ 到 C# 的跨域调用产生开销。如果对象数量巨大,可考虑禁用组件或在不需要时取消 Update(例如使用 enabled = false)。
  • FixedUpdate 处理物理,用 Update 处理视觉:物理移动直接修改 Transform.position 会导致物理模拟出错,且性能低下。
  • 将复杂计算移出 Update:例如在 StartAwake 中缓存组件引用,避免每帧使用 GetComponent
  • 使用对象池:频繁的 InstantiateDestroy 会触发 Awake/Start/OnDestroy,产生内存碎片和 GC。用 OnEnable/OnDisable 代替重建。

常见陷阱:

  1. Awake 中使用 FindObjectOfType 找单例:如果单例尚未创建会返回 null,最好在 Awake 中注册自身,在 Start 中获取引用。
  2. 忘记取消订阅事件:导致已销毁对象仍然收到回调,引发 NullReferenceException
  3. Update 中应用物理力:会使物理模拟失稳。请放入 FixedUpdate
  4. 混淆 StartOnEnableStart 只会在第一次激活时调用一次;若对象反复激活/禁用,应使用 OnEnable 重置状态。

调试小贴士

  • 利用 Debug.Log($"{Time.frameCount}: {gameObject.name} Start") 打印调用顺序,快速定位初始化问题。
  • MonoBehaviour 的各个生命周期方法中打印,可以直观看到整个循环的运行情况。
  • 使用 Unity 的 Frame DebuggerProfiler 窗口分析 Update/LateUpdate 等方法的性能消耗。

总结

MonoBehaviour 的生命周期是 Unity 游戏脚本的骨架,游戏循环的每一帧都在这些方法的交替调律中有序前进。掌握它们,你就不再是“照着教程写代码”,而是能真正理解引擎在何时做什么,从而设计出正确、高效、易维护的游戏逻辑。下一步,你可以尝试用自定义的 Update 管理器代替分散的 Update,或者探索 ScriptableObject 架构来解耦——但这都是建立在扎实理解本节内容的基础上。恭喜你,已经跨过了 Unity 脚本编程最核心的门槛。