2012-07-12

Unity 3D + Lua + iOS 實作筆記

這篇文章概略介紹如何把 Lua 當作 Unity3D 在 iOS 平台上的腳本引擎(若要實作於 Windows 平台可以參考另一篇文章)。範例大略分成三個部分:Unity3D C# 部分、Lua C 部分及 Lua Script 部分,整個程式執行的流程是:
  1. C# 啟動 Lua,並註冊 C#  函數給 Lua (讓 Lua Script 可以呼叫)
  2. 從 iOS 載入 Lua Script 檔案並由 Lua 執行
  3. Lua Script 呼叫 C# 函數
  4. C# 函數印出訊息
最後在 Xcode 的 Output 可以看到 C# 函數印出的訊息,此外值得注意的是,Unity3D 產生的程式碼在模擬器上無法呼叫 Plugins ,所以實作的步驟需要真正的實機才能測試(因此在開始之前必須做好 iOS Developer 相關的設定),底下是整個實作的流程:

1. 開啟 Unity3D 並建立一個空的專案,在專案路徑下建立 Plugins/iOS 目錄,如下圖所示:

2. 到 http://www.lua.org/ftp/ 下載原始碼 lua-5.2.1.tar.gz。解開壓縮檔之後可以看到原始碼在 src 目錄下,將該目錄下所有檔案(除了 Makefile、lua.c、luac.c 之外)複製到 Plugins/iOS 。

3. 建立一個新的 C# Script,命名為 luaTest,此時 Unity3D 的專案目錄呈現如下:

luaTest 內容分成三大部分,一是和 Lua 介接的部分,二是和 lua 之間建立 callback 的部分,三是 Unity3D 程式的部分分別敘述如下:

一、和 Lua 介接的部分:
luaL_newstate、luaL_openlibs 和 lua_close 分別為初始化及關閉 Lua 時呼叫使用。
[DllImport ("__Internal")]
public static extern IntPtr luaL_newstate();

[DllImport ("__Internal")]
public static extern void luaL_openlibs(IntPtr lua_State);

[DllImport ("__Internal")]
public static extern void lua_close(IntPtr lua_State);

lua_pushcclosure 和 lua_setglobal 為註冊 C# 函數給 Lua 時使用。
[DllImport ("__Internal")]
public static extern void lua_pushcclosure(IntPtr lua_State, LuaFunction func, int n);

[DllImport ("__Internal")]
public static extern void lua_setglobal(IntPtr lua_State, string s);

其中 LuaFunction 定義如下:
public delegate int LuaFunction(IntPtr pLuaState);

lua_pcallk 和 luaL_loadfilex 為載入及執行 Lua Script 檔案時使用。
[DllImport ("__Internal")]
public static extern int lua_pcallk(IntPtr lua_State, int nargs, int nresults, int errfunc, int ctx, LuaFunction func);

[DllImport ("__Internal")]
public static extern int luaL_loadfilex(IntPtr lua_State, string s, string mode);

luaL_checklstring 為 C# 函數被 Lua 呼叫時,取得 Lua 所給的參數時使用。
[DllImport ("__Internal")] public static extern IntPtr luaL_checklstring(IntPtr lua_State, int idx, IntPtr len);

此外,使用 DllImport 時需要引入 System.Runtime.InteropServices 名稱空間:
using System.Runtime.InteropServices;

使用 IntPter 時需要引用 System 名稱空間:
using System;

上述 Lua API 在註冊 C# 函數及載入/執行 Lua Script 的部分可寫成 lua_register 及 luaL_dofile 分別如下:
public static void lua_register(IntPtr pLuaState, string strFuncName, LuaFunction pFunc)
{
    lua_pushcclosure(pLuaState, pFunc, 0);
    lua_setglobal(pLuaState, strFuncName);
}

public static int luaL_dofile(IntPtr lua_State, string s)
{
    if (luaL_loadfilex(lua_State, s, null) != 0)
        return 1;
 
    return lua_pcallk(lua_State, 0, -1, 0, 0, null);
}

二、和 lua 之間建立 callback 的部分
1. 建立簡單的 Lua Script ( lua.txt ) 檔案,其內容為:
TestDisplay("Hello world");

將檔案置於專案的 LuaScript 路徑底下,如圖所示:

2. 在 luaTest 內加入 callback 函數如下:
[MonoPInvokeCallbackAttribute (typeof (LuaFunction))]
public static int TestDisplay(IntPtr pLuaState)
{
    IntPtr retPtr =luaL_checklstring(pLuaState, 1, IntPtr.Zero);
    string retStr = Marshal.PtrToStringAnsi(retPtr);
  
    Debug.Log("This line was plotted by TestDisplay() : msg ="+retStr);

    return 0;
}

其中 MonoPInvokeCallbackAttribute 可以預防 mono 在傳遞函數指標時啟動 JIT 的功能(相關參考連結),其定義如下:
[System.AttributeUsage(System.AttributeTargets.Method)]
public sealed class MonoPInvokeCallbackAttribute : Attribute {
    private Type type;
    public MonoPInvokeCallbackAttribute (Type t) { type =t;}
}

luaL_checklstring 負責取得 Lua 在呼叫此函數時給的 "Hello world" 字串指標

三、Unity3D 程式的部分
1. 在 Start 函數加入程式碼:
void Start()
{
    m_luaState = luaL_newstate();
    luaL_openlibs(m_luaState);
    lua_register(m_luaState, "TestDisplay", TestDisplay);
    luaL_dofile(m_luaState, GetAppPath() + "LuaScript/lua.txt");
}

程式碼初始化 Lua 並註冊函數 TestDisplay 給 Lua,最後則是載入 lua.txt 並執行。其中 GetAppPath 函數定義如下:
public static string GetAppPath()
{
    return Application.dataPath.Substring(0, Application.dataPath.Length-4);
} 
m_luaState 定義如下:
public static IntPtr m_luaState = IntPtr.Zero;

2. 在 OnApplicationQuit 函數加入程式碼:
void OnApplicationQuit()
{
    lua_close(m_luaState); 
}

3. 將 luaTest 拖曳至 Main Camera 上,此時在 Inspector 可以看到 luaTest 成為 Camera 的 Component:

編譯時需要稍微做一些設定如下:
1. 在 Build Settings 裡 Switch Platform 至 iOS 之後按下 Build,選擇欲產生的目錄之後 Unity3D 將自動產生 Xcode 專案,開啟後觀察專案底下 Libraries 是否引入 Lua 原始碼:

2. 用滑鼠拖曳 LuaScript 目錄到 Unity-iPhone 上出現訊息如下:

記住此處選擇的是 Create folder references for any added folders。按下 Finish 之後 LuaScript 出現如圖所示:

3. 將裝置接上電腦,編譯的目標選擇 Unity-iPhone > (裝置名稱) 之後按下 Run。

4. 此時觀察 output 視窗的訊息應該顯示如下:

由訊息 "This line was plotted by TestDisplay() : msg =Hello world" 可知 Lua Script 執行成功。範例程式碼可以在 https://github.com/phardera/unity3d_lua_ios 取得。

1 則留言 :