1. 程式人生 > >在Unity中使用Lua指令碼:語言層和遊戲邏輯粘合層處理

在Unity中使用Lua指令碼:語言層和遊戲邏輯粘合層處理

前言:為什麼要用Lua

首先要說,所有程式語言裡面,我最喜歡的還是C#,VisualStudio+C#,只能說太舒服了。所以說,為什麼非要在Unity裡面用Lua呢?可能主要是閒的蛋疼。。。。。另外還有一些次要原因:
  • 方便做功能的熱更新;
  • Lua語言的深度和廣度都不大,易學易用,可以降低專案成本。

C#與Lua互相呼叫的方案

坦白來將,我並沒有對現在C#與Lua互相呼叫的所有庫進行一個仔細的調研,大概搜了一下,找到這樣幾個: 最後我選用了uLua,主要原因是:uLua方案比較成熟,它並沒有太多自己的程式碼,主要是把LuaInterface和Lua直譯器整合了一下,都是比較成熟的程式碼,相對會穩定一些。另外,個人很欣賞LuaInterface這個庫。接下來我們就看一下uLua。:)

uLua的基本使用

uLua外掛的使用非常簡單,基本上看一下他自帶的幾個例子就明白了。

遊戲邏輯粘合層設計

uLua外掛解決了語言層面的問題:C#與LUA兩種語言程式碼互相呼叫,以及引數傳遞等相關的一系列底層問題。而我們遊戲邏輯開發中,到底如何使用LUA是上層的一個問題。下面給出我摸索的一個方案,個人認為:夠簡單,夠清晰,是很薄很薄的一層,不可能更薄了。

使用幾個LuaState?

曾經看過一個網友的方案,每次執行指令碼就new一個LuaState,個人認為這種方案十分不妥。整個遊戲的Lua程式碼應該執行在一個LuaState之上,原因有二:
  1. 執行在同一LuaState的Lua程式碼才能互相呼叫啊。相信一個遊戲總會有一定的程式碼量的,如果不同的lua檔案之中的程式碼,完全獨立執行,不能互相呼叫或者互相呼叫很麻煩,則遊戲邏輯組織平添很多障礙;
  2. 混合語言程式設計中原則之一就是:儘量減少程式碼執行的語言環境切換,因為這個的代價往往比程式碼字面上看上去要高很多。我的目標是:既然用了Lua,就儘量把UI事件響應等遊戲上層邏輯放到Lua程式碼中編寫。
基於以上原因,我覺得遊戲的Lua程式碼全都跑在一個LuaState之上。這也是本文方案的基礎。

實現LuaComponent

首先說一下我的目標:
  • 既然C#對於Unity來說是指令碼層了,那麼Lua應該和C#指令碼程式碼具有相同的邏輯地位;
  • Lua整合的程式碼應該很少,應儘量保持簡單;
基於以上的目標,我實現了LuaComponet類,它的實現類似MonoBehavior,只不過我們沒有C++原始碼,只能由C#層的MonoBehavior來轉發一下呼叫。這樣,我們的Lua程式碼的實現方式就是寫和寫一個C#指令碼元件完全一致了,可以說達到了和引擎天衣無縫的整合。:)OK,先上程式碼!
using UnityEngine;
using System.Collections;
using LuaInterface;

/// 
/// Lua元件 - 它呼叫的Lua指令碼可以實現類似MonoBehaviour派生類的功能
/// 
[AddComponentMenu("Lua/LuaComponent")]
public class LuaComponent : MonoBehaviour
{
    private static LuaState s_luaState; // 全域性的Lua虛擬機器

    [Tooltip("繫結的LUA指令碼路徑")]
    public TextAsset m_luaScript;

    public LuaTable LuaModule
    {
        get;
        private set;
    }
    LuaFunction m_luaUpdate;    // Lua實現的Update函式,可能為null

    /// 
    /// 找到遊戲物件上繫結的LUA元件(Module物件)
    /// 
    public static LuaTable GetLuaComponent(GameObject go)
    {
        LuaComponent luaComp = go.GetComponent();
        if (luaComp == null)
            return null;
        return luaComp.LuaModule;
    }

    /// 
    /// 向一個GameObject新增一個LUA元件
    /// 
    public static LuaTable AddLuaComponent(GameObject go, TextAsset luaFile)
    {
        LuaComponent luaComp = go.AddComponent();
        luaComp.Initilize(luaFile);  // 手動呼叫指令碼執行,以取得LuaTable返回值
        return luaComp.LuaModule;
    }

