资源目录划分

Bundle构建工具
框架开发流程
- Bundle处理:构建,加载,更新
- C#调用Lua,Lua的加载和管理,绑定和执行
- 向Lua提供接口
- 完善和优化
打包策略
- 按文件夹打包:Bundle数量少,首次下载块,但是后期更新补丁大
- 按文件打包:Bundle数量多,首次下载较慢,更新补丁小
简介
- 使用XLua之前,我们需要按照功能将模块封装为预制体,将其存放在Module目录对应的功能目录下,Tag设置为uiComponent。
- 每个功能模块至少有两个预制体脚本,一个是逻辑脚本功能(功能名.lua),视图脚本(功能名View.lua)
- 在ui.lua脚本中引用逻辑脚本



获取图片和预制体:
Public.ResMeg.GetSpritePath(图片名,功能名)
Public.ResMeg.GetPrefabPath(预制体名,功能名)
单机应用
配置文件数据
生成:配置文件由工具生成,从excel生成lua脚本
我们有一个汇总数据的GameMainData的Lua脚本,里面require了所有需要用到的数据模块(model),例如:require(“Model/DemoPlayerModel”),相当于统一管理数据的地方。
model最简单的脚本是这样的:
local DemoPlayerModel = {}
function DemoPlayerModel:SetData(v)
self.data = v
end
function DemoPlayerModel:GetData()
return self.data
end
function DemoPlayerModel:SetEquipData(v)
self.Equipdata = v
end
function DemoPlayerModel:GetEquipData()
return self.Equipdata
end
function DemoPlayerModel:GetEuipByID(equipID)
for i ,v1 in ipairs(self.Equipdata) do
if v1.ID = equipID then
return v1
end
end
return 0
end
return DemoPlayerModel
在刚开始进入游戏的lua脚本中,我们可以把数据读取进来:
local data = {}
data.money = 55
data.level = 1
data.exp = 0
GameMainData.DemoPlayerModel.SetData(data)
local temp = {}
local EquipData = {}
temp.ID = "1"
temp.Name = "武器1"
tempp.Info = "介绍1"
temp.atk = 15
table.insert(EquipData,temp)
GameMainData.DemoPlayerModel.SetEquipData(EquipData)
data,EquipData从存档中或表中读取
然后就可以require对应的model来使用对应的函数读取数据了。
GameMainData.DemoPlayerModel:GetEquipByID(equipid).Name
GameMainData.DemoPlayerModel:GetEquipByID(equipid).Info
C#与XLua的通信
在C#端有一个LuaBehaviour,用来维护C#和Lua的通信:
[CSharpCallLua]
public delegate void LuaAction(int x,int y,int z);
public static LuaAction ray;
--snip--
# 将委托事件和ray这个值进行绑定,lua端用这个值即可读取对应的数据
scriptEnv.Get("ray", out ray)
--snip--
在另外的脚本中,我们可以这样传递数据到Lua:
int x = 0;
int y = 0;
int z = 0;
LuaBehaviour.ray(x,y,z)
在Lua端,这样接收数据:
在main.lua.txt脚本中:
require 'event'
function ray(x,y,z)
-- 这个函数接收数据之后,又生成一个“ray”事件
Event.Call("ray",x,y,z)
end
然后在其他lua脚本中:
-- 订阅事件
function UITable:Awake()
self:AddListener("ray", self.ray)
end
function UITable:ray(evtName,x,y,z)
self.player.transform.DoMove(Vector3(tonumber(x),tonumber(y),tonumber(z))):OnComplete(function() end)
网游应用
Net模块功能
net.lua模块里面的方法:
- SetAccount:设置账号信息
- SetAddress:设置逻辑服务器访问地址
- SetChatAddress:设置聊天服务器访问地址
- SetPvpAddress:设置Pvp服务器访问地址
- callback:C#端收到服务器响应后回调
- Receive:解析服务器返回数据,将解析后的数据发往GameMainData,调用错误处理模块或者Lua业务端回调方法
- SetCount:设置当前等待相应请求数量
- SendLogin:发送登录请求
- Send/SendTo:发送常规业务Http请求’
- SignData:标记account,session,请求序列号
网络请求
我们需要创建两个lua脚本:
第一个是Net+请求名+.lua文件:
Create方法接收输入参数与回调lua方法
OnResult方法增加收到结果后的处理内容
第二个是Net+请求名+Base.lua:
Init方法指明请求消息msgName
Init方法将输入参数装入data
Init方法设置回调lua方法
Send方法将请求post到服务器
OnReceive方法将返回数据存入框架,由框架进行解析和存放
OnReceive方法根据是否存在回调方法进行方法调用
例子:
NetBattleArrayConversion = {}
local net = require("Net/Base/NetBattleArrayConversionBase")
net.__index = net
function NetBattleArrayConversion.Create(posid,posid2,callback)
local t = {}
setmetatable(t, net)
t:Init(posid,posid2,callback)
return t
end
function net:OnResult(v)
end
local NetBattleArrayConversionBase = {}
function NetBattleArrayConversionBase:Init(posid,posid2,callback)
self.data = {}
self.data.msgName = "NetBattleArrayConversion"
self.data.postid = posid
self.data.posid2 = posid2
self.data.callback = callback
return self
end
function NetBattleArrayConversionBase:Send()
Net.Send(self)
end
function NetBattleArrayConversionBase:OnReceive(v)
self.reponseData = v.value.data[self.data.msgName]
self.OnResult(v)
if self.callback ~= nil then
self.callback(self)
end
end
return NetBattleArrayConversionBase
使用:
require("Net/NetBattleArrayConversion")
--snip--
NetBattleArrayConversion.Create(self.posid1,self.posid2,function(rpc) self.process() end):Send()
--snip--
协议请求与响应数据
推荐流程:

这个架构的好处是解析的数据直接放入了GameMainData中(在GameMainData中订阅即可),不用我们手动赋值了,业务模块直接取即可。而且Lua的优势在于我们不用事先定义数据类型就能接收数据,接收数据后,直接就能取出data.name,data.id等信息。这个格式以服务端为准。一般属性和列表这两种数据格式就能满足需求。
框架讲解
框架目录结构
在Asset文件夹下,有以下目录:
Ant:自定义组件C#脚本
AssetBundlesLocal:热更新资源目录
Editor:编辑器开发目录
EditorPrefab:自定义组件预制体
Lua:lua脚本存放目录
Plugins:插件目录
Resources:内部资源目录
XLua:XLua插件目录
XLua框架运行流程

