2010-11-05

Unity 3D + Lua 實作筆記


幾天前實作使用 Lua 當 Untiy 3D 的腳本語言,好處是只要用記事本修改 Lua Script 文字檔就可更改程式執行流程,不用重新編譯程式。這裡使用的方法是將 Lua 以 DLL Plugin 的方式掛在 Unity 3D 裡執行,底下是整個實作的流程 (  iOS 平台的實作請參考另一篇 ) :

1. 重新編譯讓 C# 可以 Import 的 Lua DLL 檔。
因為 C 和 .net 的 calling convention 方式不同以致於需要編譯一個 .net 使用的 calling convention 方式的 DLL 檔,可以參考這篇文章

1.1 到 http://www.lua.org/ftp/ 下載 lua5.1 原始碼,此時的最新版是 lua-5.1.4.tar.gz ,解開後可以看到原始碼在 src 目錄下。

1.2 用 VC 開啟新專案 (Visual C++ 的 Win32 Console Application),Application Type 的部分選擇 DLL,Additional options 的部分選擇 Empty project。

開啟新專案

Application Settings

1.3 引入 src 目錄內的原始檔(除 Makefile、lua.c、luac.c 外)。
1.4 設定 Calling Convention 為 _stdcall(在 Project Property -> Configuration Properties -> C/C++ -> Advanced -> Calling Convention)。

Calling Convention

1.5 在 Preprocessor Definitions ( Project Property -> Configuration Properties -> C/C++ -> Preprocessor->Preprocessor Definitions)裡加入 LUA_BUILD_AS_DLL 和 LUA_CORE 兩個定義。
Preprocessor Definitions

1.6 按下 Build 即可產生一個 DLL 檔。

2 將 DLL 檔複製到 Unity3D 專案的 plugins 目錄下 ("\Assets\Plugins"),測試 Lua DLL。

2.1 測試大約區分成三個部分,第一是 "開啟和關閉 Lua" ,第二是 "註冊 C# 的函數給 Lua",第三是 "執行 Lua Script" 並在 Script 內回呼之前註冊的 C# Function。

2.2 "開啟  Lua" 執行如下:
m_luaState = luaL_newstate();
luaL_openlibs(m_luaState);

其中 m_luaState 的定義是:
protected IntPtr m_luaState = IntPtr.Zero;

luaL_newstate() 和 luaL_openlibs() 這兩個函數在執行前須 Import :
[DllImport("lua514vc")] private static extern IntPtr luaL_newstate();
[DllImport("lua514vc")] private static extern void luaL_openlibs(IntPtr lua_State);
其中 lua514vc 是編譯的 Lua 的 DLL 檔名稱。

2.3 "關閉  Lua" 執行如下:
lua_close(m_luaState);

2.4 "註冊 C# 函數",底下是 Lua 註冊 C# Function 的函數:
protected static void lua_register(IntPtr pLuaState, string strFuncName, LuaFunction pFunc)
{
    lua_pushcclosure(pLuaState, pFunc, 0);
    lua_setfield(pLuaState, (int)LuaIndexes.LUA_GLOBALSINDEX, strFuncName);
}

其中 pLuaState 是開啟 Lua 之後取得的 m_luaState,strFuncName 是欲註冊的函數名稱, pFunc 是 C# 函數, LuaFunction 是函數指標,定義如下:
public delegate int LuaFunction(IntPtr pLuaState);

函數 lua_pushcclosure() 和 lua_setfield() 的 Import 方式如下:
[DllImport("lua514vc")]
private static extern void lua_pushcclosure(
    IntPtr lua_State, 
    [MarshalAs(UnmanagedType.FunctionPtr)] LuaFunction func, 
    int n);

[DllImport("lua514vc")]
private static extern void lua_setfield(
    IntPtr lua_State, 
    int idx, 
    string s);

例如現在有一個函數為:
int TestDisplay(IntPtr pLuaState)
{
    Debug.Log("LuaWrapper.TestDisplay()");
    return 0;
}

則註冊 TestDisplay() 函數的方法就是:
lua_register(m_luaState, "TestDisplay", TestDisplay);

2.5 "執行 Lua Script" 檔案:
執行 Lua Script 檔案的函數是:
protected static int luaL_dofile(IntPtr lua_State, string s)
{
    if (luaL_loadfile(lua_State, s) != 0)
        return 1;
 
    return lua_pcall(lua_State, 0, -1, 0);
}

其中 string s 表示 Lua Script 的檔案路徑。函數 luaL_loadfile和 lua_pcall 的 Import 方式為:
[DllImport("lua514vc")]
private static extern int lua_pcall(IntPtr lua_State, int nargs, int nresults, int errfunc);

[DllImport("lua514vc")]
private static extern int luaL_loadfile(IntPtr lua_State, string s);

最後,編輯一個文字檔,儲存檔名為 test.txt,其內容為:
TestDisplay()

然後在 C# 裡執行:
luaL_dofile( m_luaState , "test.txt" );

就會看到 Untiy3D 裡的 Console 視窗出現 "LuaWrapper.TestDisplay()" 字串了。

update 2012-07-05 :
範例程式碼可以在  https://github.com/phardera/unity3d_lua 下載

2010-10-28

Unity 3D 相關筆記

  • 2012-04-30
    • JSON Serializer/Deserializer 參考這篇,程式碼參考這裡
  • 2012-04-13
    • Unity3D 卸載未使用的資源指令 : Resources.UnloadUnusedAssets()
  • 2011-02-25
    • Unicode CJK (中文, 日文, 韓國) 字元範圍,參考這篇文章
  • 2010-12-13
    • Unity 3D 執行順序 (Link)
  • 2010-11-02
    • 以 DllImport 方式把 Lua 嵌在 C# 裡(適用於 Unity3D),參考這篇文章
  • 2010-10-28
    • 利用 Command 建立 AssetBundle 參考這篇討論。
    • 執行時期載入程式碼參考這篇討論。

2010-10-11

HTML Browser in Unity 3D

採用 Qt 裡的 WebKit 來瀏覽網頁並把網頁畫成 Image 之後由 Unity 3D 繪出。

yahoo 首頁

Youtube 播放影片

GoogleMaps

QtWebKit 可由 Qt SDK 取得並編譯成 DLL 檔,但 WebKit DLL 沒辦法直接由 Unity 3D 來呼叫使用,因此在 Unity 3D 和 WebKit DLL 中間再寫一個函數對應的 DLL,整個結構大概如下:



2010-09-03

C# firestream 讀檔、struct 和 byte 之間的轉換

using System.IO;
using System.Runtime.InteropServices;
using System;
 
//1. 把檔案讀成 Byte 形式
private byte[] readFile(string strFileName)
{
 
    byte[] buffer = null;
    FileStream fs = new FileStream(strFileName, FileMode.Open, FileAccess.Read);
    try
    {
        BinaryReader br = new BinaryReader(fs);
        int len = System.Convert.ToInt32(fs.Length);
 
        buffer = br.ReadBytes(len);
 
    }
    finally
    {
        fs.Close();
    }
    return buffer;
}
 
//2. struct 轉成 byte
static byte[] StructToBytes(object structObj)
{
    int size = Marshal.SizeOf(structObj);
    IntPtr buffer = Marshal.AllocHGlobal(size);
    try
    {
        Marshal.StructureToPtr(structObj, buffer, false);
        byte[] bytes = new byte[size];
        Marshal.Copy(buffer, bytes, 0, size);
        return bytes;
    }
    finally
    {
        Marshal.FreeHGlobal(buffer);
    }
}
 
//3. byte 轉成 struct
static object BytesToStruct(byte[] bytes, Type strcutType)
{
    int size = Marshal.SizeOf(strcutType);
    IntPtr buffer = Marshal.AllocHGlobal(size);
    try
    {
        Marshal.Copy(bytes, 0, buffer, size);
        return Marshal.PtrToStructure(buffer, strcutType);
    }
    finally
    {
        Marshal.FreeHGlobal(buffer);
    }
}

2010-09-02

Unity 3D 隱藏視窗外框

Unity3D 若要客製化視窗的外框,就本人所知的方法為:

1. 隱藏原本的視窗外框
2. 在視窗內畫出客製作的視窗外框

這邊概述隱藏視窗外框的方法:
1. 編譯一個 DLL 檔(目的用來呼叫 WIN32 API),DLL 檔內提供一個隱藏視窗的函數 HideWindow() 如下:
int __declspec(dllexport) __stdcall HideWindow()
{
 //finde window
 HWND hWin =GetActiveWindow();
 if (hWin ==0)
  return 0;

 RECT rc;
 GetClientRect(hWin, &rc);

 long lStyle =GetWindowLong(hWin, GWL_STYLE);
 lStyle =lStyle & (~WS_CAPTION);
 SetWindowLong( hWin, GWL_STYLE, lStyle);
 
 SetWindowPos(hWin, NULL, 0, 0, rc.right, rc.bottom, SWP_NOMOVE);

 return 2;

}
2. 將編譯出來的 DLL 檔加至 Project 的 plugin 目錄下。

3. 在 Unity3D 裡建立一個 C# Script,並在裡面加入,其中 winframehidden 為 DLL 檔名:
[DllImport("winframehidden")]
private static extern int HideWindow();

然後在 Start() 函數內呼叫 HideWindow() 即可。

2010-08-31

擷取 std::cout 字串

std::cout 方便我們把資料列印到 console 視窗,擷取這些字串可以拿來做其它用途,例如另存成 Log file、以其它方式顯示等...。底下的方法是當 cout 呼叫時, 會將 cout 的字串以 callback 的方式傳給 callback function。

1. 建立 cout wrapper class 繼承
std::basic_streambuf< char, std::char_traits< char > >

2. 在 class 的 constructor 加入(其中  m_pBuf 為暫存的 cout buffer):
m_pBuf = std::cout.rdbuf( this );

3.在 class 的 destructor 加入下列(還原原來的 cout buffer):
m_Stream.rdbuf( m_pBuf );

4. 利用此 class override 兩個 function 分別為 xsputn 及 overflow:
/**
* Override xsputn and make it forward data to the callback function.
*/
std::streamsize xsputn( const char* _Ptr, std::streamsize _Count )
{
 m_pCbFunc( _Ptr, _Count ); //m_pCbFunc 為使用者定義的 callback function
 return _Count;
}

/**
* Override overflow and make it forward data to the callback function.
*/
std::char_traits< char >::int_type overflow( std::char_traits< char >::int_type v )
{
 char ch = std::char_traits< char >::to_char_type( v );
 m_pCbFunc( &ch, 1 ); //m_pCbFunc 為使用者定義的 callback function

 return std::char_traits< char >::not_eof( v );
}

5. 程式建立此 class 之後,所有的 cout 呼叫都會將字串以 callback 方式傳給使用者定義的 callback function 了!

2010-08-25

在 Unity 3D 裡用 C# Script 產生 Mesh

首先建立一個 C# Script,這裡取名為 SimpleRect.cs,其內容如下(特別注意的是 class name 必須與檔名相同):
using UnityEngine;
using System.Collections;

public class SimpleRect : MonoBehaviour {

    // Use this for initialization
    void Start () {

        Mesh pm = new Mesh();
        pm.Clear();
        
        Vector3[] verts = new Vector3[4];
        verts[0].x = -0.5f;
        verts[0].y = 0.0f;
        verts[0].z = 0.5f;

        verts[1].x = 0.5f;
        verts[1].y = 0.0f;
        verts[1].z = 0.5f;

        verts[2].x = -0.5f;
        verts[2].y = 0.0f;
        verts[2].z = -0.5f;

        verts[3].x = 0.5f;
        verts[3].y = 0.0f;
        verts[3].z = -0.5f;
        pm.vertices = verts;

        int[] tris = new int[6];

        tris[0] = 0;
        tris[1] = 1;
        tris[2] = 2;
        
        tris[3] = 2;
        tris[4] = 1;
        tris[5] = 3;

        pm.triangles = tris;

        pm.RecalculateBounds();
        pm.RecalculateNormals();

        pm.Optimize();

        //建立 GameObject, 並指定 MeshFilter 的 Mesh:
        GameObject go = new GameObject();
        MeshFilter mf = go.AddComponent<meshfilter>();
        mf.mesh = pm;

        MeshRenderer mr = go.AddComponent<meshrenderer>(); 
    }

    // Update is called once per frame
    void Update () {

    }
}

082501
將 SimpleRect.cs 從 Project 拖移至 Hierarchy 視窗的 MainCamera 上


082502
點選 MainCamera 時,可看到 Inspector 裡多了 SimpleRect 的 Script


082503
此時進入 GameMode 就可以看到產生的 Rect

2010-08-10

Gamma 值的筆記

  1. PC 用的 Gamma 值多為 2.2,MAC 用的 Gamma 值為 1.8。
  2. Gamma 值套用到方程式 f(x) =pow(x, G), 其中 x 為顏色, G 為 Gamma 值。
  3. PC 螢幕顯示出來的顏色都會套用 Gamma 值 2.2,所以存在硬碟裡的圖片若是原圖(Gamma 1.0),則會偏黑。為了顯示正常的圖,儲存在硬碟裡的圖必須比較亮(Gamma 0.45)。
  4. 所有在硬碟裡的圖及乎都比較亮。(特殊圖檔除外,例:raw 檔...)
  5. 圖片計算光照的順序依序為:將圖片從硬碟取出,套用 Gamma 2.2 之後再計算光源,計算完成後套用 Gamma 0.45 ,然後再丟給螢幕顯示出來。
  6. 注意圖片取樣及寫入 framebuffer 時,硬體是否已自動做 Gamma 修正。

2010-08-05

2010-08-02

Sprite Manager for Unity3D

花兩週的時間寫一個視覺化的 Sprite 編輯器:


編輯器介面


在時間軸上新增 Sprite 圖層並設定好 keyframe 之後就可以可以編輯圖片大小, 位置, 旋轉角度, 透明度, 播放第幾個 Sprite 圖, 並且在各個 keyframe 之間自動內插產生動畫。

編輯器裡比較特殊的控制項為 時間軸控制項和編輯區域的控制項,因為 unity 沒有這些控制項,所以要自己刻出來,比較辛苦:


時間軸控制項

時間軸控制項的各個圖層皆代表一個 Sprite 圖, 深藍色的色塊代表 keyframe, 淺藍色的色塊代表自動內插並顯示的 frame,最上面的刻度軸代表目前的 frame, 紅色垂直線表示目前顯示的 frame。

編輯區控制項則是顯示及編輯目前 keyframe 裡的圖片,深藍色框線是目前遊戲的解析度,當時間軸的紅線移到內插的 frame 時, 它會及時顯示內插的結果。

接著要做的是, 滑鼠在某個 Sprite 上按下時, 產生按鈕的事件, 讓它除了能編輯 Sprite 動畫外, 還能當成基本的 GUI 編輯器。