Unity热更新技术详解

Unity热更新技术详解

概述

Unity热更新(Hot Update)是指在不重新安装应用程序的情况下,动态更新游戏内容的技术。这种技术可以显著提升用户体验,减少版本迭代成本,并允许快速修复线上bug。

热更新方案分类

1. 资源热更新

主要包括贴图、模型、音频等资源的更新,使用Unity的AssetBundle系统实现。

2. 代码热更新

包括逻辑代码的动态更新,主要有以下几种方案:

  • Lua方案(xLua/toLua)
  • C#方案(ILRuntime/HybridCLR)
  • JavaScript方案(UnityWebView)

实现方案详解

一、AssetBundle资源热更新

1. AssetBundle打包策略
csharp 复制代码
using UnityEngine;
using UnityEditor;
using System.IO;

public class AssetBundleBuilder
{
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundles()
    {
        string assetBundleDirectory = "Assets/StreamingAssets";
        if (!Directory.Exists(assetBundleDirectory))
        {
            Directory.CreateDirectory(assetBundleDirectory);
        }

        BuildPipeline.BuildAssetBundles(assetBundleDirectory,
            BuildAssetBundleOptions.None,
            BuildTarget.StandaloneWindows);

        // 生成版本文件
        GenerateVersionFile(assetBundleDirectory);
    }

    static void GenerateVersionFile(string path)
    {
        VersionInfo versionInfo = new VersionInfo();
        versionInfo.version = System.DateTime.Now.ToString("yyyyMMddHHmmss");
        versionInfo.files = new System.Collections.Generic.List<FileInfo>();

        string[] files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
        foreach (string file in files)
        {
            if (!file.EndsWith(".meta") && !file.EndsWith(".manifest"))
            {
                FileInfo fileInfo = new FileInfo();
                fileInfo.name = Path.GetFileName(file);
                fileInfo.md5 = GetMD5Hash(file);
                fileInfo.size = new FileInfo(file).Length;
                versionInfo.files.Add(fileInfo);
            }
        }

        string versionJson = JsonUtility.ToJson(versionInfo, true);
        File.WriteAllText(Path.Combine(path, "version.json"), versionJson);
        AssetDatabase.Refresh();
    }

    static string GetMD5Hash(string filePath)
    {
        using (var md5 = System.Security.Cryptography.MD5.Create())
        {
            using (var stream = File.OpenRead(filePath))
            {
                byte[] hash = md5.ComputeHash(stream);
                return System.BitConverter.ToString(hash).Replace("-", "").ToLower();
            }
        }
    }
}

[System.Serializable]
public class FileInfo
{
    public string name;
    public string md5;
    public long size;
}

[System.Serializable]
public class VersionInfo
{
    public string version;
    public System.Collections.Generic.List<FileInfo> files;
}
2. 资源更新检测与下载
csharp 复制代码
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Collections.Generic;
using System.IO;

public class HotUpdateManager : MonoBehaviour
{
    private string serverUrl = "http://your-server.com/assetbundles/";
    private string localPath;
    private VersionInfo serverVersionInfo;
    private VersionInfo localVersionInfo;

    void Start()
    {
        localPath = Path.Combine(Application.persistentDataPath, "AssetBundles");
        StartCoroutine(CheckForUpdate());
    }

    IEnumerator CheckForUpdate()
    {
        // 检查本地版本
        string localVersionPath = Path.Combine(localPath, "version.json");
        if (File.Exists(localVersionPath))
        {
            string localVersionJson = File.ReadAllText(localVersionPath);
            localVersionInfo = JsonUtility.FromJson<VersionInfo>(localVersionJson);
        }

        // 获取服务器版本
        UnityWebRequest versionRequest = UnityWebRequest.Get(serverUrl + "version.json");
        yield return versionRequest.SendWebRequest();

        if (versionRequest.result == UnityWebRequest.Result.Success)
        {
            serverVersionInfo = JsonUtility.FromJson<VersionInfo>(versionRequest.downloadHandler.text);

            // 比较版本,检查是否需要更新
            if (localVersionInfo == null || localVersionInfo.version != serverVersionInfo.version)
            {
                Debug.Log("发现新版本,开始更新...");
                yield return StartCoroutine(DownloadUpdatedFiles());
            }
            else
            {
                Debug.Log("已是最新版本");
            }
        }
    }

    IEnumerator DownloadUpdatedFiles()
    {
        List<FileInfo> filesToDownload = new List<FileInfo>();

        // 确定需要下载的文件
        if (localVersionInfo == null)
        {
            filesToDownload = serverVersionInfo.files;
        }
        else
        {
            foreach (FileInfo serverFile in serverVersionInfo.files)
            {
                FileInfo localFile = localVersionInfo.files.Find(f => f.name == serverFile.name);
                if (localFile == null || localFile.md5 != serverFile.md5)
                {
                    filesToDownload.Add(serverFile);
                }
            }
        }

        // 下载文件
        foreach (FileInfo file in filesToDownload)
        {
            Debug.Log($"下载文件: {file.name}");
            yield return StartCoroutine(DownloadFile(file.name));
        }

        // 更新本地版本文件
        File.WriteAllText(Path.Combine(localPath, "version.json"),
            JsonUtility.ToJson(serverVersionInfo, true));

        Debug.Log("更新完成!");
    }

    IEnumerator DownloadFile(string fileName)
    {
        if (!Directory.Exists(localPath))
        {
            Directory.CreateDirectory(localPath);
        }

        string filePath = Path.Combine(localPath, fileName);
        string url = serverUrl + fileName;

        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            request.downloadHandler = new DownloadHandlerFile(filePath);
            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"下载失败: {fileName}, 错误: {request.error}");
            }
        }
    }
}

二、Lua代码热更新(xLua方案)

1. xLua集成与配置
csharp 复制代码
// LuaManager.cs - Lua环境管理器
using UnityEngine;
using XLua;

public class LuaManager : MonoBehaviour
{
    private static LuaManager instance;
    public static LuaManager Instance
    {
        get { return instance; }
    }

    private LuaEnv luaEnv;

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
            InitLuaEnv();
        }
        else
        {
            Destroy(gameObject);
        }
    }

    void InitLuaEnv()
    {
        luaEnv = new LuaEnv();

        // 添加自定义加载器
        luaEnv.AddLoader(CustomLoader);

        // 注入Unity相关类
        luaEnv.Global.Set("UnityEngine", typeof(UnityEngine));
        luaEnv.Global.Set("GameObject", typeof(GameObject));
        luaEnv.Global.Set("Transform", typeof(Transform));

        // 执行初始化脚本
        luaEnv.DoString("require('main')");
    }

    byte[] CustomLoader(ref string filepath)
    {
        string scriptPath = Path.Combine(Application.persistentDataPath, "LuaScripts", filepath + ".lua");

        if (File.Exists(scriptPath))
        {
            return File.ReadAllBytes(scriptPath);
        }

        // 如果本地没有,从Resources加载
        TextAsset luaScript = Resources.Load<TextAsset>("LuaScripts/" + filepath);
        if (luaScript != null)
        {
            return luaScript.bytes;
        }

        return null;
    }

    public void DoString(string script)
    {
        if (luaEnv != null)
        {
            luaEnv.DoString(script);
        }
    }

    public void Reload()
    {
        if (luaEnv != null)
        {
            luaEnv.Dispose();
        }
        InitLuaEnv();
    }

    void Update()
    {
        if (luaEnv != null)
        {
            luaEnv.Tick();
        }
    }

    void OnDestroy()
    {
        if (luaEnv != null)
        {
            luaEnv.Dispose();
        }
    }
}
2. Lua脚本示例
lua 复制代码
-- main.lua
local MainController = {}

function MainController:Start()
    print("Lua游戏逻辑启动")

    -- 创建游戏对象
    self.player = GameObject("Player")
    self.player.transform.position = Vector3(0, 0, 0)

    -- 添加组件
    local rigidbody = self.player:AddComponent(typeof(Rigidbody))

    -- 注册Update事件
    UpdateBeat:Add(self.Update, self)
end

function MainController:Update()
    if Input.GetKey(KeyCode.W) then
        self.player.transform:Translate(Vector3.forward * Time.deltaTime * 5)
    end

    if Input.GetKey(KeyCode.S) then
        self.player.transform:Translate(Vector3.back * Time.deltaTime * 5)
    end

    if Input.GetKey(KeyCode.A) then
        self.player.transform:Translate(Vector3.left * Time.deltaTime * 5)
    end

    if Input.GetKey(KeyCode.D) then
        self.player.transform:Translate(Vector3.right * Time.deltaTime * 5)
    end
