HybrydCLR. Cómo actualizar el código Unity de un juego sin descargar actualizaciones a la tienda / Sudo Null IT News
Introducción
Hoy quiero presentarles un complemento para Unity que les permite actualizar el código del juego sin cargar actualizaciones en la tienda. Funciona mediante una modificación de il2cpp, convirtiéndolo en algo parecido a Mono.
Hay muchas soluciones de actualización en caliente en el mercado, pero todas tienen restricciones en la interacción de la actualización en caliente y el código AOT, o solo pueden actualizar parte del código a través de atributos y otros tipos de granjas colectivas.
Como dicen los desarrolladores:
CLR híbrido es una solución nativa para la actualización en caliente del código C#. En pocas palabras, el código compilado il2cpp es equivalente al módulo aot en mono, y HybridCLR es equivalente al intérprete, y su combinación se vuelve mono completo. HybridCLR convierte a il2cpp en un entorno de ejecución con todas las funciones, de forma nativa (es decir, a través de System.Reflection.Assembly.Load), lo que permite que los archivos DLL se carguen dinámicamente, lo que hace posibles las actualizaciones en caliente en iOS.
Debido a que HybridCLR se implementa en el nivel de tiempo de ejecución nativo, los tipos de las bibliotecas de actualización en caliente y los tipos del código AOT son equivalentes y están perfectamente unificados. Puede llamar, heredar, usar reflexión y subprocesos múltiples, sin generar código ni escribir adaptadores.
Otras soluciones de actualización en caliente son independientes de VM y vincular con il2cpp es esencialmente equivalente a vincular la incrustación de lua en mono. Por tanto, el sistema de tipos no es uniforme. Para permitir que un tipo de actualización en caliente herede algunos tipos de AOT, se debe escribir un adaptador y el sistema de tipos del proyecto principal no puede reconocer el tipo en el intérprete. Funciones incompletas, desarrollo problemático y rendimiento deficiente.
Como dicen sobre la publicación en Google Play y App Store:
HybridCLR es muy popular en China, actualmente hay al menos cientos de juegos que usan HybridCLR, todos ellos están disponibles en App Store y Google Play.
HybridCLR tiene que ver con la interpretación y la ejecución, y desde esa perspectiva el enfoque no es diferente de la integración y el intérprete lua de Unity. Por lo tanto, cumple con los requisitos de App Store y Google Play Store y no existe ningún riesgo particular de rechazo. Y debido a la alta integración de HybridCLR e il2cpp, es incluso mucho más seguro que el esquema lua, por lo que la probabilidad de que el juego sea rechazado por incumplimiento de las reglas del sitio es muy baja.
HybridCLR hace lo siguiente:
Implementación de una biblioteca de análisis de metadatos eficiente (dll)
Cambios en el módulo de gestión de metadatos para implementar el registro dinámico de metadatos
Implementación del compilador desde un conjunto de instrucciones IL hasta un conjunto de instrucciones de registro personalizado
Implementación de un intérprete de registros eficiente
Proporcionar una gran cantidad de funciones instintivas para mejorar el desempeño del intérprete.
Cosas interesantes:
Código de puntos de actualización en caliente, pero es necesario instalar su complemento. Unitech realizó una inicialización temprana de TypeManager sin la posibilidad de llamarlo manualmente; los chicos tuvieron que agregarlo ellos mismos. Aún no se ha entregado el soporte Burst, pero Jobs está funcionando y actualizándose
actualización en caliente de código asincrónico
aprendizaje cero y costos de uso cero
Instalación
https://hybridclr.doc.code-philosophy.com/en/docs/beginner/quickstart
Instale el complemento a través del Administrador de paquetes:
A continuación, inicializamos el complemento:
Configuración
Como ejemplo, tomaré mi proyecto necro de 2019:
En Unity, todo el código que escribe un desarrollador que no está en otra definición de ensamblaje está contenido en Assembly-CSharp. El código del proyecto debe dividirse en una compilación AOT (es decir, compilada en el paquete principal del juego) y una compilación de actualización en caliente. En HybridCLR no hay restricciones sobre cómo dividir el ensamblado, e incluso el código de un proyecto de terceros se puede utilizar como una actualización en caliente. Cuando se inicia el juego, al menos un ensamblado de AOT debe ser responsable del trabajo asociado con la ejecución del código de actualización en caliente. Hay dos formas de configurar el código personalizado de ensamblaje AOT y actualización en caliente, según la configuración actual de su proyecto:
Assembly-CSharp como punto de entrada AOT. El resto del código en sí se divide en ensamblajes N AOT y ensamblajes de actualización en caliente M.
Assembly-CSharp como ensamblado de actualización en caliente. El resto del código en sí se divide en ensamblajes N AOT y ensamblajes de actualización en caliente M.
En el ejemplo, crearemos una definición de ensamblaje Maikn y el código de actualización en caliente estará en Assembly-CSharp, después de lo cual se cargará la escena o prefabricado, que contiene el código de actualización en caliente con el punto de entrada.
Puede usar cualquier cosa como sistema para administrar recursos y cargar activos: incluso AssetBundles básicos, incluso Addressables o incluso su propio sistema de administración de activos con carga desde su servidor.
En este ejemplo, usaré Addressables como la opción más sencilla de integración y configuración, que no lleva mucho tiempo (la mayoría de las veces esto no es cierto, por supuesto).
Aclaración importante: si parte del código se incluye en la actualización en caliente, no debería haber ningún vínculo explícito entre la parte AOT y la parte de la actualización en caliente. Aquí, o su proyecto tiene una división clara en áreas de responsabilidad por escenas/prefabricados/definición de ensamblaje, o todo su proyecto está en actualización en caliente y cargado desde un punto de entrada que contiene solo código AOT.
En el ejemplo, se utilizará Assembly-CSharp como punto de entrada de AOT con la parte de actualización en caliente cargada e inicializada.
Para hacer esto, creemos una Definición de ensamblaje principal y arrastremos todo el código a ella:
A continuación, creemos un ensamblaje principal y arrastremos todo el código dentro de la carpeta:
A continuación, creemos 3 grupos de direccionables:
Grupo para nuestras bibliotecas de actualización activa (en este caso Principal)
Grupo de metadatos. Los metadatos son parte de las dependencias externas de la DLL, que se consumen al eliminarlas debido a que en el momento del ensamblaje el proyecto no tiene dependencias de ninguna de ellas (debido al hecho de que el código se transfiere a la actualización en caliente desde el parte del proyecto compilado en el momento de la asamblea de la AOT).
Grupo para la escena del juego, que cargará el código AOT desde los direccionables.
A continuación, en Configuración del proyecto, en la pestaña de configuración de HybridCLR, agregue la biblioteca de actualización en caliente creada anteriormente a la lista Definiciones de ensamblaje de actualización en caliente:
Puede actualizar utilizando la actualización en caliente solo aquellas bibliotecas que estaban en la lista en el momento en que se creó el proyecto. Relativamente hablando, si creamos una apk para Android y queremos agregar otro Dll Core, esto no será posible.
Para esto, existe un campo especial Preservar ensamblajes de actualización en caliente, en el que puede escribir los nombres de las bibliotecas que se pueden agregar en el futuro:
Puede actualizar no solo el código de usuario, sino también complementos de terceros, por ejemplo, UniTask, Dotween, etc., así como archivos DLL normales que ya están importados al proyecto en forma compilada.
Creemos una escena Hot Reload en la que se colgarán el cargador de escenas y las bibliotecas con metadatos:
Creo un repositorio simple en páginas de github para alojar archivos estáticos (no escribiré instrucciones, es fácil de encontrar), creo un perfil de direcciones, lo actualizo y establezco enlaces en su configuración:
Expongo rutas de compilación y carga de DLLS y DLLSMetadata en forma remota en los grupos de Direcciones:
Por si acaso, configuro el modo de nomenclatura del paquete en DLLS y DLLSMetadata Filename para que el hash de nomenclatura no cambie constantemente:
No olvide configurar Crear catálogo remoto en Configuración de activos direccionables:
A continuación, realizaremos la generación inicial de todos los datos de Hybrid CLR. El proceso puede llevar tiempo, no te alarmes.
A continuación, en cualquier carpeta del Editor, cree el siguiente script, que creará bibliotecas de actualización en caliente, las actualizará en carpetas y recopilará direcciones. El script utiliza extensiones que estarán disponibles a través del enlace en el repositorio al final del artículo.
(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);
}
}
Creemos un script HotReloadEntry en el que cargaremos todos los DLLS y Metadatos por Etiqueta, luego de lo cual cargaremos la escena:
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();
}
}
Colguemos este script en el objeto creado en la escena HotReload y anulemos el enlace a la escena principal del juego, que contiene todo el código de actualización en caliente:
Para agregar automáticamente recursos y metadatos de la biblioteca, usaremos el importador de direcciones, que, de acuerdo con las reglas creadas, agrega recursos de la carpeta especificada al grupo Direcciones. Elimina la necesidad de escribir controladores en el script de compilación para configurar el grupo manualmente.
Cree un archivo de configuración:
Completamos el archivo de configuración para que agregue todos los archivos de las carpetas a un grupo de Direcciones específico y los marque con Etiqueta. Primero se deben crear las etiquetas y carpetas requeridas.
A continuación, llamamos al código para ensamblar el proyecto, que agregamos a cualquier carpeta del Editor en el proyecto:
Después de una compilación exitosa, los archivos DLLS y DLLSMetadata deberían aparecer en el proyecto. En la carpeta ServerData (dependiendo de dónde seleccionó el conjunto de activos que deben cargarse de forma remota), aparecerá un catálogo remoto y activos que deberán cargarse en un alojamiento estático (en nuestro caso, páginas de github).
Luego recopilamos un archivo apk normal y comprobamos su funcionalidad. Si el juego se inicia y se descargan los scripts, todo funcionará igual que antes.
En el proyecto elegí cambiar a Hybrid CLR, cuando un pollo se topa con una moneda, la moneda desaparece. Hagamos que se duplique cuando golpee una moneda:
public void OnCoinGrab(GameObject coin) {
var position = coin.transform.position;
EffectManager.Instance.PlayCoinEffect(position);
AudioManager.Instance.PlayCoinEvent();
coin.transform.localScale *= 2;
//coin.SetActive(false);
}
Luego llamamos:
Subimos archivos nuevos de ServerData al almacenamiento estático y vemos el comportamiento actualizado en el juego sin reconstruir la apk:
Si tienes alguna dificultad durante el proceso de integración o tienes alguna duda, escríbeme por telegram. Lea la documentación del complemento, hay mucha más información allí que la que proporcioné en el artículo.
Enlace al repositorio con todo el código del artículo
Nos vemos a todos)
Más información y anuncios de artículos en mi canal de telegram, donde hablo de Unity https://t.me/dedpilit.