1. 程式人生 > >[Unity3D熱更框架] LuaMVC之XLua

[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 。