Unity 域重载会清空一切:Editor 工具如何让状态在重载后续命

Unity 编辑器在脚本编译完成后会触发"域重载"(Domain Reload):整个托管脚本域重启、所有静态字段被清零、运行中的 EditorCoroutine 被中止、HTTP 监听 socket 被回收。对运行时游戏代码这没影响------Play Mode 之外的脚本对象本来就不持续------但对 Editor 工具是一场每隔几分钟就发生的"地震"。

本文记录 Unity 域重载会清掉什么,以及一个长期运行的 Editor 工具(以 Funplay Unity MCP 为例)如何用三层应对让状态在重载后无缝续命。

1. 域重载触发时机

四种典型触发:

  • C# 脚本被修改 + 保存------最常见,几乎每次开发都会遇到
  • AssetDatabase.Refresh() 检测到代码变更------MCP 工具或 Editor 脚本主动调用时
  • 菜单 Assets → Reimport All------清理项目时
  • 进入 Play Mode 时(如果项目设置 Domain Reload 选项打开)

无论哪种触发,效果一致:托管侧(CLR)的所有状态被清空,原生侧(Unity Editor 进程本体)保留。

2. 域重载会清掉什么

把重载当成一次"进程内重启"是合理近似:
域重载前
重载发生
域重载后
静态字段
运行中 task / coroutine
HTTP server / socket
反射 / 类型缓存
InitializeOnLoad 已执行
静态字段 = 默认值
task 中止 / 抛 ThreadAbort
socket 已关闭
首次访问触发重新构建
InitializeOnLoad 重新执行

对长期运行的 Editor 工具,最痛的几条:

  • HTTP 服务器(如 MCP server)的监听 socket 被关闭,端口可能短时间被占用
  • 正在执行的工具调用 ------例如一个 execute_code 跑到一半,后半段消失
  • 配置缓存 / 类型扫描结果------TypeCache 之类的查询要重新做
  • 客户端连接状态------AI 客户端发出的请求若卡在重载窗口,会超时或拿到 socket reset

3. 三类典型应对

业界对域重载的处理方式归为三类:

策略 适用 代价
重载后重建(默认) 无状态服务,重启代价低 启动时间长则影响响应性
序列化保留 有需要跨重载传递的关键状态 状态结构必须可序列化
取消并重启 可接受丢弃中断中的工作 客户端需要主动重试

Funplay Unity MCP 三类都用------HTTP server 是策略 1,正在执行的工具调用状态是策略 2,无法续传的工具走策略 3。

4. 静态状态:[InitializeOnLoad] 与 DI 容器

[InitializeOnLoad] 标记的类型在每次域重载完成后被自动加载,是静态状态重建的标准入口。Funplay Unity MCP 的 RootScopeServices 走这条路:

csharp 复制代码
[InitializeOnLoad]
internal static class RootScopeServices
{
    private static IContainer _container;

    static RootScopeServices()
    {
        _container = BuildContainer();

        AssemblyReloadEvents.beforeAssemblyReload += OnBeforeReload;
        EditorApplication.quitting += OnQuit;

        // 域重载后自动恢复中断的工具调用
        EditorApplication.delayCall += () => CheckForInterruptedExecution();
    }

    private static IContainer BuildContainer()
    {
        var c = new Container();
        c.RegisterSingleton<IEditorThreadHelper, EditorThreadHelper>();
        c.RegisterSingleton<MCPServerService>();
        c.RegisterSingleton<FunplayMcpSettings>(LoadSettings);
        return c;
    }

    private static void OnBeforeReload()
    {
        _container?.Dispose();
        _container = null;
    }
}

每次重载后构造函数重跑,DI 容器从头构建。beforeAssemblyReload 事件是关键------重载发生前主动 Dispose 容器,确保 HTTP 监听 socket 在静态字段被清空之前已经显式关闭,避免端口泄漏。

5. 中断态:SessionState 与 DomainReloadHandler

SessionState 是 Unity 提供的 Editor 会话级 KV 存储,重载后保留、Editor 关闭后清空。Funplay 用它持久化"重载发生时正在执行的工具调用"信息:

csharp 复制代码
internal static class DomainReloadHandler
{
    private const string KEY_INTERRUPTED = "Funplay.MCP.Interrupted";

    public static void RecordInterruption(string toolName, string requestId, string reason)
    {
        var record = new InterruptionRecord
        {
            ToolName = toolName,
            RequestId = requestId,
            Reason = reason,
            TimestampUtc = DateTime.UtcNow.ToString("o")
        };
        SessionState.SetString(KEY_INTERRUPTED, JsonUtility.ToJson(record));
    }

    public static InterruptionRecord ConsumePending()
    {
        var raw = SessionState.GetString(KEY_INTERRUPTED, null);
        if (string.IsNullOrEmpty(raw)) return null;
        SessionState.EraseString(KEY_INTERRUPTED);
        return JsonUtility.FromJson<InterruptionRecord>(raw);
    }
}

beforeAssemblyReload 事件触发时,正在跑的工具调用主动调用 RecordInterruption 把状态扔进 SessionState;重载完成后 RootScopeServices 静态构造函数里调用 ConsumePending,把这条记录变成 MCP 客户端可见的"上次调用被中断"摘要。

客户端可通过 get_reload_recovery_status 工具查询:

json 复制代码
{
  "success": true,
  "data": {
    "wasInterrupted": true,
    "toolName": "execute_code",
    "reason": "AssemblyReload triggered during execution",
    "timestamp": "2026-05-07T08:42:13Z"
  }
}

知道发生了什么之后,客户端可以决定是直接重试、还是先确认副作用。这种透明性比"工具调用静默丢失"或"工具调用同步等待重载"都更合理。

6. 服务重启:EditorApplication.delayCall 模式

域重载完成后立即重启 HTTP server 是个陷阱------AssemblyReloadEvents.afterAssemblyReload 事件在主线程触发,但此时 Unity 内部仍在收尾,立即调用某些 Editor API 会失败。

正确的方式是把重启推迟到 EditorApplication.delayCall

csharp 复制代码
private static void OnSettingsChanged(FunplayMcpSettings _)
{
    // 重启服务必须走主线程的 delayCall,不能直接在事件回调里执行
    EditorApplication.delayCall += () =>
    {
        var server = _container.Resolve<MCPServerService>();
        server.Restart();
    };
}

delayCall 在 Unity 的下一次 Editor 主循环空闲时触发,此时所有 Editor 内部状态已就绪。同样的模式也用于"端口配置变更后重启"------任何会触发 socket 重新绑定的操作都要走这条通道。

特别提醒:绝不在后台线程触发服务重启delayCall 不仅是延迟,更是把执行强制放回主线程。Funplay Unity MCP 在 MCPServerService 里专门有一段注释说明这条规则------历史上有过一次 bug 是设置变更监听跑在 ThreadPool 线程上直接调 socket bind,导致 Editor 偶发崩溃。

7. 反射 / 类型扫描的重建

域重载后所有反射缓存失效。Funplay 的 ToolRegistry 在每次重载后通过 [InitializeOnLoad] 触发的 ScanAssemblies 重新扫描所有 [ToolProvider] 标注的类型------并使用 TypeCache.GetTypesDerivedFrom<>() 加速:

csharp 复制代码
[InitializeOnLoad]
internal static class ToolRegistry
{
    private static Dictionary<string, ToolDefinition> _tools;

    static ToolRegistry()
    {
        _tools = ScanAssemblies();
    }

    public static void InvalidateCache()
    {
        // 提供给外部插件主动失效缓存的入口
        _tools = ScanAssemblies();
    }

