此篇文章實作將 JmDNS 包裝成可以在 Android 平台上執行之 Unity3D Plugin。使用 JmDNS 可以方便行動裝置在區域網路內發現對方,以進行後續的連線。(此機制也稱為 Bonjour ,最初由 Apple 提出。)因為 iOS 平台的 NSNetService 也提供相同的功能,所以 Android 裝置和 iOS 裝置皆可以透過此機制取得對方的網路位置。底下為實作步驟:
A) 至 jmdns.sourceforge.net 取得 JmDNS 並利用 ant 進行編譯:
1) 以 osx 平台為例,在 terminal 輸入:
2) 待程式碼下載完成後進入 jmdns 資料夾輸入 :
3) 完成之後進入資料夾 ./build/lib/ (此時應該找到檔案 jmdns.jar),並輸入底下指令:
目的在於去除重覆的 class 檔案,以避免 Android dex 工具編譯時出現錯誤。
B) 建立 Unity 專案並設定權限:
1) 將 jmdns.jar 檔案復製到 Unity 專案底下資料夾 (Assets/Plugins/Android)
2) 在 Assets/Plugins/Android 資料夾底下新增檔案 AndroidManifest.xml 並設定內容如下:
值得注意的是 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:
2) 結束此 class(停止 jmDNS):
3) 發佈/停止 Service :
4) 搜尋/停止 Service :
D) 編譯 jmDNSWrapper.java:
與 jmDNS 相同,將 jmDNSWrapper 編譯成 jar 檔案,此文章假設使用者已安裝 Android SDK 且安裝之 Android 平台版本為 4.0.3 (API Level 15)。
1) 輸入指令,將 java 編譯成 jmDNSWrapper.class :
此指令分別提供三個 jar 檔在編譯時使用。
2) 將 com 資料夾(資料夾按照 jmDNSWrapper.java 裡 "package com.macaronics" 產生)底下的 class 檔壓成 jar 檔案:
3) 將 jmDNSWrapper.jar 檔案復製到 Unity 專案底下資料夾 (Assets/Plugins/Android)
E) Unity 引用 jmDNSWrapper :
這邊的目的是讓 jmDNSWrapper.java 裡的函數能在 C# 環境下呼叫。
F) 實際範例
最後撰寫一個簡單的範例來實際測試一下 jmDNS,此範例有兩個按鈕,分別是發佈與停止服務,還有將搜尋到的服務顯示在 Console Window。
G) 最後實機測試
很不幸的在 Android Emulator 上無法做 wifi 連線測試,因此需要一台真正的 Android 手機。在 Unity 上設定好 Android 編譯參數後按下 Build And Run,順利的話會看到程式在手機上跑起來了。此時在 Android SDK 裡找到 adb 執行檔,執行 adb logcat 之後就可以看到 Unity 的 Log 訊息。
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 訊息。