之前我们简单解析uLua是如何让cs和Lua互相调用的。今天我们来分析下官方案例,并实现官方案例的热更新。这里我们需要使用到SimpleFramework的服务器,下载后解压是sln工程文件夹。我们先不管服务器的代码,把Server文件夹拖到Unity工程文件夹下(也就是和Assets同一文件夹)。准备工作大致完成了,接下来我们来解析下案例的代码。

1.GlobalGenerator.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
        void Awake() {
            InitGameMangager();
        }
        /// <summary>
        /// 实例化游戏管理器
        /// </summary>
        public void InitGameMangager() {
            string name = "GameManager";
            GameObject manager = GameObject.Find(name);
            if (manager == null) {
                manager = new GameObject(name);
                manager.name = name;

                AppFacade.Instance.StartUp();   //启动游戏
            }
        }

这是实例场景login 里GlobalGenerator挂的唯一一个脚本。这个脚本是整个框架的入口,用来启动框架。他新建了一个GameManger的GameObjet,然后启动框架。

2.StartUpCommand.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StartUpCommand : ControllerCommand {

    public override void Execute(IMessage message) {
        if (!Util.CheckEnvironment()) return;

        GameObject gameMgr = GameObject.Find("GlobalGenerator");
        if (gameMgr != null) {
            AppView appView = gameMgr.AddComponent<AppView>();
        }
        //-----------------关联命令-----------------------
        AppFacade.Instance.RegisterCommand(NotiConst.DISPATCH_MESSAGE, typeof(SocketCommand));

        //-----------------初始化管理器-----------------------
        AppFacade.Instance.AddManager(ManagerName.Lua, new LuaScriptMgr());

        AppFacade.Instance.AddManager<PanelManager>(ManagerName.Panel);
        AppFacade.Instance.AddManager<MusicManager>(ManagerName.Music);
        AppFacade.Instance.AddManager<TimerManager>(ManagerName.Timer);
        AppFacade.Instance.AddManager<NetworkManager>(ManagerName.Network);
        AppFacade.Instance.AddManager<ResourceManager>(ManagerName.Resource);
        AppFacade.Instance.AddManager<ThreadManager>(ManagerName.Thread);
        AppFacade.Instance.AddManager<GameManager>(ManagerName.Game);

        Debug.Log("SimpleFramework StartUp-------->>>>>");
    }
}

上面的StartUp()会通过命令模式调用到StartUpCommand的Execute(),在这里我们关联了新的命令以及添加管理器。 AppFacade.Instance.AddManager<>()在代码中就是把相应的管理器脚本挂在上面GameManger的物体上。因为GameManger是DontDestroy的,因此在场景切换时就不用新加管理器。管理器一般用来执行一些通用的操作,方便调用。

3.GameManager.cs

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
namespace SimpleFramework.Manager {
    public class GameManager : LuaBehaviour {
        private List<string> downloadFiles = new List<string>();

        /// <summary>
        /// 初始化游戏管理器
        /// </summary>
        void Awake() {
            Init();
        }

        /// <summary>
        /// 初始化
        /// </summary>
        void Init() {
            if (AppConst.ExampleMode) {
                InitGui();
            }
            DontDestroyOnLoad(gameObject);  //防止销毁自己

            CheckExtractResource(); //释放资源
            Screen.sleepTimeout = SleepTimeout.NeverSleep;
            Application.targetFrameRate = AppConst.GameFrameRate;
        }

        /// <summary>
        /// 初始化GUI
        /// </summary>
        public void InitGui() {
            string name = "GUI";
            GameObject gui = GameObject.Find(name);
            if (gui != null) return;

            GameObject prefab = Util.LoadPrefab(name);
            gui = Instantiate(prefab) as GameObject;
            gui.name = name;
        }

        /// <summary>
        /// 释放资源
        /// </summary>
        public void CheckExtractResource() {
            bool isExists = Directory.Exists(Util.DataPath) &&
              Directory.Exists(Util.DataPath + "lua/") && File.Exists(Util.DataPath + "files.txt");
            if (isExists || AppConst.DebugMode) {
                StartCoroutine(OnUpdateResource());
                return;   //文件已经解压过了,自己可添加检查文件列表逻辑
            }
            StartCoroutine(OnExtractResource());    //启动释放协成 
        }

