2012-12-17

刪除 Open With 選單裡重覆的項目


OSX 用久了會發現當要用右鍵開啟某些檔案時,在 Open With 選單裡有重覆的項目,此時可在 Terminal 裡輸入 :

/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local\ -domain system -domain user

待執行完成之後 Relaunch Finder (control+option+滑鼠右鍵 點選 Finder ),即可刪除重覆的項目。

ref : http://itpixie.com/2011/05/fix-duplicate-old-items-open-with-list/#.UM3vUqWVgcQ

2012-11-29

Unity 3D + JmDNS + Android

此篇文章實作將 JmDNS 包裝成可以在 Android 平台上執行之 Unity3D Plugin。使用 JmDNS 可以方便行動裝置在區域網路內發現對方,以進行後續的連線。(此機制也稱為 Bonjour ,最初由 Apple 提出。)因為 iOS 平台的 NSNetService 也提供相同的功能,所以 Android 裝置和 iOS 裝置皆可以透過此機制取得對方的網路位置。底下為實作步驟:

A) 至 jmdns.sourceforge.net 取得 JmDNS 並利用 ant 進行編譯:
1) 以 osx 平台為例,在 terminal 輸入:
svn checkout https://jmdns.svn.sourceforge.net/svnroot/jmdns/tags/jmdns-3.4.1 jmdns

2) 待程式碼下載完成後進入 jmdns 資料夾輸入 :
ant jar

3) 完成之後進入資料夾 ./build/lib/ (此時應該找到檔案 jmdns.jar),並輸入底下指令:
mkdir unjar
cd unjar
jar xf ../jmdns.jar
jar cfm ../jmdns.jar META-INF/MANIFEST.MF javax/

目的在於去除重覆的 class 檔案,以避免 Android dex 工具編譯時出現錯誤。

B) 建立 Unity 專案並設定權限:
1) 將 jmdns.jar 檔案復製到 Unity 專案底下資料夾 (Assets/Plugins/Android)
2) 在 Assets/Plugins/Android 資料夾底下新增檔案 AndroidManifest.xml 並設定內容如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest android:versionCode="1" android:versionName="1.0" 
          android:installLocation="preferExternal" 
          package="com.macaronics.jmdns" 
          xmlns:android="http://schemas.android.com/apk/res/android">
  <supports-screens android:anyDensity="true" android:smallScreens="true" 
                    android:normalScreens="true" android:largeScreens="true" 
                    android:xlargeScreens="true" />
  <application android:label="@string/app_name" 
               android:icon="@drawable/app_icon" android:debuggable="false">
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.UnityPlayerProxyActivity" 
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
              >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.UnityPlayerActivity" 
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
              />
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.UnityPlayerNativeActivity" 
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
              >
      <meta-data android:name="android.app.lib_name" android:value="unity" />
      <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" 
                 android:value="false" />
    </activity>
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.VideoPlayer" 
              android:screenOrientation="behind" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale" 
              />
  </application>
  <uses-feature android:glEsVersion="0x20000" />
 
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission 
    android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
</manifest>

值得注意的是 uses-permission 的部分,其設定網路使用權限使 JmDNS 在 Android 系統下能夠作用。

C) 撰寫使用 JmDNS 之 Java Class ( jmDNSWrapper.java ):
這裡不直接從 C# 透過 JNI 呼叫 JmDNS 之函數,而是先撰寫一個名為 JmDNSWrapper 的 Java Class 將 JmDNS 的使用簡化,再讓 Unity 透過 JNI 呼叫使用此 JmDNSWrapper Class,目的是簡化 JNI 的使用。此 Class 大略分為五個部分分別敘述如下

1) 初始化 jmDNS 及 wifi service:
public jmDNSWrapper()
{
    Log.i("jmDNSWrapper", "onStart()...");
    mActivity =UnityPlayer.currentActivity;
    if (wifi ==null){
        Log.i("jmDNSWrapper", "get wifi manager...");
        wifi = (android.net.wifi.WifiManager)mActivity.getSystemService(
            android.content.Context.WIFI_SERVICE
        );
    }
        
    if (lock ==null && wifi !=null){
        Log.i("jmDNSWrapper", "lock wifi multicast...");
        lock = wifi.createMulticastLock("mylockthereturn");
        lock.setReferenceCounted(true);
        lock.acquire();
    }
        
    if (mJmDNS ==null){
        try{
            mJmDNS =JmDNS.create();
        }
        catch(IOException e){
            e.printStackTrace();
        }
    } 
}

