热更新-AB包/Lua
AB包
配置路径:自定义的,但是一定要有一个类去存储路径
打个AB包
1.选一个文件,最右下角,加入某个AB包,如果AB包名字中有斜杠如“ui/test”,那么他就会新建个“ui”文件夹,然后里面是“test”AB包文件(AB包内不存在目录):
2.写个Editor代码(放在了顶部菜单栏):
导出选项是一个多选枚举:
3.点
点完之后就会在目标目录下生成一个AB包了。
加载AB包
加载依赖
Simple02是打包进“pre”的预制体,预制体中的Image.Sprite是打包进“ui”的精灵。
此时,打出的AB包会有依赖关系:AB包ui没有依赖关系,AB包pre有一个依赖:ui
加载有依赖的AB包内部数据
如果想处理依赖关系的加载,则必须加载主AB包,因为依赖关系的存储,都存储在主AB包的配置文件中
第一步:(加载依赖的AB包文件)
- 加载主AB包
- 根据主AB包的配置文件,获得当前需要的AB包所依赖的AB包们
- 将所有依赖的AB包们加载进来
第二步:(加载AB包文件)
- AB包 = AssetBundle.LoadFromFile(“路径”);
- AB包 = AssetBundle.LoadFromFileSync(“路径”);
第三步:(加载包内部资源)
- T obj = AB包.LoadAsset<T>("名字");
- T obj = AB包.LoadAssetSync<T>("名字");
第四步:(释放AB包内存)
AB包.Unload(false);
AB包不能重复加载!
异步加载
AB包类似
内存相关
初始运行时440MB
生成AB包478MB:
生成图片后不变:
卸载AB包440.4MB(图片大小192kb)
以上三步循环6次,442.8MB:
回收,回到初始状态附近:
代码:
解释:加载AB包时,会把整个AB包加载到内存里440MB->480MB;
生成图片时,就是加了个指针;
卸载AB包,用的Unload(false),将指针指向的图片留了下来,440->440.8MB;
重复操作,Unity为懒回收:资源空闲后不回收。因此会一直存在于内存440.8->442.8MB;
回收后,空闲资源都没了;
Destroy仅销毁游戏对象,不销毁内存空闲资源,因此要在后续释放内存;
Resources.UnloadUnusedAssets();不能频繁调用;
LUA
Why
像AB包,就是在代码运行时,还没有读取到目标AB包,可以从网上下载一个AB包,然后去读;
而C#不行,在代码运行时,所有的C#代码就是一个文件,如果要更新某一部分的C#文件,就得把整个项目关掉;
因此引入了LUA,在C#运行时,还没有运行到的LUA,可以删掉,然后下个新的放进来;
而这,就是热更新。
What
How
我们直接来讲案例
使用LUA实现一个这样的功能:其中,图片和该布局是从AB包里加载出来的(预制体依赖UI);53是LUA读写Json的持久化数据
事先准备:
1.自然是要一个存放ui图片和预制体的AB包:其中,是pre依赖ui。
2.引擎布局:一个相机、一个画布(也可以没有,但用LUA去创建太麻烦);画布上挂了一个脚本
3.自然是在引擎里要几个函数去读取LUA文件:LiveCycle
a.首先,我们需要xLua插件sdk,github有,直接下就行
b.对xLua的sdk进行封装:
C#封装的xLua
using System.IO; using XLua; namespace lua { /// <summary> /// Lua环境单例 /// </summary> public class xLuaEnv { #region Singleton private static xLuaEnv instance; public static xLuaEnv Instance { get { if (instance == null) { instance = new xLuaEnv(); } return instance; } } #endregion #region CreatLuaEnv private LuaEnv Env; private xLuaEnv() { Env = new LuaEnv(); Env.AddLoader(data); } #endregion #region Loader public byte[] data(ref string path)//返回的路径 { path = E.Config.LuaPath + path + ".lua"; if (File.Exists(path)) return File.ReadAllBytes(path); else return null; } #endregion #region FreeEnv public void Free() { Env.Dispose(); instance = null; } #endregion #region RunEnv public object[] DoString(string code) { return Env.DoString(code); } #endregion #region EnvGlobal public LuaTable Global { get { return Env.Global; } private set { } } #endregion } }
tips:
Singleton:做成了一个单例,不解释了
CreatLuaEnv:需要创建一个Lua运行环境,但这还不够,运行某个lua文件时需要require("LuaName.lua")那环境肯定不知道去哪找这个文件,因此,看data
Loader:在运行某个Lua文件时(DoString(string code)),会传一个字符串,如果定义了AddLoader,就会把字符串传到这,然后使用IO把读到的lua文件以字节数组的形式传出去
FreeEnv:释放资源,减少内存占用
RunEnv:运行传进来的lua文件
EnvGlobal:像就是在一个目录下新建了一个lua文件去运行LiveCycle.lua文件,但其实我们需要的是LiveCycle.lua文件
因此使用该函数去拿到这个文件,并且绑定起来:
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using XLua; namespace lua { public delegate void LC(); public struct LuaLiveCycle { public LC Start; public LC Update; public LC OnDestory; public string name; } /// <summary> /// /// </summary> public class LiveCycle : MonoBehaviour { LuaLiveCycle _luaLiveCycle = new LuaLiveCycle(); private void Start() { xLuaEnv.Instance.DoString("require('LiveCycle')"); _luaLiveCycle = xLuaEnv.Instance.Global.Get<LuaLiveCycle>("LiveCycle"); _luaLiveCycle.Start(); _luaLiveCycle.Update(); //TestFunc(); } private void TestFunc() { GameObject go = new GameObject("img"); go.transform.SetParent(GameObject.Find("Canvas").transform); Image img = go.AddComponent<Image>(); AssetBundle ab = AssetBundle.LoadFromFile(E.Config.NewABPath + "ui"); img.sprite = ab.LoadAsset<Sprite>("奥拓"); ab.Unload(false); RectTransform rt = go.transform as RectTransform; rt.localPosition = Vector3.zero; rt.sizeDelta = new Vector2(1920, 1080); } private void Update() { } private void OnDestroy() { _luaLiveCycle.OnDestory(); _luaLiveCycle.Start = null; _luaLiveCycle.Update = null; _luaLiveCycle.OnDestory = null; xLuaEnv.Instance.Free(); } } }
用委托去存定义要存lua方法的委托
用结构体去定义LiveCycle.lua,里面有所有LiveCycle的方法(只要名字一样,会自动绑定)
Lua部分:MVC框架,还有一个AB包加载器,还有个读写json
首先就是LiveCycle.lua:
LiveCycle={}; LiveCycle.Controllers={}; LiveCycle.Start=function () LiveCycle.LoadPage("ImageController"); end LiveCycle.Update=function () for k,v in pairs(LiveCycle.Controllers) do if(v.Update~=nil) then v.Update(); end end end LiveCycle.OnDestory=function () print("OnDestory"); end LiveCycle.LoadPage=function(name) local c=require("Controller/"..name); LiveCycle.Controllers[name]=c; c:Start(); end require("Config"); require("ABManager"); LuaJson=require("LuaJson");
卧槽,感觉有点不整齐啊
额,总之这个就是去模拟生命周期函数,C#那边就会调这边的生命周期函数
然后就是LiveCycle.LoadPage("ImageController");
以后如果有其他的Controller,也可以直接去LoadPage("OthersController");
这可以直接把各个Controller绑定到周期函数里面的
好像这个圈子基本都是这种架构了
然后就是MVC内部了
先来解释一下MVC:
Model:dataModel数据模型,创建/存放对后台的数据进行操作的方法,比如这里就是对Json文件进行一个读写
View:视图,创建/存放引擎直接的对象,比如这里就是创建了AB包中的对象,存了该对象中的Text,后续可以对Text进行读写
Controller:控制器,去调用M和V的方法,初始化、更新等,事件也会在这里注册,什么周期函数也只有这里有
Controller
local Controller = {}; package.path=package.path..";"..Config.Path.."\\LUA\\View\\?.lua" package.path=package.path..";"..Config.Path.."\\LUA\\DataModel\\?.lua" Controller.Data = require("ImageDataModel"); Controller.View = require("ImageView"); function Controller:Start() Controller.Data:New();--去看看有没有json,没有就创建 Controller.View:Instantiate();--去加载AB包预制体,更新预制体↓ Controller.View:Update(Controller.Data:Read().count); local button = Controller.View.GO:GetComponentInChildren(typeof(CS.UnityEngine.UI.Button)); button.onClick:AddListener(Controller.AddAndUpdate);--注册事件 end function Controller:Update() print("Image:Update()"); end function Controller:AddAndUpdate() Controller.Data:Add(1); Controller.View:Update(Controller.Data:Read().count); end return Controller;
主要看Start();
DataModel
local DataModel={} DataModel.Path=Config.Json; DataModel.File={} function DataModel:New() if(not CS.System.IO.File.Exists(DataModel.Path)) then CS.System.IO.File.WriteAllText(DataModel.Path,'{"count":11,"test":111}'); else DataModel.File=LuaJson.decode(CS.System.IO.File.ReadAllText(DataModel.Path)); end end function DataModel:Read() if(DataModel.File) then return DataModel.File; else return nil; end end function DataModel:Add(num) DataModel.File.count=DataModel.File.count+num; CS.System.IO.File.WriteAllText(DataModel.Path,LuaJson.encode(DataModel.File)); end return DataModel;
New:去路径找Json文件,有就拷贝一份到内存,没有就创建一份
Read:读取存起来的Json文件
Add:需要加多少,直接传参,该类去写入Json
View
local View={}; local go; local text; local abName = "pre"; local name = "OBJ"; function View:Instantiate() go = CS.UnityEngine.GameObject.Instantiate(ABManager:LoadObj(abName,name,typeof(CS.UnityEngine.GameObject))); go.transform:SetParent(CS.UnityEngine.GameObject.Find("Canvas").transform); go.transform.localPosition=CS.UnityEngine.Vector3.zero; text=go:GetComponentInChildren(typeof(CS.UnityEngine.UI.Text)); View.GO=go; end function View:Update(num) text.text=num; end return View;
Instantiate:初始化,去AB包创建物体,放到Canvas下面;把Text存起来,因为后面要用
Update:这个可不是生命周期函数,这就是要更新text的时候调一下
Config里面就是存了几个路径,有一个要讲的点是:
Config={}; Config.ABPath="G:/0-YJS/UnityProject/202308/HotUpdate"; Config.Path="G:\\0-YJS\\UnityProject\\202308\\HotUpdate"; Config.Json="G:/0-YJS/UnityProject/202308/HotUpdate/LUA/DataModel/data.json";
能require到的对象必定是注册过的
本案例是在Controller里面注册的,不知道你有没有发现
package.path=package.path..";"..Config.Path.."\\LUA\\View\\?.lua" package.path=package.path..";"..Config.Path.."\\LUA\\DataModel\\?.lua" Controller.Data = require("ImageDataModel"); Controller.View = require("ImageView");
说实话注册很麻烦。。。
最后是将C#的ABManager改写成Lua的ABManager:
C#
//加载主AB包 AssetBundle main = AssetBundle.LoadFromFile(Config.ABPath + "/AB"); //加载主AB包的配置文件 AssetBundleManifest manifest = main.LoadAsset<AssetBundleManifest>("AssetBundleManifest"); //分析需要加载的资源文件所依赖的AB包 string[] deps = manifest.GetAllDependencies("pre"); //加载依赖的AB包 for (int i = 0; i < deps.Length; i++) AssetBundle.LoadFromFile(Config.ABPath + "/" + deps[i]); //加载资源文件所在的AB包 AssetBundle target = AssetBundle.LoadFromFile(Config.ABPath + "/pre"); //加载资源文件 GameObject prefabs = target.LoadAsset<GameObject>("Simple02"); Instantiate(prefabs,transform.parent);
Lua
ABManager={}; ABManager.Files={}; --加载AB主包 local mian=CS.UnityEngine.AssetBundle.LoadFromFile(Config.ABPath.."/AB"); ABManager.Mainifest=mian:LoadAsset("AssetBundleManifest",typeof(CS.UnityEngine.AssetBundleManifest)); mian:Unload(false); --加载AB包并缓存起来,下次来加载时,能够直接在缓存里拿 function ABManager:LoadAB(name) --有缓存就不重复加载 if(ABManager.Files[name] ~= nil) then return; end --加载依赖列表string[] local deps = ABManager.Mainifest:GetAllDependencies(name); --遍历依赖,并加载 for i = 0,deps.Length-1 do local temp = deps[i]; if(ABManager.Files[temp] == nil) then ABManager:LoadAB(temp); end end --加载目标AB包(此时依赖已经加载完毕) if(CS.System.IO.File.Exists(Config.ABPath.."/"..name)) then ABManager.Files[name] = CS.UnityEngine.AssetBundle.LoadFromFile(Config.ABPath.."/"..name); end end --加载对象,AB包(ABName)下的对象(objName),并且返回一个T类型的对象 function ABManager:LoadObj(ABName,ObjName,T) --加载AB包 ABManager:LoadAB(ABName); --加载并返回对象 if(ABManager.Files[ABName]~=nil) then return ABManager.Files[ABName]:LoadAsset(ObjName,T); else return nil; end end
tnnd没有变颜色
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!