end

function MainController:OnDestroy()
    UpdateBeat:Remove(self.Update, self)
end

-- 启动控制器
MainController:Start()
return MainController

三、HybridCLR C#热更新方案

1. HybridCLR配置与使用
csharp 复制代码
// HotFixManager.cs - C#热更新管理器
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using HybridCLR;

public class HotFixManager : MonoBehaviour
{
    private const string DLL_URL = "http://your-server.com/hotfix/";

    void Start()
    {
        StartCoroutine(LoadMetadataAndAOTDlls());
    }

    IEnumerator LoadMetadataAndAOTDlls()
    {
        // 加载元数据
        yield return LoadDll("mscorlib.dll");
        yield return LoadDll("System.Core.dll");
        yield return LoadDll("System.dll");

        // 加载AOT补充元数据
        yield return LoadDll("UnityEngine.CoreModule.dll");
        yield return LoadDll("Assembly-CSharp.dll");

        // 加载热更新DLL
        yield return LoadHotFixAssembly();

        // 执行热更新逻辑
        ExecuteHotFixLogic();
    }

    IEnumerator LoadDll(string dllName)
    {
        string dllPath = Path.Combine(Application.streamingAssetsPath, dllName);

        if (!File.Exists(dllPath))
        {
            Debug.LogError($"DLL文件不存在: {dllPath}");
            yield break;
        }

        byte[] dllBytes = File.ReadAllBytes(dllPath);
        LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes);

        if (err != LoadImageErrorCode.OK)
        {
            Debug.LogError($"加载AOT元数据失败: {dllName}, 错误码: {err}");
        }
        else
        {
            Debug.Log($"成功加载AOT元数据: {dllName}");
        }

        yield return null;
    }

    IEnumerator LoadHotFixAssembly()
    {
        // 从服务器获取最新的热更新DLL
        using (UnityWebRequest request = UnityWebRequest.Get(DLL_URL + "HotFix.dll"))
        {
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                byte[] dllBytes = request.downloadHandler.data;

                // 保存到本地
                string localPath = Path.Combine(Application.persistentDataPath, "HotFix.dll");
                File.WriteAllBytes(localPath, dllBytes);

                // 加载热更新程序集
                System.Reflection.Assembly hotFixAssembly = System.Reflection.Assembly.Load(dllBytes);

                // 缓存程序集
                HybridCLRManager.Instance.AddAssembly(hotFixAssembly);

                Debug.Log("热更新DLL加载成功");
            }
            else
            {
                // 如果服务器下载失败,尝试加载本地缓存
                string localPath = Path.Combine(Application.persistentDataPath, "HotFix.dll");
                if (File.Exists(localPath))
                {
                    byte[] dllBytes = File.ReadAllBytes(localPath);
                    System.Reflection.Assembly hotFixAssembly = System.Reflection.Assembly.Load(dllBytes);
                    HybridCLRManager.Instance.AddAssembly(hotFixAssembly);

                    Debug.Log("使用本地缓存的热更新DLL");
                }
                else
                {
                    Debug.LogError("无法加载热更新DLL");
                }
            }
        }
    }

    void ExecuteHotFixLogic()
    {
        try
        {
            // 通过反射执行热更新代码
            var hotFixType = HybridCLRManager.Instance.GetType("HotFix.HotFixMain");
            if (hotFixType != null)
            {
                var method = hotFixType.GetMethod("Start");
                if (method != null)
                {
                    method.Invoke(null, null);
                    Debug.Log("热更新逻辑执行成功");
                }
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"执行热更新逻辑失败: {e}");
        }
    }
}
2. 热更新DLL项目结构
csharp 复制代码
// HotFixMain.cs - 热更新入口类
using UnityEngine;

namespace HotFix
{
    public class HotFixMain
    {
        public static void Start()
        {
            Debug.Log("热更新代码已启动");

            // 创建热更新管理器
            var go = new GameObject("HotFixManager");
            go.AddComponent<HotFixBehaviour>();
        }
    }

    public class HotFixBehaviour : MonoBehaviour
    {
        void Start()
        {
            // 热更新初始化逻辑
            Debug.Log("热更新行为组件已初始化");
        }

        void Update()
        {
            // 热更新帧逻辑
            if (Input.GetKeyDown(KeyCode.H))
            {
                Debug.Log("热更新按键被触发");
                // 执行热更新特定功能
            }
        }
    }
}