2) 結束此 class(停止 jmDNS):
public void dispose()
{
    Log.i("jmDNSWrapper", "onStop()...");
 
    stopListener();
    stopService();
        
    if (mJmDNS !=null){
        try{
            mJmDNS.close();
        }
        catch(IOException e){
            e.printStackTrace();
        }
        mJmDNS =null;
    }
        
    if (lock !=null){
        Log.i("jmDNSWrapper", "unlock wifi multicast...");
        lock.release();
    }
    lock =null; 
}

3) 發佈/停止 Service :
public void publishService(
    String strService, 
    String strDeviceName, 
    String strDesc, 
    int port)
{
    if (mJmDNS ==null)
        return;
        
    stopService();
        
    Log.i("jmDNSWrapper", "trying to create service...");
    try{
        ServiceInfo serviceInfo = 
            ServiceInfo.create(strService, strDeviceName, port, strDesc);
        mJmDNS.registerService(serviceInfo);
            
    }
    catch(IOException e){
        e.printStackTrace();
    }    
}
public void stopService()
{
    if (mJmDNS !=null){
        Log.i("jmDNSWrapper", "unregisterAllServices()...");
        mJmDNS.unregisterAllServices();
    }
}

4) 搜尋/停止 Service :
public void startListener(
    String strServiceType, 
    String strEventHandlerObjName)
{
    if (mJmDNS ==null)
        return;
        
    stopListener();
    mStrEventHandlerName =strEventHandlerObjName;
    mListener =new ServiceListener()
    {
        @Override
        public void serviceResolved(ServiceEvent e)
        {
            String additions = "";
            if (e.getInfo().getInetAddresses() != null && 
                e.getInfo().getInetAddresses().length > 0) {
                additions = 
                    e.getInfo().getInetAddresses()[0].getHostAddress();
            }
                
            Log.i("jmDNSWrapper", "service resolved: "+
                e.getInfo().getQualifiedName()+", port: "+
                e.getInfo().getPort()+", ip: "+additions);
 
            UnityPlayer.UnitySendMessage(
                mStrEventHandlerName, 
                "serviceResolved", "{\"name\":\""+
                e.getInfo().getQualifiedName()+"\",\"ip\":\""+additions+
                "\",\"port\":\""+e.getInfo().getPort()+"\"}");
        }
            
        @Override
        public void serviceRemoved(ServiceEvent e)
        {
            String additions = "";
            if (e.getInfo().getInetAddresses() != null && 
                e.getInfo().getInetAddresses().length > 0) {
                additions = 
                    e.getInfo().getInetAddresses()[0].getHostAddress();
            }
                
            Log.i("jmDNSWrapper", "service removed: "+e.getName());
            UnityPlayer.UnitySendMessage(mStrEventHandlerName, 
                "serviceRemoved", 
                "{\"name\":\""+e.getInfo().getQualifiedName()+
                "\",\"ip\":\""+additions+"\",\"port\":\""+
                e.getInfo().getPort()+"\"}");
        }
            
        @Override
        public void serviceAdded(ServiceEvent e)
        {
            mJmDNS.requestServiceInfo(e.getType(), e.getName(), 1);
        }
    };
        
    Log.i("jmDNSWrapper", "add service listener...");
    mStrServiceType =strServiceType;        
    mJmDNS.addServiceListener( mStrServiceType, mListener);
}
    
public void stopListener()
{
    if (mJmDNS !=null && mListener !=null){
        Log.i("jmDNSWrapper", "removeServiceListener()...");            
        mJmDNS.removeServiceListener( mStrServiceType, mListener );
    }
        
    mListener =null;
    mStrServiceType =null;
}
其中使用 UnitySendMessage 來傳送字串資訊給 Unity,字串格式為 JSON 格式。

D) 編譯 jmDNSWrapper.java:
與 jmDNS 相同,將 jmDNSWrapper 編譯成 jar 檔案,此文章假設使用者已安裝 Android SDK 且安裝之 Android 平台版本為 4.0.3 (API Level 15)。

1) 輸入指令,將 java 編譯成 jmDNSWrapper.class :
javac jmDNSWrapper.java -cp ~/Downloads/android-sdk-macosx/platforms/android-15/android.jar:../jmDNS.jar:/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar -d .

此指令分別提供三個 jar 檔在編譯時使用。

2) 將 com 資料夾(資料夾按照 jmDNSWrapper.java 裡 "package com.macaronics" 產生)底下的 class 檔壓成 jar 檔案:
jar cvfM jmDNSWrapper.jar com/

3) 將 jmDNSWrapper.jar 檔案復製到 Unity 專案底下資料夾 (Assets/Plugins/Android)

