[Unity3D熱更框架] LuaMVC之XLua
1.Lua
本篇部落格內容本著“我們只是大自然的搬運工”這樣的理念,為大家快速入門Lua和學習使用XLua(Unity Lua程式設計解決方案)提供一個學習線路以方便快速上手使用LuaMVC框架(基於pureMVC+XLua開發的Unity熱更新框架)。
1.1 Lua特性
Lua是一種輕量語言,它的官方版本只包括一個精簡的核心和最基本的庫。這使得Lua體積小、啟動速度快。它用ANSI C語言編寫並以原始碼形式開放,編譯後僅僅一百餘K,可以很方便的嵌入別的程式裡。和許多“大而全”的語言不一樣,網路通訊、圖形介面等都沒有預設提供。但是Lua可以很容易地被擴充套件:由宿主語言(通常是C或C++)提供這些功能,Lua可以使用它們,就像是本來就內建的功能一樣。事實上,現在已經有很多成熟的擴充套件模組可供選用。
Lua是一種動態型別語言,因此語言中沒有型別的定義,不需要宣告變數型別,每個變數自己儲存了型別。有8種基本型別:nil、布林值(boolean)、數字體(number)、字串型(string)、使用者自定義型別(userdata)、函式(function)、執行緒(thread)和表(table)。
print(type(nil)) -- 輸出 nil
print(type(99.7+12*9)) -- 輸出 number
print(type(true)) -- 輸出 boolean
print(type("Hello Wikipedia")) -- 輸出 string
print(type(print)) -- 輸出 function
print(type{1, 2, test = "test"}) -- 輸出 table
1.2 Lua示例
以下是一段專案中的Lua程式碼,簡單的語法讓學習Lua語言變的較為容易,在專案中使用Lua甚至不需要刻意的去學習Lua,跟著Lua教程案例寫幾篇,然後針對具體的幾個難點(型別實現、構造、繼承等)學習一下即可快速入門。
- Hello World
在lua環境執行以下程式碼或者是在.lua檔案中寫入以下程式碼由loader載入,具體方式見
《第一個 Lua 程式》。
print('Hello LuaMVC')
- 專案Lua指令碼
-- 引用包 和C#中的using類似,但有些區別
require('NotificationType')
require('ViewNames')
-- XLua中使用CS.呼叫C#型別
UnityEngine = CS.UnityEngine
GameObject = CS.UnityEngine.GameObject
LuaMVC = CS.LuaMVC.LuaApplicationFacade -- 用於給luaMVC傳送通知
AssetLoader = CS.LuaMVC.AssetLoader -- 用於載入Resources/Assetbundle資源
-- 宣告awake方法,此方法由C#呼叫
function awake()
-- XLua中用print可在unity console面板列印輸出
print('lua part framework start up.')
-- self類似C#中的this指標,一下是呼叫C#中的方法
-- 注意區分呼叫方法時'.'和':'的區別
self:RegisterLuaCommand("StartUpCommand")
local canvasParent = GameObject.Find("Canvas/UICamera").transform
-- 呼叫C#中帶委託引數的方法,可以用匿名函式
AssetLoader.LuaLoadAsset("Views.unity3d","LoginView",function(asset)
local loginView = GameObject.Instantiate(asset)
loginView.transform:SetParent(canvasParent)
loginView.transform.localScale = UnityEngine.Vector3.one
loginView:AddComponent(typeof(CS.LuaMVC.LuaMonobehaviour)):Init('LoginView')
self:RegisterLuaMediator('LoginViewMediator')
end)
end
-- 宣告ondestroy,此方法由C#呼叫
function ondestroy()
print('lua part framework shut down.')
end
2.XLua
以下內容均搬運至XLua官方,或由官方內容總結,可直接前往XLua官方瞭解。
2.1 什麼是XLua
XLua是針對Unity的Lua程式設計解決方案,xLua為Unity、 .Net、 Mono等C#環境增加Lua指令碼程式設計的能力,藉助xLua,這些Lua程式碼可以方便的和C#相互呼叫。它支援安卓,iOS,Windows等其他系統。
2.2 XLua特性與優勢
xLua在功能、效能、易用性都有不少突破,這幾方面分別最具代表性的是:
- 可以執行時把C#實現(方法,操作符,屬性,事件等等)替換成lua實現
- 出色的GC優化,自定義struct,列舉在Lua和C#間傳遞無C# gc alloc
- 編輯器下無需生成程式碼,開發更輕量
- 熱補丁
- 侵入性小,老專案原有程式碼不做任何調整就可使用
- 執行時影響小,不打補丁基本和原有程式一樣
- 出問題了可以用Lua來打補丁,這時才會走到lua程式碼邏輯
2.3 XLua快速入門
下載XLua官方Release包,匯入Unity工程,新建C#指令碼繼承至MonoBehaviour,新增到一個遊戲物體上,在Start方法中新增以下程式碼:
XLua.LuaEnv luaenv = new XLua.LuaEnv();
luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");
luaenv.Dispose();
切回到Unity介面,等待編譯,點選Play,可看到Console面板輸出。
3.LuaMVC中Lua基礎
3.1 Lua與C#的互相訪問
以下預設為最推薦的方法,效率最好
3.1.1 C#訪問Lua
- 欄位
// 訪問全域性欄位
// luaenv為當前執行的lua虛擬機器(虛擬環境)
luaenv.Global.Get<int>("a");
// 設定全域性欄位
luaenv.Global.Set("a",1);
// 訪問Lua物件欄位
// scriptEnv為當前Lua物件在C#中對映的LuaTable
// scriptName為Lua物件的名稱
string name = scriptEnv.GetInPath<string>(Person+".Name");
// 設定Lua物件欄位
scriptEnv.SetInPath(Person+".age",30);
以下為Lua物件的實現方式,Lua本沒有面向物件的能力,但是我們可以利用Lua的Table構造出物件。
-- Lua'類'的實現
Person = {}
this = Person
Person.Name = 'default'
Person.age = 28
return Person
- 方法
// 將Lua方法對映到委託,再呼叫 (推薦 ,推薦 推薦 )
Action act = luaenv.Global.Get<Action>("FunctionName");
act();
// 將Lua方法對映到LuaFunction,再呼叫 (效率低)
LuaFunction func = luaenv.Global.Get<LuaFunction>("FunctionName");
func.Call();
使用建議:1、訪問lua全域性資料,特別是table以及function,代價比較大,建議儘量少做,比如在初始化時把要呼叫的lua function獲取一次(對映到delegate)後,儲存下來,後續直接呼叫該delegate即可。table也類似。
2、如果lua測的實現的部分都以delegate和interface的方式提供,使用方可以完全和xLua解耦:由一個專門的模組負責xlua的初始化以及delegate、interface的對映,然後把這些delegate和interface設定到要用到它們的地方。LuaMVC中就是使用將Lua程式碼對映委託,再注入到PureMVCz中的方式,使得使用C#或是Lua編碼,或者同時使用兩種語言都完全沒有耦合,開發時不需處理兩者間的呼叫問題,只需要關注業務邏輯。
3.1.2 Lua訪問C
- 構造方法(支援過載)
local newGameObj = CS.UnityEngine.GameObject("gameobject")
- 靜態屬性、方法
CS.UnityEngine.Time.deltaTime
- 成員(物件)屬性、方法
-- 欄位、屬性
person.Name = 'LuaMVC'
-- 方法
-- 呼叫成員方法,第一個引數需要傳該物件,建議用冒號語法糖
person:Say()
3.2 C#如何載入Lua指令碼
- 載入String
luaenv = new LuaEnv();
luaenv.DoString("print('hello world')");
這種方式雖簡單,但因為直接寫在C#指令碼中,導致失去了熱更新的能力,基本只在測試時可用。
- 載入檔案
luaenv = new LuaEnv();
luaenv.DoString("require 'byfile'");
這種方式可載入.lua檔案,XLua對DoString做了拓展,使得這種方式可以載入Resource資料夾下的lua指令碼,而且由於Resource資料夾下檔案字尾的限制,lua指令碼必須改為.lua.txt字尾,使得在很多編輯器中需要手動調整語法才能適配。
- 自定義Loader
private void Start()
{
luaEnv.AddLoader(LuaPathLoader);
}
private byte[] LuaPathLoader(string filePath)
{
string fullPath = Application.persistentDataPath + "/LuaScripts/" + filePath + ".lua" + luaExtension;
return Encoding.UTF8.GetBytes(File.ReadAllText(fullPath));
}
利用以上自定義Loader的方法可以直接載入本地檔案,也可以載入從伺服器獲取的Lua指令碼,同時執行解密,也可直接載入assetbundle檔案種的lua指令碼。
3.3 Lua面向物件程式設計核心
3.3.1 Lua中’.’和’:’的區別
呼叫成員方法時,C#中是用.來呼叫的,而在Lua中是用:呼叫,其實Lua中的:是一種語法糖,相比’.’呼叫,它省略了傳遞一個物件作為引數,是一種簡寫的方式,而C#只是已經處理了這一點。我們用Lua程式碼來還原一下這個過程:
Account = {balance = 0}
function Account.withdraw(v)
Account.balance = Account.balance -v
end
-- 直接以表物件呼叫方法
Account.withdraw(100)
a = Account
Account = nil
a.withdraw(100) -- 報錯
報錯原因:因為a表違背了物件應有的獨立生命週期的原則,也就是說a.withdraw()時,withdraw並不知道操作的是哪一個物件,我們修改一下withdraw方法,如下:
function Account.withdraw(self,v)
self.balance = self.balance - v
end
b = Account
Account = nil
b.withdraw(b,100) -- 正確
原理解釋:self引數的使用是很多面向物件語言的要點,大多數語言隱藏了這一機制,所以’:’相比於’.’只是一種語法的便利,當然相反的,如果你定義函式時使用的是’:’,在使用’.’呼叫函式時,也需要傳入物件作為引數。
3.3.2 型別實現
Lua中我們用表來效仿型別,基於類似js中的原型(prototype)。在Lua中我們使用__index和metatable來效仿prototype。
Account = {balance = 0}
function Account:new(o)
o = o or {}
setmetatable(o,self)
self.__index = self
return o
end
function Account:deposit(v)
self.balance = self.balance + v
end
a = Account:new{balance = 100}
a:deposit(100)
a物件的metatable物件為Account,因此在a物件中找不到deposit方法時,會呼叫Account.__index:deposit()方法。
3.3.3 Lua繼承
Account = {balance = 0}
function Account:new(o)
o = o or {}
setmetatable(o,self)
self.__index = self
return o
end
function Account:deposit(v)
self.balance = self.balance + v
end
-- SpecialAccount是Account的例項
SpecialAccount = Account:new()
-- 繼承Account,並將self指標指向SpecialAccount
s = SpecialAccount:new(limit = 1000)
-- 為SpcicalAccount新增新的成員函式
function SpecialAccount:getLimit()
return self.limit or 0
end
-- s物件呼叫diposit方法
s:deposit(50)
原理解釋:s物件的metatable物件是SpecialAccount,而SpecialAccount的metatable物件是Account,因此s呼叫deposit方法是SpecialAccount從父類Account繼承來的方法。
4.關於LuaMVC框架
LuaMVC是我在專案種的經驗總結,如果恰巧你也需要這樣的框架來快速開發,那你可以期待後續的更新哦。
如果你有什麼更好的意見與建議歡迎加留言或者加群:LuaMVC/SpringGUI交流群 593906968 。