        IEnumerator OnExtractResource() {
            string dataPath = Util.DataPath;  //数据目录
            string resPath = Util.AppContentPath(); //游戏包资源目录

            if (Directory.Exists(dataPath)) Directory.Delete(dataPath, true);
            Directory.CreateDirectory(dataPath);

            string infile = resPath + "files.txt";
            string outfile = dataPath + "files.txt";
            if (File.Exists(outfile)) File.Delete(outfile);

            string message = "正在解包文件:>files.txt";
            Debug.Log(message);
            facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);

            if (Application.platform == RuntimePlatform.Android) {
                WWW www = new WWW(infile);
                yield return www;

                if (www.isDone) {
                    File.WriteAllBytes(outfile, www.bytes);
                }
                yield return 0;
            } else File.Copy(infile, outfile, true);
            yield return new WaitForEndOfFrame();

            //释放所有文件到数据目录
            string[] files = File.ReadAllLines(outfile);
            foreach (var file in files) {
                string[] fs = file.Split('|');
                infile = resPath + fs[0];  //
                outfile = dataPath + fs[0];

                message = "正在解包文件:>" + fs[0];
                Debug.Log("正在解包文件:>" + infile);
                facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);

                string dir = Path.GetDirectoryName(outfile);
                if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);

                if (Application.platform == RuntimePlatform.Android) {
                    WWW www = new WWW(infile);
                    yield return www;

                    if (www.isDone) {
                        File.WriteAllBytes(outfile, www.bytes);
                    }
                    yield return 0;
                } else {
                    if (File.Exists(outfile)) {
                        File.Delete(outfile);
                    }
                    File.Copy(infile, outfile, true);
                }
                yield return new WaitForEndOfFrame();
            }
            message = "解包完成!!!";
            facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);

            yield return new WaitForSeconds(0.1f);
            message = string.Empty;

            //释放完成,开始启动更新资源
            StartCoroutine(OnUpdateResource());
        }

        /// <summary>
        /// 启动更新下载,这里只是个思路演示,此处可启动线程下载更新
        /// </summary>
        IEnumerator OnUpdateResource() {
            downloadFiles.Clear();

            if (!AppConst.UpdateMode) {
                ResManager.initialize(OnResourceInited);
                yield break;
            }
            string dataPath = Util.DataPath;  //数据目录
            string url = AppConst.WebUrl;
            string random = DateTime.Now.ToString("yyyymmddhhmmss");
            string listUrl = url + "files.txt?v=" + random;
            Debug.LogWarning("LoadUpdate---->>>" + listUrl);

            WWW www = new WWW(listUrl); yield return www;
            if ( www.error != "") {
                OnUpdateFailed(string.Empty);
                yield break;
            }
            if (!Directory.Exists(dataPath)) {
                Directory.CreateDirectory(dataPath);
            }
            File.WriteAllBytes(dataPath + "files.txt", www.bytes);

            string filesText = www.text;
            string[] files = filesText.Split('\n');

            string message = string.Empty;
            for (int i = 0; i < files.Length; i++) {
                if (string.IsNullOrEmpty(files[i])) continue;
                string[] keyValue = files[i].Split('|');
                string f = keyValue[0];
                string localfile = (dataPath + f).Trim();
                string path = Path.GetDirectoryName(localfile);
                if (!Directory.Exists(path)) {
                    Directory.CreateDirectory(path);
                }
                string fileUrl = url + keyValue[0] + "?v=" + random;
                bool canUpdate = !File.Exists(localfile);
                if (!canUpdate) {
                    string remoteMd5 = keyValue[1].Trim();
                    string localMd5 = Util.md5file(localfile);
                    canUpdate = !remoteMd5.Equals(localMd5);
                    if (canUpdate) File.Delete(localfile);
                }
                if (canUpdate) {   //本地缺少文件
                    Debug.Log(fileUrl);
                    message = "downloading>>" + fileUrl;
                    facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
                    /*
                    www = new WWW(fileUrl); yield return www;
                    if (www.error != null) {
                        OnUpdateFailed(path);   //
                        yield break;
                    }
                    File.WriteAllBytes(localfile, www.bytes);
                     * */
                    //这里都是资源文件,用线程下载
                    BeginDownload(fileUrl, localfile);
                    while (!(IsDownOK(localfile))) { yield return new WaitForEndOfFrame(); }
                }
            }
            yield return new WaitForEndOfFrame();
            message = "更新完成!!";
            facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);

            ResManager.initialize(OnResourceInited);
        }

        /// <summary>
        /// 是否下载完成
        /// </summary>
        bool IsDownOK(string file) {
            return downloadFiles.Contains(file);
        }

        /// <summary>
        /// 线程下载
        /// </summary>
        void BeginDownload(string url, string file) {     //线程下载
            object[] param = new object[2] {url, file};

            ThreadEvent ev = new ThreadEvent();
            ev.Key = NotiConst.UPDATE_DOWNLOAD;
            ev.evParams.AddRange(param);
            ThreadManager.AddEvent(ev, OnThreadCompleted);   //线程下载
        }

        /// <summary>
        /// 线程完成
        /// </summary>
        /// <param name="data"></param>
        void OnThreadCompleted(NotiData data) {
            switch (data.evName) {
                case NotiConst.UPDATE_EXTRACT:  //解压一个完成
                    //
                break;
                case NotiConst.UPDATE_DOWNLOAD: //下载一个完成
                    downloadFiles.Add(data.evParam.ToString());
                break;
            }
        }

        /// <summary>
        /// 资源初始化结束
        /// </summary>
        public void OnResourceInited() {
            LuaManager.Start();
            LuaManager.DoFile("Logic/Network");       //加载网络
            LuaManager.DoFile("Logic/GameManager");    //加载游戏
            initialize = true;                     //初始化完 

            NetManager.OnInit();    //初始化网络

            object[] panels = CallMethod("LuaScriptPanel");  
            //---------------------Lua面板---------------------------
            foreach (object o in panels) {
                string name = o.ToString().Trim();
                if (string.IsNullOrEmpty(name)) continue;
                name += "Panel";    //添加

                LuaManager.DoFile("View/" + name);
                Debug.LogWarning("LoadLua---->>>>" + name + ".lua");
            }
            //------------------------------------------------------------
            CallMethod("OnInitOK");   //初始化完成
        }

        void OnUpdateFailed(string file) {
            string message = "更新失败!>" + file;
            facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
        }

   
}