E) Unity 引用 jmDNSWrapper :
這邊的目的是讓 jmDNSWrapper.java 裡的函數能在 C# 環境下呼叫。
public class jmDNSWrapper
{
    AndroidJavaObject mJmDNSWrapperObj = null;
    public void init()
    {
        if (mJmDNSWrapperObj != null){
            mJmDNSWrapperObj.Call("dispose");
            mJmDNSWrapperObj.Dispose();
            mJmDNSWrapperObj = null;
        }
        mJmDNSWrapperObj = 
            new AndroidJavaObject("com.macaronics.jmDNSWrapper");
    }
 
    public void dispose()
    {
        if (mJmDNSWrapperObj != null){
            mJmDNSWrapperObj.Call("dispose");
            mJmDNSWrapperObj.Dispose();
        }
        mJmDNSWrapperObj = null;
    }
 
    public void publishService(
        string strService,
        string strDeviceName,
        string strDesc,
        int port)
    {
        if (mJmDNSWrapperObj == null)
            return;
        mJmDNSWrapperObj.Call("publishService",
            new object[4] { strService, strDeviceName, strDesc, port });
    }
 
    public void stopService()
    {
        if (mJmDNSWrapperObj == null)
            return;
        mJmDNSWrapperObj.Call("stopService");
    }
 
    public void startListener(
        string strServiceType, 
        string strEventHandlerObjName){
        if (mJmDNSWrapperObj == null)
            return;
        mJmDNSWrapperObj.Call("startListener",
            new object[2] { strServiceType, strEventHandlerObjName });
    }
 
    public void stopListener()
    {
        if (mJmDNSWrapperObj == null)
            return;
        mJmDNSWrapperObj.Call("stopListener");
    }
}
其利用 Unity 提供的 AndroidJavaObject 來呼叫連結外部 Java 程式碼。

F) 實際範例
最後撰寫一個簡單的範例來實際測試一下 jmDNS,此範例有兩個按鈕,分別是發佈與停止服務,還有將搜尋到的服務顯示在 Console Window。
public class main : MonoBehaviour
{
    string strServiceType = "_macaronics._tcp.local.";
    jmDNSWrapper pp = null;
 
    void Start()
    {
        pp = new jmDNSWrapper();
        pp.init();
 
        //設定服務名稱及 jmDNS 回呼之物件名稱
        pp.startListener(strServiceType, gameObject.name);
    }
 
    void OnDisable()
    {
        if (pp != null)
        {
            pp.dispose();
            pp = null;
        }
    }
 
    void OnGUI()
    {
        if (GUI.Button(new Rect(15, 125, 450, 100), "publish service"))
        {
            pp.publishService(strServiceType,
                "MACARONICS",
                "BONJOUR TEST FROM MACARONICS",
                9000);
        }
 
        if (GUI.Button(new Rect(15, 125 + 100, 450, 100), "stop service"))
        {
            pp.stopService();
        }
    }
 
    public void serviceResolved(string msg)
    {
        //顯示發現的服務
        Debug.Log(msg);
    }
 
    public void serviceRemoved(string msg)
    {
        //顯示移除的服務
        Debug.Log(msg);
    }
}

G) 最後實機測試
很不幸的在 Android Emulator 上無法做 wifi 連線測試,因此需要一台真正的 Android 手機。在 Unity 上設定好 Android 編譯參數後按下 Build And Run,順利的話會看到程式在手機上跑起來了。此時在 Android SDK 裡找到 adb 執行檔,執行 adb logcat 之後就可以看到 Unity 的 Log 訊息。

2012-11-14

Unity 3D 使用 Animation 工具調整 NGUI 之 UISprite 的 Color 及 Alpha 值

Unity 的 Animation 工具可以在 NGUI 的 UISprite 上增加位移,縮放,旋轉等動作控制,如下圖所示,圖中使用 Animation 工具編輯 Sprite 物件的位置資訊,值得注意的是,移動時間軸 (紅色垂直線) 即可同步觀察 Sprite 物件移動。

Animation 工具控制 Sprite 的位置資訊

但若更改 Sprite 的顏色 (Animation 工具的 MColor) ,會發現只有 Inspector 視窗內 Color Tint 屬性及 Previw 會依照時間軸的移動作相對應的變化,Scene 視窗裡 Sprite 的顏色沒有變化。如下圖所示,在時間軸所在位置的 Sprite 應該呈現黃色,但 Scene 內的 Sprite 仍然為紅色。

Animation 工具對顏色的控制

