{"id":20588047,"url":"https://github.com/stalomeow/quickplaymode","last_synced_at":"2025-04-14T21:44:27.524Z","repository":{"id":203508399,"uuid":"709360949","full_name":"stalomeow/QuickPlayMode","owner":"stalomeow","description":"When entering play mode, automatically resetting static members of dirty types, instead of reloading the domain.","archived":false,"fork":false,"pushed_at":"2024-02-01T08:49:01.000Z","size":184,"stargazers_count":29,"open_issues_count":0,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-28T09:51:26.860Z","etag":null,"topics":["domain-reload","domain-reloading","reload","unity","unity3d"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stalomeow.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-10-24T15:02:50.000Z","updated_at":"2025-03-21T12:46:21.000Z","dependencies_parsed_at":null,"dependency_job_id":"2ed82cbf-85f2-48b3-88eb-9a06accdba8f","html_url":"https://github.com/stalomeow/QuickPlayMode","commit_stats":null,"previous_names":["stalomeow/easytypereload"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalomeow%2FQuickPlayMode","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalomeow%2FQuickPlayMode/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalomeow%2FQuickPlayMode/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalomeow%2FQuickPlayMode/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stalomeow","download_url":"https://codeload.github.com/stalomeow/QuickPlayMode/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248966548,"owners_count":21190788,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["domain-reload","domain-reloading","reload","unity","unity3d"],"created_at":"2024-11-16T07:20:35.529Z","updated_at":"2025-04-14T21:44:27.506Z","avatar_url":"https://github.com/stalomeow.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Quick Play Mode\n\n[\u003e\u003e English Version \u003c\u003c](/README_EN.md)\n\n进入播放模式时，自动按需重置类型的静态成员，不使用 [Domain Reloading](https://docs.unity3d.com/Manual/DomainReloading.html)。优势：\n\n- 速度快，额外开销低。\n- 没有复杂配置，只需要加几个 `Attribute`，其他全自动。\n- 不影响发布构建时的代码。\n\n\u003e 之前叫 EasyTypeReload。\n\n## 要求\n\n- Unity \u003e= 2021.3.\n- Mono Cecil \u003e= 1.10.1.\n\n## 从 git URL 安装\n\n![install-git-url-1](/Screenshots~/install-git-url-1.png)\n\n![install-git-url-2](/Screenshots~/install-git-url-2.png)\n\n## 编辑器扩展\n\n![menu-item](/Screenshots~/menu_item.png)\n\n可以手动重置类型的静态成员，或者手动 Reload Domain。\n\n**记得点 `Enter Play Mode Options/Reload Domain` 关闭 Domain Reloading！**\n\n## 示例\n\n``` csharp\n// 使用命名空间\nusing QuickPlayMode;\n// using ...\n\n// 标记类型\n[ReloadOnEnterPlayMode]\npublic static class ExampleGeneric\u003cT\u003e // 支持泛型\n{\n    // 会被自动重置为 default(T)\n    public static T Value;\n\n    // 会被自动重置为 null\n    public static event Action\u003cT\u003e Event;\n\n    // 会被自动重置为 new List\u003cT\u003e(114)\n    public static List\u003cT\u003e Property { get; set; } = new List\u003cT\u003e(114);\n\n    // 在类型被重置前，会调用这个方法\n    // OrderInType 默认为 0，数字越小执行越早\n    // * 一个类型中的多个回调会被排序，但类型与类型间不会排序\n    [RunBeforeReload(OrderInType = 100)]\n    static void UnloadSecond()\n    {\n        Debug.Log(\"514\");\n    }\n\n    // 在类型被重置前，也会调用这个方法\n    // UnloadFirst() 在 UnloadSecond() 前被调用\n    [RunBeforeReload]\n    static void UnloadFirst()\n    {\n        Debug.Log(\"114\");\n    }\n\n    // 标记类型\n    [ReloadOnEnterPlayMode]\n    public static class ExampleNestedNonGeneric // 支持嵌套类型\n    {\n        // 会被自动重置为 new()\n        // {\n        //     \"Hello\",\n        //     \"World\"\n        // }\n        public static List\u003cstring\u003e ListValue = new()\n        {\n            \"Hello\",\n            \"World\"\n        };\n\n        // .cctor 会被重新执行\n        static ExampleNestedNonGeneric()\n        {\n            Debug.Log(\"ExampleNestedNonGeneric..cctor()\");\n        }\n    }\n\n    // 标记类型\n    [ReloadOnEnterPlayMode]\n    public static class ExampleNestedGeneric\u003cU\u003e // 支持泛型嵌套泛型\n    {\n        // 会被自动重置为 default(KeyValuePair\u003cT, U\u003e)\n        public static KeyValuePair\u003cT, U\u003e KVPValue;\n    }\n}\n\n// 没有标记 [ReloadOnEnterPlayMode]\npublic static class ExampleIgnoredClass\n{\n    // 不会被自动重置\n    public static string Value;\n\n    // 不会重新执行\n    static ExampleIgnoredClass() { }\n\n    // 不会被调用\n    [RunBeforeReload]\n    static void Unload() { }\n}\n```\n\n## 只读字段\n\n在 Unity Editor 里，会把被标记的类型中所有的字段的 `readonly` 关键字去掉。例如：\n\n``` csharp\n[ReloadOnEnterPlayMode]\npublic class Program\n{\n    public static readonly string a = \"aaa\";\n    public static readonly string b = a + \"bbb\";\n\n    // 在 Unity Editor 里，插件会把字段上的 readonly 去掉\n    // public static string a = \"aaa\";\n    // public static string b = a + \"bbb\";\n}\n```\n\n这个过程是自动完成的，一般不需要在意。如果要保留 `readonly` 关键字，需要加上 `PreserveReadonly` 参数。\n\n``` csharp\n[ReloadOnEnterPlayMode(PreserveReadonly = true)]\npublic class Program\n{\n    public static readonly string a = \"aaa\";\n    public static readonly string b = a + \"bbb\";\n\n    // 保留 readonly 关键字\n    // public static readonly string a = \"aaa\";\n    // public static readonly string b = a + \"bbb\";\n}\n```\n\n然而，保留 `readonly` 关键字可能会导致问题。\n\n上面的代码中，`a` 可以正常被重置。`b` 被重置后的值是无法预测的，因为它的值依赖 `a`。这大概是 Unity Mono 的问题。在重置完 `a` 的值后，在底层，`a` 似乎会短暂地变成野指针，导致 `a + \"bbb\"` 的结果无法预测。\n\n通常情况下，在 Unity Editor 里不需要保留 `readonly` 关键字，除非你要用这部分元数据。如果一定要保留它的话，请先做一下测试，看看会不会出错。\n\n## 原理？\n\n下面所有的工作都是自动完成的，且只在 Editor 里才执行。正式打包时，经过 [Managed code stripping](https://docs.unity3d.com/Manual/ManagedCodeStripping.html)，这个插件的痕迹会完全消失，连元数据都不会留下。\n\n### 1. Hook Assembly\n\n在程序集中插入下面的代码。运行时用来记录该程序集中被使用过的类型。\n\n``` csharp\nusing System;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\n\n[CompilerGenerated]\ninternal static class \u003cAssemblyTypeReloader\u003e\n{\n    private static Action s_UnloadActions;\n\n    private static Action s_LoadActions;\n\n    public static void RegisterUnload(Action value)\n    {\n        Action action = s_UnloadActions;\n        Action action2;\n        do\n        {\n            action2 = action;\n            Action value2 = (Action)Delegate.Combine(action2, value);\n            action = Interlocked.CompareExchange(ref s_UnloadActions, value2, action2);\n        }\n        while ((object)action != action2);\n    }\n\n    public static void Unload()\n    {\n        s_UnloadActions?.Invoke();\n    }\n\n    public static void RegisterLoad(Action value)\n    {\n        Action action = s_LoadActions;\n        Action action2;\n        do\n        {\n            action2 = action;\n            Action value2 = (Action)Delegate.Combine(action2, value);\n            action = Interlocked.CompareExchange(ref s_LoadActions, value2, action2);\n        }\n        while ((object)action != action2);\n    }\n\n    public static void Load()\n    {\n        s_LoadActions?.Invoke();\n    }\n}\n```\n\n### 2. Hook Type\n\n以示例中的 `ExampleGeneric\u003cT\u003e` 为例。\n\n#### 复制 Class Constructor（.cctor）\n\n``` csharp\n[CompilerGenerated]\nprivate static void \u003cExampleGeneric`1\u003e__ClassConstructor__Copy()\n{\n    Property = new List\u003cT\u003e(114);\n}\n```\n\n#### 生成代码：按顺序调用 RunBeforeReload 回调\n\n``` csharp\n[CompilerGenerated]\nprivate static void \u003cExampleGeneric`1\u003e__UnloadType__Impl()\n{\n    UnloadFirst();\n    UnloadSecond();\n}\n```\n\n#### 生成代码：重置所有字段，重新执行 .cctor\n\n``` csharp\n[CompilerGenerated]\nprivate static void \u003cExampleGeneric`1\u003e__LoadType__Impl()\n{\n    Value = default(T);\n    ExampleGeneric\u003cT\u003e.Event = null;\n    Property = null;\n    \u003cExampleGeneric`1\u003e__ClassConstructor__Copy();\n}\n```\n\n#### 在原来的 .cctor 中插入代码\n\n``` csharp\nstatic ExampleGeneric()\n{\n    Property = new List\u003cT\u003e(114);\n\n    // 下面是被插入的代码\n    \u003cAssemblyTypeReloader\u003e.RegisterUnload(\u003cExampleGeneric`1\u003e__UnloadType__Impl);\n    \u003cAssemblyTypeReloader\u003e.RegisterLoad(\u003cExampleGeneric`1\u003e__LoadType__Impl);\n}\n```\n\n### 3. 在 Unity Editor 中监听 EnterPlayMode 事件\n\n进入 Play Mode 时，重置类型。\n\n``` csharp\npublic static class TypeReloader\n{\n    private static bool s_Initialized = false;\n    private static Action s_UnloadTypesAction;\n    private static Action s_LoadTypesAction;\n\n    public static void ReloadDirtyTypes()\n    {\n        try\n        {\n            InitializeIfNot();\n\n            s_UnloadTypesAction?.Invoke();\n\n            GC.Collect();\n            GC.WaitForPendingFinalizers();\n\n            s_LoadTypesAction?.Invoke();\n        }\n        catch (Exception e)\n        {\n            Debug.LogException(e);\n            Debug.LogError(\"Failed to reload dirty types!\");\n        }\n    }\n\n    [InitializeOnEnterPlayMode]\n    private static void OnEnterPlayModeInEditor(EnterPlayModeOptions options)\n    {\n        if ((options \u0026 EnterPlayModeOptions.DisableDomainReload) == 0)\n        {\n            return;\n        }\n\n        ReloadDirtyTypes();\n    }\n\n    // ...\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstalomeow%2Fquickplaymode","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstalomeow%2Fquickplaymode","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstalomeow%2Fquickplaymode/lists"}