You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity所有GameObject销毁后执行CleanUp逻辑的可靠方案

Unity 最晚执行清理逻辑组件实现方案

问题背景

现有一套初始化系统,通过给组件添加[DefaultExecutionOrder(-100)]特性,可保证其Awake()逻辑早于所有其他游戏对象执行。需要实现对称的反向逻辑:在场景内所有游戏对象全部销毁完成后,触发指定清理函数。

已尝试的失效方案如下:

  • 接力传递方案:在主组件OnDestroy()中遍历根场景对象,给存活对象挂载清理组件接力触发检查,实际运行判断逻辑不准,清理时机错误
  • 异步轮询方案:将OnDestroy()设为异步方法,间隔100ms检测存活对象,退出播放模式后循环无法终止,会误检测编辑模式对象
  • OnDestroy实例化方案:在主组件OnDestroy()中实例化挂载清理组件的对象,Unity引擎禁止在销毁生命周期内执行新对象实例化操作
  • 析构函数方案:在组件C#析构函数中写清理逻辑,析构函数调用时机由GC决定,存在早于Awake()执行的异常情况
  • ISubsystem方案:尝试使用Unity引擎ISubsystem接口实现,缺乏清晰业务层使用文档,接入成本过高

约束条件:

  • 不允许使用全局工厂模式实例化管控对象
  • OnApplicationQuit()逻辑仅作为最后兜底选项,优先使用OnDestroy()链路实现
  • 要求同一个组件既能最早执行初始化,又能最晚执行清理

实现原理

利用Unity引擎原生生命周期和销毁顺序规则实现,无额外侵入性:
Unity销毁对象的固定顺序为:

  1. 先销毁所有激活普通场景内的GameObject,同节点上组件的OnDestroy()触发顺序和DefaultExecutionOrder值负相关:值越小越先触发,值越大越晚触发
  2. 所有普通场景对象完全销毁后,才会开始销毁标记了DontDestroyOnLoad的永久存活场景对象

只要提前在初始化阶段创建一个存放在永久场景、设置了最高执行顺序的隐藏锚点对象,它的OnDestroy()就会是全场景最后一批触发的生命周期函数,此时所有业务对象已经完成销毁,刚好满足清理时机要求。


具体实现步骤

  1. 给核心初始化组件设置[DefaultExecutionOrder(-32000)](Unity支持的最低执行顺序值,比之前的-100更早,保证绝对最先执行Awake)
  2. 在核心组件的Awake()最开头创建隐藏的清理锚点对象,不需要外部预制体或全局工厂,代码内直接创建即可:
    • 锚点对象添加HideFlags.HideAndDontSave标记,不会显示在层级面板、不会被保存到场景资源、不会被用户误操作
    • 给锚点挂载清理触发组件,给该组件设置[DefaultExecutionOrder(32000)](Unity支持的最高执行顺序值,保证同批对象里最晚触发OnDestroy)
    • 调用DontDestroyOnLoad()把锚点移到永久存活场景
  3. 在清理触发组件的OnDestroy()中调用核心系统的最终清理逻辑即可。

代码示例

核心系统组件:

using UnityEngine;

// 最低执行顺序,保证Awake全局最先运行
[DefaultExecutionOrder(-32000)]
public class CoreSystem : MonoBehaviour
{
    public static CoreSystem Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        
        // 初始化阶段提前创建清理锚点,禁止在OnDestroy阶段创建
        CreateCleanupAnchor();
        
        // 原有业务初始化逻辑
        InitAllSystems();
    }

    private void CreateCleanupAnchor()
    {
        GameObject anchor = new GameObject("InternalCleanupAnchor");
        // 隐藏对象,不写入场景文件,不显示在层级面板
        anchor.hideFlags = HideFlags.HideAndDontSave;
        // 挂载清理触发器
        anchor.AddComponent<FinalCleanupTrigger>();
        // 移到永久存活场景,最后才会被销毁
        DontDestroyOnLoad(anchor);
    }

    private void InitAllSystems()
    {
        // 这里写你的原有初始化逻辑
    }

    /// <summary>
    /// 所有对象销毁完成后触发的最终清理逻辑
    /// </summary>
    public void ExecuteFinalCleanup()
    {
        // 这里写你需要的清理逻辑
        Debug.Log("All scene objects destroyed, final cleanup executed.");
    }
}

清理触发器组件:

using UnityEngine;

// 最高执行顺序,保证同销毁批次内OnDestroy全局最晚触发
[DefaultExecutionOrder(32000)]
public class FinalCleanupTrigger : MonoBehaviour
{
    private void OnDestroy()
    {
        // 触发时所有普通场景对象已经完成销毁
        CoreSystem.Instance?.ExecuteFinalCleanup();
    }
}

避坑说明

  • 之前“接力棒”方案失效的核心原因:Unity销毁对象是分帧分批执行的,同一销毁帧内大量对象处于“正在销毁但判空仍返回true”的中间状态,无法准确判断是否所有对象都完成销毁
  • 不要使用异步轮询方案:Unity退出播放模式后域重载不会等待异步任务执行完成,会出现逻辑跑飞、误操作编辑模式资源的问题
  • 不要在任何OnDestroy()生命周期内执行新对象实例化操作,Unity引擎会直接拦截该操作
  • C#析构函数由GC调度,和Unity引擎生命周期完全脱钩,绝对不能用来承载引擎对象相关的清理逻辑

内容的提问来源于stack exchange,提问作者IC_

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 00:54:32