礙於 NGUI 程式架構的關係,無法直接以修改 MColor 參數的方法控制顏色。
本文提供方法讓 Animation 工具可以編輯顏色且 "在編輯的同時就可以同步觀察變化"
建立一 Component (C# Script 如下所述) 並加入到要編輯的物件上。
using UnityEngine;
using UnityEditor;
using System.Collections;
 
public class ngui_color_ani_setup : MonoBehaviour
{
    public float Alpha = 1.0f;
    public float Red = 1.0f;
    public float Green = 1.0f;
    public float Blue = 1.0f;
 
    protected UISprite uis = null;
 
    void LateUpdate()
    {
        setupcolor();
    }
 
    void OnDrawGizmos()
    {
        //關閉自動產生 keyframe
        //(避免因為更動 mColor 而自動產生新的 keyframe)
        SetDisableAutoRecordMode();
        setupcolor();
    }
 
    void setupcolor()
    {
        if (uis == null)
            uis = gameObject.GetComponent<UISprite>();
 
        uis.color = new Color(Red, Green, Blue, Alpha);
    }
 
    void SetDisableAutoRecordMode()
    {
        System.Type T =
            System.Type.GetType("UnityEditor.AnimationWindow,UnityEditor");
        Object[] allAniWindows = Resources.FindObjectsOfTypeAll(T);
        if (allAniWindows.Length > 0)
        {
            UnityEditor.EditorWindow win = 
                (UnityEditor.EditorWindow)allAniWindows[0];
            T.InvokeMember("SetAutoRecordMode",
                System.Reflection.BindingFlags.InvokeMethod,
                null,
                win,
                new object[1] { false });
        }
    }
 
}

如下圖所示,只要編輯 ngui_color_ani_setup 之值即可控制該 Sprite 之顏色,且在編輯時就能同步觀察顏色的變化。

編輯 NGUI 之 Sprite 的顏色動畫曲線,編輯時能同步觀察變化


2012-11-01

Lua Callback Function

底下在 Unity3D ( MAC OSX ) 環境下實作 Lua 呼叫 C# 函數 (tellMeNum) 並且同時把 Lua 的函數 (dispResult) 告訴 C#,待 C# 回呼此函數並告知數字 12345.678。
C# 端程式碼如下 :
using UnityEngine;
using System;
using System.Collections;
using System.Runtime.InteropServices;
 
public class luaTest : MonoBehaviour
{
    [DllImport("lua521")]
    public static extern IntPtr luaL_newstate();
    [DllImport("lua521")]
    public static extern void luaL_openlibs(IntPtr lua_State);
    [DllImport("lua521")]
    public static extern void lua_close(IntPtr lua_State);
    [DllImport("lua521")]
    public static extern void lua_pushcclosure(IntPtr lua_State, LuaFunction func, int n);
    [DllImport("lua521")]
    public static extern void lua_setglobal(IntPtr lua_State, string s);
    [DllImport("lua521")]
    public static extern int lua_pcallk(IntPtr lua_State, int nargs, int nresults, int errfunc, int ctx, LuaFunction func);
    [DllImport("lua521")]
    public static extern int luaL_loadfilex(IntPtr lua_State, string s, string mode);
    [DllImport("lua521")]
    public static extern int luaL_loadstring(IntPtr lua_State, string s);
    [DllImport("lua521")]
    public static extern IntPtr luaL_checklstring(IntPtr lua_State, int idx, IntPtr len);
 
    [DllImport("lua521")]
    public static extern int lua_rawgeti(IntPtr lua_State, int idx0, int idx1);
    [DllImport("lua521")]
    public static extern int luaL_ref(IntPtr lua_State, int idx);
    [DllImport("lua521")]
    public static extern int luaL_unref(IntPtr lua_State, int idx0, int idx1);
    [DllImport("lua521")]
    public static extern int lua_pushnumber(IntPtr lua_State, double num);
 
    //------------------------------------
    // Lua
    //
 
    //註冊 C# Function 讓 Lua 可以呼叫執行
    public delegate int LuaFunction(IntPtr pLuaState);
    public static void lua_register(IntPtr pLuaState, string strFuncName, LuaFunction pFunc)
    {
        lua_pushcclosure(pLuaState, pFunc, 0);
        lua_setglobal(pLuaState, strFuncName);
    }
 
    //執行 Lua 檔案
    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 Function
    public static int luaL_doCallBackFromCBIdx(IntPtr lua_State, int idx, double num)
    {
        lua_rawgeti(lua_State, -1000000 - 1000, idx);
        lua_pushnumber(lua_State, num);
        int ret = lua_pcallk(lua_State, 1, 0, 0, 0, null);
        if (ret != 0)
        {
            //Error...
        }
        luaL_unref(lua_State, -1000000 - 1000, idx);
        return ret;
    }
 
 
    //------------------------------------
    // Application
    //
    public static int nLuaCBFuncIdx = -1;
    public static IntPtr m_luaState = IntPtr.Zero;
 
    public static int tellMeNum(IntPtr pLuaState)
    {
        //取得回呼函數堆疊上的 index
        nLuaCBFuncIdx = luaL_ref(pLuaState, -1000000 - 1000);
        return 0;
    }
 
    //讓 Lua 在 unity 裡顯示 log 訊息
    public static int msg(IntPtr pLuaState)
    {
        IntPtr retPtr = luaL_checklstring(pLuaState, 1, IntPtr.Zero);
        string retStr = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(retPtr);
 
        Debug.Log(retStr);
        return 0;
    }
 
    //取得 unity 執行路徑
    public static string GetAppPath()
    {
        return Application.dataPath.Substring(0, Application.dataPath.Length - 6);
    }
 
    // Use this for initialization
    void Start()
    {
        //初始化 Lua
        m_luaState = luaL_newstate();
        luaL_openlibs(m_luaState);
 
        //註冊 C# Function
        lua_register(m_luaState, "tellMeNum", tellMeNum);
        lua_register(m_luaState, "msg", msg);
 
        //執行 Lua 檔案
        string strPath = GetAppPath() + "LuaScript/lua.txt";
        luaL_dofile(m_luaState, strPath);
    }
 
    // Update is called once per frame
    void Update()
    {
        if (nLuaCBFuncIdx != -1)
        {
            //回呼取得的 lua callback function, 並給予數字 12345.678
            luaL_doCallBackFromCBIdx(m_luaState, nLuaCBFuncIdx, 12345.678);
            nLuaCBFuncIdx = -1;
        }
    }
 
    void OnApplicationQuit()
    {
        lua_close(m_luaState);
    }
}
其中值得注意的是 luaL_doCallBackFromCBIdx 函數,其用來回呼 Lua Callback Function。

lua.txt 內容如下:
function dispResult(num)
   msg(num)
end
 
tellMeNum(dispResult)
此 lua script 呼叫 tellMeNum 並將 dispResult 以參數傳給 C#,待 C# 呼叫 dispResult,dispResult 接收到 num 之後呼叫 msg 使 num 顯示在 Unity Console。

完整範例程式可在此取得:https://github.com/phardera/unity3d_lua_callback_function.git

2012-10-25

C# byte array 和變數之間的轉換

C# 將字串或數字轉成 Byte Array 或是將 Byte Array 轉回字串或數字,這是在檔案讀寫或是網路封包傳送/接收時可能會用到的功能,底下是轉換的範例:
//資料轉成 byte
static byte[] c2b()
{
    List<byte> myPacket = new List<byte>();
 
    string str ="hello world";
 
    //取得字串 byte 長度
    int nByteCt =System.Text.UTF8Encoding.UTF8.GetByteCount(str);
 
    //寫入字串長度及字串
    myPacket.AddRange(BitConverter.GetBytes((Int32)nByteCt));
    myPacket.AddRange(System.Text.UTF8Encoding.UTF8.GetBytes(str));
 
    //直接寫入 byte data
    myPacket.Add((byte)77);
 
    //寫入整數及單精度浮點數
    myPacket.AddRange(BitConverter.GetBytes((Int32)79927));
    myPacket.AddRange(BitConverter.GetBytes((float)12345.678f));
 
    return myPacket.ToArray();
}
 
//byte 轉成資料
static void c2d(byte[] data)
{
    //讀取字串 byte 長度
    int nByteCt =BitConverter.ToInt32(data, 0); //4bytes
 
    //讀取字串
    string str = System.Text.UTF8Encoding.UTF8.GetString(data, 4, nByteCt);
 
    //直接讀取 byte data
    int byteval = (int)data[4 + nByteCt]; //1byte
 
    //讀取整數及單精度浮點數
    int val0 = BitConverter.ToInt32(data, 4 + nByteCt + 1); //4bytes
    float val1 = BitConverter.ToSingle(data, 4 + nByteCt + 1 + 4);
}
 
//測試
static void Main(string[] args)
{
    c2d(c2b());
}

2012-09-17

Convert char** (string array) to C# string[]

底下範例是將 C/C++ 的字串陣列指標 (char**) 轉成 C# 內部使用的 string[] :

C/C++ 端的函數指標為 :
typedef void (* pCBFunc)(int, char**);

對應的 C# 端的函數為 :
public static void callback(int nArrsz, IntPtr pStringArray)
{
    string[] data = new string[nArrsz];
    IntPtr tmpPtr = pStringArray;
 
    for (int i = 0; i < nArrsz; ++i)
    {
        IntPtr strPtr = 
            (IntPtr)Marshal.PtrToStructure(tmpPtr, typeof(IntPtr));
        data[i] = Marshal.PtrToStringAuto(strPtr);
 
        tmpPtr = new IntPtr(tmpPtr.ToInt64() + IntPtr.Size);
    }
}
其中的 int nArrsz 為字串數目,IntPtr pStringArray 為字串陣列 ( 對應至 C 的 char** ) ,string[] data 是轉型後的資料。

2012-09-08

gitolite + cygwin + windows xp

使用 gitolite 具有更進階的資料夾管理功能,意思是我可以針對各別資料夾去指定誰擁有寫入或者讀取的權限。使用者仍然可以做 pull / push 等動作,但若是違背權限的規則,pull / push 的動作就會被 server reject。此外,gitolite的使用者權限是以 ssh key 來分辨,因此 Server 的系統只需要建立一個使用者帳號即可 (此例使用的帳號名稱是 git )

這篇記錄在 windows 上架設 gitolite 的步驟:

安裝 cygwin (Server Side)
1) 在 xp 上安裝 cygwin (http://cygwin.com/setup.exe)
2) 選擇 Install from Internet

