XLua热更新框架


资源目录划分

Bundle构建工具

框架开发流程

  1. Bundle处理:构建,加载,更新
  2. C#调用Lua,Lua的加载和管理,绑定和执行
  3. 向Lua提供接口
  4. 完善和优化

打包策略

  1. 按文件夹打包:Bundle数量少,首次下载块,但是后期更新补丁大
  2. 按文件打包:Bundle数量多,首次下载较慢,更新补丁小

简介

  1. 使用XLua之前,我们需要按照功能将模块封装为预制体,将其存放在Module目录对应的功能目录下,Tag设置为uiComponent。
  2. 每个功能模块至少有两个预制体脚本,一个是逻辑脚本功能(功能名.lua),视图脚本(功能名View.lua)
  3. 在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模块里面的方法:

  1. SetAccount:设置账号信息
  2. SetAddress:设置逻辑服务器访问地址
  3. SetChatAddress:设置聊天服务器访问地址
  4. SetPvpAddress:设置Pvp服务器访问地址
  5. callback:C#端收到服务器响应后回调
  6. Receive:解析服务器返回数据,将解析后的数据发往GameMainData,调用错误处理模块或者Lua业务端回调方法
  7. SetCount:设置当前等待相应请求数量
  8. SendLogin:发送登录请求
  9. Send/SendTo:发送常规业务Http请求’
  10. 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

自定义控件

开发思路:

  1. 能够完成C#版本
  2. 控件绑定了C#操作脚本(可选)
  3. 控件有lua逻辑脚本创建
  4. C#提供初始化方法,lua端调用方法完成初始化信息
  5. 渲染逻辑由C#端完成,lua端提供渲染方法,C#端调用渲染方法完成渲染
  6. 操作逻辑由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})

文章作者: 微笑紫瞳星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 微笑紫瞳星 !
评论
  目录