HybrydCLR. How to update Unity game code without downloading updates to the store

Picture from the official website

Picture from the official website

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.

Link to documentation

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.

Changes to the user code execution scheme

Changes to the user code execution scheme

HybridCLR does the following:

  1. Implementation of an efficient metadata analysis library (dll)

  2. Changes to the metadata management module to implement dynamic metadata registration

  3. Implementation of a compiler from the IL instruction set to the custom register instruction set

  4. Implementing an effective register interpreter

  5. Providing a large number of instinctive functions to improve the interpreter's performance

Interesting facts:

  1. 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

  2. hot update of async code

  3. zero learning and zero usage costs

Installation

https://hybridclr.doc.code-philosophy.com/en/docs/beginner/quickstart

Install the plugin via Package Manager:

Link to the plugin

Next, we initialize the plugin:

Need to initialize plugin

Need to initialize plugin

Click install

Click install

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:

Create a folder for Main assembly

Create a folder for Main assembly

Next, let's create the Main assembly and drag all the code inside the folder:

Create Main assembly

Create Main assembly

Next, we will create 3 Addressables groups:

  1. Group for our hot update libraries (in this case Main)

  2. 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).

  3. Group for the game scene that will be loaded by AOT code from addressables

Created Addressable Groups

Created Addressable Groups

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:

Add the Main library to the list

Add the Main library to the 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:

Create a HotReload scene

Create a HotReload scene

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:

The exposed paths to github pages hosting in the Addressables profile

The exposed paths to github pages hosting in the Addressables profile

I set the DLLS and DLLSMetadat Build & Load Paths to Remote in the Addressables groups:

I make it so that assets from this group are downloaded from a remote address

I make it so that assets from this group are downloaded from a remote address

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:

Let's hang the script on the HotReload scene on a new object

Let's hang the script on the HotReload scene on a new object

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.

Link to addresable importer

Create a settings file:

Create a settings file

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.

Setting up rules for adding to a group

Setting up rules for adding to a group

Next, we call the code for building the project, which we added to any Editor folder in the project:

Calling the assembly

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).

Metadata after a successful build

Metadata after a successful build

Libraries after successful build

Libraries after successful build

These files need to be uploaded to the hosting

These files need to be uploaded to the hosting

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)

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *