其实热更新的原理比较简单,就是本地的客户端与资源服务器进行比对相关信息与文件,然后从资源服务器获取到需要更新的资源与代码逻辑,从而完成热更新,效果图如下:
本文所有开发工具
xLua基础 其实xLua可以说是Lua热更新的升级版,无论是在功能,性能还是易用性上都有诸多的突破的,主要表现在以下几个方面(这里主要参考了官方网站:介绍 — XLua (tencent.github.io) )
可以运行时把C#实现(方法,操作符,属性,事件等等)替换成lua实现;
出色的GC优化,自定义struct,枚举在Lua和C#间传递无C# gc alloc;
编辑器下无需生成代码,开发更轻量;
引入命名空间 在一切准备之前,需要导入相关文件,点击下方的下载按钮下载后解压直接拖到Unity3d工程文件中就ok了
点击此处下载文件
然后需要用的时候还是需要引入命名空间,这个还是比较好记忆的
xLua的基本使用 在Unity3d中执行Lua,需要使用Lua解析器,也就是 LuaEnv ,那我们就可以先初始化一下 LuaEnv
1 LuaEnv env = new LuaEnv();
注意点: 一般情况下,处于减少性能的消耗和减少执行时间,最好保持它的唯一性
那我们就可以来打印一句lua语句试试,
1 2 3 LuaEnv env = new LuaEnv(); env.DoString("print('你好世界')" );
这里回顾一个lua知识点,在lua中双引号和单引号都是可以代表字符串的,但是在c#中是不能全都用双引号,要么就用转义字符来划分,要么就是外面双引号,里面单引号来区分,这里为了节省时间,就用第二种方式了,这样一个渐渐单单的在unity中执行lua语言就ok了
那如果是读取Lua脚本时
1 2 LuaEnv env = new LuaEnv(); env.DoString("require('main')" );
知识点:
执行一个Lua脚本 Lua知识点 :多脚本执行 require;
默认寻找脚本的路径 是在 Resources下 并且 因为在这里,估计是通过 Resources.Load去加载Lua脚本 txt bytes等等,所以Lua脚本 后缀要加一个txt;
然后我们就可以在项目的Resources中创建一个main.lua.txt(原因上面已经说了,不过可以创建一个main.lua的lua文件,但是肯定会报LuaException: [string “chunk”]:1: module ‘main’ not found; 大致意思就是找不到该文件)
剩下还有两个方法分别是
1 2 env.Tick(); env.Dispose();
重定向路径 上面说到xLua解析器读取lua文件默认是在项目的Resources中,而且要把lua脚本文件的格式写成xx.lua.txt,这样在实际的开发过程中是非常不方便,况且放到项目的Resources文件中打包时是定死的,而且只可作为只读文件来使用,无法进行修改,这对于热更新的理念是背道而驰的,所以就需要用到xLua提供的路径重定向方法
1 2 env.AddLoader(MyCustomLoader);
这里需要记住的便是env.AddLoader可以注册多个回调,该回调参数是字符串,字符串的名称则是需要回调的lua脚本名称,xLua解析器通过对注册的回调方法进行逐层回调,回调的返回值是一个byte数组,如果为空表示该loader找不到,否则则为lua文件的内容
这里也是设置自定义的路径的回调方法,此此方法在env.AddLoader注册后会自动执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private byte [] MyCustomLoader (ref string filePath ) { string path = Application.dataPath + "/Lua/" + filePath + ".lua" ; Debug.Log(path); if ( File.Exists(path) ) { return File.ReadAllBytes(path); } else { Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath); } return null ; }
这样xLua解析器最最最基本的东西就罗列出来了
封装xLua管理器 我们可以结合唐老狮的【唐老狮】Unity中Lua热更新解决方案 - 泰课在线 — 志存高远,稳如泰山 - 国内专业的在线学习平台|Unity3d培训|Unity教程|Unity教程 Unreal 虚幻 AR|移动开发|美术CG - Powered By EduSoho (taikr.com) 视频封装一套lua管理器类
首先需要写一个BaseManager,之前有说到对于lua解析器最好保证其唯一性,唯一性最大的特点便是单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using System.Collections;using System.Collections.Generic;using UnityEngine;public class BaseManager <T > where T :new (){ private static T _instance; public static T GetInstance () { if (_instance == null ) { _instance = new T(); } return _instance; } }
在unity3d中实现热更新最重要的便是生成ab包,而ab包一般打包生成的路径是assetbundle文件夹中,所以封装一套ab包管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Events;public class ABMgr : SingletonAutoMono <ABMgr >{ private AssetBundle mainAB = null ; private AssetBundleManifest manifest = null ; private Dictionary<string , AssetBundle> abDic = new Dictionary<string , AssetBundle>(); private string PathUrl { get { return Application.persistentDataPath + "/" ; } } private string MainName { get { #if UNITY_IOS return "IOS" ; #elif UNITY_ANDROID return "Android" ; #else return "PC" ; #endif } } private void LoadMainAB () { if (mainAB == null ) { mainAB = AssetBundle.LoadFromFile(PathUrl + MainName); manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest" ); } } private void LoadDependencies (string abName ) { LoadMainAB(); string [] strs = manifest.GetAllDependencies(abName); for (int i = 0 ; i < strs.Length; i++) { if (!abDic.ContainsKey(strs[i])) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]); abDic.Add(strs[i], ab); } } } public T LoadRes <T >(string abName, string resName ) where T : Object { LoadDependencies(abName); if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, ab); } T obj = abDic[abName].LoadAsset<T>(resName); if (obj is GameObject) return Instantiate(obj); else return obj; } public Object LoadRes (string abName, string resName, System.Type type ) { LoadDependencies(abName); if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, ab); } Object obj = abDic[abName].LoadAsset(resName, type); if (obj is GameObject) return Instantiate(obj); else return obj; } public Object LoadRes (string abName, string resName ) { LoadDependencies(abName); if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, ab); } Object obj = abDic[abName].LoadAsset(resName); if (obj is GameObject) return Instantiate(obj); else return obj; } public void LoadResAsync <T >(string abName, string resName, UnityAction<T> callBack ) where T : Object { StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack)); } private IEnumerator ReallyLoadResAsync <T >(string abName, string resName, UnityAction<T> callBack ) where T : Object { LoadDependencies(abName); if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, ab); } AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName); yield return abq; if (abq.asset is GameObject) callBack(Instantiate(abq.asset) as T); else callBack(abq.asset as T); } public void LoadResAsync (string abName, string resName, System.Type type, UnityAction<Object> callBack ) { StartCoroutine(ReallyLoadResAsync(abName, resName, type, callBack)); } private IEnumerator ReallyLoadResAsync (string abName, string resName, System.Type type, UnityAction<Object> callBack ) { LoadDependencies(abName); if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, ab); } AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName, type); yield return abq; if (abq.asset is GameObject) callBack(Instantiate(abq.asset)); else callBack(abq.asset); } public void LoadResAsync (string abName, string resName, UnityAction<Object> callBack ) { StartCoroutine(ReallyLoadResAsync(abName, resName, callBack)); } private IEnumerator ReallyLoadResAsync (string abName, string resName, UnityAction<Object> callBack ) { LoadDependencies(abName); if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName); abDic.Add(abName, ab); } AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName); yield return abq; if (abq.asset is GameObject) callBack(Instantiate(abq.asset)); else callBack(abq.asset); } public void UnLoadAB (string name ) { if (abDic.ContainsKey(name)){ abDic[name].Unload(false ); abDic.Remove(name); } } public void ClearAB () { AssetBundle.UnloadAllAssetBundles(false ); abDic.Clear(); mainAB = null ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 using System.Collections;using System.Collections.Generic;using System.IO;using UnityEngine;using XLua;public class LuaMgr : BaseManager <LuaMgr >{ private LuaEnv luaEnv; public LuaTable Global { get { return luaEnv.Global; } } public void Init () { if (luaEnv != null ) return ; luaEnv = new LuaEnv(); luaEnv.AddLoader(MyCustomLoader); luaEnv.AddLoader(MyCustomABLoader); } private byte [] MyCustomLoader (ref string filePath ) { string path = Application.dataPath + "/Lua/" + filePath + ".lua" ; if (File.Exists(path)) { return File.ReadAllBytes(path); } else { Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath); } return null ; } private byte [] MyCustomABLoader (ref string filePath ) { TextAsset lua = ABMgr.GetInstance().LoadRes<TextAsset>("lua" , filePath + ".lua" ); if (lua != null ) return lua.bytes; else Debug.Log("MyCustomABLoader重定向失败,文件名为:" + filePath); return null ; } public void DoLuaFile (string fileName ) { string str = string .Format("require('{0}')" , fileName); DoString(str); } public void DoString (string str ) { if (luaEnv == null ) { Debug.Log("解析器为初始化" ); return ; } luaEnv.DoString(str); } public void Tick () { if (luaEnv == null ) { Debug.Log("解析器为初始化" ); return ; } luaEnv.Tick(); } public void Dispose () { if (luaEnv == null ) { Debug.Log("解析器为初始化" ); return ; } luaEnv.Dispose(); luaEnv = null ; } }
基本上这样就封装好了,具体的使用就比较容易了
1 2 3 4 LuaMgr.GetInstance().Init(); LuaMgr.GetInstance().DoLuaFile("main" );