黑神话上线了,想起来学Lua了?
👉目录
1 免责(shuai guo)声明
2 前言
3 游戏行业热更新技术史
4 热更新方案选型与架构设计
5 Asset Bundle + dll 替换反射 C# 热更新方案穿刺验证报告
6 小结
近年来,随着游戏行业的发展和国产游戏的崛起,越来越多的作品选择了 Unreal 引擎,《黑神话:悟空》这一旷世大作便是基于 Unreal 开发,更多我们熟知的游戏如《原神》、《王者荣耀》则是基于 Unity 开发。
鲁迅曾经说过:一个成功游戏引擎的背后,离不开一个默默支持他的热更脚本,不论你使用 Unreal 还是 Unity,不论你写 C++ 还是 C#,开发一款大型游戏,总是离不开热更新技术的支持,也离不开背后默默支持你热更的 Lua。
本文作者及其团队以 Lua 与游戏行业热更新技术史为基础,创作了这样一篇女朋友也能看懂的科普向技术好文,看完以后记得分享给你的女朋友哦!(你是怎么知道我女朋友给我买了典藏版的?)
01
02
03
------------------------------------------------------------------------- file or directory exists-----------------------------------------------------------------------function os.path.exists(name)if name == '/' thenreturn trueendif os.native and os.native.exists thenreturn os.native.exists(name)endlocal ok, err, code = os.rename(name, name)if not ok thenif code == 13 or code == 17 thenreturn trueelseif code == 30 thenlocal f = io.open(name,"r")if f ~= nil thenio.close(f)return trueendelseif name:sub(-1) == '/' and code == 20 and (not windows) thenlocal test = name .. '.'ok, err, code = os.rename(test, test)if code == 16 or code == 13 or code == 22 thenreturn trueendendreturn falseendreturn trueend
------------------------------------------------------------------------- absolute path (system call, can fall back to os.path.absolute)-----------------------------------------------------------------------function os.path.abspath(path)if path == '' then path = '.' endif os.native and os.native.GetFullPathName thenlocal test = os.native.GetFullPathName(path)if test then return test endendif windows thenlocal script = 'FOR /f "delims=" %%i IN ("%s") DO @echo %%~fi'local script = string.format(script, path)local script = 'cmd.exe /C ' .. script .. ' 2> nul'local output = os.call(script)local test = output:gsub('%s$', '')if test ~= nil and test ~= '' thenreturn testendelselocal test = os.path.which('realpath')if test ~= nil and test ~= '' thentest = os.call('realpath -s \'' .. path .. '\' 2> /dev/null')if test ~= nil and test ~= '' thenreturn testendtest = os.call('realpath \'' .. path .. '\' 2> /dev/null')if test ~= nil and test ~= '' thenreturn testendendlocal test = os.path.which('perl')if test ~= nil and test ~= '' thenlocal s = 'perl -MCwd -e "print Cwd::realpath(\\$ARGV[0])" \'%s\''local s = string.format(s, path)test = os.call(s)if test ~= nil and test ~= '' thenreturn testendendfor _, python in pairs({'python3', 'python2', 'python'}) dolocal s = 'sys.stdout.write(os.path.abspath(sys.argv[1]))'local s = '-c "import os, sys;' .. s .. '" \'' .. path .. '\''local s = python .. ' ' .. slocal test = os.path.which(python)if test ~= nil and test ~= '' thentest = os.call(s)if test ~= nil and test ~= '' thenreturn testendendendendreturn os.path.absolute(path)end
JavaScript生态有众多的库和工具链,结合专业商业引擎的渲染能力,快速打造游戏。 相比游戏领域常用的 Lua 脚本,TypeScript 的静态类型检查有助于编写更健壮,可维护性更好的程序。 高效:全引擎、全平台支持反射调用,无需额外步骤即可与宿主 C++/C# 通信。 高性能:全引擎、全平台支持生成静态调用桥梁,兼顾了高性能的场景。 WebGL 平台下的天生优势:相比 Lua 脚本在 WebGL 版本的表现,PuerTS 在性能和效率上都有极大提升,目前极限情况甚至比 C# 更快。
直接使用 C# 进行热更,特性完整。近乎完整实现了ECMA-335规范,只有极少量的不支持的特性,零学习和使用成本。 HybridCLR 将纯 AOT runtime 增强为完整的 runtime,使得热更新代码与AOT代码无缝工作。脚本类与AOT类在同一个运行时内,可以随意写继承、反射、多线程(volatile、ThreadStatic、Task、async)之类的代码。无需额外写特殊代码、无代码生成,几乎没有限制。 执行高效,内存高效。实现了一个极其高效的寄存器解释器,定义的类跟普通 C# 类占用一样的内存空间,许多指标都大幅优于其他热更新方案。 由于对泛型的完美支持,使得因为AOT泛型问题跟il2cpp不兼容的库现在能够完美地在il2cpp下运行,支持一些il2cpp不支持的特性,如__makeref、 __reftype、__refvalue指令。 Differential Hybrid Execution(差分混合执行技术)。
04
SDK 作为 unity package 方式供外部开发者引入,采用空壳实现,需要做好接入方不更新的打算,将业务逻辑全权交给热更资源包,SDK 空壳只负责以下功能。 正确拉取服务端的 Asset Bundle 包,加载其中资源与代码,并执行其中的业务入口方法。 在指定路径缓存上次更新好的 Asset Bundle 包,快速加载。 为外部开发者封装好 C#/Android 层调用。 SDK 采用 Android + unity 层混合实现,Android 层主要负责跨进程调用服务,启动业务工程子进程,unity 层主要负责和业务工程的 Asset Bundle 包交互,同时约定 Android 和 unity 两套 api。 采用 Asset Bundle 打包热更新资源,包括场景资源以及 dll 以 TextAsset 形式加载并动态绑定给 GameObject,详见 C# 代码动态加载方案。 SDK 每次调用时比对版本,有需要才增量更新,可以采用分包方式实现用户无感知的后台小步更新。 需要注意的是,我们的热更新属于插件自更新,SDK 不具有宿主 App 控制权,虽然理论上与宿主热更新不会有太大却别,但可能会在一些生命周期的以及后台更新服务的常驻等问题上受制于宿主。
使用系统 api System.Reflection.Assembly::Load 反射加载 dll 中的 C# 代码,将 C# 脚本动态绑定到物体上。 如果需要动态执行脚本,使用 Mono api 使用 Mono.CSharp::Compile 编译,或者使用 Mono.CSharp::Run Mono.CSharp::Evaluate 直接执行代码;也可以使用 C# 系统 api System.CodeDom::Compiler 编译或 System.Reflection::Emit 直接执行代码。
由于反射调用 C# 热更新可能存在未知的坑,可以采用 iFix(InjectFix) 进行 C# 热修复,iFix 能支持框架中原有 C# 代码的热修复。 同时可以内置 xlua 与 puerts,同时支持 lua 与 JS/TS 以备不时之需,需要的时候走业界成熟方案编写脚本进行救火操作。
05
using System;using UnityEngine;namespace HotUpdateCodeSample{public class HotCodeSample : MonoBehaviour{private long lastLogUpdateMinute = 0;// Start is called before the first frame updatevoid Start(){Debug.Log("[HotCodeSample::Start] this is a HotUpdateCodeSample::HotCodeSample::Start from hot update dll");}// Update is called once per framevoid Update(){long currentMinute = DateTime.Now.Minute;// 控制打印频率if (currentMinute <= lastLogUpdateMinute){return;}this.lastLogUpdateMinute = currentMinute;Debug.Log("[HotCodeSample::Update] this is a HotUpdateCodeSample::HotCodeSample::Update invoke from hot update dll");}}}
核心实现: 使用一个空物体 AssetBundleLoader,挂载如下 AssetBundleLoader.cs 脚本,核心逻辑为 AssetBundleLoader::ReadAssetFromRequest ,其使用 UnityWebRequest 实现了请求热更新资源 Cube 以及请求热更新代码 dll,读取其中 C# 脚本并挂载到物体上。 Demo 实现为请求本机文件 file:///,请求 http 资源原理和请求 file 协议的 C# 调用是一样的,只需部署一个文件服务器替换 url 即可验证(并补上请求失败的逻辑)。
using System;using System.Collections;using System.IO;using System.Reflection;using UnityEngine;using UnityEngine.Networking;public class AssetBundleLoader : MonoBehaviour{// Start is called before the first frame updatevoid Start(){Debug.Log("[AssetBundleLoader::Start]");// StartCoroutine(ReadAssetFromMemery());StartCoroutine(ReadAssetFromRequest());}// Update is called once per framevoid Update(){}IEnumerator ReadAssetFromMemery(){Debug.Log("[AssetBundleLoader::ReadAssetFromMemery]");// string path = @"pathToYourProject/AssetBundleDemo/AB/cube0.u3d";string path = @"AB/cube0.u3d";byte[] bytes = File.ReadAllBytes(path);Debug.Log($"[ReadAssetFromMemery] bytes.length: {bytes.Length}");AssetBundle cubeAssetBundle = AssetBundle.LoadFromMemory(bytes);Debug.Log($"[ReadAssetFromMemery] cubeAssetBundle: {cubeAssetBundle}");GameObject prefabGameObject = cubeAssetBundle.LoadAsset<GameObject>("Cube");Debug.Log($"[ReadAssetFromMemery] prefabGameObject: {prefabGameObject}");GameObject cubeGameObject = GameObject.Instantiate(prefabGameObject);Debug.Log("[ReadAssetFromMemery] finish");yield return null;}IEnumerator ReadAssetFromRequest(){Debug.Log("[AssetBundleLoader::ReadAssetFromRequest]");string cubeUrl =@"file:///pathToYourProject/AssetBundleDemo/AB/cube0.u3d";UnityWebRequest cubeRequest = UnityWebRequestAssetBundle.GetAssetBundle(cubeUrl);yield return cubeRequest.SendWebRequest();AssetBundle cubeAssetBundle = DownloadHandlerAssetBundle.GetContent(cubeRequest);GameObject prefabGameObject = cubeAssetBundle.LoadAsset<GameObject>("Cube");GameObject cubeGameObject = GameObject.Instantiate(prefabGameObject);yield return null;string srcUrl =@"file:///pathToYourProject/AssetBundleDemo/AB/hotupatesample.dll";UnityWebRequest srcRequest = UnityWebRequestAssetBundle.GetAssetBundle(srcUrl);yield return srcRequest.SendWebRequest();AssetBundle srcAssetBundle = DownloadHandlerAssetBundle.GetContent(srcRequest);Debug.Log($"[ReadAssetFromRequest] srcAssetBundle: {srcAssetBundle}");// 取 Asset 用 打包前的文件 Name,不区分大小写TextAsset textAsset = srcAssetBundle.LoadAsset<TextAsset>("hotupdatecodesample.dll.bytes");Debug.Log($"textAsset: {textAsset}");Assembly assembly = Assembly.Load(textAsset.bytes);// 取类用类全名 FullName,用 `.` 连接Type type = assembly.GetType("HotUpdateCodeSample.HotCodeSample");cubeGameObject.AddComponent(type);}}
using System.IO;using UnityEngine;using UnityEditor;public class AssetBundleUtil : Editor{[MenuItem("Utils/MakeAssetBundle")]static void CreateAssetBundle(){Debug.Log("[CreateAssetBundle]");string path = "AB";if (!Directory.Exists(path)){Directory.CreateDirectory(path);}BuildPipeline.BuildAssetBundles(path,BuildAssetBundleOptions.ChunkBasedCompression,BuildTarget.Android);Debug.Log("[CreateAssetBundle] finish");}}
资源热更新: 实现了了从 AB 包资源中加载图中物体 Cube(Clone)。 代码热更新: 实现了从资源 dll 中读取 C# 类 HotCodeSample (见下文)并挂载到图中 Cube(Clone) 上,该脚本成功被 unity 调用执行 HotCodeSample::Start 以及 HotCodeSample::Update ,在控制台输出 Debug::Log。
.Net Framework 工程引用的底层依赖是否存在和 unity 的兼容问题。 IL2CPP VM 下反射 dll 代码的管理、GC 及性能优化,以及 dll Release 混淆后能否跑通。
06
📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~
(长按图片立即扫码)