    /// 
    /// 提供給外部手動執行LUA指令碼的介面
    /// 
    public void Initilize(TextAsset luaFile)
    {
        m_luaScript = luaFile;
        RunLuaFile(luaFile);

        //-- 取得常用的函式回撥
        if (this.LuaModule != null)
        {
            m_luaUpdate = this.LuaModule["Update"] as LuaFunction;
        }
    }

    /// 
    /// 呼叫Lua虛擬機器,執行一個指令碼檔案
    /// 
    void RunLuaFile(TextAsset luaFile)
    {
        if (luaFile == null || string.IsNullOrEmpty(luaFile.text))
            return;

        if (s_luaState == null)
            s_luaState = new LuaState();

        object[] luaRet = s_luaState.DoString(luaFile.text, luaFile.name, null);
        if (luaRet != null && luaRet.Length >= 1)
        {
            // 約定:第一個返回的Table物件作為Lua模組
            this.LuaModule = luaRet[0] as LuaTable;
        }
        else
        {
            Debug.LogError("Lua指令碼沒有返回Table物件:" + luaFile.name);
        }
    }

    // MonoBehaviour callback
    void Awake()
    {
        RunLuaFile(m_luaScript);
        CallLuaFunction("Awake", this.LuaModule, this.gameObject);
    }

    // MonoBehaviour callback
    void Start()
    {
        CallLuaFunction("Start", this.LuaModule, this.gameObject);
    }

    // MonoBehaviour callback
    void Update()
    {
        if (m_luaUpdate != null)
            m_luaUpdate.Call(this.LuaModule, this.gameObject);
    }

    /// 
    /// 呼叫一個Lua元件中的函式
    /// 
    void CallLuaFunction(string funcName, params object[] args)
    {
        if (this.LuaModule == null)
            return;

        LuaFunction func = this.LuaModule[funcName] as LuaFunction;
        if (func != null)
            func.Call(args);
    }
}



這段程式碼非常簡單,實現以下幾個功能點:
  • 管理一個全域性的LuaState;
  • 負責將MonoBehavior的呼叫轉發到相應的LUA函式;
  • 提供了GetComponent()、AddComponent()對應的LUA指令碼版本介面;這點非常重要。

LUA程式碼約定

為了很好的和LuaComponent協作,Lua指令碼需要遵循一些約定:
  • LUA指令碼應該返回一個Table,可以是LUA的Module,也可以是任何的Table物件;
  • 返回的Table物件應該含有MonoBehaviour相應的回撥函式;
例如:
require "EngineMain"

local demoComponent = {}

function demoComponent:Awake( gameObject )
	Debug.Log(gameObject.name.."Awake")
end

return demoComponent
LuaComponent回撥函式中,主動將GameObject物件作為引數傳遞給Lua層,以方便其進行相應的處理。

Lua元件之間的互相呼叫(在Lua程式碼中)

基於以上結構,就很容易實現Lua元件之間的互相呼叫。在Demo工程中,有一個“Sphere”物件,綁定了如下指令碼:

require "EngineMain"

local sphereComponent = {}

sphereComponent.text = "Hello World"

function sphereComponent:Awake( gameObject )
	Debug.Log(gameObject.name.."Awake")
end

return sphereComponent
還有另外一個“Cube”物件,綁定了如下指令碼,用來演示呼叫上面這個Lua元件的成員:
require "EngineMain"

local demoComponent = {}

function demoComponent:Awake( gameObject )
	Debug.Log(gameObject.name.."Awake")
end

function demoComponent:Start( gameObject )
	Debug.Log(gameObject.name.."Start")

	--演示LuaComponent程式碼互相呼叫
	local sphereGO = GameObject.Find("Sphere")
	local sphereLuaComp = LuaComponent.GetLuaComponent(sphereGO)
	Debug.log("Sphere.LuaDemoB:"..sphereLuaComp.text)

end

return demoComponent


完整版DEMO下載地址: 最後,順帶總結一下:在設計上次遊戲邏輯框架時,比較好的思路是:在透徹的理解Unity自身架構的前提下,在其架構下進行下一層設計,而不是想一種新的框架。因為Unity本身就是一個框架。