热更新-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包文件)

  1. 加载主AB包
  2. 根据主AB包的配置文件,获得当前需要的AB包所依赖的AB包们
  3. 将所有依赖的AB包们加载进来

第二步:(加载AB包文件)

  1. AB包 = AssetBundle.LoadFromFile(“路径”);
  2. AB包 = AssetBundle.LoadFromFileSync(“路径”);

第三步:(加载包内部资源)

  1. T obj = AB包.LoadAsset<T>("名字");
  2. 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没有变颜色

posted @   被迫吃冰淇淋的小学生  阅读(131)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示