stalomeow / QuickPlayMode

When entering play mode, automatically resetting static members of dirty types, instead of reloading the domain.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Quick Play Mode

>> English Version <<

进入播放模式时,自动按需重置类型的静态成员,不使用 Domain Reloading。优势:

  • 速度快,额外开销低。
  • 没有复杂配置,只需要加几个 Attribute,其他全自动。
  • 不影响发布构建时的代码。

之前叫 EasyTypeReload。

要求

  • Unity >= 2021.3.
  • Mono Cecil >= 1.10.1.

从 git URL 安装

install-git-url-1

install-git-url-2

编辑器扩展

menu-item

可以手动重置类型的静态成员,或者手动 Reload Domain。

记得点 Enter Play Mode Options/Reload Domain 关闭 Domain Reloading!

示例

// 使用命名空间
using QuickPlayMode;
// using ...

// 标记类型
[ReloadOnEnterPlayMode]
public static class ExampleGeneric<T> // 支持泛型
{
    // 会被自动重置为 default(T)
    public static T Value;

    // 会被自动重置为 null
    public static event Action<T> Event;

    // 会被自动重置为 new List<T>(114)
    public static List<T> Property { get; set; } = new List<T>(114);

    // 在类型被重置前,会调用这个方法
    // OrderInType 默认为 0,数字越小执行越早
    // * 一个类型中的多个回调会被排序,但类型与类型间不会排序
    [RunBeforeReload(OrderInType = 100)]
    static void UnloadSecond()
    {
        Debug.Log("514");
    }

    // 在类型被重置前,也会调用这个方法
    // UnloadFirst() 在 UnloadSecond() 前被调用
    [RunBeforeReload]
    static void UnloadFirst()
    {
        Debug.Log("114");
    }

    // 标记类型
    [ReloadOnEnterPlayMode]
    public static class ExampleNestedNonGeneric // 支持嵌套类型
    {
        // 会被自动重置为 new()
        // {
        //     "Hello",
        //     "World"
        // }
        public static List<string> ListValue = new()
        {
            "Hello",
            "World"
        };

        // .cctor 会被重新执行
        static ExampleNestedNonGeneric()
        {
            Debug.Log("ExampleNestedNonGeneric..cctor()");
        }
    }

    // 标记类型
    [ReloadOnEnterPlayMode]
    public static class ExampleNestedGeneric<U> // 支持泛型嵌套泛型
    {
        // 会被自动重置为 default(KeyValuePair<T, U>)
        public static KeyValuePair<T, U> KVPValue;
    }
}

// 没有标记 [ReloadOnEnterPlayMode]
public static class ExampleIgnoredClass
{
    // 不会被自动重置
    public static string Value;

    // 不会重新执行
    static ExampleIgnoredClass() { }

    // 不会被调用
    [RunBeforeReload]
    static void Unload() { }
}

只读字段

在 Unity Editor 里,会把被标记的类型中所有的字段的 readonly 关键字去掉。例如:

[ReloadOnEnterPlayMode]
public class Program
{
    public static readonly string a = "aaa";
    public static readonly string b = a + "bbb";

    // 在 Unity Editor 里,插件会把字段上的 readonly 去掉
    // public static string a = "aaa";
    // public static string b = a + "bbb";
}

这个过程是自动完成的,一般不需要在意。如果要保留 readonly 关键字,需要加上 PreserveReadonly 参数。

[ReloadOnEnterPlayMode(PreserveReadonly = true)]
public class Program
{
    public static readonly string a = "aaa";
    public static readonly string b = a + "bbb";

    // 保留 readonly 关键字
    // public static readonly string a = "aaa";
    // public static readonly string b = a + "bbb";
}

然而,保留 readonly 关键字可能会导致问题。

上面的代码中,a 可以正常被重置。b 被重置后的值是无法预测的,因为它的值依赖 a。这大概是 Unity Mono 的问题。在重置完 a 的值后,在底层,a 似乎会短暂地变成野指针,导致 a + "bbb" 的结果无法预测。