3) 路徑及選項使用預設值

4) LocalPackage 暫存路徑使用預設值

5) 預設直接連線

6) 隨意選擇一個下載來源網址吧

7) 出現警告訊息,如果之前有裝過舊版 cygwin,至 http://cygwin.com/ 參閱此次安裝可能會影響到舊版的哪些內容,如果是第一次安裝就直接按確定繼續吧

8) 出現 Select Packages 畫面,選擇加入一些常用 package (bin) 列出如下:
a) Utils :: util-linux (2.21-1)
b) Utils :: screen (4.0.3-7)
c) Devel :: git (1.7.9-1)
d) Editors :: nano (2.25-1)
e) Net :: openssh (6.0p1-2)
圖片為勾選 Editor 底下之 nano 的 Bin Package

8) 安裝必要相關套件

9) 等待安裝完成
10) 完成

設定環境 sshd (Server Side)
1) 開啟桌面上的 Cygwin Terminal 捷徑
2) 設定 sshd:輸入 ssh-host-config

3) 出現 Should privilege separation be used 輸入 yes
4) 出現 new local account 'sshd' ? 輸入 yes
5) 出現 Do you want to install sshd as a service ? 輸入 yes
6) 出現 Enter the value of CYGWIN for the daemon : [] 直接按 Enter
7) 設定完成