最佳实践

1. 版本控制

csharp 复制代码
public class VersionController
{
    private const string VERSION_FILE = "version.json";

    public static VersionInfo GetCurrentVersion()
    {
        string path = Path.Combine(Application.persistentDataPath, VERSION_FILE);
        if (File.Exists(path))
        {
            return JsonUtility.FromJson<VersionInfo>(File.ReadAllText(path));
        }
        return new VersionInfo { version = "1.0.0" };
    }

    public static void UpdateVersion(VersionInfo newVersion)
    {
        string path = Path.Combine(Application.persistentDataPath, VERSION_FILE);
        File.WriteAllText(path, JsonUtility.ToJson(newVersion, true));
    }
}

2. 安全校验

csharp 复制代码
public class SecurityChecker
{
    public static bool VerifyFileIntegrity(string filePath, string expectedMD5)
    {
        if (!File.Exists(filePath))
            return false;

        string actualMD5 = CalculateMD5(filePath);
        return string.Equals(actualMD5, expectedMD5, StringComparison.OrdinalIgnoreCase);
    }

    private static string CalculateMD5(string filePath)
    {
        using (var md5 = System.Security.Cryptography.MD5.Create())
        {
            using (var stream = File.OpenRead(filePath))
            {
                byte[] hash = md5.ComputeHash(stream);
                return System.BitConverter.ToString(hash).Replace("-", "").ToLower();
            }
        }
    }
}

3. 断点续传

csharp 复制代码
public class ResumableDownloader
{
    public static IEnumerator DownloadWithResume(string url, string savePath, Action<float> onProgress)
    {
        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            long existingLength = 0;

            if (File.Exists(savePath))
            {
                existingLength = new FileInfo(savePath).Length;
                request.SetRequestHeader("Range", $"bytes={existingLength}-");
            }

            var downloadHandler = new DownloadHandlerFile(savePath, true);
            request.downloadHandler = downloadHandler;

            var operation = request.SendWebRequest();

            while (!operation.isDone)
            {
                float progress = (existingLength + request.downloadedBytes) / (float)request.GetResponseHeader("Content-Length").ToLong();
                onProgress?.Invoke(progress);
                yield return null;
            }

            if (request.result == UnityWebRequest.Result.Success)
            {
                onProgress?.Invoke(1f);
            }
            else
            {
                File.Delete(savePath);
                Debug.LogError($"下载失败: {request.error}");
            }
        }
    }
}

注意事项

  1. 平台兼容性:不同平台的热更新方案支持情况不同,需要针对目标平台选择合适的技术方案。

  2. 性能优化

    • 资源压缩减少下载量
    • 增量更新减少更新时间
    • 使用CDN加速下载
  3. 安全考虑

    • DLL加密防止代码泄露
    • 资源完整性校验
    • 防止中间人攻击
  4. 用户体验

    • 提供更新进度提示
    • 支持后台下载
    • 网络异常处理

总结

Unity热更新技术是现代游戏开发中的重要组成部分,通过合理的架构设计和实现方案,可以显著提升游戏的迭代效率和用户体验。选择合适的热更新方案需要综合考虑项目需求、性能要求、安全性和开发复杂度等因素。随着Unity技术的发展,热更新方案也在不断演进,开发者需要持续关注最新的技术动态,以便选择最适合自己项目的解决方案。

相关推荐
地狱为王10 小时前
Cesium for Unity 去除Cesium Logo
unity·游戏引擎·cesium
BuHuaX10 小时前
Lua入门
开发语言·unity·junit·c#·游戏引擎·lua
wonder1357912 小时前
RectTransform位置计算方法和UI自适应
ui·unity·ugui
世洋Blog13 小时前
Unity发布自己的插件包
unity·游戏引擎
ytttr8731 天前
基于C#的CAN总线数据解析BMS上位机
android·unity·c#
雪下的新火1 天前
ASE07-魔法药剂炼制效果
经验分享·unity·shader·ase·游戏效果
璞瑜无文1 天前
Unity 游戏开发之入门
unity·游戏引擎
一线灵1 天前
Axmol 引擎系列教程之 - 如何切换引擎依赖库镜像
游戏引擎
毛甘木1 天前
Unity ComputeShader 基础语法与使用教程
unity·computeshader