这个人也是小白,不对的地方请指出他好更改。
1.热更新是干什么用的?
我们拿Android手机的APP为例,假如一个一二十M的APP更新了版本,一般是叫用户重新下载一个最新版本的APK文件重新安装。
但是我们手机游戏客户端APK文件动辄几百M,一G两个G的,假如一个小更新就让玩家重新下载过一遍客户端再安装,那就很麻烦。 热更新就是让游戏客户端更新的时候不需要重新安装游戏的技术,读个条加载一些资源就完成了游戏的更新。
玩家不需要重新下载一个客户端
2.热更新的大概步骤
2.1 大概原理
更新游戏时,更新的东西无非是两类,1.代码文件 2.图像音频模型等资源文件。游戏更新时,直接把代码文件和资源文件从服务器上下载到游戏目录,这样更新时就不用重新下载安装包重新安装了。
2.2 图象音频资源文件的热更新大概步骤
平时给朋友传文件时,一般用rar把文件打包成压缩包再传过去。
热更新中把服务器上的资源发送给玩家,也得打个包再发送过去,但是不是用rar,而是用的Unity自带的AssetBundle进行打包。这个可以百度一下AssetBundle打包解包的使用方法。
2.3 代码文件的热更新大概步骤
我们Unity的代码,无非就是*.cs后缀的C#脚本文件。那按照之前的思路,游戏更新时,直接把*.cs后缀的C#脚本文件,也就是游戏中的代码文件,全部也AssetBundle打包一下,从服务器上下载给玩家客户端,然后解压缩不就可以了。然而,Unity不支持这样搞,因为AssetBundle不让打包*.cs后缀的C#脚本文件。
那咋办呢?目前常用的方法是:*.cs后缀的C#脚本文件不能打包,那换成*.lua后缀的Lua脚本文件,就能打包了。
什么意思呢,就是我把更新的代码不写在*.cs后缀的C#脚本文件里,而是写在*.lua后缀的Lua脚本文件里,这样支持AssetBundle打包的Lua脚本文件就能和资源文件那样的方法进行热更新了。那问题来了,玩家把Lua脚本更新过来,Unity能看的懂Lua脚本不?答案是能,通过一个叫toLua的Unity开源的组件,Unity就能读取和识别Lua脚本中的代码并运行,和C#脚本没啥区别。
3.实现LuaFramework热更新的入门操作
游戏客户端将从服务器下载AssetBundle打好的资源包文件,解包成代码并执行,或者解包成资源并显示。
3.1 简易热更新一行LUA脚本代码
步骤一:
下载框架地址: https://github.com/jarjin/LuaFramework_UGUI ,把压缩包解压并用Unity打开
步骤二:
LuaFramework框架搭建流程,走一遍。
1.单击菜单Lua/Generate All (生成Wrap文件)
2.单击菜单LuaFramework/Build Windows Resources (根据不同平台生成AssetBundle资源(必须),这里选的Windows)
3.单击菜单Lua/Clear Wrap Files (改完注册到Lua的C#类,需清除文件缓存,重新生成)
4.单击菜单Lua/Encode LuaFile with UTF-8 (Lua需要统一的UTF-8文件编码)
5.打开场景main,资源文件目录LuaFramework/Scenes/main ,点击运行
步骤三:
添加自己的Lua代码,打开资源文件目录LuaFramework/Lua/Main.lua ,这是框架运行时会自动执行的Lua代码文件,在function Main()函数中添加语句:LuaFramework.Util.Log("LLLLLLLLLLLLLLLLLLL") --这是个弹Log的函数,内容是“LLLLLLLLLLLLLLLLLLLL”。
并点击菜单LuaFramework/Build Windows Resources,把资源和代码打包生成相应的AssetBundle文件,这些文件全部存放在StreamingAssets目录,热更新就是从服务器上更新这些AssetBundle文件,然后解包成代码、资源并做出相应的执行。
步骤四:
把/StreamingAssets目录以及里面的AssetBundle文件,放到服务器里,这里可以在本地先搞个简易版本的文件服务器。这里我用的网上找的一个叫HFS的HTTP文件服务器软件,把StreamingAssets文件夹拖进去就行,通过浏览器就能访问到里面的文件。(HFS下载地址:http://www.rejetto.com/hfs/?f=dl)
搭建好了测试从浏览器里访问StreamingAssets目录里的files.txt文件
步骤五:
配置热更新地址,并运行测试。
打开App配置文件目录:LuaFramework/Scrips/ConstDefine/AppConst.cs ,找到这几行修改一下。
运行工程,此时客户端自动下载服务器上/StreamingAssets目录里的所有AssetBundle文件,并解包和执行,我们打开Console窗口,可以找到我们Lua写的Log也被解包和执行了:
3.2 热更新一个面板并显示出来
步骤一:搭建初始场景
新建一个Scene,我这叫TestScene,放在文件目录:LuaFramework/Scenes/
在TestScene中添加一个GameObject,命名为GameManager,为这个对象添加上Main脚本(目录:LuaFramework/Scripts/Main ,内容是热更新框架的启动)
创建一个UI/Canvas 对象,并把Main Camera对象放到其子目录,总体目录结构如下图:
最后,给Main Camera组件加上一个名叫GuiCamera的Tag,注意这个必须的不然会报错。
步骤二:创建一个UGUI的界面框,并制成prefab,以及生成相应的AssetBundle打好的包
用UI/Image和UI/Button生成一个界面框,起名为TestPanel,如图
在:/LuaFramework/Examples/Builds/ 目录建立一个文件夹叫Test,并把场景中的TestPanel拖入到Test文件夹生成一个预设体prefab,如图:
打开/LuaFramework/Editor/Packager.cs,找到static void HandleExampleBundle() 函数(位置大概在中间),在函数中加入这行AddBuildMap("test" + AppConst.ExtName, "*.prefab", "Assets/LuaFramework/Examples/Builds/Test"); //加了这行之后,假如点击Build Windows Resources之后,就会生成相应的Test的AssetBundle包了。
点击菜单LuaFramework/Build Windows Resources,之后便能够在目录:/StreamingAssets/ 找到test的AssetBundle包
删除掉场景视图中TestPanel组件,之后我们会利用prefab和热更新重新加载出来。
步骤三:用Lua脚本以MVC框架的规则构建出TestPanel组件
以MVC框架的格式增加一个组件,其中包括5步骤
1.新建/LuaFramework/Lua/Controller/TestCtrl.lua
2.新建/LuaFramework/Lua/View/TestPanel.lua
3./LuaFramework/Lua/Common/define.lua 增加条目
4./LuaFramework/Lua/Logic/CtrlManager.lua 增加条目
5.在/LuaFramework/Lua/Logic/Game.lua 中加上我们组件的运行的代码
一条一条来,首先新建文件/LuaFramework/Lua/Controller/TestCtrl.lua ,其中的内容为规定格式:
--这是TestCtrl.lua的代码
require "Common/define"
require "3rd/pblua/login_pb"
require "3rd/pbc/protobuf"
local sproto = require "3rd/sproto/sproto"
local core = require "sproto.core"
local print_r = require "3rd/sproto/print_r"
TestCtrl = {};
local this = TestCtrl;
local panel;
local test;
local transform;
local gameObject;
--构建函数--
function TestCtrl.New()
logWarn("TestCtrl.New--->>");
return this;
end
function TestCtrl.Awake()
logWarn("TestCtrl.Awake--->>");
panelMgr:CreatePanel('Test', this.OnCreate);
end
--启动事件--
function TestCtrl.OnCreate(obj)
gameObject = obj;
transform = obj.transform;
logWarn("Start lua--->>"..gameObject.name);
end
然后是新建/LuaFramework/Lua/View/TestPanel.lua,其中的内容是规定格式:
--这是TestPanel.lua的代码
local transform;
local gameObject;
TestPanel = {};
local this = TestPanel;
--启动事件--
function TestPanel.Awake(obj)
gameObject = obj;
transform = obj.transform;
this.InitPanel();
logWarn("Awake lua--->>"..gameObject.name);
end
--初始化面板--
function TestPanel.InitPanel()
end
--单击事件--
function TestPanel.OnDestroy()
logWarn("OnDestroy---->>>");
end
打开/LuaFramework/Lua/Common/define.lua ,在开头的CtrlNames,PanelNames表里添加上Test = "TestCtrl"与"TestPanel"。define.lua代码如下:
--这是define.lua修改后的代码
CtrlNames = {
Prompt = "PromptCtrl",
Message = "MessageCtrl",
Test = "TestCtrl", --新增的条行
}
PanelNames = {
"PromptPanel",
"MessagePanel",
"TestPanel", --新增的条行
}
--协议类型--
ProtocalType = {
BINARY = 0,
PB_LUA = 1,
PBC = 2,
SPROTO = 3,
}
--当前使用的协议类型--
TestProtoType = ProtocalType.BINARY;
Util = LuaFramework.Util;
AppConst = LuaFramework.AppConst;
LuaHelper = LuaFramework.LuaHelper;
ByteBuffer = LuaFramework.ByteBuffer;
resMgr = LuaHelper.GetResManager();
panelMgr = LuaHelper.GetPanelManager();
soundMgr = LuaHelper.GetSoundManager();
networkMgr = LuaHelper.GetNetManager();
WWW = UnityEngine.WWW;
GameObject = UnityEngine.GameObject;
打开/LuaFramework/Lua/Logic/CtrlManager.lua ,在开头添加上require "Controller/TestCtrl" ,并在function CtrlManager.Init()中添加上ctrlList[CtrlNames.Test] = TestCtrl.New();语句,CtrlManager代码如下:
--这是CtrlManager.lua修改后的代码
require "Common/define"
require "Controller/PromptCtrl"
require "Controller/MessageCtrl"
require "Controller/TestCtrl" --新增的条行
CtrlManager = {};
local this = CtrlManager;
local ctrlList = {}; --控制器列表--
function CtrlManager.Init()
logWarn("CtrlManager.Init----->>>");
ctrlList[CtrlNames.Prompt] = PromptCtrl.New();
ctrlList[CtrlNames.Message] = MessageCtrl.New();
ctrlList[CtrlNames.Test] = TestCtrl.New(); --新增的条行
return this;
end
--添加控制器--
function CtrlManager.AddCtrl(ctrlName, ctrlObj)
ctrlList[ctrlName] = ctrlObj;
end
--获取控制器--
function CtrlManager.GetCtrl(ctrlName)
return ctrlList[ctrlName];
end
--移除控制器--
function CtrlManager.RemoveCtrl(ctrlName)
ctrlList[ctrlName] = nil;
end
--关闭控制器--
function CtrlManager.Close()
logWarn('CtrlManager.Close---->>>');
end
打开/LuaFramework/Lua/Logic/Game.lua ,引用部分加上 require "Controller/TestCtrl" ,在function Game.OnInitOK()中加入代码:(启动我们的TestPanel组件)
local ctrl = CtrlManager.GetCtrl(CtrlNames.Test);
if ctrl ~= nil and AppConst.ExampleMode == 1 then
ctrl:Awake();
end
Game.lua代码如下:
--这是Game.lua修改后的代码
require "3rd/pblua/login_pb"
require "3rd/pbc/protobuf"
local lpeg = require "lpeg"
local json = require "cjson"
local util = require "3rd/cjson/util"
local sproto = require "3rd/sproto/sproto"
local core = require "sproto.core"
local print_r = require "3rd/sproto/print_r"
require "Logic/LuaClass"
require "Logic/CtrlManager"
require "Common/functions"
require "Controller/PromptCtrl"
require "Controller/TestCtrl" --新增的条行
--管理器--
Game = {};
local this = Game;
local game;
local transform;
local gameObject;
local WWW = UnityEngine.WWW;
function Game.InitViewPanels()
for i = 1, #PanelNames do
require ("View/"..tostring(PanelNames[i]))
end
end
--初始化完成,发送链接服务器信息--
function Game.OnInitOK()
AppConst.SocketPort = 2012;
AppConst.SocketAddress = "127.0.0.1";
networkMgr:SendConnect();
--注册LuaView--
this.InitViewPanels();
this.test_class_func();
this.test_pblua_func();
this.test_cjson_func();
this.test_pbc_func();
this.test_lpeg_func();
this.test_sproto_func();
coroutine.start(this.test_coroutine);
CtrlManager.Init();
local ctrl = CtrlManager.GetCtrl(CtrlNames.Prompt);
if ctrl ~= nil and AppConst.ExampleMode == 1 then
ctrl:Awake();
end
local ctrl = CtrlManager.GetCtrl(CtrlNames.Test); --新增的条行
if ctrl ~= nil and AppConst.ExampleMode == 1 then --新增的条行
ctrl:Awake(); --新增的条行
end --新增的条行
logWarn('LuaFramework InitOK--->>>');
end
--测试协同--
function Game.test_coroutine()
logWarn("1111");
coroutine.wait(1);
logWarn("2222");
local www = WWW("http://bbs.ulua.org/readme.txt");
coroutine.www(www);
logWarn(www.text);
end
--测试sproto--
function Game.test_sproto_func()
logWarn("test_sproto_func-------->>");
local sp = sproto.parse [[
.Person {
name 0 : string
id 1 : integer
email 2 : string
.PhoneNumber {
number 0 : string
type 1 : integer
}
phone 3 : *PhoneNumber
}
.AddressBook {
person 0 : *Person(id)
others 1 : *Person
}
]]
local ab = {
person = {
[10000] = {
name = "Alice",
id = 10000,
phone = {
{ number = "123456789" , type = 1 },
{ number = "87654321" , type = 2 },
}
},
[20000] = {
name = "Bob",
id = 20000,
phone = {
{ number = "01234567890" , type = 3 },
}
}
},
others = {
{
name = "Carol",
id = 30000,
phone = {
{ number = "9876543210" },
}
},
}
}
local code = sp:encode("AddressBook", ab)
local addr = sp:decode("AddressBook", code)
print_r(addr)
end
--测试lpeg--
function Game.test_lpeg_func()
logWarn("test_lpeg_func-------->>");
-- matches a word followed by end-of-string
local p = lpeg.R"az"^1 * -1
print(p:match("hello")) --> 6
print(lpeg.match(p, "hello")) --> 6
print(p:match("1 hello")) --> nil
end
--测试lua类--
function Game.test_class_func()
LuaClass:New(10, 20):test();
end
--测试pblua--
function Game.test_pblua_func()
local login = login_pb.LoginRequest();
login.id = 2000;
login.name = 'game';
login.email = '[email protected]';
local msg = login:SerializeToString();
LuaHelper.OnCallLuaFunc(msg, this.OnPbluaCall);
end
--pblua callback--
function Game.OnPbluaCall(data)
local msg = login_pb.LoginRequest();
msg:ParseFromString(data);
print(msg);
print(msg.id..' '..msg.name);
end
--测试pbc--
function Game.test_pbc_func()
local path = Util.DataPath.."lua/3rd/pbc/addressbook.pb";
log('io.open--->>>'..path);
local addr = io.open(path, "rb")
local buffer = addr:read "*a"
addr:close()
protobuf.register(buffer)
local addressbook = {
name = "Alice",
id = 12345,
phone = {
{ number = "1301234567" },
{ number = "87654321", type = "WORK" },
}
}
local code = protobuf.encode("tutorial.Person", addressbook)
LuaHelper.OnCallLuaFunc(code, this.OnPbcCall)
end
--pbc callback--
function Game.OnPbcCall(data)
local path = Util.DataPath.."lua/3rd/pbc/addressbook.pb";
local addr = io.open(path, "rb")
local buffer = addr:read "*a"
addr:close()
protobuf.register(buffer)
local decode = protobuf.decode("tutorial.Person" , data)
print(decode.name)
print(decode.id)
for _,v in ipairs(decode.phone) do
print("\t"..v.number, v.type)
end
end
--测试cjson--
function Game.test_cjson_func()
local path = Util.DataPath.."lua/3rd/cjson/example2.json";
local text = util.file_load(path);
LuaHelper.OnJsonCallFunc(text, this.OnJsonCall);
end
--cjson callback--
function Game.OnJsonCall(data)
local obj = json.decode(data);
print(obj['menu']['id']);
end
--销毁--
function Game.OnDestroy()
--logWarn('OnDestroy--->>>');
end
步骤四:点击菜单LuaFramework/Build Windows Resources,重新在目录:/StreamingAssets/ 生成AssetBundle包。重新把/StreamingAssets/文件夹,包括里面的这些AssetBundle包文件,扔服务器里,运行项目。
把/StreamingAssets/文件夹丢HFS文件服务器里后,运行Unity项目,此时客户端从服务器的/StreamingAssets目录下载里面所有的AssetBundle资源包,解包并执行,发现此时我们的TestPanel已经被生成了:
把PromptPanel屏蔽掉之后我们的TestPanel界面框就出来了:
至于如何用代码把框架自带的演示PromptPanel屏蔽掉,在/LuaFramework/Lua/Logic/Game.lua中把这一段屏蔽掉,重新Build Windows Resources,重新把/StreamingAssets/文件夹扔服务器里再运行就没有这个了。
4.热更新框架LuaFramework的详细原理与结构
在简单粗暴的学习了LuaFramework框架使用方法后,接着就是更加系统的学习LuaFramework框架的原理与结构了,接下来的内容今天先不写了,下次再写。