    private static Dictionary<string, ToolDefinition> ScanAssemblies()
    {
        var result = new Dictionary<string, ToolDefinition>();
        foreach (var t in TypeCache.GetTypesWithAttribute<ToolProviderAttribute>())
        {
            foreach (var m in t.GetMethods(BindingFlags.Public | BindingFlags.Static))
            {
                var name = ToSnakeCase(m.Name);
                if (BlockedTools.Contains(name)) continue;
                result[name] = BuildDefinition(m);
            }
        }
        return result;
    }
}

TypeCache 是 Unity 提供的 O(1) 类型查询缓存,专门为这种"每次重载后重建类型集合"的场景设计。相比传统的 AppDomain.CurrentDomain.GetAssemblies() 遍历,TypeCache 命中速度是数量级的差异。

8. 客户端视角的"无缝续命"

把上述三层组合起来,AI 客户端发出 MCP 请求时遇到域重载的体感是:
Unity Editor MCP Server AI 客户端 Unity Editor MCP Server AI 客户端 socket 中断;客户端收到错误 tools/call execute_code 调度到主线程 触发域重载(脚本编译) tools/call get_reload_recovery_status { wasInterrupted: true, toolName: execute_code, reason } tools/call execute_code(重试) 正常返回

整个过程对客户端是显式的------它能从 get_reload_recovery_status 拿到结构化的中断摘要,决定下一步是重试、回滚、还是询问用户。比"请求静默失败"或"客户端无限等待"都更可控。

9. 设计原则总结

让 Editor 工具在域重载下续命的几条共通设计原则:

原则 实现
容器层重建,不持久化 [InitializeOnLoad] 静态构造 + beforeAssemblyReload Dispose
业务态显式持久化 SessionState 序列化关键中断态
服务重启走主线程 EditorApplication.delayCall 而非后台线程
类型扫描走 TypeCache 避免每次重载手动遍历所有 Assembly
中断对客户端透明 提供专门的 recovery 查询工具

这些原则不止 MCP server 适用------任何长期运行、需要响应外部请求的 Editor 工具(场景同步插件、远程调试桥、自动化测试 runner)都会面临同样的重载挑战。

10. 写在最后

Unity 域重载在底层逻辑上无法避免------Mono / IL2CPP 的运行时模型决定了脚本变更后必须重新加载托管程序集。Editor 工具能做的不是阻止它,而是让它从"用户感知到的崩溃"变成"用户感知不到的边界事件"。

Funplay Unity MCP 的完整实现可在 FunplayAI/funplay-unity-mcp 查看。Editor/State/DomainReloadHandler.csEditor/DI/RootScopeServices.cs 是本文示例的源头。MIT 协议,欢迎 issue 与讨论。

相关推荐
深度森林3 小时前
无人机“路径规划”高价值专利案例:基于抗干扰粒子群优化的无人机路径规划方法
游戏引擎·cocos2d
小贺儿开发3 小时前
Unity3D 串口通信上位机联调系统
unity·串口·协议·数据·通信·传输·互动
tedcloud1235 小时前
ppt-master部署教程:快速搭建智能演示文稿系统
服务器·人工智能·系统架构·游戏引擎·powerpoint
垂葛酒肝汤1 天前
Unity的UI扫光效果Shader
ui·unity·游戏引擎
mxwin1 天前
Unity Shader Alpha测试 · 模板测试 · 深度测试
unity·游戏引擎
2601_956002811 天前
冬日狂想曲(赠去马赛克补丁)2026.5.13最新版免费下载 转存后自动更新 (看到请立即转存 资源随时失效)pc手机版通用
智能手机·游戏引擎·电脑·游戏程序·动画·游戏美术
Sator11 天前
unity解决粒子与物体接触时的硬边缘问题
unity·游戏引擎
RPGMZ1 天前
RPGMZ NPC头顶自动显示一段消息
前端·游戏引擎·rpgmz·rpgmakermz
程序员JerrySUN2 天前
Jetson边缘嵌入式实战课程第三讲:L4T 与 Jetson 系统架构
linux·服务器·人工智能·安全·unity·系统架构·游戏引擎