設定 git 使用者帳號 (Server Side)
1) 在控制台 ---> 使用者帳戶 ---> 建立新的帳戶
2) 名稱輸入 git

3) 確定
4) 為 git 帳號設定密碼 ( ps. 吴波 指出 : "把git用户加入Administrators管理员组" ,可以解決在 Windows 2012 Server 環境下,ssh 無法 login 的問題 ! )

5) 在 cygwin terminal 裡輸入 mkpasswd -l -u git >> /etc/passwd

6) 輸入 sc start sshd 啟動 ssh server

7) 可以輸入 ipconfig 取得電腦的 ip 位址,後續設定會用到

安裝 gitolite (在 Client 端以 ssh 連線到 Server 來做設定)
1) 在 Client 端電腦 (通常是管理者的電腦,也可在 Server 端電腦上操作) 以 ssh 方式連至 Server,若 client 端是 windows 系統則可以使用 putty (http://chiark.greenend.org.uk/~sgtatham/putty/download.html) 來連線
2) 下載 putty.exe 執行畫面 (此例的 Server ip 是 192.168.11.7 所以在 Host Name 處輸入 192.168.11.7,Connection type 選擇 SSH)

3) 按下 Open 之後出現警告視窗,按是,則下次不再顯示此訊息

4) 提示 login as : 時輸入 git (使用者名稱)
5) 提示 git@192.168.11.7's password : 時輸入剛剛在 Server 端設定的密碼


6) 剛登入所在路徑應該為 /home/git (可輸入 pwd 來確定)
7) 輸入 git clone git://github.com/sitaramc/gitolite.git (若無法成功可改輸入 git clone https://github.com/sitaramc/gitolite.git 試看看)
8) 確定 /home/git/.ssh/authorized_keys 檔案不存在或者內容空白 (輸入 cat /home/git/.ssh/authorized_keys 來測試)


設定 gitolite 管理者權限
gitolite 本身也使用 git 機制來管理權限,但只有第一個使用者 (管理者) 的設定比較麻煩,此例子是假設 Server 和管理者並非使用同一台電腦的狀態下做設定,換句話說管理者在自已的電腦上透過 ssh 來遠端連線到 Server 做設定

1) 安裝 git (http://git-scm.com) 此篇文章使用的是 1.7.11 版,安裝步驟可參考此篇文章
2) 在桌面上按滑鼠右鍵選擇 Git Bash

3) 輸入 ssh-keygen (出現所有提示皆按 Enter)


