记录最近在学习Lua热更新的过程中遇到的一些坑
笔者这里使用的是Slua,关于Lua文件的打包下载和读取,不论是什么Lua框架,应该都是通用的
在Slua官方给出的Demo中,加载的lua文件是.txt后缀,而大多数编辑器只能打开.lua文件,所以需要让Unity3D能够加载.lua文件是必要的一步,否则开发过程中就需要频繁的修改后缀名,非常麻烦,Slua中其实也提供了接口(如果直接用记事本写代码这一步可以省略。。。):
LuaSvr svr;
LuaTable self;
LuaFunction update;
[CustomLuaClass]
public delegate void UpdateDelegate(object self);
//调用这个方法,读取Entry.lua文件并执行
private void StartLua()
{
string dataPath="lua文件所在的根文件夹"
svr = new LuaSvr();
svr.init(null, () =>
{
string initLua = @"return require(""Entry"")";
IntPtr l = LuaSvr.mainState.L;
LuaDLL.lua_getglobal(l, "package");
LuaDLL.lua_pushstring(l,dataPath+ "?.lua");//这里也可以改成.txt
LuaDLL.lua_setfield(l, -2, "path");
LuaDLL.lua_pop(l, 1);
self = LuaSvr.mainState.doString(initLua) as LuaTable;
update = (LuaFunction)self["update"];//绑定Entry.lua中的update方法
UD = update.cast();
});
}
在Unity3D所加载的Entry.lua文件中,一定要返回一个class,否则绑定Update方法会错,这是Entry.lua文件:
import "UnityEngine"
if not UnityEngine.GameObject or not UnityEngine.UI then
error("Click Make/All to generate lua wrap file")
end
print("Unity3D你好,我是Lua")
local class = { }
function class:update() -- gc alloc is zero
print("这里是Lua脚本的Update方法")
end
return class
如果需要在Lua脚本中调用C#中的类,只需要在类的上面加上一个 [CustomLuaClass] 特性,然后点击Slua/All/Make会自动导出
这里有个坑,Unity3D在打AB包的时候是无法识别.lua文件的,所以打包的时候需要将.lua文件拷贝一份,改成.txt或者其他Unity3D支持的后缀名,再进行打包,打包完毕后再删除,笔者是改成.txt文件,写了一个自用的打包工具,注释不多但是应该是可以看懂,直接用就好了,在菜单栏Tool/Resouce Manager下打开,注意:要选中Lua脚本的代码文件夹,且不要选中场景中的物体,否则会报错!!!
以下是工具代码,要放记得在Unity3D的Editor文件夹下:
using System.IO;
using UnityEditor;
using UnityEngine;
public class Tools {
[MenuItem("Tools/Resources Manager #q", false, 1)]
static void ResourcesManager()
{
ScriptableWizard.DisplayWizard("资源打包工具","确定");
}
}
public enum BuildPath
{
Android,
Windows,
//IOS
}
public class ResourcesWindow : ScriptableWizard
{
//检测create按钮的点击
public BuildPath type = BuildPath.Android;
public string LuaPath = "D:/Unity3D/Slua/Assets/Slua/Resources/LuaCode";//lua脚本所在文件夹
public string BuildRootPath= "C:/Users/Administrator.SC-201902281819/Desktop/坦克/AssetBoundles";//AB存放路径
public bool Code = true;
public bool Resouce = false;
void OnWizardCreate()
{
string dir = BuildRootPath;
if (type == BuildPath.Android)
{
dir += "/Android";
}
else
{
dir += "/Windows";
}
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
DirectoryInfo TheFolder = new DirectoryInfo(LuaPath);
string bundlePath = dir + "/LuaCode.unity3d";//Lua的AB包名称
ChangeLua2Txt(TheFolder, LuaPath);//拷贝并修改后缀为.txt
AssetDatabase.Refresh();//刷新一下
if (type == BuildPath.Android)
{
if(Resouce)
BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None, BuildTarget.Android);
if(Code)
{
Object[] selection = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets);
//打包
BuildPipeline.BuildAssetBundle(Selection.activeObject, selection, bundlePath,
BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BuildTarget.Android);
}
}
else
{
if (Resouce)//打包资源
BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
if (Code)//打包lua脚本
{
Object[] selection = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets);
//打包
BuildPipeline.BuildAssetBundle(Selection.activeObject, selection, bundlePath,
BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BuildTarget.StandaloneWindows64);
}
}
DeleteTxt(TheFolder);//删除.txt文件
AssetDatabase.Refresh();
}
static private void ChangeLua2Txt(DirectoryInfo TheFolder, string path)
{
foreach (var NextFile in TheFolder.GetFiles("*.lua"))
{
var utf8WithoutBom = new System.Text.UTF8Encoding(false);
StreamReader sr = new StreamReader(NextFile.FullName, utf8WithoutBom);
string text = sr.ReadToEnd();
FileStream fs = new FileStream(path + "/" + Path.ChangeExtension(NextFile.Name, "txt"), FileMode.Create);
StreamWriter sw = new StreamWriter(fs, utf8WithoutBom);
sw.Write(text);
sr.Close();
sw.Flush();
sw.Close();
fs.Close();
}
foreach (DirectoryInfo NextFolder in TheFolder.GetDirectories())
{
ChangeLua2Txt(NextFolder, path);
}
}
static private void DeleteTxt(DirectoryInfo TheFolder)
{
foreach (var NextFile in TheFolder.GetFiles("*.txt"))
{
NextFile.Delete();
}
foreach (DirectoryInfo NextFolder in TheFolder.GetDirectories())
{
DeleteTxt(NextFolder);
}
}
//当前字段值修改的时候会被调用,对话框弹出时也会调用一次
void OnEnable()
{
Debug.Log("本地IP地址:" + Network.player.ipAddress);
}
}
这样就可以把lua代码打包成AB文件了
设置LuaSvr.mainState.loaderDelegate,返回AB包中的lua文件就可以了,每次在require lua文件都会调用这个委托,如果打包的时候是.txt文件,就要改成.txt后缀
// 加载lua文件Delagate
private byte[] LuaFileLoader(string strFile, ref string absoluteFn)
{
if (strFile == null)
{
return null;
}
string filename = "assets/resources/hotfix/lua/" + strFile.Replace('.', '/') + ".bytes";
ab = AssetBundle.LoadFromFile(Application.persistentDataPath + "/lua.unity3d");
TextAsset textAsset = ab.LoadAsset(filename);
return textAsset.bytes;
}
因为笔者之前打包进AB包的lua文件本身就是.txt格式,有一个比较笨的办法(不建议使用。。。)——将AB包中的lua文件写入txt中保存到StreamingAssets目录下,然后再从StreamingAssets下去加载运行lua代码,到这里lua的热更新就实现了
using SLua;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
[CustomLuaClass]
public class Entry : MonoBehaviour {
LuaSvr svr;
LuaTable self;
LuaFunction update;
[CustomLuaClass]
public delegate void UpdateDelegate(object self);
UpdateDelegate UD;
AssetBundle AB;
string dataPath
{
get { return Application.streamingAssetsPath + "/"; }
}
void Start()
{
StartCoroutine(LoadAssetsBoundle());
}
void Update()
{
if (UD != null) UD(self);
}
IEnumerator LoadAssetsBoundle()
{
//这里可以先下载版本文件,对比服务器版本,如果版本有更新再下载资源
string path= @"http://127.0.0.1/C:/Users/Administrator.SC-201902281819/Desktop/坦克/AssetBoundles/Windows/LuaCode.unity3d";
UnityWebRequest webRequest = UnityWebRequest.GetAssetBundle(path);
yield return webRequest.SendWebRequest();
if (webRequest.error != null) Debug.Log(webRequest.error);
AssetBundle AB = DownloadHandlerAssetBundle.GetContent(webRequest);
object[] luaList = AB.LoadAllAssets();
foreach (var item in luaList)
{
var lua = item as TextAsset;
var savePath = dataPath + lua.name + ".txt";
FileStream FS = new FileStream(savePath, FileMode.Create, FileAccess.Write);//创建写入文件
StreamWriter SW = new StreamWriter(FS);
SW.Write(lua.text);
SW.Close();
FS.Close();
}
StartLua();
}
private void StartLua()
{
//将AB中的lua文件保存到本地,读取
svr = new LuaSvr();
svr.init(null, () =>
{
string initLua = @"return require(""Entry"")";
IntPtr l = LuaSvr.mainState.L;
LuaDLL.lua_getglobal(l, "package");
LuaDLL.lua_pushstring(l,dataPath+ "?.txt");
LuaDLL.lua_setfield(l, -2, "path");
LuaDLL.lua_pop(l, 1);
self = LuaSvr.mainState.doString(initLua) as LuaTable;
update = (LuaFunction)self["update"];
UD = update.cast();
});
}
// lua调用 C# 脚本函数
public void Log(string str)
{
Debug.Log("Lua调用C#Log方法:" + str);
}
}