说实话,刚入行做游戏开发的时候,我对“热更新”这个词的理解特别片面。那时候我觉得,热更新不就是把脚本换掉嘛,有啥难的?直到我第一次上线正式服,第二天早上收到运营的电话说有个严重的数值Bug,玩家吐槽不停。如果走正式打包上架流程,光是苹果审核就要等两三天,那时候黄花菜都凉了。
也就是那次之后,我才真正开始深入钻研热更新的底层逻辑。今天我就把这些年踩过的坑、总结的经验,掰开了揉碎了讲给你听。无论你是用Unity还是Cocos Creator,原理是相通的,关键是要懂“为什么”,而不是只会套模板。
为什么我们非得搞热更新?
先别急着看代码,咱们得先搞清楚这件事的本质。想象一下,你的游戏发出去了,突然发现了个致命Bug,或者运营想要临时调整一下抽奖概率。
没有热更新的日子里:
- 你修复Bug。
- 重新打包APK/IPA。
- 上传到Google Play/App Store审核。
- 等待审核通过(短则几小时,长则一周)。
- 用户下载新包。
这一套流程下来,小公司的服务器可能已经炸了,大公司的财报可能已经难看了。
有了热更新之后:
- 你修复Bug。
- 把修复后的脚本和资源打成补丁包(Bundle/AssetBundle)。
- 用户进入游戏,客户端检查版本号,发现有新补丁。
- 用户下载补丁,本地加载,Bug修复,无需重启或仅需轻量重启。
- 整个过程可能在几秒到几分钟内完成,完全在玩家感知范围内。
热更新的核心价值就两个词:速度和存活。它让游戏产品在上线后依然拥有“生长”和“纠错”的能力。
热更新的底层原理:到底换的是什么?
很多小白有个误区,认为热更新就是“替换整个游戏”。其实不是。
热更新的本质是动态加载与资源置换。
我们可以把游戏想象成一个乐高城堡。
- 核心骨架:Unity/Cocos引擎本身、基础的C#或TypeScript运行时。这部分通常随安装包一起下发,比较稳定,不容易变。
- 积木块:你的游戏逻辑代码(脚本)、美术资源(贴图、模型)、配置表(Excel转出的JSON)。这部分是变化最快的。
热更新的工作原理就是:
- 版本校验:客户端启动时,问服务器“你最新版的积木块清单是什么?版本号是多少?”
- 差异对比:客户端拿出自己本地的清单,和服务器对比。发现哪些积木块是新加的,哪些是改过的。
- 按需下载:只下载那些有变化的积木块。
- 运行时加载:当游戏运行到需要用到这些积木的地方(比如打开一个UI界面),动态加载最新的积木块,替换掉旧的。
这里有个关键概念要记住:IL2CPP vs JIT vs ES6。
Unity (C#):IL2CPP是将C#编译成C++再编译成机器码。这意味着你的C#代码最终变成了.so文件(Android)或.framework(iOS)。如果你想热更新C#代码,传统手段很难直接替换编译后的机器码。所以Unity的热更新通常有两种流派:
- Mono后端:代码以DLL形式存在,可以直接替换DLL。这是最经典的热更新方式,但iOS不支持Mono(Apple严禁JIT),所以主要用在Android。
- IL2CPP后端 + 脚本语言:在Unity里跑Lua(如xLua、ToLua)或C#热更方案(如DOTS、HotReload)。
Cocos Creator (TypeScript):Cocos Creator本质是Web技术栈。TypeScript编译后是JavaScript。浏览器(以及嵌入浏览器的WebView)天然支持动态执行JavaScript。所以Cocos的热更新逻辑比Unity更“原生”——你可以直接替换.ts编译出来的.js文件。
Unity热更新实战:两条腿走路
Unity的热更新体系比较复杂,我建议你根据项目需求选择技术栈。
方案一:Lua热更新(经典、稳定、生态成熟)
这是目前市场上90%的商业手游采用的方案。核心思路是:用Lua写核心逻辑,C#写底层接口。C#代码打包进安装包,Lua脚本作为热更资源。
为什么选Lua?
- Lua运行速度快,语法简单。
- 生态极其成熟,tolua、xLua、SLua都有完善的社区支持。
- 不受iOS JIT限制,因为Lua不是执行JIT编译的代码,而是解释执行的。
1. 集成xLua(以Unity 2021 + xLua为例)
首先,你需要在Unity Asset Store下载xLua插件并导入。
第一步:生成C#与Lua的绑定代码
在Unity编辑器菜单栏,点击 Lua -> Generate Code。这会根据你指定的C#类生成对应的Lua引用代码。
第二步:编写主入口Lua脚本
创建一个Main.lua,这是游戏的入口。
-- Main.lua
print("游戏开始初始化...")
-- 引入C#命名空间
local UnityEngine = CS.UnityEngine
local MyGame = CS.MyGame -- 假设你有个C#类叫MyGame
-- 初始化游戏逻辑
local function OnInit()
print("Lua逻辑加载完成")
-- 这里调用C#接口
MyGame.InitGame()
end
-- 监听事件
local function OnUpdate()
-- 这里处理游戏循环中的逻辑
-- 比如检查网络、处理输入等
end
-- 启动
OnInit()
UnityEngine.Time.register_Update(OnUpdate)
2. 热更新资源打包
你需要将Lua脚本、图片、预制体等打成AssetBundle。
// Editor/BuildAssetBundles.cs
using UnityEditor;
using System.IO;
public class BuildAssetBundles
{
[MenuItem("AssetBundle/Build All")]
static void Build()
{
string outputDir = "AssetBundles";
if (!Directory.Exists(outputDir))
Directory.CreateDirectory(outputDir);
BuildPipeline.BuildAssetBundles(outputDir,
BuildAssetBundleOptions.ChunkBasedCompression, // 使用Chunk压缩,节省内存
BuildTarget.StandaloneWindows64); // 先在Windows测试,再切到Android/iOS
}
}
打包出来后,你会得到一堆.assetbundle文件和AssetBundles-Manifest文件。把这些文件上传到你的CDN服务器。
3. 客户端热更逻辑
// HotUpdateManager.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
public class HotUpdateManager : MonoBehaviour
{
private string remoteUrl = "https://your-cdn-server.com/hotfix/";
private string localPath = Application.persistentDataPath + "/HotUpdate/";
public void StartCheckUpdate()
{
StartCoroutine(CheckAndDownload());
}
IEnumerator CheckAndDownload()
{
// 1. 下载服务器上的version.txt,获取远程版本号
using (UnityWebRequest www = UnityWebRequest.Get(remoteUrl + "version.txt"))
{
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.LogError(www.error);
yield break;
}
string remoteVersion = www.downloadHandler.text;
string localVersion = ReadLocalVersion();
if (remoteVersion != localVersion)
{
// 2. 有更新,下载AssetBundles
DownloadAssetBundles(remoteUrl, localPath);
}
else
{
Debug.Log("已是最新版本");
// 3. 加载Lua
LoadLua();
}
}
}
void DownloadAssetBundles(string url, string path)
{
// 实际项目中建议用AsyncHTTP或Addressables
// 这里简化演示
Debug.Log("开始下载补丁包...");
}
void LoadLua()
{
// 加载本地Lua脚本
string luaPath = Path.Combine(localPath, "Main.lua");
if (File.Exists(luaPath))
{
XLua.LuaEnv luaEnv = new XLua.LuaEnv();
luaEnv.DoScript(File.ReadAllText(luaPath));
}
}
}
方案二:C#热更(Unity 2020+ 推荐)
如果你不想引入Lua,希望全程用C#开发,Unity官方和第三方提供了很好的方案。
Unity内置方案:IL2CPP + 脚本热更(早期版本) 实际上,Unity官方并不推荐直接热更IL2CPP编译出的.dll,因为IL2CPP优化的目的是性能,直接替换代码风险极大。
主流第三方方案:DOTween、HybridCLR、ILRuntime
这里重点介绍HybridCLR,它是目前Unity社区最火的C#热更方案,完全免费且开源,支持IL2CPP。
HybridCLR原理: 它不是简单地替换DLL,而是通过一种“混合运行模式”,让IL2CPP编译的代码能够加载并执行热更新后的IL代码(存储在资源中)。
使用步骤简述:
- 安装HybridCLR插件:从GitHub下载或在Unity Package Manager添加。
- 标记热更代码:在你想要热更的C#类上加特性
[GenerateHotfix]。[GenerateHotfix] public class PlayerController : MonoBehaviour { public void Move() { // 原始逻辑 } } - 生成修补代码:插件会自动生成修补所需的中间代码。
- 打包热更资源:使用HybridCLR的打包工具,将包含热更代码的DLL打成AB包。
- 运行时加载:
// 运行时加载热更DLL var assembly = HybridCLR.RuntimeApi.LoadDll(PathToDll); // 然后正常调用
HybridCLR的优点是全程C#,调试方便,且对IL2CPP兼容性好,解决了iOS无法运行JIT代码的痛点。
Cocos Creator热更新:Web技术的天然优势
Cocos Creator的热更新比Unity简单得多,因为它的底层是浏览器。你只需要理解如何替换JS文件和如何替换资源。
1. 资源热更(Asset Bundle)
Cocos Creator内置了Asset Bundle系统。
发布设置:
在Creator编辑器中,选择 构建发布,平台选 Android 或 iOS。在 打包选项 中,勾选 打包资源 为 AssetBundle。
这会生成一个assets文件夹,里面全是.ab文件。
热更逻辑(TypeScript):
import { _decorator, Component, AssetManager, Assets, resources } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('HotUpdateManager')
export class HotUpdateManager extends Component {
private remoteVersionUrl = "https://your-cdn.com/version.json";
private localVersionUrl = "local://version.json"; // 本地存储的版本
private remoteBundleUrl = "https://your-cdn.com/asset-bundles/";
private localBundleUrl = "res://bundles/";
onLoad() {
this.checkUpdate();
}
async checkUpdate() {
// 1. 获取远程版本信息
const remoteVersionInfo = await this.fetchRemoteVersion();
// 2. 获取本地版本信息
const localVersionInfo = await this.fetchLocalVersion();
if (remoteVersionInfo.version !== localVersionInfo.version) {
console.log("发现新版本,开始下载...");
await this.downloadUpdate(remoteVersionInfo.urls);
this.saveLocalVersion(remoteVersionInfo);
}
// 3. 初始化游戏
this.initGame();
}
async fetchRemoteVersion() {
// 使用fetch或XMLHttpRequest获取服务器上的manifest
const response = await fetch(this.remoteVersionUrl);
return await response.json();
}
async downloadUpdate(urls: string[]) {
for (const url of urls) {
// 使用AssetManager下载单个bundle
const bundleName = url.split('/').pop()?.split('.')[0];
const request = new AssetManager.RequestItem();
AssetManager.downloader.downloadBuffer(url, {
type: AssetManager.BuiltinFileType.BINARY
}, (error, data) => {
if (error) {
console.error("下载失败", error);
} else {
// 保存到本地文件系统
// 注意:移动端需要处理文件读写权限
this.saveToLocal(bundleName, data);
}
});
}
}
initGame() {
// 加载本地资源启动游戏
resources.load("scenes/main", (err, asset) => {
if (err) console.error(err);
else console.log("游戏启动成功");
});
}
}
2. 代码热更(TypeScript/JavaScript)
这是Cocos比Unity方便的地方。你可以直接将热更后的.js文件下载到本地,然后使用require或import动态加载,或者更简单地,修改cc.sys.localStorage中的逻辑,重新加载整个脚本模块。
动态加载脚本示例:
// 假设你有一个远程脚本地址
const scriptUrl = "https://your-cdn.com/scripts/patch_player.js";
// 创建script标签注入
const script = document.createElement('script');
script.src = scriptUrl;
script.onload = () => {
console.log("脚本加载成功,新逻辑已生效");
// 重新实例化玩家控制器
this.reInitPlayer();
};
document.head.appendChild(script);
注意:这种方式依赖于浏览器环境。在Cocos Creator构建为原生包(Android/iOS)时,实际上还是运行在一个WebView或JSCore环境中,所以理论上可行。但为了稳定性,通常建议将代码热更和资源热更结合:把逻辑封装在资源包里的TS文件中,通过AssetManager加载整个Bundle,而不是单独替换JS文件。
Android与iOS的兼容方案:最容易踩坑的地方
热更新做好了,如果无法上架,那一切都是白费。iOS和Android在热更新上有巨大的差异。
Android:相对宽松
Android只要你的APK签名正确,安装时没有错误,基本上你想怎么热更就怎么热更。下载的文件可以放在Application.externalDataPath或Application.persistentDataPath。
关键点:
- 确保网络权限
INTERNET已开启。 - 使用
UnityWebRequest或HttpClient下载文件。 - 注意文件大小,避免主包过大被用户卸载(虽然热更包在主包外,但用户体验要考虑)。
iOS:严格管控
苹果对热更新有严格的规定,这也是为什么Unity的Mono方案在iOS上行不通的核心原因。
苹果的红线:
- 禁止下载并执行可执行代码:你不能下载一个
.app或动态链接库并在运行时加载执行(这被称为JIT或热代码替换)。 - 必须通过App Store审核:任何影响App主要功能的更新,理论上都应该经过审核。
那么,iOS热更新是怎么做到的?
答案是:绕过执行代码,只热更数据。
策略一:Lua脚本方案(Unity iOS)
Lua是解释型语言。你下载的Lua文件是纯文本,运行时由Lua解释器解释执行。苹果允许这种做法,因为Lua代码不是“可执行二进制代码”,而是数据。
- 前提:你的核心逻辑必须用Lua写,或者至少关键的热更逻辑用Lua写。
- 操作:下载
.lua文件到沙盒目录,然后加载执行。
策略二:C#代码方案(Unity iOS - HybridCLR)
HybridCLR等方案通过特定的方式,将热更的C#代码编译成IL(中间语言),存储在资源包中。运行时,通过CLR虚拟机解释执行这些IL。这同样避免了执行原生机器码,符合苹果规定。
- 前提:使用HybridCLR或类似的VM方案。
- 操作:打包时生成IL资源,运行时加载IL并解释执行。
策略三:Cocos Creator的“远程服务器配置”方案
Cocos Creator官方推荐的方式是不要替换核心代码,只替换资源。
操作:
- 将需要变更的内容(如UI配置、文案、道具数值)做成JSON配置文件。
- 上传到服务器。
- 客户端启动时读取服务器配置。
- 如果配置有变化,就用新配置覆盖本地配置。
- 对于美术资源,使用Asset Bundle热更。
优点:完全合规,审核秒过,因为你的App二进制文件从未改变。
缺点:逻辑Bug无法通过这种方式修复,只能重新发版。
策略四:云游戏/小程序模式(终极方案)
如果你发现某次更新改动太大,涉及核心逻辑,又担心iOS审核:
- 方案:将游戏主体做成微信小游戏或抖音小游戏,通过云游戏服务器串流到App。
- 或者:使用Cocos的“远程引擎”能力,将核心游戏逻辑部署在服务器上,客户端只负责渲染。
兼容方案总结表
| 特性 | Android | iOS |
|---|---|---|
| 资源热更 (AB包) | ✅ 完全支持 | ✅ 完全支持 |
| Lua脚本热更 | ✅ 完全支持 | ✅ 完全支持 (解释型) |
| C# DLL热更 (Mono) | ✅ 支持 | ❌ 禁止 (JIT限制) |
| C#代码热更 (HybridCLR) | ✅ 支持 | ✅ 支持 (IL解释执行) |
| 纯JS代码热更 (Cocos) | ✅ 支持 | ⚠️ 部分支持 (视具体实现) |
| 审核风险 | 低 | 高 (若检测到动态代码执行) |
