HybrydCLR. How to update Unity game code without downloading updates to the store
Introduction
Today I want to introduce you to a plugin for Unity that allows you to update the game code without uploading updates to the store. It works through a modification of il2cpp, turning it into something like Mono.
There are many hot update solutions on the market, but all of them either have limitations on the interaction of hot update and AOT code, or can update only part of the code through attributes and other various kinds of collective farm.
As the developers say:
HybridCLR — is a native solution for hot update of C# code. Simply put, compiled il2cpp code is equivalent to aot module in mono, and HybridCLR is equivalent to the interpreter, and their combination becomes full mono. HybridCLR makes il2cpp a fully functional runtime environment, initially (i.e. via System.Reflection.Assembly.Load) allowing dynamic loading of dll, thus making hot update possible on iOS.
Because HybridCLR is implemented at the native runtime level, types from hot update libraries and types from AOT code are equivalent and seamlessly unified. You can call, inherit, use reflection and multithreading, without code generation or writing adapters.
Other hot update solutions are vm independent, and linking to il2cpp is essentially equivalent to linking lua embeds to mono. Consequently, the type system is not consistent. To allow a hot update type to inherit some AOT types, an adapter must be written, and the type in the interpreter cannot be recognized by the host project's type system. Incomplete features, problematic development, and poor performance.
As the developers say:
HybridCLR is very popular in China, at least hundreds of games currently use HybridCLR, all of them are available in the App Store and Google Play.
HybridCLR is still based on interpretation and execution, and in this respect, this approach is no different from the integration of the lua interpreter into Unity. Therefore, it complies with the requirements of the App Store and Google Play Store, and there is no particular risk of rejection. And due to the high integration of HybridCLR and il2cpp, it is even much safer than the lua scheme, hence the probability of rejection of the game due to non-compliance with the rules of the platform is very low.
HybridCLR does the following:
Implementation of an efficient metadata analysis library (dll)
Changes to the metadata management module to implement dynamic metadata registration
Implementation of a compiler from the IL instruction set to the custom register instruction set
Implementing an effective register interpreter
Providing a large number of instinctive functions to improve the interpreter's performance
Interesting facts:
hot update dots code, but you need to install their fork of the plugin. Unitech made early initialization of TypeManager without the ability to call it manually, the guys had to finish writing it themselves. Burst support has not yet been brought in, but Jobs works and is updated
hot update of async code
zero learning and zero usage costs
Installation
https://hybridclr.doc.code-philosophy.com/en/docs/beginner/quickstart
Install the plugin via Package Manager:
Next, we initialize the plugin:
Setting
As an example, I will take my necro project from 2019:
In Unity, all the code that a developer writes that is not in another Assembly Definition is in Assembly-CSharp. The project code should be split into an AOT assembly (i.e. compiled into the main game package) and a hot update assembly. In HybridCLR, there are no restrictions on how to split the assembly, and even code in a third-party project can be used as a hot update. When the game starts, at least one AOT assembly should be responsible for the work associated with running hot update code. There are 2 ways to set up a custom AOT Assembly and Hot Update code, depending on your current project setup:
Assembly-CSharp as AOT entry point. The rest of the code is itself split into N AOT assemblies and M hot update assemblies.
Assembly-CSharp as a hot update build. The rest of the code itself is split into N AOT builds and M hot update builds.
In the example, we will create one Assembly Definition Maikn, and the hot update code will be in Assembly-CSharp, after which the scene or prefab will be loaded, on which there is a hot update code with an entry point.
You can use anything as a resource management and asset loading system: bare AssetBundles, Addressables, or your own asset management system with loading from your server.
In this example, I will use Addressables, as the easiest option for integration and setup, which does not take much time (most often this is not true, of course).
Important clarification: if part of the code is carried away in hot update, there should be no explicit links between the AOT part and the hot update part. Here, either you have a clear division into zones of responsibility by scenes/prefabs/assembly definition in your project, or your entire project is in hot update and is loaded from the entry point containing only AOT code.
In the example, Assembly-CSharp will be used as an AOT entry point with loading and initialization of the hot update part.
To do this, create a Main Assembly Definition and drag all the code into it:
Next, let's create the Main assembly and drag all the code inside the folder:
Next, we will create 3 Addressables groups:
Group for our hot update libraries (in this case Main)
Group for metadata. Metadata is a part of dll external dependencies, which are eaten by stripping because at the moment of assembly in the project there are no dependencies on any of them (due to the fact that the code is carried away in hot update from the part of the project compiled at the moment of AOT assembly).
Group for the game scene that will be loaded by AOT code from addressables
Next, in Project Settings, in the HybridCLR settings tab, you need to add the previously created hot update library to the Hot Update Assembly Definitions list:
Hot update can only update those libraries that were in the list at the time of project assembly. Roughly speaking, if we build an Apk for Android and want to add another Dll Core, we won't be able to do it.
For this purpose, there is a special field Preserve Hot Update Assemblies, in which you can write the names of libraries that may be added in the future:
You can update not only user code, but also third-party plugins, such as UniTask, Dotween, and so on, as well as regular DLLs that are already imported into the project in compiled form.
Let's create a Hot Reload scene, on which the scene loader and libraries with metadata will hang:
I create a simple repository for hosting static files on github pages (I won’t write instructions, instructions are easy to find), create an Addressables Profile, make it current and set links in its settings:
I set the DLLS and DLLSMetadat Build & Load Paths to Remote in the Addressables groups:
Just in case, I set Bundle Naming Mode to DLLS and DLLSMetadata Filename so that the hash in the naming does not constantly change:
Don't forget to set Build Remote Catalog in Addressables Asset Settings:
Next, we will perform the initial generation of all Hybrid CLR data. The process may take time, do not be alarmed.
Next, in any Editor folder, create the following script, which will perform a hot update library build, update them in folders, and build Addressables. The script uses extensions that will be available via the link in the repository at the end of the article.
[MenuItem("Finiki Games/Build/HybridCLR/Build hybrid clr fresh")]
public static void BuildHybridCLRFresh() {
// Создаем installer hybrid clr
var installerController = new HybridCLR.Editor.Installer.InstallerController();
// Проверяем, был ли он проинициализирован
if (!installerController.HasInstalledHybridCLR()) {
installerController.InstallDefaultHybridCLR();
}
// Вызываем основную сборку
MainBuild();
// Собираем Addressables
AddressableAssetSettings.BuildPlayerContent();
}
[MenuItem("Finiki Games/Build/HybridCLR/Build hybrid clr update")]
public static void BuildHybridCLRUpdate() {
// Вызываем основную сборку
MainBuild();
// Создаем входящие настройки для сборки и обновления Addressables
var input = new AddressablesDataBuilderInput(AddressableAssetSettingsDefaultObject.Settings);
var updateBuild = new AddressablesBuildMenuUpdateAPreviousBuild();
updateBuild.OnPrebuild(input);
AddressableAssetSettings.BuildPlayerContent(out AddressablesPlayerBuildResult _);
}
private static void MainBuild() {
// Генерируем всю необходимую информацию для Hybrid CLR
HybridCLRExtensions.GenerateAllLite(true, BuildTarget.Android);
string projectPath = Application.dataPath;
projectPath = projectPath.Replace($"/Assets", "");
var hybridCLRConfig = FindFirstAssetByType<HybridCLRConfig>();
// Получаем список DLLS
var assemblies = HybridCLR.Editor.SettingsUtil.HotUpdateAssemblyFilesExcludePreserved;
var assembliesUrl = HybridCLR.Editor.SettingsUtil.GetHotUpdateDllsOutputDirByTarget(BuildTarget.Android);
var settings = HybridCLR.Editor.SettingsUtil.HybridCLRSettings;
foreach (var assemblyName in assemblies) {
if (settings.hotUpdateAssemblies.Contains(assemblyName.Replace(".dll", ""))) continue;
var fullAssemblyPath = projectPath + "\\" + assembliesUrl + "\\" + assemblyName;
var newAssemblyPath = RenameFile(fullAssemblyPath, assemblyName + ".bytes");
var inProjectAssemblyPath =
projectPath + "\\" + hybridCLRConfig.DllPath + "\\" + assemblyName + ".bytes";
// Копируем каждый скомпилированный файл в проект
MoveFile(newAssemblyPath, inProjectAssemblyPath);
}
// Получаем список DLLSMetadata
var metadataAssemblies = hybridCLRConfig.MetadataAssemblyList;
var metadataUrl = HybridCLR.Editor.SettingsUtil.GetAssembliesPostIl2CppStripDir(BuildTarget.Android);
foreach (var metadataAssemblyName in metadataAssemblies) {
var fullAssemblyPath = projectPath + "\\" + metadataUrl + "\\" + metadataAssemblyName;
var newAssemblyPath = RenameFile(fullAssemblyPath, metadataAssemblyName + ".bytes");
var inProjectAssemblyPath =
projectPath + "\\" + hybridCLRConfig.MetadataPath + "\\" + metadataAssemblyName + ".bytes";
// Копируем каждый скомпилированный файл в проект
MoveFile(newAssemblyPath, inProjectAssemblyPath);
}
}
Let's create a HotReloadEntry script in which we'll load all the DLLS and Metadata by Label, and then load the scene:
public class HotReloadEntry : MonoBehaviour {
public AssetReference StartSceneReference;
private void Awake() {
LoadDLLS().Forget();
}
public async UniTask LoadDLLS() {
try {
// Загружаем все библиотеки по Label
var dlls = await HotReloadAddressableAssetService.LoadByLabel<TextAsset>("DLLS");
foreach (var dll in dlls) {
#if !UNITY_EDITOR
// Загружаем библиотеки
Assembly hotUpdateAss = Assembly.Load(dll.bytes);
#endif
}
}
catch (Exception e) {
Debug.LogError($"Load DLLs error: {e.Message}");
}
try {
// Загружаем все метаданные по Label
var supplementaryMetadataDlls =
await HotReloadAddressableAssetService.LoadByLabel<TextAsset>("DLLSMetadata");
foreach (var dll in supplementaryMetadataDlls) {
#if !UNITY_EDITOR
// Загружаем метаданные через runtime библиотеку hybrid clr
var err = HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(dll.bytes, HomologousImageMode.SuperSet);
Debug.Log($"LoadMetadataForAOTAssembly");
#endif
}
}
catch (Exception e) {
Debug.LogError($"Load Metadata DLLs error: {e.Message}");
}
// Загружаем сцену, нак которой находится hot reload код
await StartSceneReference.LoadSceneAsync();
}
}
Let's hang this script on the object created on the HotReload scene and drop a link to the main game scene, which contains all the hot update code:
To automatically add library assets and metadata, we will use the Addressable importer, which adds assets from the specified folder to the Addressables group according to the created rules. It will eliminate the need to write handlers in the build script to set the group manually.
Create a settings file:
Fill in the settings file so that it adds all files from folders to a certain Addressables group and marks them with a Label. The required Label and folders must be created in advance.
Next, we call the code for building the project, which we added to any Editor folder in the project:
After successful assembly, DLLS and DLLSMetadata files should appear in the project. In the ServerData folder (depending on where you chose to assemble assets that should be uploaded remotely), a Remote catalog and assets will appear that will need to be uploaded to static hosting (in our case, github pages).
After that, we assemble a regular Apk file and check its functionality. If the game has launched and the scripts have downloaded, then everything will function as before.
In the project I chose to migrate to Hybrid CLR, when the chicken runs into a coin, it disappears. Let's make it so that when it collides with a coin, it doubles in size:
public void OnCoinGrab(GameObject coin) {
var position = coin.transform.position;
EffectManager.Instance.PlayCoinEffect(position);
AudioManager.Instance.PlayCoinEvent();
coin.transform.localScale *= 2;
//coin.SetActive(false);
}
After which we call:
We upload fresh files from ServerData to static storage and see updated behavior in the game without rebuilding the Apk:
If you have any difficulties during the integration process or have any questions, write to me in telegram. Read the documentation for the plugin, there is much more information there than I provided in the article.
Link to the repository with all the code from the article
See you all)