跳到主要内容
版本:8.15.0

DOTS支持

DOTS的TypeManager初始化时机过早,而且不支持动态注册Component和System等类型。为了让热更新模块能在DOTS系统中正常 运行,需要调整World的初始化时机,确保加载完热更新程序集之后、创建任何World之前完成类型注册。

不同Entities版本的接入方式不同:

  • 0.51.1 / 1.0.16:需要替换为HybridCLR修改后的com.unity.entities包,并调用包内扩展的注册API。
  • 1.3.15:不需要修改com.unity.entities源码,使用Unity官方包即可;通过TypeManager.Shutdown + TypeManager.Initialize重建类型表。

各版本在手动调用热更程序集 EarlyInit 时,默认可使用项目侧 TypeManagerEarlyInitHelper.EarlyInitAssemblies(assemblies)(纯反射,实现简单)。热更程序集较大时可再按优化 EarlyInitAssemblies 的性能切换到 JSON 方案。

Jobs and BurstCompile​

如果项目中仅仅用到了Jobs和Burst,并没有使用 com.unity.entities,则不需要对com.unity.entities进行任何改造(也不需要下文的World推迟初始化流程)。

在热更新代码中可以正常使用jobs和burst,但是,对于旗舰版本以外的版本(社区版、专业版和热重载版),burst代码会退化为纯解释执行。 对于旗舰版本,只要没有改动函数,仍然以burst方式运行,性能没有任何衰减。

支持的版本​

由于DOTS仍然在快速迭代和修改,为了减少维护成本,只维护以下几个版本的com.unity.entities:

  • 0.51.1-preview.21
  • 1.0.16
  • 1.3.15

目前仅在Unity 2021+版本上完成测试,Unity 2020及更低版本未测试兼容性。一般来说,只要对应版本的com.unity.entities能 在该Unity版本上正常运行,也能支持hybridclr。

其中 1.3.15 建议在 Unity 2022.3 LTS 上使用(已完成验证)。

有特殊DOTS版本需求的开发者,由于维护单独的DOTS版本成本较高,需要联系我们单独付费定制。

支持的特性​

目前绝大多数DOTS特性都可以在hybridclr下正常运行,只有跟BurstCompile及资源序列化相关的特性支持较差。

1.3.15版本​

特性社区版本专业版旗舰版热重载版
Jobs✔✔✔✔
Managed Component✔✔✔✔
Unmanaged Component✔✔✔✔
Managed System✔✔✔✔
Unmanaged System✔✔✔✔
Aspect✔✔✔✔
IJobEntity✔✔✔✔
BurstCompile✔
SubScene

1.0.16版本​

特性社区版本专业版旗舰版热重载版
Jobs✔✔✔✔
Managed Component✔✔✔✔
Unmanaged Component✔✔✔✔
Managed System✔✔✔✔
Unmanaged System✔✔✔✔
Aspect✔✔✔✔
IJobEntity✔✔✔✔
BurstCompile✔
SubScene

0.51.1-preview.21版本​

特性社区版本专业版旗舰版热重载版
Jobs✔✔✔✔
Managed Component✔✔✔✔
Unmanaged Component✔✔✔✔
Managed System✔✔✔✔
Unmanaged System✔✔✔✔
IJobEntity✔✔✔✔
BurstCompile✔
SubScene

安装​

安装com.unity.entities​

危险

如果项目中仅仅用到了Jobs和Burst,并没有使用 com.unity.entities,则不需要安装或改造Entities包。

0.51.1 / 1.0.16(替换修改后的包)​

  • 在项目中移除 com.unity.entities包,退出Unity Editor,清空 Library\PackageCache 目录下该包对应的目录
  • 根据项目使用的版本,下载修改后的com.unity.entities,将对应目录下的com.unity.entities.7z 解压到 Packages目录。请确保解压后的目录名为com.unity.entities。

重新打开Unity Editor时可能会提示是否要进行Api升级,根据项目情况自行决定是否升级。

1.3.15(使用官方包,不改源码)​

1.3.15 不要替换为修改后的Entities包,直接通过 Package Manager / manifest.json 安装官方 com.unity.entities 1.3.15 即可。

与旧版本的关键差异:

项0.51.1 / 1.0.161.3.15
Entities包必须替换HybridCLR修改包使用官方包,不改源码
类型注册CollectComponentTypes / AddComponentTypes 等扩展APITypeManager.Shutdown + TypeManager.Initialize 整表重建
EarlyInit改包 TypeManager.EarlyInitAssemblies,或项目侧简单反射版项目侧 TypeManagerEarlyInitHelper.EarlyInitAssemblies(assemblies)(简单反射版)

修改项目设置​

为了避免DOTS运行过程中带动态注册Component或System可能引发的问题,需要调整World的初始化时机以确保运行所有World之前已经注册了所有热更新类型。

在Player Settings的Scripting Define Symbols中,添加编译宏 UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_RUNTIME_WORLD。详细介绍可以参见World的 自定义初始化文档。

建议在各个目标平台(Standalone / Android / iOS / WebGL等)都加上该宏。

初始化​

为了避免遇到问题,请在加载完热更新代码后,运行任何dots代码之前进行初始化。

初始化中主要包含两部分:

  • 注册热更新的dots类型
  • 初始化World

不同的com.unity.entities版本的初始化实现略有差别。

1.3.15版本初始化​

1.3.15 官方TypeManager在Initialize()时会扫描当前AppDomain中的程序集(含ILPP生成的AssemblyTypeRegistry)。因此只要热更新DLL已经加载进AppDomain,再执行一次Shutdown(若已初始化)+ Initialize,即可把热更类型注册进去。

AOT程序集的Unmanaged ISystem 会通过RuntimeInitializeOnLoadMethod自动执行codegen生成的EarlyInit;晚加载的热更新/DHE程序集不会触发该回调,必须手动调用。1.3.15 没有修改包内的TypeManager.EarlyInitAssemblies,请使用项目侧辅助类(下方为默认的简单反射版;性能优化见优化 EarlyInitAssemblies 的性能)。