UIGameLoading:热更新检测模块,检测版本,检测资源,开启下载,并进入游戏主界面
AppBoot:启动器模块,初始化音频模块,SDK初始化完毕方法,Lua回调时间列表监听与调用
LuaTools:Lua工具类,有网络请求方法,网络请求回调方法,音频播放,等等
LuaBehaviour:Lua纽带类,与main.lua通信,定义luaAction,启动lua虚拟机,执行自定义lua资源目录,绑定lua端方法
ResourceManager:预制体管理、游戏体管理,图片管理,功能模块管理,lua脚本管理,音频资源管理,字体资源管理,材质贴图管理
SystemTool:系统拒绝,引擎碰撞回调,射线回调,输入回调等
ui.lua:业务功能管理模块,功能打开关闭,生成预制体,loading界面控制
main.lua:lua主脚本,加载框架模块,打开登录功能,绑定方法接收端
net.lua:网络管理模块,设置请求IP,设置session,设置账户,设置请求数量,网络请求响应回调方法,网络请求发送方法
list.lua:列表工具,列表创建方法
GameMainData:主数据管理模块,引用主数据model,设置主数据,提供数据操作方法
event.lua:事件管理模块,包含事件操作方法
plugin.lua:插件管理目录,设置插件相关信息
public.lua:功能资源管理模块,管理公共方法,管理公共变量
tools.lua:工具管理模块,有工具类方法
ui.lua
引用:LuaAntList、UIBase
重要变量:Canvas、3DObject、摄像机
重要列表:layerBox、uiTableBox
net.lua
网络请求流程:
业务模块请求方法->Net资源脚本->Net.lua模块->LuaTools Http方法->服务器端->LuaTools回调方法->AppBoot delayCall->Net.lua Receive方法->Net资源脚本 OnReceive OnResult方法->业务模块回调处理
UIGameLoading
using System.Collections;
using System.Collections.Generic;
#if !UNITY_WEBPLAYER
using System.IO;
#endif
using UnityEngine;
using UnityEngine.UI;
using System;
public class UIGameLoading : MonoBehaviour
{
string luaABname = "lua";
public Text txtVersion;
public Text txtRes;
public Text txtSize;
public Text txtSize2;
public Text txtSpeed;
public Slider progressBar;
public GameObject objMessage;
public Text txtContent;
public Button btnConfirm;
public GameObject Tip;
#if !UNITY_WEBPLAYER
List<AssetBundleInfo> listDown;
Dictionary<string, AssetBundleInfo> dicServer;
Dictionary<string, AssetBundleInfo> dicLocal;
string timeServerString;
string timeLocalString;
uint timeServer { get { return uint.Parse(timeServerString); } }
uint timeLocal { get { return uint.Parse(timeLocalString); } }
float allSize;
float downSize;
public void Start()
{
// if (int.Parse(GetTimeStamp(true))>1560139201){
// Tip.SetActive(true);
// return;
// }
txtVersion.text = "当前版本:" + GameConfig.Version;
txtRes.text = "";
txtSize.text = "";
txtSize2.text = "";
progressBar.gameObject.SetActive(false);
dicLocal = new Dictionary<string, AssetBundleInfo>();
if (Directory.Exists(LoadTools.assetBundlePath) == false)
Directory.CreateDirectory(LoadTools.assetBundlePath);
#if UNITY_EDITOR
if (LoadTools.useAssetBundle == false){
Debug.Log("aa");
StartLogin();
}
else
CopyDataFromStreaming();
#else
CopyDataFromStreaming(); //重要
#endif
}
public static string GetTimeStamp(bool bflag)
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
string ret = string.Empty;
if (bflag)
ret = Convert.ToInt64(ts.TotalSeconds).ToString();
else
ret = Convert.ToInt64(ts.TotalMilliseconds).ToString();
return ret;
}
private void CopyDataFromStreaming()
{
Debug.Log("CopyDataFromStreaming LoadTools.assetBundlePath " +LoadTools.assetBundlePath);
// if (File.Exists(Path.Combine(LoadTools.assetBundlePath, "assetslist.txt")) == false)
// {
StartCoroutine(CopyStreaming());
//}
// else
// {
// StartDown();
// }
}
private IEnumerator CopyStreaming()
{
progressBar.gameObject.SetActive(true);
progressBar.value = 0;
txtRes.text = "首次进入游戏初始化资源";//(不消耗流量)
yield return new WaitForEndOfFrame();
string wwwStreamingAssetBundlePath = Path.Combine(LoadTools.wwwStreamingAssetsPath, "AssetBundle");
Debug.Log("CopyStreaming wwwStreamingAssetBundlePath " +wwwStreamingAssetBundlePath);
// 拿到文件
WWW www = new WWW(Path.Combine(Path.Combine(LoadTools.wwwStreamingAssetsPath, "AssetBundle"), "assetslist.txt"));
while (www.isDone == false)
{
yield return new WaitForEndOfFrame();
}
if (string.IsNullOrEmpty(www.error) == false)
{
StartCoroutine(CopyStreaming());
yield break;
}
Debug.Log("CopyStreaming 1111 " );
string txtAssetslist = www.text;
string[] arrAssetslist = www.text.Split('\n');
for (int i = 1; i < arrAssetslist.Length; i++)
{
//Debug.Log(i+" CopyStreaming arrAssetslist[i] " + arrAssetslist[i] );
string[] arrData = arrAssetslist[i].Split(',');
progressBar.value = (i + 1f) / arrAssetslist.Length;
string fileName = arrData[1]+"_"+arrData[2];
string abname = arrData[1];
//Debug.Log("CopyStreaming fileName " +Path.Combine(wwwStreamingAssetBundlePath, fileName) );
// 通过文件名拿文件(本地ab包)
www = new WWW(Path.Combine(wwwStreamingAssetBundlePath, fileName));
while (www.isDone == false)
{
yield return new WaitForEndOfFrame();
}
if (string.IsNullOrEmpty(www.error) == false)
{
i--;
continue;
}
if (fileName.IndexOf("lua") != -1 )
{
Debug.Log("fileName yy *** "+fileName);
}
// 将本地ab包写入assetBundlePath(热更目录)
File.WriteAllBytes(Path.Combine(LoadTools.assetBundlePath, abname), www.bytes);
yield return new WaitForFixedUpdate();
}
//File.Copy(ResourcesTools.streamingAssetsPath + "/AssetBundle/assetslist.txt", Path.Combine(ResourcesTools.assetBundlePath, "assetslist.txt"));
// assetslist.txt写入assetBundlePath
File.WriteAllText(Path.Combine(LoadTools.assetBundlePath, "assetslist.txt"), txtAssetslist);
txtRes.text = "";
// 开始下载
StartDown();
}
private void StartDown()
{
Debug.Log("StartDown StartDown " );
listDown = new List<AssetBundleInfo>();
if (string.IsNullOrEmpty(GameConfig.AssetIP) == false)
{
StartCoroutine(CheckResources());
}
else
{
Debug.Log("bb");
StartLogin();
}
}
private IEnumerator CheckResources()
{
string s = "检查更新";
txtRes.text = s;
#if UNITY_EDITOR
WWW www = new WWW(GameConfig.AssetIP + "/assetslist.txt");
#else
// 从服务端下载assetslist.txt
WWW www = new WWW(GameConfig.AssetIP + "/assetslist.txt?v="+UnityEngine.Random.Range(10000,99999));
#endif
int index = 0;
while (www.isDone == false)
{
txtRes.text = s + "...".Substring(index);
index = (index + 1) % 2;
yield return new WaitForEndOfFrame();
}
if (string.IsNullOrEmpty(www.error) == false)
{
Debug.Log(www.url + "\n" + www.error);
yield return new WaitForSeconds(5f);
StartCoroutine(CheckResources());
}
else
{
string[] arr = www.text.Split('\n');
timeServerString = arr[0];
// 创建AssetBundleInfo字典
dicServer = CreateAssetDictionary(arr);
// 取当前版本信息
if (File.Exists(LoadTools.assetBundlePath + "/assetslist.txt") == true)
{
#if !UNITY_WEBPLAYER
arr = File.ReadAllText(LoadTools.assetBundlePath + "/assetslist.txt").Split('\n');
#endif
timeLocalString = arr[0];
dicLocal = CreateAssetDictionary(arr);
GameConfig.Assetversion = (timeLocal % 100000).ToString();
txtVersion.text = "当前版本:" + GameConfig.Version + ":" + GameConfig.Assetversion;
}
else
{
timeLocalString = "0";
dicLocal = new Dictionary<string, AssetBundleInfo>();
}
yield return new WaitForEndOfFrame();
// 打包时间不一样,版本不一样
if (timeServer > timeLocal)
{
foreach (var item in dicServer.Values)
{
// 不包含资源
if (dicLocal.ContainsKey(item.name) == false)
{
allSize += item.size;
listDown.Add(item);
}
// 资源不是最新
else if (dicLocal[item.name].md5 != item.md5)
{
allSize += item.size;
listDown.Add(item);
}
}
// 下载资源
//objMessage.SetActive(true);
StartCoroutine(DownAssets());
// txtContent.text = "发现版本更新,本地更新大小约"+GetSize(allSize)+",是否更新?";
/*
btnCancel.onClick.AddListener(() => { Application.Quit(); });
btnConfirm.onClick.AddListener(() => {
// objMessage.SetActive(false);
StartCoroutine(DownAssets()); }); */
}
else
{
Debug.Log("cc");
StartLogin();
}
}
}
private string GetSize(float v)
{
if (v < 1024)
return v + "K";
if (v < 1024 * 1024)
return (v / 1024f).ToString("0.00") + "KB";
if (v < 1024 * 1024 * 1024)
return (v / (1024f * 1024f)).ToString("0.00") + "MB";
return "";
}
private IEnumerator DownAssets()
{
txtRes.text = "正在下载更新文件";
txtSize2.text = "0%";
txtSize.text = string.Format("{0}MB/{1}", (downSize).ToString("0.00"), GetSize(allSize));
txtSpeed.text = "0KB/S";
progressBar.value = 0;
progressBar.gameObject.SetActive(true);
for (int i = 0; i < listDown.Count; i++)
{
AssetBundleInfo ab = listDown[i];
WWW www = new WWW(GameConfig.AssetIP + "/" + ab.name + "_" + ab.md5);
while (www.isDone == false)
{
yield return new WaitForEndOfFrame();
progressBar.value = (1f * (downSize + www.bytesDownloaded)) / allSize;
txtSize.text = string.Format("{0}/{1}", GetSize(downSize + www.bytesDownloaded), GetSize(allSize));
txtSpeed.text = string.Format("{0}/S", GetSize(www.bytesDownloaded / Time.deltaTime));
txtSize2.text = (progressBar.value * 100).ToString("0.00") + "%";
}
//yield return www;
if (string.IsNullOrEmpty(www.error) == false)
{
Debug.Log(ab.name + " " + www.error + " " + www.url);
yield return new WaitForSeconds(5f);
i--;
continue;
}
downSize += www.bytesDownloaded;
#if !UNITY_WEBPLAYER
File.WriteAllBytes(LoadTools.assetBundlePath + "/" + ab.name, www.bytes);
#endif
dicLocal[ab.name] = ab;
if (i == listDown.Count - 1)
{
timeLocalString = timeServerString;
GameConfig.Assetversion = (timeLocal % 100000).ToString();
}
SaveLocal();
}
StartLogin();
}
private void SaveLocal()
{
string txt = timeLocalString;
foreach (var item in dicLocal.Values)
{
txt += "\n" + item.type + "," + item.name + "," + item.md5 + "," + item.size + ","+item.level;
}
#if !UNITY_WEBPLAYER
File.WriteAllText(LoadTools.assetBundlePath + "/assetslist.txt", txt);
#endif
}
private Dictionary<string, AssetBundleInfo> CreateAssetDictionary(string[] arrServerList)
{
Dictionary<string, AssetBundleInfo> result = new Dictionary<string, AssetBundleInfo>();
for (int i = 1; i < arrServerList.Length; i++)
{
if (string.IsNullOrEmpty(arrServerList[i]) == true)
continue;
string[] arr = arrServerList[i].Split(',');
AssetBundleInfo ab = new AssetBundleInfo(int.Parse(arr[0]), arr[1], arr[2], float.Parse(arr[3]), int.Parse(arr[4]));
result.Add(ab.name, ab);
}
return result;
}
private void StartLogin()
{
StartCoroutine(InitGame());
}
private IEnumerator InitGame()
{
txtRes.text = "游戏初始化";
progressBar.gameObject.SetActive(true);
txtSpeed.gameObject.SetActive(false);
txtSize.gameObject.SetActive(false);
txtSize2.gameObject.SetActive(false);
progressBar.value = 0f;
yield return new WaitForEndOfFrame();
LoadTools.Init();
// List<AssetBundleInfo> list = new List<AssetBundleInfo>(dicLocal.Values);
// for (int i = 0; i < list.Count; i++)
// {
// if (list[i].level == 1)
// {
// AssetBundleCreateRequest ar = LoadTools.LoadAssetBundleAsync(list[i].name);
// yield return ar;
// LoadTools.AddAssetBundle(ar.assetBundle);
// }
// progressBar.value = (1f+i)/list.Count * 0.9f;
// yield return new WaitForEndOfFrame();
// }
txtRes.text = "";
progressBar.gameObject.SetActive(false);
Debug.Log("over load");
#if ( UNITY_ANDROID|| UNITY_IOS) && !UNITY_EDITOR
luaABname = "";
Debug.Log("UNITY_ANDROID UNITY_ANDROID luaABname = "+luaABname);
if (luaABname==""){
foreach (string key in dicLocal.Keys)
{
// Debug.Log("key "+key);
if (key.IndexOf("lua") != -1 )
{
luaABname = key ;
Debug.Log("luaABname == " + luaABname);
ResourceManager.luaFileName = luaABname ;
}else if (key.IndexOf("font") != -1 )
{
Debug.Log("font == " + key);
ResourceManager.InitFontResource(key);
}
}
}
Debug.Log("luaABname yy "+luaABname);
ResourceManager.loadLuaToByte(luaABname);
#endif
// 正式开始游戏,LuaBehaviour在Main上面运行,直接与lua交互
GameObject go = Resources.Load<GameObject>("Main");
Instantiate(go);
Destroy(this.gameObject);
AppBoot.instance.Init();
}
private void SluaTickHandler(int obj)
{
progressBar.value = obj / 100f / 10f + 0.9f;
}
private bool appBootInit;
private void DoComplete()
{
appBootInit = true;
}
#endif
}
public class AssetBundleInfo
{
public string name;
public string md5;
public float size;
public int type;
public int level;
public AssetBundleInfo(int type, string name, string md5, float size,int level)
{
this.type = type;
this.name = name;
this.size = size;
this.md5 = md5;
this.level = level;
}
}
AppBoot
音频初始化,并存放消息回调,在对应时间调用事件:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using DG.Tweening;
using System.IO;
//using SDKFly;
using UnityEngine;
public class AppBoot : MonoBehaviour {
public static AppBoot instance;
AudioSource audioMusic;
AudioSource audioEffect;
public GameTcpClient tcpClient;
public SoundMoudule sound;
public RectTransform RootUI;
public List<DelayCall> delayCall = new List<DelayCall>();
#if UNITY_EDITOR
public bool useAssetBundle;
#endif
private void Awake()
{
instance = this;
UnityEngine.Debug.Log("AppBoot Awake");
}
void Start ()
{
Screen.sleepTimeout = SleepTimeout.NeverSleep;
StartCoroutine(PlayMovie());
}
private IEnumerator PlayMovie()
{
#if !UNITY_EDITOR
// Handheld.PlayFullScreenMovie("logo.mp4", Color.black, FullScreenMovieControlMode.Hidden);
#endif
yield return new WaitForEndOfFrame();
// SdkTool.InitSDK(SdkInitComplete);
}
private void SdkInitComplete()
{
#if UNITY_EDITOR
// LoadTools.useAssetBundle = useAssetBundle;
#endif
DOTween.Init();
GameConfig.Init();
ServicePointManager.ServerCertificateValidationCallback = (p1, p2, p3, p4) => true;
Application.logMessageReceived += HandLog;
DontDestroyOnLoad(this.gameObject);
Instantiate<GameObject>(Resources.Load<GameObject>("UIGameLoading"), RootUI);
}
void HandLog(string logString, string stackTrace, LogType type)
{
}
// Action _callback;
public void Init() // Init(Action callback,Action<int> SluaTickHandler)
{ audioEffect = GameObject.Find("ASsound").GetComponent<AudioSource>();
audioMusic = GameObject.Find("ASmusic").GetComponent<AudioSource>();
// _callback = callback;
// StaticData.Init();
sound = new SoundMoudule(audioMusic, audioEffect);
// slua = new SLuaMoudule(SluaTickHandler, SluaCompleteHandler);
}
void Update ()
{
if(delayCall.Count > 0)
{
for (int i = delayCall.Count - 1; i >= 0; i--)
{
//if(delayCall[i].enable)
//{
if (Time.time > delayCall[i].time)
{
DelayCall d = delayCall[i];
delayCall.RemoveAt(i);
d.action();
//if (d.data == null)
// d.luaCallback.call();
//else
// d.luaCallback.call(d.data);
//d.luaCallback = null;
}
//}
//else
//{
// delayCall.RemoveAt(i);
//}
}
}
}
private void OnDestroy()
{
if (tcpClient != null)
tcpClient.Close();
}
public void AddDelayCall(float time,Action action)
{
DelayCall d = new DelayCall() { time = time + Time.time, action = action};
delayCall.Add(d);
}
//public void AddDelayCall(float time, LuaFunction timeCallback)
//{
// DelayCall d = new DelayCall() { time = time + Time.time, luaCallback = timeCallback, data = null };
// delayCall.Add(d);
//}
//public void AddKeyDelayCall(float time, LuaFunction timeCallback,string key)
//{
// DelayCall d = new DelayCall() { time = time + Time.time, luaCallback = timeCallback, data = null , key = key };
// delayCall.Add(d);
//}
//public void RemoveDelayCall(string key)
//{
// for (int i = delayCall.Count - 1; i >= 0; i--)
// {
// if (key == delayCall[i].key)
// {
// delayCall.RemoveAt(i);
// }
// }
//}
public void AddCallFromAsync(Action action)
{
//Debug.Log("AddCallFromAsync ************** ");
DelayCall d = new DelayCall() { time = 0, action = action};
delayCall.Add(d);
}
}
public class DelayCall
{
public Action action;
//public LuaFunction luaCallback;
//public string data;
public float time;
//public bool enable=true;
//public string key;
}
LuaTools
这个脚本存放了lua端调用C#端的一些通用方法
//using SLua;
using UnityEngine;
using System.Text;
using System;
using DG.Tweening;
//using Spine.Unity;
//using Spine;
using UnityEngine.UI;
using XLua;
//[CustomLuaClass]
public class LuaTools
{
public static string GetVersion()
{
return GameConfig.Version + ":" + GameConfig.Assetversion;
}
private static string[] DressName = new string[] { "Phiz", "Hairstyle", "Coat", "Pants", "arms" };
[CSharpCallLua]
public delegate void CallBack(string content);//
// 回调委托
public static CallBack callBack;
// 长链接
public static void SendMessage(string v)
{
AppBoot.instance.tcpClient.Send(v);
}
// 和Http函数一个作用
public static void HttpServer(string path, string data, LuaFunction callback)
{
Debug.Log("HttpServer ~!!!!");
Http("https://" + GameConfig.ServerIP + "/" + path, data, callback);
}
public static void Init()
{
// callBack映射到lua的callBack
callBack = LuaBehaviour.luaEnv.Global.Get<CallBack>("callBack");
}
public static void Http(string url, string data, LuaFunction callback)
{
if (callBack == null)
{
callBack = LuaBehaviour.luaEnv.Global.Get<CallBack>("callBack");
}
if (url == "")
{
url = "http://" + GameConfig.ServerIP + "/SceneServer";
}
HTTPRequest client = new HTTPRequest(url, "POST", 5000, (HTTPResponse response) => {
if (response.StatusCode == -1)
{
Debug.LogError("HTTPResponse error " + response.Error);
}
else
{
Debug.Log("response.GetResponseText() == " + response.GetResponseText());
// Debug.Log(AppBoot.instance);
// 添加回调方法进AppBoot
AppBoot.instance.AddCallFromAsync(() => { callBack(response.GetResponseText()); });
}
});
client.ContentType = "application/x-www-form-urlencoded";
string str = WWW.EscapeURL(data);
client.AddPostData("data", str);
client.Start();
}
// 建立长链接
public static void SocketConnet(string ip, int port)
{
if (AppBoot.instance.tcpClient != null)
AppBoot.instance.tcpClient.Close();
AppBoot.instance.tcpClient = new GameTcpClient(ip, port);
}
public static long ToUnixTime(string timeString)
{
DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1, 0, 0, 0, 0));
long t = (DateTime.Parse(timeString).Ticks - startTime.Ticks) / 10000; //除10000调整为13位
return t;
}
// 添加回调
public static void DelayCall(float time, Action callback)
{
// UnityEngine.Debug.Log(time);
// UnityEngine.Debug.Log(callback);
// UnityEngine.Debug.Log( AppBoot.instance);
AppBoot.instance.AddDelayCall(time, callback);
}
public static void PlaySound(string name)
{
AppBoot.instance.sound.PlaySound(name);
}
public static void PlaySoundWav(string name)
{
AppBoot.instance.sound.PlaySoundWav(name);
}
public static void PlayMusic(string name, bool loop)
{
AppBoot.instance.sound.PlayMusic(name,loop);
}
public static void SetEnableMusic(bool v)
{
AppBoot.instance.sound.enableMusic = v;
}
public static void SetEnableEffect(bool v)
{
AppBoot.instance.sound.enableEffect = v;
}
public static bool GetEnableMusic()
{
return AppBoot.instance.sound.enableMusic;
}
public static bool GetEnableEffect()
{
return AppBoot.instance.sound.enableEffect;
}
}
当脚本未初始化完毕时,我们想在lua调用其方法需要延迟调用,延迟一秒播放音乐写法如下:
local obj = CS.UnityEngine.GameObject(){
obj.transform:DoScaleX(1,1):OnComplete(
function()
LuaTools.PlayMusic("music1",true)
end
)
}
LuaBehaviour
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using XLua;
using System;
using System.IO;
[System.Serializable]
public class Injection
{
public string name;
public GameObject value;
}
[LuaCallCSharp]
public class LuaBehaviour : MonoBehaviour {
public TextAsset luaScript;
public string luaScript_ls;
public Injection[] injections;
internal static LuaEnv luaEnv = new LuaEnv(); //all lua behaviour shared one luaenv only!
internal static float lastGCTime = 0;
internal const float GCInterval = 1;//1 second
private static Action luaStart;
private static Action luaUpdate;
private static Action luaOnDestroy;
public static LuaTable scriptEnv;
// float times = 2;
private TextAsset text_;
string main;
string main2;
// 关键!C#调用Lua的委托
[CSharpCallLua]
public delegate void LuaAction(string v);
[CSharpCallLua]
public delegate void LuaAction2(float x, float y ,float z);
//[CSharpCallLua]
//public delegate void test22();
[CSharpCallLua]
public delegate string ConfigNameList_GetRandomName();
public static ConfigNameList_GetRandomName configNameList_GetRandomName;
[CSharpCallLua]
public delegate string ConfigYuanFen_GetData(string name,string value,string name2);// 拿到缘分数据
public static ConfigYuanFen_GetData configYuanFen_GetData;
//[CSharpCallLua]
//public delegate void luaStart();
//[CSharpCallLua]
//public delegate void luaUpdate();
//[CSharpCallLua]
//public delegate void luaOnDestroy();
//[CSharpCallLua]
//public delegate void luaAwake();
public static LuaAction2 ray;
public static LuaAction rayF;
public static LuaAction TriggerEnter_;
public static LuaAction sockerSendMsg;
// 根据不同的系统加载文件
private byte[] CustomLoaderMethod(ref string fileName)
{
// Debug.Log("CustomLoaderMethod fileName == "+fileName);
#if UNITY_ANDROID && !UNITY_EDITOR
// Debug.Log("UNITY_ANDROID fileName == "+fileName);
//Debug.Log("ResourceManager.luaFileName "+ ResourceManager.luaFileName);
// Debug.Log("fileName "+fileName);
// AssetBundle ab = ResourceManager.Load( ResourceManager.luaFileName ); //"lua_945ca74524bdea802078acbf98db6f0b"
// string str = ab.LoadAsset<TextAsset>(fileName).text;
// Debug.Log("str "+str);
// byte[] byteArray = System.Text.UTF8Encoding.Default.GetBytes ( str );
// return File.ReadAllBytes(fileName);
return ResourceManager.allLuaByte[ fileName.ToLower() ];
#elif UNITY_IOS && !UNITY_EDITOR
// AssetBundle ab = ResourceManager.Load( ResourceManager.luaFileName ); //"lua_945ca74524bdea802078acbf98db6f0b"
// string str = ab.LoadAsset<TextAsset>(fileName).text;
// byte[] byteArray = System.Text.UTF8Encoding.Default.GetBytes ( str );
// return File.ReadAllBytes(fileName);
// Debug.Log("ResourceManager.allLuaByte fileName == "+fileName.ToLower());
// Debug.Log("ResourceManager.allLuaByte == "+ResourceManager.allLuaByte[fileName.ToLower()]);
return ResourceManager.allLuaByte[fileName.ToLower()];
#else
// Debug.Log("UNITY_IOS fileName == "+fileName);
//找到指定文件
fileName = main2 + fileName.Replace('.', '/') + ".lua.txt";
// Debug.Log("fileName " + fileName);
if (File.Exists(fileName))
{
// Debug.Log("File.Exists " );
return File.ReadAllBytes(fileName.ToLower());
}
else
{
// Debug.Log("! File.Exists " );
return null;
}
#endif
}
void Awake()
{
Debug.Log ("Awake");
initWWWPath();
LuaEnv.CustomLoader method = CustomLoaderMethod;
luaEnv = new LuaEnv();
luaEnv.AddLoader(method);
scriptEnv = luaEnv.NewTable();
// scriptEnv = luaEnv.Global;
// 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
LuaTable meta = luaEnv.NewTable();
meta.Set("__index", luaEnv.Global);
scriptEnv.SetMetaTable(meta);
meta.Dispose();
scriptEnv.Set("self", this);
//foreach (var injection in injections)
//{
// scriptEnv.Set(injection.name, injection.value);
//}
//luaScript.name.Replace("lua", "") +
//StartCoroutine(LoadLua());
// 这里启动lua主脚本
LoadLua() ;
// Debug.Log ("Awake11");
}
void initWWWPath(){ //main。lua的下载地址
string streamingPath = LoadTools.assetBundlePath;// Application.persistentDataPath;
#if UNITY_5 || UNITY_2017 || UNITY_2018
#if UNITY_ANDROID && !UNITY_EDITOR
main = "file://"+streamingPath + "/assets/lua/" ;
main2 = streamingPath + "/assets/lua/" ;
#else
main = "file://" + Application.dataPath + "/" + "Lua/" ;
main2 = Application.dataPath + "/" + "Lua/" ;
#endif
#endif
}
void LoadLua() // IEnumerator
{
// WWW www = new WWW(main + "main.lua.txt");
TextAsset mainText = Resources.Load<TextAsset>("main.lua");
// 等待WWW代码执行完毕之后后面的代码才会执行。
// yield return www;
// if (www.error == null && www.isDone)
// {
// luaScript_ls = www.text;
luaScript_ls = mainText.text;
Init2();
// }else{
// Debug.Log("www.error " + www.error);
// }
}
void Init2(){
// 真正执行lua脚本
// TextAsset luaScript = Resources.Load("enter.lua") as TextAsset;
luaEnv.DoString(luaScript_ls, "LuaBehaviour", scriptEnv); //, "LuaBehaviour", null
// C#与lua的方法相绑定
Action luaAwake = scriptEnv.Get<Action>( "Awake");
scriptEnv.Get( "Start", out luaStart);
scriptEnv.Get( "update", out luaUpdate);
scriptEnv.Get( "ondestroy", out luaOnDestroy);
//scriptEnv.Get("sockerSendMsg", out sockerSendMsg);
scriptEnv.Get("ray", out ray);
scriptEnv.Get("OnTriggerEnter", out TriggerEnter_);
scriptEnv.Get("rayF", out rayF);
////Test("1");
///// Debug.Log("PostReceive = " + Test);
if (luaAwake != null)
{
UnityEngine.Debug.Log("luaAwake 2 " + luaAwake);
luaAwake();
}
Start1();
}
// Use this for initialization
void Start1 ()
{
// 这里调用了lua脚本中的两个方法
if (luaStart != null)
{
luaStart();
}
if(sockerSendMsg !=null){
sockerSendMsg("5555 &&UUUU");
}
// configNameList_GetRandomName = luaEnv.Global.Get<ConfigNameList_GetRandomName>("ConfigNameList_GetRandomName");
// configYuanFen_GetData = luaEnv.Global.Get<ConfigYuanFen_GetData>("ConfigYuanFenData_getVarByCustom2");
// if(configYuanFen_GetData != null)
// {
// Debug.Log("55555555555555555555555555555555 " +configYuanFen_GetData("yf_id", "205", "yf_name"));
// }
}
// Update is called once per frame
void Update ()
{
if (luaUpdate != null)
{
luaUpdate();
}
if (Time.time - LuaBehaviour.lastGCTime > GCInterval)
{
luaEnv.Tick();
LuaBehaviour.lastGCTime = Time.time;
}
}
void OnDestroy()
{
if (luaOnDestroy != null)
{
luaOnDestroy();
}
luaOnDestroy = null;
luaUpdate = null;
luaStart = null;
scriptEnv.Dispose();
injections = null;
configNameList_GetRandomName = null;
}
[LuaCallCSharp]
public static void SendMsg(int tag , string data){
SocketHelper.SendMsg( (PvpMsg)(tag),data);
}
}
main.lua
在main.lua中对应着C#绑定的方法:
require 'net'
require 'plugin'
require("tools")
require 'list'
require 'event'
require 'GameMainData'
require 'StaticData/StaticData'
--math.randomseed(os.time())
function Start()
require("UI/ui_1")
--UI.OpenUI("DemoEnter")
UI.OpenUI("loginDemo")
end
function sockerSendMsg(v)
Event.Call("sockerSendMsg" , v)
end
function ray(x,y,z)
Event.Call("ray" , x,y,z)
end
function rayF(v)
Event.Call("rayF",v,x,y,z)
end
function OnTriggerEnter(v)
Event.Call("OnTriggerEnter" , v)
end
function update()
Event.Call("updateG" )
end
其他部分
event.lua
管理事件添加,删除,调用的脚本:
Event = {}
local dic = {}
function Event.Add(type, func)
if dic[type] == nil then
dic[type] = {}
end
table.insert(dic[type],func)
end
function Event.Clear(type)
dic[type] = nil
end
function Event.Remove(type,func)
if dic[type] == nil then
return
end
for i,v in ipairs(dic[type]) do
if v == func then
table.remove(dic[type],i)
break
end
end
end
function Event.Call(type,... )
if dic[type] == nil then
return
end
for i=#dic[type],1,-1 do
if dic[type] == nil then
break
end
if dic[type][i] ~= nil then
dic[type][i](type,...)
end
end
end
EventType = {
MainUIRefreshUserData="MainUIRefreshUserData",
LoadUI = "LoadUI",
}
plugin.lua
功能:简化别名,差异功能
DOTween = CS.DG.Tweening.DOTween
Ease = CS.DG.Tweening.Ease
Vector3 = CS.UnityEngine.Vector3
Destroy = CS.UnityEngine.GameObject.Destroy
tool.lua
存放一些公共方法
JSON = require 'json'
function MathRound(value)
value = tonumber(value) or 0
return math.floor(value + 0.5)
end
function Split(inputstr, sep)
-- if sep == nil then
-- sep = "%s"
-- end
local t={} ; i=1
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
t[i] = str
i = i + 1
end
return t
end
function ToJsonString(v)
return JSON:encode(v)
end
function ToJsonTable(v)
return JSON:decode(v)
end
function DestroyObj(v)
UnityEngine.GameObject.Destroy(v)
end
function ToTimeString(time)
if time < 0 then
time = 0
end
local hour = math.floor(time/3600);
local minute = math.fmod(math.floor(time/60), 60)
local second = math.fmod(time, 60)
local rtTime = string.format("%02d:%02d:%02d", hour, minute, second)
return rtTime
end
function FormatTime(time)
local tab=os.date("*t",time)
local strTime = string.format("%04d.%02d.%02d %02d:%02d:%02d", tab.year, tab.month, tab.day, tab.hour, tab.min, tab.sec)
return strTime
end
function FormatDesc(desc,data)
return string.gsub(desc, "{[^}]+}", function(s)
local key = string.sub(s, 2,-2)
local pre = false
if string.find(key,"%%") ~= nil then
key = string.sub(key, 1,-2)
pre = true
end
if pre then
return string.format("%.1f%%",data[key]*100)
end
--return string.format("%d",data[key])
return data[key]
end)
end
function SubString(s,start,len)
local tb = {}
local index = 1
local result = ""
for utfChar in string.gmatch(s, "[%z\1-\127\194-\244][\128-\191]*") do
if index >= start then
if index > start + len then
return result
else
result = result..utfChar
end
end
index = index + 1
end
return ""
end
public.lua
放置业务相关的公用方法,例如通用提示板,警告方法,显示等。
list.lua
提供list操作方法
--metatable必须要有GetKey方法
function CreateList(metatable)
local table = {}
table.isList = true
table.metatable = metatable
metatable.__index = metatable
table.__index = table
local result = {}
setmetatable(result, table)
return result
end
自定义控件
开发思路:
- 能够完成C#版本
- 控件绑定了C#操作脚本(可选)
- 控件有lua逻辑脚本创建
- C#提供初始化方法,lua端调用方法完成初始化信息
- 渲染逻辑由C#端完成,lua端提供渲染方法,C#端调用渲染方法完成渲染
- 操作逻辑由lua端完成
这里我们把自定义控件放到Asset/EditorPrefab下,现在以倒计时控件为例:
C#脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using XLua;
public class AntTimer : MonoBehaviour {
protected int leftMinute;
protected Text timeText ;
protected int startHTtime ;
protected string ID ;
protected void Awake()
{
timeText = this.GetComponent<Text>();
}
public bool startflag = true ;
[CSharpCallLua]
public delegate void AntTimerCallBackDelegate(string rpc);
public AntTimerCallBackDelegate antTimerCallBack;
void Update(){
if (leftMinute>=0 && startflag ){ //startflag
startflag = false;
TimeStartWork();
}
}
protected void TimeStartWork()
{
if (Application.isPlaying == false)
return;
StartCoroutine("timerWorking");
}
public void SetTime(int leftMinute, bool startflag, string ID)
{
StopAllCoroutines();
this.leftMinute = leftMinute;
this.startflag = startflag;
this.ID = ID;
}
protected IEnumerator timerWorking()
{
while (leftMinute >= 0)
{
if ((leftMinute) / 3600 ==0){
timeText.text = (((leftMinute % 86400) % 3600) / 60) + "分" + (leftMinute % 60) + "秒";
}else{
timeText.text = ((leftMinute) / 3600) + "时" + (((leftMinute % 86400) % 3600) / 60) + "分" + (leftMinute % 60) + "秒";
}
yield return new WaitForSeconds(1);
leftMinute--;
}
// 计时结束,回调方法
if(antTimerCallBack != null)
antTimerCallBack( this.ID );
}
public void OnApplicationPause(bool isPause)
{
if (isPause)
{
startHTtime = ((int)Time.realtimeSinceStartup);
}
else
{
Refresh((int)Time.realtimeSinceStartup - startHTtime);
}
}
public void Refresh(int num)
{
leftMinute = leftMinute - num > 2 ? leftMinute - num : 2 ;
}
}
Lua端:
local LuaAntTimer = {}
LuaAntTimer.__index = LuaAntTimer
function CreateLuaAntTimer(ui,TimerObject,renderFun ,...)
local t = {}
setmetatable(t,LuaAntTimer)
t:Init(ui,TimerObject,renderFun,...)
return t
end
function LuaAntTimer:Init(ui,TimerObject,renderFun, ID)
-- 这里TimerObject指的就是C#端脚本
self.TimerObject = TimerObject
self.renderFun = renderFun
self.leftTime = 0
self.startflag = true
self.ID = ID
-- 回调方法赋值
self.TimerObject.antTimerCallBack = function( ID )
self.renderFun(ui,ID )
end
end
function LuaAntTimer:SetTime(v)
self.leftTime = v
--这里调用C#端脚本
self.TimerObject:SetTime(self.leftTime,self.startflag,self.ID)
end
function LuaAntTimer:Dispose()
for k,v in pairs(self.items) do
if v.Dispose ~= nil then
v:Dispose()
end
for k1,v1 in pairs(v) do
v[k1] = nil
end
end
for k,v in pairs(self) do
self[k] = nil
end
end
逻辑脚本创建插件:
self.a = CreateLuaAntTimer(self,self.antTimer, self.callback, 1)
self.a:SetTime(25)
function UITable:callback(param)
print("aram == " .. param)
end
视图脚本获取插件
function loginDemoView:SetUICompoent(Child){
--snip--
elseif child.name == "AntTimer" then
loginDemoView.AntTimer = child:GetComponent("AntTimer")
--snip--
}
资源打包
打包之前,我们需要创建一个编辑器脚本CreateAssetBundle,并创建打包方法
里面包含以下方法
ClearAll:清空AssetBundle的Name,设为None
CreateLuaBundler
CreateSoundBundle
CreateFontBundle
CreateCustomBundle
CreateSpriteBundle
CreatePrefabBundle
CreateMoudleBundle
CreateTextureBundle
打包的时候按照文件夹进行打包,在Asset/AssetBundlesLocal文件夹下,所有最下层的文件夹都会被打成一个包。
部分代码如下:
public static void BuildAssetBundleAndroid()
{
BuildAssetBundles(BuildTarget.Android);
}
public static void BuildAssetBundles(BuildTarget buildTarget)
{
string p = Application.dataPath.Replace("Assets", "AssetBundles");
if (Directory.Exists(p) == true)
Directory.Delete(p, true);
Directory.CreateDirectory(p);
p = Application.dataPath + "/StreamingAssets/AssetBundle";
if (Directory.Exists(p) == true)
Directory.Delete(p, true);
Directory.CreateDirectory(p);
CreateBundle();
// 正式开始打包
BuildPipeline.BuildAssetBundles("AssetBundles", BuildAssetBundleOptions.ChunkBasedCompression, buildTarget);
// 创建一个文件目录
CreateAssetslist();
Debug.Log("BuildAssetBundles End");
}
public static void CreateBundle()
{
ClearAll();
string path = "Assets/AssetBundlesLocal";
string[] allDir = Directory.GetDirectories(path);
Debug.Log(" allDir.Length " + allDir.Length);
for (int i = 0; i < allDir.Length; i++)
{
string[] allFiles = Directory.GetFiles(allDir[i]);
string dirName = Path.GetFileName(allDir[i]);
Debug.Log("dirName " + dirName);
for (int j = 0; j < allFiles.Length; j++)
{
Debug.Log("allFiles[j] " +allFiles[j]);
EditorUtility.DisplayProgressBar(dirName, allFiles[j], (1f+j)/allFiles.Length);
if (Path.GetExtension(allFiles[j]) == ".meta")
continue;
AssetImporter importer = AssetImporter.GetAtPath(allFiles[j]);
if(importer != null)
importer.assetBundleName = dirName.ToLower();
}
}
//CopyLua();
CreateLuaBundler();
CreateSoundBundle();
CreateFontBundle();
CreateCustomBundle();
CreateSpriteBundle();
CreatePrefabBundle();
CreateMoudleBundle();
CreateTextureBundle();
EditorUtility.ClearProgressBar();
AssetDatabase.Refresh();
}
点击编辑器对应按 钮之后,就能进行打包
资源云服务器搭建
首先我们需要一个云服务器,例如:阿里云,腾讯云
我们需要安装tomcat,然后把资源放到服务器对应的文件夹下,访问服务器IP对应的端口后面跟/文件夹名即可。
资源加载
在lua脚本一句话即可拿到图片,其他同理:
Public.ResMeg.GetSpritePath("gold","Public")
gold是图片名,Public是文件夹,其实,这里调用了C#端的ResourceManager脚本中的方法:
public static Sprite GetSpritePath(string key, string path)
{
//Debug.Log("public GetModule 2 " + key );
if (!GameConfig.UseAssetsBundle)
{
#if UNITY_EDITOR
return AssetDatabase.LoadAssetAtPath<Sprite>("Assets/AssetBundlesLocal/Sprite/" + path + "/" + key + ".png");
#else
return GetSprite(key,path);
#endif
}
else
{
return GetSprite(key,path);
}
}
static Sprite GetSprite(string key ,string path )
{
path = path.ToLower();
if (allSprite.ContainsKey(path+"_"+key))
{
return allSprite[path+"_"+key];
}
else
{
if(allAssetBundle.ContainsKey(path)){
SetAssetOne("Sprite",key,path, allAssetBundle[path]);
}else{
AssetBundle ab = Load(path);
allAssetBundle[path] = ab;
SetAssetOne("Sprite",key,path, ab);
}
return allSprite[path+"_"+key];
}
}
public static void SetAssetOne( string type , string key ,string path , AssetBundle ab )
{
if (type == "GameObject")
{
allGameObject[path+"_"+key] = ab.LoadAsset<GameObject>(key);
}
else if (type == "Sprite")
{
allSprite[path+"_"+key] = ab.LoadAsset<Sprite>(key);
}
else if (type == "Prefab")
{
allPrefab[path+"_"+key] = ab.LoadAsset<GameObject>(key);
}
else if (type == "Module")
{
allModule[path+"_"+key] = ab.LoadAsset<GameObject>(key);
}
else if (type == "AudioClip")
{
allAudioClip[path+"_"+key] = ab.LoadAsset<AudioClip>(key);
}
else if (type == "Texture")
{
allTexture[ key] = ab.LoadAsset<Texture>(key);
}
// allAssetBundle[ path ] = ab;
}
脚本加载
脚本的ab包加载和资源加载不一样,它在游戏开始之前就加载完毕,并且常驻内存:
游戏开始之前有这么一段代码,它把lua的包转化为二进制进内存了
#if ( UNITY_ANDROID|| UNITY_IOS) && !UNITY_EDITOR
luaABname = "";
Debug.Log("UNITY_ANDROID UNITY_ANDROID luaABname = "+luaABname);
if (luaABname==""){
foreach (string key in dicLocal.Keys)
{
// Debug.Log("key "+key);
if (key.IndexOf("lua") != -1 )
{
luaABname = key ;
Debug.Log("luaABname == " + luaABname);
ResourceManager.luaFileName = luaABname ;
}else if (key.IndexOf("font") != -1 )
{
Debug.Log("font == " + key);
ResourceManager.InitFontResource(key);
}
}
}
Debug.Log("luaABname yy "+luaABname);
ResourceManager.loadLuaToByte(luaABname);
#endif
加载lua脚本之后我们可以使用Unload卸载其ab包
public static void loadLuaToByteTest( string name ){
AssetBundle ab = Load(name);
string[] abs = ab.GetAllAssetNames();
for (int i = 0; i < abs.Length; i++)
{
string temp = abs[i].Substring(11 );
temp = temp.Substring(0 , temp.Length-8);
if ( allLuaByte.ContainsKey( temp)){
return ;
}
allLuaByte.Add(temp, System.Text.UTF8Encoding.Default.GetBytes (ab.LoadAsset<TextAsset>(abs[i]).text ) );
}
ab.Unload(true);//卸载ab包,防止占用内存
}
这样吧lua文件就存到allLuaByte中了。
读取
在LuaBehaviour中有读取脚本的方法:
void Awake(){
--snip--
//这里使用自定义加载器,可以让用户灵活地管理 Lua 代码的加载和存储方式,当调用 luaEnv.DoString() 方法时,XLua 将首先尝试使用这些自定义加载器加载指定的 Lua 脚本,如果加载成功则将其编译执行,否则才会尝试从文件或网络加载 Lua 代码。
LuaEnv.CustomLoader method = CustomLoaderMethod;
luaEnv = new LuaEnv()
luaEnv.AddLoader(method)
--snip--
}
private byte[] CustomLoaderMethod(ref string fileName)
{
#if UNITY_ANDROID && !UNITY_EDITOR
return ResourceManager.allLuaByte[ fileName.ToLower() ];
#elif UNITY_IOS && !UNITY_EDITOR
return ResourceManager.allLuaByte[fileName.ToLower()];
#else
//找到指定文件
fileName = main2 + fileName.Replace('.', '/') + ".lua.txt";
// Debug.Log("fileName " + fileName);
if (File.Exists(fileName))
{
// Debug.Log("File.Exists " );
return File.ReadAllBytes(fileName.ToLower());
}
else
{
// Debug.Log("! File.Exists " );
return null;
}
#endif
}
在ui.lua的脚本中:
function UI.OpenUI(name,...)
local uiTable = nil
print("OpenUI name ******************************************** = " , name)
--print("OpenUI _G[name] ******************************************** = " , _G[name])
--_G 是 Lua 中的全局环境变量,它是一个 table 类型。所有未定义在局部环境中的变量都被放在 _G 这个 table 中。因此,当你在 Lua 脚本中定义一个全局变量时,实际上是在 _G 这个 table 中新增了一个键值对,键是变量名,值是变量值
if _G[name] ~= nil then
uiTable = _G[name].Create()
else
return nil
end
if select("#", ...) > 0 then
uiTable:Awake(...)
else
uiTable:Awake()
end
因此我们可以通过_G来访问lua脚本
资源预加载
如果资源加载的时候其依赖的资源没有被加载,就会出现加载出错的情况,例如字体,不在一开始加载处理就会发现UI出现字体丢失的情况,所以我们新增一个脚本一开始就初始化字体:
ABM = {}
Public.InitFont()
--调取每个功能的大模块显示
function ABM.ClearAB(name_ , path_)
-- 通过配置文件, 获取这个资源的清理策略。
-- 如果驻内存时间不等于-1
-- 倒计时清理,如果玩家再倒计时归零之前,再次点进模块,倒计时要重置。
Public.ClearAB( name_ , path_)
end
return ABM
我们可以在ui.lua的RemoveUI中调用ClearAB,在关闭UI的时候清理资源,避免占用内存。这是包较小的情况,如果包比较大的情况下,每次打开加载的时间长会造成卡顿,要看情况清理。也可以延迟清理,关闭UI后不马上清理,这个可以通过配置文件来设置不同ab包的清理策略。
实现快速开发工具
生成过程
1 配置每个代码中不同的部分
2 代码中相同的部分,使用工具固化
3 将1和2拼在一起,形成完整代码
4 将代码写入文件,转化为需要的格式。
工具种类
1 编辑器工具 制作一些 快速开发工具
生成ab包的工具
GameTools Create Net Code 快速生成我们的Net资源。(RpcMakeCode)
NETxx.lua NETxxxbase.lua
我们可以用一个request.json文件管理所有的网络请求参数,我们可以把这个叫协议,这个json文件通常有excel生成,放到Asset/Editor目录下,然后这个json文件就可以用代码生成一系列的lua脚本
2 常规工具 (一般指可以脱离编辑器独立运行的exe可执行程序)
根据用了哪些组件来生成lua的功能模块逻辑与view脚本的框架,生成之后再用来写逻辑代码。
3 转化工具
将json格式的配置文件,转化成lua配置文件。
使用方法
1 将json文件 命名好,放入 json文件夹。
2 双击run.bat
3 lua配置文件,生成在 lua文件夹下
Xlua第三方库
通信协议有protobuf,sproto,json,cjson等等,用于服务器和客户端通信的数据格式,不同的协议有不同的序列化和反序列化过程。这些一般是第三方库提供,不在lua之内,需要编译进Xlua工程中。具体查看Xlua的文档。
现在已经有大佬帮我们集成好了常用的库,我们不用再去使用Xlua原生的build了,网址如下:https://github.com/chexiongsheng/build_xlua_with_libs
下载解压后,修改LuaDLL.cs并修改,到网上下载一个cmake,检查build里面的CMakeList,然后双击执行build里面的make_win64_lua53.bat(Windows平台,其他平台执行其他对应文件),就能编译对应的Xlua.dll,到plugin_lua53中把这个文件复制到项目的Xlua插件中替换对应的文件,重启Unity。
网络通信
C#端
NetManager:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NetManager : MonoBehaviour
{
NetClient m_NetClient;
Queue<KeyValuePair<int, string>> m_MessageQueue = new Queue<KeyValuePair<int, string>>();
XLua.LuaFunction ReceiveMessage;
public void Init()
{
m_NetClient = new NetClient();
ReceiveMessage = Manager.Lua.LuaEnv.Global.Get<XLua.LuaFunction>("ReceiveMessage"); // 重要,C#调用lua的手段
}
//发送消息
public void SendMessage(int messageId, string message)
{
m_NetClient.SendMessage(messageId, message);
}
//连接到服务器
public void ConnectedServer(string post, int port)
{
m_NetClient.OnConnectServer(post, port);
}
//网络连接
public void OnNetConnected()
{
}
//被服务器断开连接
public void OnDisConnected()
{
}
//接收到数据
public void Receive(int msgId, string message)
{
m_MessageQueue.Enqueue(new KeyValuePair<int, string>(msgId, message));
}
private void Update()
{
if (m_MessageQueue.Count > 0) // 消息队列有消息,发送给lua处理
{
KeyValuePair<int, string> msg = m_MessageQueue.Dequeue();
ReceiveMessage?.Call(msg.Key, msg.Value);
}
}
}
NetClient:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
public class NetClient
{
private TcpClient m_Client;
private NetworkStream m_TcpStream;
private const int BufferSize = 1024 * 64;
private byte[] m_Buffer = new byte[BufferSize];
private MemoryStream m_MemStream;
private BinaryReader m_BinaryReader;
public NetClient()
{
m_MemStream = new MemoryStream();
m_BinaryReader = new BinaryReader(m_MemStream);
}
public void OnConnectServer(string host, int port)
{
try
{
IPAddress[] addresses = Dns.GetHostAddresses(host);
if (addresses.Length == 0)
{
Debug.LogError("host invalid");
return;
}
if (addresses[0].AddressFamily == AddressFamily.InterNetworkV6)
m_Client = new TcpClient(AddressFamily.InterNetworkV6);
else
m_Client = new TcpClient(AddressFamily.InterNetwork);
m_Client.SendTimeout = 1000;
m_Client.ReceiveTimeout = 1000;
m_Client.NoDelay = true;
m_Client.BeginConnect(host, port, OnConnect, null); // 正式链接服务器,OnConnect是回调方法
}
catch (Exception e)
{
Debug.LogError(e.Message);
}
}
private void OnConnect(IAsyncResult asyncResult)
{
if (m_Client == null || !m_Client.Connected)
{
Debug.LogError("connect server error!!!");
return;
}
Manager.Net.OnNetConnected(); // 通知lua
m_TcpStream = m_Client.GetStream();
m_TcpStream.BeginRead(m_Buffer, 0, BufferSize, OnRead, null); // 开始接受数据,回调OnRead方法,m_Buffers是内容
}
private void OnRead(IAsyncResult asyncResult)
{
try
{
if (m_Client == null || m_TcpStream == null)
return;
//收到的消息长度
int length = m_TcpStream.EndRead(asyncResult);
if (length < 1) // 收到额数据有问题
{
OnDisConnected(); // 主动断开链接
return;
}
ReceiveData(length); // 解析数据,传给lua
lock (m_TcpStream)
{
Array.Clear(m_Buffer, 0, m_Buffer.Length); // 清空内容
m_TcpStream.BeginRead(m_Buffer, 0, BufferSize, OnRead, null); // 重新开始接收数据
}
}
catch (Exception e)
{
Debug.LogError(e.Message);
OnDisConnected();
}
}
/// <summary>
/// 解析数据
/// </summary>
private void ReceiveData(int len)
{
m_MemStream.Seek(0, SeekOrigin.End); // 从末尾开始添加
m_MemStream.Write(m_Buffer, 0, len); // 添加数据
m_MemStream.Seek(0, SeekOrigin.Begin);// 指针移到开始
while (RemainingBytesLength() >= 8) // 剩余字节数至少为8才算消息完整
{
int msgId = m_BinaryReader.ReadInt32();
int msgLen = m_BinaryReader.ReadInt32();
if (RemainingBytesLength() >= msgLen)
{
byte[] data = m_BinaryReader.ReadBytes(msgLen); // 读取字节数据
string message = System.Text.Encoding.UTF8.GetString(data); // 转化为json
//转到lua
Manager.Net.Receive(msgId, message); // 传递给lua端
}
else
{
m_MemStream.Position = m_MemStream.Position - 8; // 消息有误,还回去
break;
}
}
//剩余字节
byte[] leftover = m_BinaryReader.ReadBytes(RemainingBytesLength());
m_MemStream.SetLength(0);
m_MemStream.Write(leftover, 0, leftover.Length); // 没能顺利读出的字节重新写进去
}
//剩余长度
private int RemainingBytesLength()
{
return (int)(m_MemStream.Length - m_MemStream.Position);
}
//发送消息
public void SendMessage(int msgID, string message)
{
using (MemoryStream ms = new MemoryStream())
{
ms.Position = 0;
BinaryWriter bw = new BinaryWriter(ms);
byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
//协议id
bw.Write(msgID);
//消息长度
bw.Write((int)data.Length);
//消息内容
bw.Write(data);
bw.Flush();
if (m_Client != null && m_Client.Connected)
{
byte[] sendData = ms.ToArray();
m_TcpStream.BeginWrite(sendData, 0, sendData.Length, OnEndSend, null); // 发送数据,发送成功回调函数为OnEndSend
}
else
{
Debug.LogError("服务器未连接");
}
}
}
void OnEndSend(IAsyncResult ar)
{
try
{
m_TcpStream.EndWrite(ar); // 结束发送
}
catch (Exception ex)
{
OnDisConnected();
Debug.LogError(ex.Message);
}
}
public void OnDisConnected()
{
if (m_Client != null && m_Client.Connected)
{
m_Client.Close();
m_Client = null;
m_TcpStream.Close();
m_TcpStream = null;
}
Manager.Net.OnDisConnected();
}
}
Lua端(包含面向对象用法)
框架
lua模拟面向对象的继承:
function Class(super)
local class = nil;
if super then
class = setmetatable({}, {__index = super})
class.super = super
else
class = {ctor = function() end}
end
class.__index = class
function class.new(...)
local instance = setmetatable({}, class)
local function create(inst, ...)
if type(inst.super) == "table" then
create(inst.super, ...);
end
if type(inst.ctor) == "function" then
inst.ctor(instance, ...);
end
end
create(instance, ...);
return instance
end
return class;
end
继承的用法:
-- 定义一个父类
local ParentClass = {
name = "Parent",
age = 30,
}
function ParentClass:sayHello()
print("Hello, I am " .. self.name)
end
-- 定义一个子类
local ChildClass = Class(ParentClass)
function ChildClass:ctor()
self.name = "Child"
self.grade = 5
end
function ChildClass:sayGrade()
print("I am in grade " .. self.grade)
end
-- 创建子类的实例
local childObj = ChildClass.new()
-- 访问子类的成员
childObj:sayHello() -- 输出:Hello, I am Child
childObj:sayGrade() -- 输出:I am in grade 5
-- 访问父类的成员
print(childObj.name) -- 输出:Child
print(childObj.age) -- 输出:30
base_msg:
local base_msg = Class();
--消息注册
function base_msg:add_req_res(msg_name,msg_id,...)
local keys = {...};
--消息请求
self["req_"..msg_name] = function(self,... )
local values = {...};
if #keys ~= #values then
Log.Error("参数不正确:",msg_name);
end
local send_data = {};
for i = 1,#keys do
send_data[keys[i]] = values[i];
end
msg_mgr.send_msg(msg_id,send_data);
end
--消息接收
if type(self["res_" .. msg_name]) == "function" then
msg_mgr.register(msg_id,
function(data)
local msg = Json.decode(data);
if msg.code ~= 0 then
Log.Error("错误码:",msg.code);
return;
end
self["res_" .. msg_name](self,msg);
end)
else
Log.Error("请注册消息返回回调:"..msg_name);
end
end
return base_msg;
local msg_mgr = {}
local msg_model_list = {}
local msg_responses = {};
--手动添加每个网络模块名字
local msg_name_list =
{
"msg_test",
}
function msg_mgr.init()
for k,v in pairs(msg_name_list) do
msg_model_list[v] = require("message."..v).new();
end
end
--获取模块
function msg_mgr.get_msg(key)
if not msg_model_list[key] then
Log.Error("脚本不存在:"..key);
return
end
return msg_model_list[key];
end
function msg_mgr.register(msg_id,func)
if msg_responses[msg_id] then
Log.Error("消息已注册:"..msg_id);
return
end
msg_responses[msg_id] = func;
end
-- C#与lua通信的入口
function ReceiveMessage(msg_id,message)
Log.Info("<color=#A0522D>receive:<<<<<<<<<<<<<<<<<<<<<<<<<<:id = "..msg_id .. " : "..message.."</color>");
if type(msg_responses[msg_id]) == "function" then
msg_responses[msg_id](message);
else
Log.Error("此消息没有res:",msg_id);
end
end
-- lua与C#通信的入口
function msg_mgr.send_msg(msg_id,send_data)
local str = Json.encode(send_data);
Log.Info("<color=#9400D3>send:>>>>>>>>>>>>>>>>>>>>>>>>>:id = "..msg_id.." : "..str.."</color>");
Manager.Net:SendMessage(msg_id,str);
end
return msg_mgr;
使用
定义模块:
local msg_test = Class(base_msg)
--构造函数,消息注册
function msg_test:ctor()
self:add_req_res("first_test",1000,"id","user","password","listTest");
end
function msg_test:res_first_test(message )
Log.Warning(message);
end
return msg_test;
使用模块:
--发送数据
msg_mgr.get_msg("msg_test"):req_first_test(666,"Tom","123456",{1,2,3})