目录
一、LuaFramework的资源更新流程如下图:
二、LuaFramework框架打包流程:
三、LuaFramework框架获取资源方法:
四、加载面板流程
五、热更新真正发挥作用的部分
六、Lua框架的基本用法套路
6.1 自定义Wrap类(即C#转Lua代码)
6.2 Lua框架的事件event
6.3 lua中的获取组件、添加组件、注册点击事件、寻找子物体
6.4 lua协同程序
6.5 关于Lua框架的其他内容
LuaFramework2018版的github链接
直接点击Clone or download ,然后点击download ZIP
我介绍的版本具体是May 21 ,2018版 其他版本的可能不会有我所说的一些毛病出现。
//-------------2018-05-26-------------
(1)更新tolua#到May 21, 2018版
游戏工程资源目录是在我们点击了Build Window Resources 后 自动对 .../Assets/LuaFramework/Lua下的文件 和 在代码中Packages.es 指定的 打包资源 进行打包 放入 .../Assets/StreamingAssets目录下,若没有这个目录它会自动创建。
这里所说的资源是指 .lua文件 (必须是放入.../Assets/LuaFramework/Lua下的文件),在代码中指定打包的U3D资源。
Package.cs 类 的BuildWindowsResource方法 是我们扩展到Unity编辑器菜单上的BuildWindowResources功能方法。
这个方法会自动帮我们在工程下创建StreamingAssets文件夹并且将打包的资源放入到该文件夹下。
LuaBundleMode为true时,会将 .../Assets/LuaFramework/Lua下的全部lua文件打包,放入StreamingAssets/Lua文件夹下,
为false时,就不会打包,而是直接拷贝全部lua文件到StreamingAssets/Lua下。
发布出去前,一定要确保LuaBundleMode为true,切记,在编辑模式下,我们都改为false。
改为false的话 我们就不需要每次更新lua代码都必须按Build Window Resources了。
当UpdateMode为true时才是真正的热更新,它会将本地的资源与服务器的资源进行比对然后更新到本地,如果你没有服务器 或者没有开启框架自带的服务器(我还不知道怎么开启框架自带的),就不要把UpdateMode为true,因为会报错!为false时候就会直接开始框架的逻辑部分。
ExampleMode为true时,只会打包框架自带的资源,如果要打包自己的资源,请进入HandleExampleBundle改写里面的方法,其中AddBuildMap是用来添加打包资源的,第一个参数是包名(自定义的,获取的时候需要用到),第二个参数是资源类型,第三个参数是资源所在工程的路径。
注意:AddBuildMap一定要模仿框架自带的去写,不然会出错。
------------------------------------------------2019年1月20日补充开始-----------------------------------------------
注意:使用LuaFramework框架的局限性
当在Lua使用PanelManager加载(比如:PromptPanel)面板调用上面的方法,会固定包名和资源名。也就是你的文件必须是这样的! 在Lua是这样子的
这个abName包名也是固定的,在Packager.cs
第一个参数是包名,第二个参数是第三个参数文件目录下的类型(必须相同),第三个参数是打包的文件目录,注意打包是将整个文件内的资源全部打包的,而不是一个资源一个包,所以在下面获取资源的时候,指定包名后还需要指定资源名!
看到这里的我,瞬间懵逼,为什么那么死板,我包名和资源名居然有绝对性关联,这不行,我想包名和资源名互不影响!
修改PanelManager的CreatePanel代码如下:其中发现了一个很无语的东西,因为abName根本没被使用到!!!
新增_abName 外部传递包名,其中包名是不用加后缀的!源码是加了AppConst.ExtName,箭头指向的地方都修改了。
ResManager代码(没有修改贴出来给我看)
-----------------------------------------------2019年1月20日补充结尾-----------------------------------------------
进入ResourceManager.cs方法:
public void LoadPrefab(string abName, string assetName, Action func) {
LoadAsset(abName, new string[] { assetName }, func);
}
public void LoadPrefab(string abName, string[] assetNames, Action func) {
LoadAsset(abName, assetNames, func);
}
public void LoadPrefab(string abName, string[] assetNames, LuaFunction func) {
LoadAsset(abName, assetNames, null, func);
}
这3个方法都是可以加载AB包指定资源的。
基本上,第一个参数是包名(带后缀.unity3d)第二个参数是资源名称(预制体名称,可以指定一个数组,意味着从一个包加载多个资源),第三个参数:Action
在lua代码,一般是通过 LuaHelper的GetResManager 来获取ResourceManager 然后调用方法。
详细一点的解释,就是通过这个框架解析出的LuaHelperWrap脚本来调用GetResManager获取ResrouceManager的。
至于为什么,能直接这样获取,可以自己慢慢看框架 ,也许能懂,我就不看了- -。
首先,Main.cs 脚本中在Start里,启动了StartUp,触发了StartUpCommand命令,初始化了全部管理器,其中GameManager是我们要讨论的,它会在Init方法执行CheckExtractResource 该方法会去检查c:/luaframework是否存在(Windows系统才会这样的,其他的会相应地在别的地方检查 我就不说明了)若存在,进入OnUpdateResource方法,它会去检查c:/luaframework下的文件是否与服务器上的文件一样,若不一样,则更新c:/luaframework下的文件为最新文件,更新完了后会直接return,注意下面有一个方法OnExtractResource,这个是相当于 (一)里面说的①操作,即检查c:/luaframework是否存在,若不存在,则不会进入OnUpdateResource方法,而是进入OnExtractResource,去执行解压StreamingAssets下的文件到这个c:/luaframework下。若执行了OnUpdateResource方法,那么会进入OnResourceInited方法,初始化ResourceManager,之后进入GameManager的OnInitialize。
OnInitialize会调用LuaManager的InitStart,其实就是启动Lua框架,Lua入口文件是Main.lua文件,肯定会执行Main.lua文件的Main方法,但是,这不是重点,重点是它还会调用Util.CallMethod("Game", "OnInitOK"); 执行Game.lua的OnInitOK方法。
Game.lua的OnInitOK方法,调用this.InitViewPanels();实现requires View/XxxPanel即引用2个Panel的lua代码 , 调用 CtrlManager.Init();实现了将PromptCtrl和MessageCtrl的创建和保存,它会放入一个叫ctrlList的lua表中,该表在CtrlManager.lua文件,这样,我们就能够通过CtrlManager来根据Ctrl名称获取相应的Ctrl了。在OnInitOK方法中,对PromptCtrl进行了初始化,即Awake方法的调用。在PromptCtrl.lua 的Awake方法中,它会调用PanelManager的CreatePanel方法,指定一个名称和回调方法,注意这个名称千万不要写错,它与我们打包出去的那个PromptPanel预制体有关的,打包时,如果面板预制体名称叫PromptPanel,而且必须是XxxxPanel的形式,必须后面有Panel!!注意了。那么包名,必须叫prompt,不要问我为什么,因为...框架内定的,除非你去改动内定的东西。这样子,我们在CreatePanel那里指定的名称是Prompt,必须写Prompt,不要写错!大小写也不要写错!然后你就创建出了一个PromptPanel了,那个创建时会对这个Panel进行一些位置调整,以及加一个很重要的脚本上去 LuaBehaviour 脚本,之后,它会调用回调方法OnCreate(obj) obj是那个创建出来的面板,我们通过LuaBehaviour的AddClick方法来给PromptPanel的按钮添加点击事件!
疑惑点1:PromptPanel.lua脚本的Awake方法谁调用的?我很仔细地查看所有这一路过来的代码都没发现谁哪个方法调用了它,若没有人调用Awake方法,那么就获取不到Panel的按钮了。
疑惑点2:PromptPanel有UIPanel脚本么?在PromptCtrl.OnCreate方法中,居然去获取这个组件了。
总体流程大致如上,有2个疑惑点 我没有看明白 ,哪位高手看懂了,麻烦 说一下。。。直接写评论告诉下 头晕- -
关于疑惑点1 的确是在LuaBehaviour.cs代码上通过Util.CallMethod来执行的,疑惑点2可以忽略了。
最后说一下,整个Lua框架的启动是Main.cs 脚本带动的,它必须挂在一个名为GameManager的物体身上。
第一点:Lua框架在玩家第一次运行游戏时,会将工程内的StreamingAssets文件的内容复制到玩家电脑上,window是c:/luaframework文件,它会自动生成。
第二点:当玩家第二次运行游戏时,会先检查服务器与本地c:/luaframework文件下的资源,不为最新则自动将服务器最新的资源更新到本地上,它是通过一个files.txt的文件来检查的,里面的内容是以如下形式出现(每一行)
是以' | ' 分割,左边是 资源相对路径 ,右边是MD5码(资源身份证(唯一))
下面解释框架内部的上面2点的代码部分(内涵注释,应该能看懂)
第一点:在加载面板流程那我讲了大概的那么一点点涉及到这里的东西,就是运行到GameManager.cs脚本的Init方法后如下:
执行CheckExtractResource方法(开始了!)
下面的方法是复制资源到本地上的,很轻松能理解
//将工程.../Assets/StreamingAssets下的东西全部复制到本地c:/luaframework/下,
//复制完毕后,进行第二点操作(与服务器比对更新资源文件)
//(需要那么长的代码么,还真的需要因为过程还是有点复杂的)
//详细解释:第一点:它首先将工程下的files.txt文件下载了先,
//因为files.txt包含了所有资源文件的相对路径,便于复制..
// 第二点:读取files.txt 解析每一行 然后逐个文件开始进行复制到本地
IEnumerator OnExtractResource()
{
string dataPath = Util.DataPath; //数据目录
string resPath = Util.AppContentPath(); //游戏包资源目录
//即使本地有那个streamingassets目录,因为不完整所以要删除
if (Directory.Exists(dataPath)) Directory.Delete(dataPath, true);
Directory.CreateDirectory(dataPath);//重新创建本地数据目录
string infile = resPath + "files.txt";
string outfile = dataPath + "files.txt";
if (File.Exists(outfile)) File.Delete(outfile);//本地上已存在files.txt 先删除
string message = "正在解包文件:>files.txt";
Debug.Log(infile);
Debug.Log(outfile);
if (Application.platform == RuntimePlatform.Android)
{
WWW www = new WWW(infile);//用www来将游戏工程内的files.txt下载
yield return www;
if (www.isDone)
{
File.WriteAllBytes(outfile, www.bytes);//写入到本地上的files.txt
}
yield return 0;
}
else File.Copy(infile, outfile, true);//其他脚本就是直接利用文件复制方式下载到本地
yield return new WaitForEndOfFrame();
//(将游戏工程内的streamingAssets目录下的东西全部加载到本地StreamingAssets目录上
string[] files = File.ReadAllLines(outfile);//注意此时的outfile是本地的files.txt文件
//遍历files.txt文件所有行 每行都是以"资源相对路径|md5"的形式存在,是相对于LuaFramework目录的路径
foreach (var file in files)
{
string[] fs = file.Split('|');
infile = resPath + fs[0]; //此时的infile是游戏工程unity内的资源绝对路径
outfile = dataPath + fs[0]; //此时的outfile是本地资源绝对路径(注意这里的本地资源还没有诞生)
message = "正在解包文件:>" + fs[0];
Debug.Log("正在解包文件:>" + infile);
facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
string dir = Path.GetDirectoryName(outfile); //创建本地资源目录
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
if (Application.platform == RuntimePlatform.Android)
{
WWW www = new WWW(infile); //将游戏unity工程资源下载
yield return www;
if (www.isDone)
{
File.WriteAllBytes(outfile, www.bytes);//写入本地
}
yield return 0;
}
else
{
if (File.Exists(outfile))
{
File.Delete(outfile);
}
File.Copy(infile, outfile, true);
}
yield return new WaitForEndOfFrame();
}
message = "解包完成!!!";
facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
yield return new WaitForSeconds(0.1f);
message = string.Empty;
//释放完成,开始启动更新资源
StartCoroutine(OnUpdateResource());
}
最关键的代码来了!
///
/// 启动更新下载,这里只是个思路演示,此处可启动线程下载更新
/// 首先,将服务器上的files.txt更新到本地(还是以它来遍历服务器上的文件与本地资源作比对)
/// 接着,遍历files.txt每行(资源相对路径|MD5)
/// 1、若本地上存在与服务器同样的资源(同签名、地址相同),进行MD5校验,不同就删除本地的,
/// 然后下载服务器的到本地
/// 2、若本地上不存在与服务器同样的资源,那么服务器的资源下载到本地
///
IEnumerator OnUpdateResource()
{
if (!AppConst.UpdateMode)
{
OnResourceInited(); //直接开始资源管理器的初始化 以及 一些lua代码的加载开始,测试等操作
yield break;
}
string dataPath = Util.DataPath; //本地数据目录c:/luaframework/
//下面url相当于.../Assets/StreamingAssets/
string url = AppConst.WebUrl; //服务器url默认为 http://localhost:6688/ 要改的话需要改为自己的服务器url
string message = string.Empty;
string random = DateTime.Now.ToString("yyyymmddhhmmss");
string listUrl = url + "files.txt?v=" + random; //服务器上的files.txt 和url串起来就是http://xxx/files.txt?v=版本号(当前时间)
Debug.LogWarning("LoadUpdate---->>>" + listUrl);
WWW www = new WWW(listUrl); yield return www;//下载服务器上的files.txt文件
if (www.error != null)
{
OnUpdateFailed(string.Empty);
yield break;
}
//这里本地还不存在 那个c:/luaframework的话就创建,
//其实貌似没什么意义,因为肯定存在啊!
if (!Directory.Exists(dataPath))
{
Directory.CreateDirectory(dataPath);
}
File.WriteAllBytes(dataPath + "files.txt", www.bytes);//将服务器上files.txt写到本地上(第一环更新本地的files.txt完毕)
//接下来还是与之前一样获取files.txt内容 遍历 解析 然后逐个下载资源到本地
string filesText = www.text;
string[] files = filesText.Split('\n');
for (int i = 0; i < files.Length; i++)
{
if (string.IsNullOrEmpty(files[i])) continue;
string[] keyValue = files[i].Split('|');
string f = keyValue[0]; //资源相对路径 例如:lua/3rd/cjson/example2.json
string localfile = (dataPath + f).Trim(); //可能将要下载资源到的资源绝对路径,例如:c:/luaframework/lua/3rd/cjson/example2.json
string path = Path.GetDirectoryName(localfile);//此时path就是c:/luaframework/lua/3rd/cjson
//保证目录要存在
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);//若不存在那个文件目录就创建出来
}
string fileUrl = url + f + "?v=" + random;//服务器上的资源绝对路径
bool canUpdate = !File.Exists(localfile);//若本地上存在服务器上的资源
//,则可以更新(存在才能更新!,不存在的话说明本地缺少文件)
if (!canUpdate)
{
string remoteMd5 = keyValue[1].Trim();//服务器资源的md5码
string localMd5 = Util.md5file(localfile);//本地资源的md5码
canUpdate = !remoteMd5.Equals(localMd5);//若不同,那么可以更新
if (canUpdate) File.Delete(localfile);//删除本地上的资源
}
if (canUpdate)
{ //本地缺少文件
Debug.Log(fileUrl);
message = "downloading>>" + fileUrl;
facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
/*
www = new WWW(fileUrl); yield return www;
if (www.error != null) {
OnUpdateFailed(path); //
yield break;
}
File.WriteAllBytes(localfile, www.bytes);
*/
//这里都是资源文件,用线程下载
BeginDownload(fileUrl, localfile);//这里的话,目前还不懂,无法解释...
while (!(IsDownOK(localfile))) { yield return new WaitForEndOfFrame(); }
}
}
yield return new WaitForEndOfFrame();
message = "更新完成!!";
facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
OnResourceInited();//资源更新结束后的一些逻辑,不解释了
}
关于那个Util.md5file 其实都是写死的一些东西,计算出文件MD5码,不作过多解释,线程下载部分也是。其实是不懂哈哈。。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyLog {
public static void LogWarning(string msg)
{
Debug.LogWarning(msg);
}
public static void Log(string msg)
{
Debug.Log(msg);
}
public static void LogError(string msg)
{
Debug.LogError(msg);
}
}
然后,点击Lua -> Clear wrap files 等待Clear完毕后 点击Lua -> Generate All 就OK啦。
到Lua代码上通过 MyLog.Log('Hello World!');的 方式调用C#上的MyLog脚本的Log方法。不用require!因为你会发现根本没有MyLog.lua这个文件,它是如何调用的, 还没研究出来。
在event.lua 文件下(...\Assets\LuaFramework\ToLua\Lua\event.lua)有如下三个玩意
非常的熟悉,它们就是Unity生命周期事件 Update、LateUpdate、FixedUpdate。
使用方法: UpdateBeat:Add(this.MethodName, self);
MyLuaClass = {}
local this = MyLuaClass
function MyLuaClass.Awake()
-- 开启Update功能(每帧执行Update方法)
UpdateBeat:Add(this.Update, self);
end
function MyLuaClass.Update()
end
其他的类似。
this.btnClose = transform:Find("CloseBtn").gameObject; -- 获取子物体CloseBtn按钮
luaBehaviour = gameObject:GetComponent('LuaBehaviour');
gameObject:AddComponent('TestScript');
luaBehaviour:AddClick(this.btnClose, this.OnClick);
local Input = UnityEngine.Input; -- 拿到Input类
if Input ~= nil and Input.GetMouseButtonDown(0) then
log("***开启协同程序***");
if myTestCoroutine ~= nil then
coroutine.stop(myTestCoroutine);
myTestCoroutine = nil;
end
myTestCoroutine = coroutine.start(this.TestCoroutine);
end
function MTestPanel.TestCoroutine()
log("TestCoroutine剛開始");
coroutine.wait(2);
log("TestCoroutine執行2秒后");
myTestCoroutine = nil;
end
myTestCoroutine是局部变量 ,lua的都是local xxx;这样就OK了,相当于var...
1、require "xxx/yy/dd" 这个"xxx/yy/dd"是相对于LuaFramework/Lua的路径
2、细心可以发现为什么Game.lua 没有require "Common/define" 却可以使用 PanelNames[] 这个表,是因为它
require "Logic/CtrlManager" ,在CtrlManager.lua里面就require了define.lua (这段可能要自己去看才懂我说啥)
3、凡是被PanelManager加载出的资源,都会加上LuaBehaviour.cs脚本,它会使一个名为加载出的资源名的lua文件,类似Unity
生命周期 函数那样,模拟了一个在物体出现后执行Awake 方法,执行Start方法,为此你还可以再添加OnDestroy或者其他类似的。如下:
这个OnDestroy是我自己加的,那么在Lua写个同样的OnDestroy方法 就会在销毁的时候执行lua的OnDestroy()了。其他类似。。
4、我到现在才发现,设置UI的位置是要用 RectTransform的localPosition来设置的,不能用position!(实测)
大概写到这里结束吧,后期无聊回来继续看看。。