这个是热更新核心的管理器,大部分是从服务器下载资源的代码。OnResourceInited()函数里加载了lua网络模块还有GameManager.lua,并且调用了GameManager.lua里的LuaScriptPanel和OnInitOK。 从代码可以看出,uLua从服务器上下载了files.txt,然后和本地的files.txt进行对比来进行更新。使用Unity 2017的同学这里要注意,这里有个,源代码里加载如果没有问题使用www.error != null来判断。而在Unity 2017版本已经改为www.error != ““。我们这里可以使用String.IsNullOrEmpty(www.error)。这样可以兼容2017及2017以下版本。

4.GameManager.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
--管理器--
GameManager = {};
local this = GameManager;

local game; 
local transform;
local gameObject;
local WWW = UnityEngine.WWW;

function GameManager.LuaScriptPanel()
	return 'Prompt', 'Message';
end

function GameManager.Awake()
    --warn('Awake--->>>');
end

--启动事件--
function GameManager.Start()
	--warn('Start--->>>');
end

--初始化完成,发送链接服务器信息--
function GameManager.OnInitOK()
    AppConst.SocketPort = 2012;
    AppConst.SocketAddress = "127.0.0.1";
    NetManager:SendConnect();
    CtrlManager.Init();
    local ctrl = CtrlManager.GetCtrl(CtrlName.Prompt);
    if ctrl ~= nil and AppConst.ExampleMode then
        ctrl:Awake();
    end
       
    logWarn('SimpleFramework InitOK--->>>');
end

--销毁--
function GameManager.OnDestroy()
	--logWarn('OnDestroy--->>>');
end

LuaScriptPanel用来返回游戏里需要使用的面板名,并且返回给GameManager.cs,用来调用 xxxPanel.lua,初始化面板的一些属性数据。当初始化完成后GameManager.cs回调OnInitOK,发送连接服务器,并且获取了PromptCtrl的控制器,进行Prompt面板位置,外观等初始化。

5.PromptCtrl.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
--构建函数--
function PromptCtrl.New()
	logWarn("PromptCtrl.New--->>");
	return this;
end

function PromptCtrl.Awake()
	logWarn("PromptCtrl.Awake--->>");
	PanelManager:CreatePanel('Prompt', this.OnCreate);
end

--启动事件--
function PromptCtrl.OnCreate(obj)
	gameObject = obj;
	transform = obj.transform;

	panel = transform:GetComponent('UIPanel');
	prompt = transform:GetComponent('LuaBehaviour');
	logWarn("Start lua--->>"..gameObject.name);

	this.InitPanel();	--初始化面板--
	prompt:AddClick(PromptPanel.btnOpen, this.OnClick);
end