请将以下辅助类放到项目中(热更新或AOT程序集均可),不要改Entities包源码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Unity.Entities
{
public static class TypeManagerEarlyInitHelper
{
public const string EarlyInitTypePrefix = "__UnmanagedPostProcessorOutput__";
public const string EarlyInitMethodName = "EarlyInit";
const BindingFlags EarlyInitFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

/// <summary>
/// 简单版:Assembly.GetTypes() 扫描后调用 EarlyInit。实现简单,但会初始化程序集内几乎所有类型。
/// </summary>
public static void EarlyInitAssemblies(IEnumerable<Assembly> assemblies)
{
if (assemblies == null)
return;

foreach (var assembly in assemblies)
{
if (assembly == null)
continue;

Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.Where(t => t != null).ToArray();
}

foreach (var type in types)
{
if (type == null || !type.Name.StartsWith(EarlyInitTypePrefix, StringComparison.Ordinal))
continue;

var earlyInit = type.GetMethod(EarlyInitMethodName, EarlyInitFlags, null, Type.EmptyTypes, null);
earlyInit?.Invoke(null, null);
}
}
}
}
}
private static void InitializeWorld()
{
var dotsAssemblies = new Assembly[] { /* 热更新程序集 */ };
#if !UNITY_EDITOR
// TypeManager.IsInitialized 为 internal,通过反射读取
var isInitializedProp = typeof(TypeManager).GetProperty("IsInitialized",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
if (isInitializedProp != null && (bool)isInitializedProp.GetValue(null))
TypeManager.Shutdown();

TypeManager.Initialize();
TypeManagerEarlyInitHelper.EarlyInitAssemblies(dotsAssemblies);
#endif
DefaultWorldInitialization.Initialize("Default World", false);
}
危险
  • 必须在创建任何 World / EntityManager 之前完成上述流程;若World已创建再Shutdown,已有Archetype中的TypeIndex会失效。
  • 热更新程序集需经过Entities ILPP(生成AssemblyTypeRegistry等),否则Initialize扫描不到组件类型。
  • Editor下热更程序集通常已在域内,可按项目情况决定是否走Shutdown/Initialize;Player上务必按上面流程执行。

0.51.1版本初始化​

    private static void InitializeWorld()
{
#if !UNITY_EDITOR
// dotsAsseemblies为所有包含自定义Component、System等DOTS类型的AOT和热更新程序集
var dotsAssemblies = new Assembly[] { ... };
var componentTypes = new HashSet<System.Type>();
TypeManager.CollectComponentTypes(dotsAssemblies, componentTypes);
TypeManager.AddNewComponentTypes(componentTypes.ToArray());
// 也可用项目侧简单版:TypeManagerEarlyInitHelper.EarlyInitAssemblies(dotsAssemblies);
TypeManager.EarlyInitAssemblies(dotsAssemblies);
#endif


DefaultWorldInitialization.Initialize("Default World", false);

}

1.0.16版本初始化​

    private static void InitializeWorld()
{
#if !UNITY_EDITOR
// dotsAsseemblies为所有包含自定义Component、System等DOTS类型的AOT和热更新程序集
var dotsAssemblies = new Assembly[] { ... };
var componentTypes = new HashSet<Type>();
TypeManager.CollectComponentTypes(dotsAssemblies, componentTypes);
TypeManager.AddComponentTypes(dotsAssemblies, componentTypes);
TypeManager.RegisterSystemTypes(dotsAssemblies);
TypeManager.InitializeSharedStatics();
// 也可用项目侧简单版:TypeManagerEarlyInitHelper.EarlyInitAssemblies(dotsAssemblies);
TypeManager.EarlyInitAssemblies(dotsAssemblies);
#endif


DefaultWorldInitialization.Initialize("Default World", false);
}
信息

默认初始化示例使用的是简单反射版(GetTypes()),接入成本最低。
热更程序集较大、启动耗时明显时,再按下一节切换到 JSON 优化版。

优化 EarlyInitAssemblies 的性能(各版本通用)​

晚加载的热更新/DHE程序集不会走 RuntimeInitializeOnLoadMethod,需要手动调用 Entities codegen 生成的 EarlyInit(类型名形如 __UnmanagedPostProcessorOutput__*),才能把 Unmanaged ISystem 注册进 SystemBaseRegistry。

上一节默认介绍的简单版会调用 Assembly.GetTypes(),这会:

  • 触发该程序集内几乎所有类型的静态初始化
  • 产生大量不必要的 Type 反射对象

这与具体 Entities 版本无关,0.51.1 / 1.0.16 / 1.3.15 均适用同一优化:

  1. Editor:用 dnlib 扫描热更 DLL,生成 EarlyInit 类型清单 JSON(TypeManagerEarlyInitGenerator)。
  2. Runtime:调用重载 TypeManagerEarlyInitHelper.EarlyInitAssemblies(assemblies, json),按 JSON 中的类型名 Assembly.GetType 后调用 EarlyInit(不再 GetTypes())。

在上一节简单版 TypeManagerEarlyInitHelper 基础上,补充 Manifest 类型与 JSON 重载(可与简单版放在同一文件):

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

namespace Unity.Entities
{
[Serializable]
public class TypeManagerEarlyInitManifest
{
public List<TypeManagerEarlyInitAssemblyEntry> assemblies = new List<TypeManagerEarlyInitAssemblyEntry>();
}

[Serializable]
public class TypeManagerEarlyInitAssemblyEntry
{
public string name;
public List<string> earlyInitTypes = new List<string>();
}

public static partial class TypeManagerEarlyInitHelper
{
/// <summary>
/// 优化版:仅调用 JSON 中列出的 EarlyInit,避免 Assembly.GetTypes()。
/// </summary>
public static void EarlyInitAssemblies(IEnumerable<Assembly> assemblies, string earlyInitJson)
{
if (assemblies == null)
throw new ArgumentNullException(nameof(assemblies));
if (string.IsNullOrEmpty(earlyInitJson))
throw new ArgumentException("earlyInitJson is null or empty", nameof(earlyInitJson));

var manifest = JsonUtility.FromJson<TypeManagerEarlyInitManifest>(earlyInitJson);
if (manifest?.assemblies == null || manifest.assemblies.Count == 0)
return;

var assemblyByName = new Dictionary<string, Assembly>(StringComparer.Ordinal);
foreach (var assembly in assemblies)
{
if (assembly == null)
continue;
assemblyByName[assembly.GetName().Name] = assembly;
}

foreach (var entry in manifest.assemblies)
{
if (entry == null || string.IsNullOrEmpty(entry.name) || entry.earlyInitTypes == null)
continue;
if (!assemblyByName.TryGetValue(entry.name, out var assembly))
{
Debug.LogWarning($"[TypeManagerEarlyInitHelper] Assembly not loaded: {entry.name}");
continue;
}

foreach (var typeName in entry.earlyInitTypes)
{
if (string.IsNullOrEmpty(typeName))
continue;

var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
if (type == null)
{
Debug.LogError($"[TypeManagerEarlyInitHelper] Type not found: {entry.name}::{typeName}");
continue;
}

var earlyInit = type.GetMethod(EarlyInitMethodName, EarlyInitFlags, null, Type.EmptyTypes, null);
if (earlyInit == null)
{
Debug.LogError($"[TypeManagerEarlyInitHelper] EarlyInit not found: {type.FullName}");
continue;
}

earlyInit.Invoke(null, null);
}
}
}
}
}

请将以下Editor类放到 Editor 程序集(依赖 HybridCLR 自带的 dnlib;SettingsUtil 来自 HybridCLR.Editor)。打包热更 DLL 后调用 Generate,把 JSON 随热更资源一起发布:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using dnlib.DotNet;
using Unity.Entities;
using UnityEditor;
using UnityEngine;

namespace HybridCLR.Editor
{
/// <summary>
/// Editor-only: scan hot-update DLLs with dnlib and write a JSON manifest of codegen EarlyInit types.
/// Runtime loads that JSON and calls TypeManagerEarlyInitHelper.EarlyInitAssemblies.
/// </summary>
public static class TypeManagerEarlyInitGenerator
{
public static string DefaultOutputJsonPath =>
Path.Combine(Application.streamingAssetsPath, "TypeManagerEarlyInit.json");

/// <param name="hotUpdateDllPaths">热更新 dll 文件路径列表</param>
/// <param name="outputJsonPath">输出的 json 路径(可放到 StreamingAssets)</param>
public static TypeManagerEarlyInitManifest Generate(IEnumerable<string> hotUpdateDllPaths, string outputJsonPath)
{
if (hotUpdateDllPaths == null)
throw new ArgumentNullException(nameof(hotUpdateDllPaths));
if (string.IsNullOrEmpty(outputJsonPath))
throw new ArgumentException("outputJsonPath is null or empty", nameof(outputJsonPath));

var manifest = new TypeManagerEarlyInitManifest();
var dllPaths = hotUpdateDllPaths.Where(p => !string.IsNullOrEmpty(p)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();

foreach (var dllPath in dllPaths)
{
if (!File.Exists(dllPath))
{
Debug.LogError($"[TypeManagerEarlyInitGenerator] DLL not found: {dllPath}");
continue;
}

ModuleDefMD module = null;
try
{
module = ModuleDefMD.Load(File.ReadAllBytes(dllPath));
var assemblyName = module.Assembly?.Name?.String;
if (string.IsNullOrEmpty(assemblyName))
assemblyName = Path.GetFileNameWithoutExtension(dllPath);

var entry = new TypeManagerEarlyInitAssemblyEntry
{
name = assemblyName,
earlyInitTypes = new List<string>()
};

foreach (var type in module.GetTypes())
{
if (type == null || type.IsGlobalModuleType)
continue;

var typeName = type.Name?.String;
if (string.IsNullOrEmpty(typeName) ||
!typeName.StartsWith(TypeManagerEarlyInitHelper.EarlyInitTypePrefix, StringComparison.Ordinal))
continue;

if (!HasStaticParameterlessEarlyInit(type))
continue;

var fullName = string.IsNullOrEmpty(type.Namespace)
? typeName
: type.FullName;
entry.earlyInitTypes.Add(fullName);
}

entry.earlyInitTypes.Sort(StringComparer.Ordinal);
if (entry.earlyInitTypes.Count > 0)
manifest.assemblies.Add(entry);

Debug.Log($"[TypeManagerEarlyInitGenerator] {assemblyName}: {entry.earlyInitTypes.Count} EarlyInit type(s) from {dllPath}");
}
catch (Exception e)
{
Debug.LogException(e);
Debug.LogError($"[TypeManagerEarlyInitGenerator] Failed to scan: {dllPath}");
}
finally
{
module?.Dispose();
}
}

manifest.assemblies.Sort((a, b) => string.CompareOrdinal(a.name, b.name));

var dir = Path.GetDirectoryName(outputJsonPath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);

var json = JsonUtility.ToJson(manifest, true);
File.WriteAllText(outputJsonPath, json);
Debug.Log($"[TypeManagerEarlyInitGenerator] Wrote {outputJsonPath}");

if (outputJsonPath.Replace('\\', '/').Contains("/Assets/"))
AssetDatabase.Refresh();

return manifest;
}

[MenuItem("HybridCLR/Generate/TypeManager EarlyInit JSON")]
public static void GenerateFromHybridCLRHotUpdateDlls()
{
var target = EditorUserBuildSettings.activeBuildTarget;
var dllDir = SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target);
var dllPaths = SettingsUtil.HotUpdateAssemblyFilesExcludePreserved
.Select(dll => Path.Combine(dllDir, dll))
.ToList();

if (dllPaths.Count == 0)
{
Debug.LogError("[TypeManagerEarlyInitGenerator] No hot update assemblies configured in HybridCLR Settings.");
return;
}

Generate(dllPaths, DefaultOutputJsonPath);
}

static bool HasStaticParameterlessEarlyInit(TypeDef type)
{
foreach (var method in type.Methods)
{
if (method == null || !method.IsStatic)
continue;
if (method.Name != TypeManagerEarlyInitHelper.EarlyInitMethodName)
continue;
if (method.MethodSig != null && method.MethodSig.Params.Count == 0)
return true;
}

return false;
}
}
}
提示
  • 简单版:TypeManagerEarlyInitHelper.EarlyInitAssemblies(assemblies)(各版本初始化默认用法)。
  • 优化版:TypeManagerEarlyInitGenerator.Generate(hotUpdateDllPaths, outputJsonPath),再在运行时调用 TypeManagerEarlyInitHelper.EarlyInitAssemblies(assemblies, json)。
  • 打包热更 DLL 后请重新生成 JSON,并随热更资源一起发布。
  • 若热更程序集中没有任何 Unmanaged ISystem,JSON 中对应列表可为空,调用仍是安全的。

解决ReversePInvokeCallback的问题​

DOTS系统初始化Unmanaged System时会尝试获得它的OnStart之类函数的Marshal指针。hybridclr需要为每个这种函数绑定一个运行时唯一的cpp函数指针, 否则运行过程中会出现GetReversePInvokeWrapper fail. exceed max wrapper num of method错误。详细介绍可见HybridCLR+lua/js/python文档。

简单来说,需要预留足够多的 SystemBaseRegistry.ForwardingFunc对应的 wrapper函数。在热更新模块(也可以在DHE程序集,但不能在AOT程序集)中添加如下代码:

public static class PreserveDOTSReversePInvokeWrapper
{
[ReversePInvokeWrapperGeneration(100)]
[MonoPInvokeCallback(typeof(SystemBaseRegistry.ForwardingFunc))]
public static void ForwordMethod(IntPtr system, IntPtr state)
{

}
}


将代码中的100改为一个适当的数字即可,推荐为Unmanaged System类型个数的5-10倍。

热更新带[BurstCompile]的代码​

分为两种情况:

  1. 仅使用 jobs和burst,没有使用com.unity.entities

如果是非旗舰版本,则用法与普通热更新代码没有任何区别,随意添加、删除和修改相关代码。

如果是旗舰版本,则需要修改Job类的名字,像这样:

        /// 热更新前的代码
[BurstCompile]
public struct MyJobBeforeHotUpdate : IJobParallelFor
{
public void Execute(int index)
{
}
}


/// 热更新后的代码
[BurstCompile]
public struct MyJobAfterHotUpdate : IJobParallelFor
{
public void Execute(int index)
{
}
}
  1. 使用com.unity.entities

如果是非旗舰版本,则用法与普通热更新代码没有任何区别,随意添加、删除和修改相关代码。但由于解释执行模式下burst代码不仅不能提升性能,还会导致unity会插入大量复杂的辅助代码, 导致性能严重下降。建议非旗舰版本开发者移除热更新代码中[BurstCompile]。

如果是旗舰版本,包含[BurstCompile]的热更新函数发生变化,需要移除[BurstCompile]特性,否则仍然会运行了旧的代码!