4) 將產生的 public ssh key 傳送至 Server,輸入 cd ~/.ssh 然後輸入 scp id_rsa.pub git@192.168.11.7:git.pub,其中 id_rsa.pub 為產生的 public key,git 為先前設定好的使用者帳號,192.168.11.7 為 Server IP,git.pub 為最後擺在 Server 上的檔案名稱
5) 提示出現密碼則輸入先前為 git 帳號設定的密碼


6) 輸入 ssh git@192.168.11.7 登入至 Server
7) 輸入 mkdir -p $HOME/bin
8) 輸入 ./gitolite/install -to $HOME/bin
9) 輸入 $HOME/bin/gitolite setup -pk ./git.pub


10) 輸入 exit 離開 ssh
11) 輸入 cd ~ (回到 home 目錄)
12) 輸入 git clone git@192.168.11.7:gitolite-admin ( 此時應可直接完成 clone,若被提示問密碼則代表先前的設定有誤)
13) 完成

新增 git repo (例如現在要新開一個 repo 名叫 newrepo)
1) 將  gitolite-admin clone 下來之後,打開 gitolite-admin/conf/gitolite.conf 檔案
2) 在底下新增
repo newrepo
    RW+ = @all
意思是增加 repo 名叫 newrepo, 且所有 user 有有權利讀寫

3) 儲存之後依序 commit 並 push 就算設定完成了

新增 git user
1) 利用先前提的步驟,在欲新增的 user 的電腦上執行 ssh-keygen,然後將產生的 id_rsa.pub 複製到管理者電腦的 gitolite-admin/keydir 路徑底下並重新命名為 jack.pub (此處假設這個 user 叫做 jack)
2) 依序 commit 並 push 就算設定完成了 (假若 push 之後仍然無效則在 Server 端的 /home/git/bin 底下重新執行 ./gitolite setup )

設定 git user 之權限
1) 先前提的 RW+ =@all 代表所有人可讀取此 repo,若是改為 RW+ =jack 則代表只有 jack 這個  user 可以讀寫此 repo,詳細的設定可以參考 https://github.com/sitaramc/gitolite 連結的教學
2) gitolite 甚至可以限制 repo 裡的某些資料夾只有某些人可以讀寫:

RW+ = QA-guy
RW+ VREF/NAME/CHANGELOG =QA-guy
RW+ VREF/NAME/ReleaseNotes/ = QA-guy
- VREF/NAME/ =QA-guy

上面例子意思是 QA-guy user 只能讀寫此 repo 的 CHANGELOG 檔案及 ReleaseNote 資料夾底下的檔案。


此外,底下是吴波熱心提供的幾個建議提供參考 :

1) 開啟桌面上的 Cygwin Terminal 捷徑
Vista、Win7、Win8、Win2012等系统最好以管理员权限打开

6) 輸入 sc start sshd 啟動 ssh server
这步之后如果无法正常启动,查看/var/log/sshd.log,如果提示权限过大等,执行以下命令限制权限:
chmod 600 /etc/ssh_host_dsa_key
chmod 600 /etc/ssh_host_ecdsa_key
chmod 600 /etc/ssh_host_key
chmod 600 /etc/ssh_host_rsa_key

chmod 600 /var/empty

7) 可以輸入 ipconfig 取得電腦的 ip 位址,後續設定會用到
注意,可能要添加防火墙例外

git + apache + windows xp

日前在 windows xp 上架了 git server,第一種是用 git (for windows ) ,架在 apache 上,第二種是用 gitolite ,架在 cygwin 上,這篇記錄第一種 git server 安裝設定步驟:

1) 至 http://git-scm.com 下載  git for windows (這篇文章使用的是 1.7.11 版)

2) 安裝時都使用預設的選項,此頁選項選擇 Use Git Bash only 。

3) 安裝完成之後到 git 安裝目錄下(C:\Program Files\Git\bin\)將 libiconv-2.dll 檔案複製到(C:\Program Files\Git\libexec\git-core\)

4) 至 http://httpd.apache.org/download.cgi 下載 apache Win32 Binary including OpenSSL 0.9.8t(這篇文章使用的是 2.2.22 版)
5) 至 C:\Program Files\Apache Software Foundation\Apache2.2\conf\ 底下,開啟檔案 httpd.conf 並找到其中:

<Directory />

    Options FollowSymLinks
    AllowOverride None
    Order deny,allow
    Deny from all
</Directory>

將 Deny from all 改成 Allow from all,然後插入底下內容:


<Directory "C:/git_repo">

    Options Indexes FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
</Directory>

<Directory "C:/Program Files/Git/libexec/git-core/">

    Options Indexes FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
</Directory>

其中 C:/git_repo 表示未來 git repo 將要置放的路徑。結果如圖:


6) 在檔案最後加入底下內容:


SetEnv GIT_PROJECT_ROOT C:/git_repo

SetEnv GIT_HTTP_EXPORT_ALL
ScriptAliasMatch \
        "(?x)^/git/(.*/(HEAD | \
                        info/refs | \
                        objects/(info/[^/]+ | \
                                 [0-9a-f]{2}/[0-9a-f]{38} | \
                                 pack/pack-[0-9a-f]{40}\.(pack|idx)) | \
                        git-(upload|receive)-pack))$" \
"C:/Program Files/Git/libexec/git-core/git-http-backend.exe/$1"

<Location /git>

AuthType Basic
AuthName "GIT Repository"
AuthUserFile "C:/git_repo/htpasswd"
Require valid-user
</Location>

結果如圖:


7) 建立一個 repo。開啟檔案管理員,在 C:\ 底下建立資料夾 git_repo ,在此資料夾底下按右鍵,選擇 Git Bash。


8) 在 console 模式下輸入 git init --bare test.git,然後進入 test.git 目錄之後再輸入 git update-server-info。


9) 輸入 touch git-daemon-export-ok。


10) 為目錄 git 增加使用者權限:輸入 cd /C/Program\ Files/Apache\ Software\ Foundation/Apache2.2/bin/ 進入該資料夾之後輸入 htpasswd -cmb /C/git_repo/htpasswd git gitpwd 其中 git 為使用者名稱,gitpwd 為密碼。


11) 測試:至根目錄下輸入 git clone http://127.0.0.1/git/test.git,鍵入使用者名稱及密碼之後看到訊息 warning : You appear to have cloned an empty repository 代表 git server 建立完成。



2012-08-27

關閉 "F7" 按鈕開啟 iTunes

mac 的 F6, F7, F8 等 Media Key 可以控制 VLC, Cog,... 等程式播放音樂歌曲。但其中的 F7 按鈕除了 Play / Pause 之外就是會啟動  iTunes。

若要取消這個功能有以下作法 :
1) 關閉 Function Key, 改用 keymapper ( Function key 改由 Fn+F1, F2,... 啟動 )
   a. 到 System Preferences -> Keyboard, 勾選 
        Use all F1, F2, etc. keys as standard function keys
   b. 安裝 keymapper, 自己設定 F6, F7, F8 的功能

2) 更改 itunes 名稱讓 media key 無法啟動之 (參考這裡)
3) 直接 patch rcd daemon (參考這裡)

第3種方法作者用 python 寫了一個程式去改 rcp 執行檔,改完之後除了啟動 itunes 之外的其它功能都還在,若要復原則只要把原來備份的 rcp 執行檔覆蓋回去就行了,似乎是最好的方法。

2012-08-18

Unity 3D 從 AssetBundle 載入 Lightmap

當場景裡的光源與模型皆靜止時,可使用 Lightmapping 預先計算光照結果,下圖為使用 Lightmap 前 (Directional Light) 及使用 Lightmap 後的差異:

All Terrain Tactical Enforcer (Directional Light)

All Terrain Tactical Enforcer (Lightmapping)

1. 製作 AssetBundle
Lightmap Bake 完成之後將 Model 打包成 AssetBundle,使用指令:
BuildPipeline.BuildAssetBundle(
    Selection.activeObject, 
    Selection.objects, 
    path, 
    BuildAssetBundleOptions.CollectDependencies | 
    BuildAssetBundleOptions.CompleteAssets
);
選擇的檔案除了 Prefab/GameObject 之外,Lightmap 圖片也要選擇:

選擇 Prefab 及 Lightmap

2. 載入 AssetBundle
載入 AssetBundle 時先設定 Lightmap 再載入 GameObject,此場景的 Lightmapping 參數如下:
Lightmapping 參數

按照當初 Lightmapping 之參數做對應的設定:
List arrld = new List();
LightmapData ld = new LightmapData();
ld.lightmapFar = 
aa.assetBundle.Load("LightmapFar-0", typeof(Texture2D)) as Texture2D;
ld.lightmapNear = 
aa.assetBundle.Load("LightmapNear-0", typeof(Texture2D)) as Texture2D;
arrld.Add(ld);
 
LightmapSettings.lightmapsMode = LightmapsMode.Dual;
LightmapSettings.lightmaps = arrld.ToArray();

再載入 GameObject :
GameObject go = GameObject.Instantiate(aa.assetBundle.Load("StaticObj", typeof(GameObject))) as GameObject;
此外慶幸的是,GameObject 之 MeshRenderer 內有關 Lightmapping 之參數在存取 AssetBundle 時皆有自動設定,不需再額外寫程式存取設定之。

程式碼可以在:https://github.com/phardera/unity3d_assetbundle_lightmap.git 取得

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 取得。