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}");
}
}
}
}
注意事项
-
平台兼容性:不同平台的热更新方案支持情况不同,需要针对目标平台选择合适的技术方案。
-
性能优化:
- 资源压缩减少下载量
- 增量更新减少更新时间
- 使用CDN加速下载
-
安全考虑:
- DLL加密防止代码泄露
- 资源完整性校验
- 防止中间人攻击
-
用户体验:
- 提供更新进度提示
- 支持后台下载
- 网络异常处理
总结
Unity热更新技术是现代游戏开发中的重要组成部分,通过合理的架构设计和实现方案,可以显著提升游戏的迭代效率和用户体验。选择合适的热更新方案需要综合考虑项目需求、性能要求、安全性和开发复杂度等因素。随着Unity技术的发展,热更新方案也在不断演进,开发者需要持续关注最新的技术动态,以便选择最适合自己项目的解决方案。