--初始化面板--
function PromptCtrl.InitPanel()
	panel.depth = 1;	--设置纵深--
	local parent = PromptPanel.gridParent;
	local itemPrefab = prompt:LoadAsset('PromptItem');
	for i = 1, 100 do
		local go = newObject(itemPrefab);
		go.name = tostring(i);
		go.transform.parent = parent;
		go.transform.localScale = Vector3.one;
		go.transform.localPosition = Vector3.zero;
		prompt:AddClick(go, this.OnItemClick);

		local goo = go.transform:Find('Label');
		goo:GetComponent('UILabel').text = i;
	end
	local grid = parent:GetComponent('UIGrid');
	grid:Reposition();
	grid.repositionNow = true;
	parent:GetComponent('WrapGrid'):InitGrid();
end

--滚动项单击事件--
function PromptCtrl.OnItemClick(go)
	log(go.name);
end

--单击事件--
function PromptCtrl.OnClick(go)
	if TestProtoType == ProtocalType.BINARY then
		this.TestSendBinary();
	end
end

--测试发送二进制--
function PromptCtrl.TestSendBinary()
    local buffer = ByteBuffer.New();
    buffer:WriteShort(Login);
    buffer:WriteByte(ProtocalType.BINARY);
    buffer:WriteString("ffff我的ffffQ靈uuu");
    buffer:WriteInt(200);
    NetManager:SendMessage(buffer);
end

--关闭事件--
function PromptCtrl.Close()
	PanelManager:ClosePanel(CtrlName.Prompt);
end

InitPanel(),OnItemClick()用来初始化面板和列表Item。通过PanelManager:CreatePanel()来实例化预制体,回调初始化。绑定按钮点击事件调用OnClick(),向服务器发送消息。

6.NetWork.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
function Network.Start() 
    logWarn("Network.Start!!");
    Event.AddListener(Connect, this.OnConnect); 
    Event.AddListener(Login, this.OnLogin); 
    Event.AddListener(Exception, this.OnException); 
    Event.AddListener(Disconnect, this.OnDisconnect); 
end

--Socket消息--
function Network.OnSocket(key, data)
    Event.Brocast(tostring(key), data);
end

--当连接建立时--
function Network.OnConnect() 
    logWarn("Game Server connected!!");
end

--异常断线--
function Network.OnException() 
    islogging = false; 
    NetManager:SendConnect();
   	logError("OnException------->>>>");
end

--连接中断,或者被踢掉--
function Network.OnDisconnect() 
    islogging = false; 
    logError("OnDisconnect------->>>>");
end

--登录返回--
function Network.OnLogin(buffer) 
	if TestProtoType == ProtocalType.BINARY then
		this.TestLoginBinary(buffer);
	end
	----------------------------------------------------
    --createPanel("Message"); --Lua里创建面板
    local ctrl = CtrlManager.GetCtrl(CtrlName.Message);
    if ctrl ~= nil then
        ctrl:Awake();
    end
    logWarn('OnLogin----------->>>');
end

--二进制登录--
function Network.TestLoginBinary(buffer)
	local protocal = buffer:ReadByte();
	local str = buffer:ReadString();
	log('TestLoginBinary: protocal:>'..protocal..' str:>'..str);
end
--卸载网络监听--
function Network.Unload()
    Event.RemoveListener(Connect);
    Event.RemoveListener(Login);
    Event.RemoveListener(Exception);
    Event.RemoveListener(Disconnect);
    logWarn('Unload Network...');
end

PromptCtrl.lua中用来向服务器发送数据的NetManager.cs是StartUpCommand.cs初始化的管理器之一。它在Update()中对从服务器收到的消息进行派发,而发送的NotiConst.DISPATCH_MESSAGE命令仅仅在StartUpCommand.cs关联过SocketCommand.cs,这里又调用了Network.lua的OnSocket()函数,从Lua中进行消息的派发。而事件监听我们早早的在GameManager.cs就调用了NetWork.lua,对事件进行监听。因为我们在PromptCtrl.lua中,向服务器发送的是Login消息,案例的服务器会下发Login。根据代码调用了OnLogin(),然后开始创建Message面板,这里初始化等和上面的PromptCtrl.lua差不多就不详细讲了。

7.实际操作

把AppConst.cs中的UpdateMode设为true。然后把StreamingAssets文件夹下的文件删除。点击Lua/Clear LuaBinder File + Wrap Files 清除wrap文件,再点击Lua/Gen Lua Wrap Files 生成wrap文件,点击Game/Build Windows Resource打包资源。上面这些操作是为了保证文件和资源为最新的。

然后打开服务器,启动项目就可以看到服务器接受到Login,并且点击按钮显示出消息面板。