通常情况下,在 Unity Editor 里不需要保留 readonly 关键字,除非你要用这部分元数据。如果一定要保留它的话,请先做一下测试,看看会不会出错。

原理?

下面所有的工作都是自动完成的,且只在 Editor 里才执行。正式打包时,经过 Managed code stripping,这个插件的痕迹会完全消失,连元数据都不会留下。

1. Hook Assembly

在程序集中插入下面的代码。运行时用来记录该程序集中被使用过的类型。

using System;
using System.Runtime.CompilerServices;
using System.Threading;

[CompilerGenerated]
internal static class <AssemblyTypeReloader>
{
    private static Action s_UnloadActions;

    private static Action s_LoadActions;

    public static void RegisterUnload(Action value)
    {
        Action action = s_UnloadActions;
        Action action2;
        do
        {
            action2 = action;
            Action value2 = (Action)Delegate.Combine(action2, value);
            action = Interlocked.CompareExchange(ref s_UnloadActions, value2, action2);
        }
        while ((object)action != action2);
    }

    public static void Unload()
    {
        s_UnloadActions?.Invoke();
    }

    public static void RegisterLoad(Action value)
    {
        Action action = s_LoadActions;
        Action action2;
        do
        {
            action2 = action;
            Action value2 = (Action)Delegate.Combine(action2, value);
            action = Interlocked.CompareExchange(ref s_LoadActions, value2, action2);
        }
        while ((object)action != action2);
    }

    public static void Load()
    {
        s_LoadActions?.Invoke();
    }
}

2. Hook Type

以示例中的 ExampleGeneric<T> 为例。

复制 Class Constructor(.cctor)

[CompilerGenerated]
private static void <ExampleGeneric`1>__ClassConstructor__Copy()
{
    Property = new List<T>(114);
}

生成代码:按顺序调用 RunBeforeReload 回调

[CompilerGenerated]
private static void <ExampleGeneric`1>__UnloadType__Impl()
{
    UnloadFirst();
    UnloadSecond();
}

生成代码:重置所有字段,重新执行 .cctor

[CompilerGenerated]
private static void <ExampleGeneric`1>__LoadType__Impl()
{
    Value = default(T);
    ExampleGeneric<T>.Event = null;
    Property = null;
    <ExampleGeneric`1>__ClassConstructor__Copy();
}

在原来的 .cctor 中插入代码

static ExampleGeneric()
{
    Property = new List<T>(114);

    // 下面是被插入的代码
    <AssemblyTypeReloader>.RegisterUnload(<ExampleGeneric`1>__UnloadType__Impl);
    <AssemblyTypeReloader>.RegisterLoad(<ExampleGeneric`1>__LoadType__Impl);
}

3. 在 Unity Editor 中监听 EnterPlayMode 事件

进入 Play Mode 时,重置类型。

public static class TypeReloader
{
    private static bool s_Initialized = false;
    private static Action s_UnloadTypesAction;
    private static Action s_LoadTypesAction;

    public static void ReloadDirtyTypes()
    {
        try
        {
            InitializeIfNot();

            s_UnloadTypesAction?.Invoke();

            GC.Collect();
            GC.WaitForPendingFinalizers();

            s_LoadTypesAction?.Invoke();
        }
        catch (Exception e)
        {
            Debug.LogException(e);
            Debug.LogError("Failed to reload dirty types!");
        }
    }

    [InitializeOnEnterPlayMode]
    private static void OnEnterPlayModeInEditor(EnterPlayModeOptions options)
    {
        if ((options & EnterPlayModeOptions.DisableDomainReload) == 0)
        {
            return;
        }

        ReloadDirtyTypes();
    }

    // ...
}

About

When entering play mode, automatically resetting static members of dirty types, instead of reloading the domain.

License:MIT License


Languages

Language:C# 100.0%