底下是將 Facebook SDK 打包成 Facebook Plugin 的步驟,執行的平台是 Android,Unity3D 的版本是 3.5。
1. 在這裡取得 Facebook SDK 。
2. 開啟 eclipse 之後在功能表上選擇 File --> Import...,出現對話框之後在 import source 選擇 Existing Android Code Into Workspace。
3. 在 Root Directory 選擇剛剛下載的 SDK 資料夾 (此範例為 /Users/macaronics/facebook-android-sdk-3.0.1),Project to Import 的部分只要勾選 facebook (Facebook SDK) 即可。
4. 按下 Finish 之後在 Package Explorer 應該可以發現 Eclipse 已經自動產生一些檔案 (FacebookSDK/gen) 並且自動編譯好 facebooksdk.jar (FacebookSDK/bin) 了。
6. 複製 android-support-v4.jar 至 Unity 專案的 Assets/Plugins/Android 資料夾底下。( ps. facebook sdk 裡的 android-support-v4.jar 版本似乎太舊所以在編譯時會出問題,因此檔案可以從 android sdk 裡的 extras/android/support/v4 資料夾取得)
7. 建立 AndroidManifest.xml 檔案至 Assets/Plugins/Android 資料夾,內容如下 :
<?xml version="1.0" encoding="utf-8"?>
<manifest android:versionCode="1" android:versionName="1.0"
android:installLocation="preferExternal"
package="com.macaronics.fbonandroid"
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.macaronics.iab.overrideActivity"
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"
/>
<activity android:name="com.facebook.LoginActivity" android:label="@string/app_name" />
</application>
<uses-feature android:glEsVersion="0x20000" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
值得注意的是這裡設定了 activity "com.facebook.LoginActivity" 以及 Permission "INTERNET"。8. 利用 git ( https://github.com/douglascrockford/JSON-java.git ) 取得 org.json 原始碼並以下述指令編譯 Java (在相同資料夾輸入指令) :
javac *.java -d .完成之後輸入 :
jar cvfM json.jar org/然後將產生的 json.jar 檔案置於 Assets/Plugins/Android 資料夾底下。
9. 建立 overrideActivity.jar ,功用在於 relay 系統資訊給 facebook SDK。首先建立 overrideActivity.java 檔案如下 :
package com.macaronics.iab;
import com.unity3d.player.UnityPlayerActivity;
import android.os.Bundle;
import android.util.Log;
import android.content.Intent;
public class overrideActivity extends UnityPlayerActivity {
public interface cbEvent{
public boolean cbEvent(int requestCode, int resultCode, Intent data);
}
public interface cbSaveInstState{
public boolean cbSaveInstState(Bundle outState);
}
protected cbEvent ie;
protected cbSaveInstState isis;
protected Bundle mSavedInstState;
static protected overrideActivity inst;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
inst =this;
mSavedInstState =savedInstanceState;
// print debug message to logcat
Log.d("Unity::overrideActivity", "onCreate called!");
}
@Override
public void onDestroy(){
super.onDestroy();
inst =null;
Log.d("Unity::overrideActivity", "onDestroy called!");
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
Log.d("Unity::overrideActivity", "onActivityResult called!");
boolean ret =false;
if (ie !=null){
try{
ret =ie.cbEvent(requestCode, resultCode, data);
}
catch(Exception e){
ret =false;
}
}
if (ret ==false){
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
Log.d("Unity::overrideActivity", "onSaveInstanceState called!");
boolean ret =false;
if (isis !=null)
{
try{
ret =isis.cbSaveInstState(outState);
}
catch(Exception e){
ret =false;
}
}
if (ret ==false){
super.onSaveInstanceState(outState);
}
}
static public void registerOnActivityResultCBFunc(final cbEvent pcbfunc){
if (inst !=null)
inst.ie =pcbfunc;
}
static public void registerOnSaveInstanceSataeCBFunc(final cbSaveInstState pcbfunc){
if (inst !=null)
inst.isis =pcbfunc;
}
static public Bundle getSavedInstanceState()
{
if (inst !=null)
return inst.mSavedInstState;
return null;
}
}
編譯指令為 :
javac ./overrideActivity.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Applications/Unity3.5.6/Unity/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar -d .其中,引入的 library 包含了 android-10/android.jar (Android 2.3.3) ,以及 Unity 的 classes.jar。編譯完成後將其打包成 overrideActivity.jar :
jar cvfM ../overrideActivity.jar com/將產生的 overrideActivity.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下。此外,須注意資料夾 com 要刪除,以免後續執行 jar 時,打包到舊的 com package。
10. 建立 fbWrapper.jar (主要用於介接 facebook SDK 及 Unity),首先建立 fbWrapper.java 檔案如下 :
package com.macaronics;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.util.Base64;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Toast;
import com.facebook.*;
import com.facebook.widget.WebDialog;
import com.facebook.widget.WebDialog.OnCompleteListener;
import com.facebook.model.GraphObject;
import com.unity3d.player.UnityPlayer;
import com.macaronics.iab.overrideActivity;
import org.json.JSONArray;
public class fbWrapper
{
private String myHash;
private String mEventHandler;
private Activity mActivity;
private String mAppId;
public fbWrapper(String packageName, String applicationId, String strEventHandler){
mActivity =UnityPlayer.currentActivity;
mEventHandler =strEventHandler;
mAppId =applicationId;
//--------------------
//fetch keyhash
try
{
PackageInfo info =mActivity.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
for (Signature sig : info.signatures){
MessageDigest md =MessageDigest.getInstance("SHA");
md.update(sig.toByteArray());
myHash =Base64.encodeToString(md.digest(), Base64.DEFAULT);
}
}
catch(NameNotFoundException e){
Log.d("Unity::fbWrapper()", "error on fetch haskkey -name not found");
}
catch(NoSuchAlgorithmException e){
Log.d("Unity::fbWrapper()", "error on fetch haskkey -no such algorithm");
}
//--------------------
//register callback (onActivityResult)
overrideActivity.registerOnActivityResultCBFunc(
new overrideActivity.cbEvent(){
public boolean cbEvent(int requestCode, int resultCode, Intent data)
{
Session.getActiveSession().onActivityResult(mActivity, requestCode, resultCode, data);
return false;
}
});
//--------------------
//register callback (onSaveInstanceState)
overrideActivity.registerOnSaveInstanceSataeCBFunc(
new overrideActivity.cbSaveInstState(){
public boolean cbSaveInstState(Bundle outState)
{
Session session = Session.getActiveSession();
Session.saveSession(session, outState);
return false;
}
});
//--------------------
//facebook
Bundle savedInstanceState =overrideActivity.getSavedInstanceState();
Settings.addLoggingBehavior(LoggingBehavior.INCLUDE_ACCESS_TOKENS);
Session session =Session.getActiveSession();
if (session == null) {
if (savedInstanceState != null) {
session = Session.restoreSession(mActivity, null, statusCallback, savedInstanceState);
Log.d("Unity::onCreate()", "Session.restoreSession()");
}
if (session == null) {
session = new Session.Builder(mActivity).setApplicationId(mAppId).build();
Log.d("Unity::onCreate()", "new Session.Builder(this)");
}
Session.setActiveSession(session);
if (session.getState().equals(SessionState.CREATED_TOKEN_LOADED)) {
session.openForRead(new Session.OpenRequest(mActivity).setCallback(statusCallback));
Log.d("Unity::onCreate()", "openForRead()");
}
}
}
public void dispose()
{
Log.d("Unity::dispose()", "dispose called");
}
public boolean isSessionOpened()
{
Session session =Session.getActiveSession();
if (session !=null && session.isOpened()==true && session.getAccessToken() !=null)
{
return true;
}
return false;
}
public String getHash()
{
return myHash;
}
public void login()
{
Session session = Session.getActiveSession();
if (!session.isOpened() && !session.isClosed()) {
Log.d("Unity::onLogin()","session.openForRead()");
session.openForRead(new Session.OpenRequest(mActivity).setCallback(statusCallback));
}else if (session !=null){
Log.d("Unity::onLogin()","Session.openActiveSession()");
//regenerate session
Session newsession = new Session.Builder(mActivity).setApplicationId(mAppId).build();
Session.setActiveSession(newsession);
newsession.openForRead(new Session.OpenRequest(mActivity).setCallback(statusCallback));
}
else
{
Log.d("Unity", "error -session ==null");
}
}
public void logout()
{
Log.d("Unity::onLogout()","on logout called !");
Session session = Session.getActiveSession();
if (!session.isClosed()) {
session.closeAndClearTokenInformation();
Log.d("Unity::onLogout()","session.closeAndClearTokenInformation()");
}
}
public void invokeQuery(String fqlQueryStr)
{
Session session =Session.getActiveSession();
if (session.isOpened()==false)
{
Log.d("Unity::invokeQuery()", "invalid session");
return;
}
Bundle params =new Bundle();
params.putString("q", fqlQueryStr);
final Request request =new Request(session,
"/fql",
params,
HttpMethod.GET,
new Request.Callback(){
public void onCompleted(Response response){
GraphObject graphObject = response.getGraphObject();
if (graphObject !=null)
{
if (graphObject.getProperty("data")!=null &&
mEventHandler !=null)
{
try{
String arr =graphObject.getProperty("data").toString();
JSONArray pj =new JSONArray(arr);
UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"ret\":\"true\",\"dat\":"+pj.toString()+"}");
}
catch(Exception e)
{
if (mEventHandler!=null)
UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"ret\":\"false\"}");
}
}
else
{
if (mEventHandler!=null)
UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"ret\":\"false\"}");
}
}
else
{
if (mEventHandler!=null)
UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"ret\":\"false\"}");
}
}
});
Log.d("Unity::invokeQuery()", "send fql request...");
mActivity.runOnUiThread(new Runnable(){
public void run(){
Request.executeBatchAsync(request);
}
});
}
public void sendRequest(String reqMessage)
{
final Session session =Session.getActiveSession();
if (session.isOpened()==false)
{
Log.d("Unity::SendRequest()", "invalid session");
return;
}
final Bundle params =new Bundle();
params.putString("message", reqMessage);
final Activity act =mActivity;
mActivity.runOnUiThread(new Runnable(){
public void run(){
WebDialog requestDialog =(
new WebDialog.RequestsDialogBuilder(act,
session.getActiveSession(),
params))
.setOnCompleteListener(new OnCompleteListener(){
@Override
public void onComplete(Bundle values,
FacebookException error){
if (error !=null)
{
if (error instanceof FacebookOperationCanceledException){
Toast.makeText(act.getApplicationContext(),
"Request cancelled",
Toast.LENGTH_SHORT).show();
}
else
{
Toast.makeText(act.getApplicationContext(), "Network Error", Toast.LENGTH_SHORT).show();
}
}
else
{
final String requestId =values.getString("request");
if (requestId !=null)
{
Toast.makeText(act.getApplicationContext(),
"Request sent",
Toast.LENGTH_SHORT).show();
}
else
{
Toast.makeText(act.getApplicationContext(),
"Request cancelled",
Toast.LENGTH_SHORT).show();
}
}
}
})
.build();
requestDialog.show();
}
});
}
private Session.StatusCallback statusCallback = new SessionStatusCallback();
private class SessionStatusCallback implements Session.StatusCallback {
@Override
public void call(Session session, SessionState state, Exception exception) {
if (state.isOpened())
{
Log.d("Unity::onSessionStateChange()", "state.isOpened()");
}
else
{
Log.d("Unity::onSessionStateChange()", "state.isOpened() ===false");
}
}
}
}
其中除了 login 及 logout 外,還提供了兩個重要的功能,一是 FQL Query,另一個是 Requests Dialog。執行編譯的指令為 :
javac ./fbWrapper.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar:./facebooksdk.jar:./overrideActivity.jar:./android-support-v4.jar:./json.jar -d .其中,引入的 library 包含了 android-10/android.jar (Android 2.3.3),Unity 的 classes.jar,facebooksdk.jar,先前編譯的 overrideActivity.jar,json.jar,以及 android-support-v4.jar。編譯完成後將其打包成 fbWrapper.jar :
jar cvfM ../fbWrapper.jar com/將產生的 fbWrapper.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下。直至目前為止,資料夾底下應該有這些檔案 :
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class fbWrapper : MonoBehaviour {
AndroidJavaObject fbWrapperObj =null;
public static fbWrapper inst =null;
// Use this for initialization
void Start () {
inst =this;
}
void OnApplicationQuit(){
dispose();
inst =null;
}
public bool init(string unity_bundle_id, string fb_app_id)
{
dispose();
fbWrapperObj =new AndroidJavaObject("com.macaronics.fbWrapper", new object[3]{unity_bundle_id, fb_app_id, "fbWrapper"});
return true;
}
public void dispose()
{
if (fbWrapperObj !=null)
{
fbWrapperObj.Call("dispose");
fbWrapperObj.Dispose();
fbWrapperObj =null;
}
}
void msgReceiver(string msg)
{
if (tmpCBFunc !=null)
{
Dictionary<string, object> cache =(Dictionary<string, object>)MiniJSON.Json.Deserialize(msg);
if (cache.ContainsKey("ret")==true)
{
string ret =(string)cache["ret"];
if (ret=="false")
{
tmpCBFunc(false, null);
}
else
{
if (cache.ContainsKey("dat")==true)
{
tmpCBFunc(true, cache["dat"]);
}
}
}
else
{
tmpCBFunc(false, null);
}
}
}
//目前是否已登入
public bool isSessionAvailable()
{
if(fbWrapperObj==null)
return false;
return fbWrapperObj.Call<bool>("isSessionOpened");
}
//登入
public bool login()
{
if (fbWrapperObj !=null)
{
fbWrapperObj.Call("login");
return true;
}
return false;
}
//登出
public bool logout()
{
if (fbWrapperObj !=null)
{
fbWrapperObj.Call("logout");
return true;
}
return false;
}
public string getHashKey()
{
string myhash =fbWrapperObj.Call<string>("getHash");
if (myhash ==null)
myhash ="package name not found";
return myhash;
}
public delegate void queryCBFunc(bool ret, object data);
protected queryCBFunc tmpCBFunc =null;
public bool invokeQuery(string fql, queryCBFunc pcbfunc)
{
tmpCBFunc =pcbfunc;
if (fbWrapperObj !=null)
{
fbWrapperObj.Call("invokeQuery", new object[1]{fql});
return true;
}
return false;
}
//送出邀請
public bool sendRequest(string message)
{
if (fbWrapperObj !=null)
{
fbWrapperObj.Call("sendRequest", new object[1]{message});
return true;
}
return false;
}
}
在 Unity 建立一個 GameObject 並命名為 fbWrapper,然後將 fbWrapper.cs 設定成此 GameObject 之 Component。此外值得注意的是,函數 init 的第一個參數是 Unity 之 Bundle Identifier,第二個參數是 Facebook App ID,此 ID 應該在建立 Facebook App 時,由 Facebook 所提供,如圖所示 :
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class test : MonoBehaviour {
// Use this for initialization
string myhash =null;
void Start () {
fbWrapper.inst.init("com.macaronics.fbonandroid", "FB_APP_ID");
myhash =fbWrapper.inst.getHashKey();
}
void OnGUI()
{
if (GUI.Button(new Rect(10,10,100,100), "login"))
{
fbWrapper.inst.login();
myhash =fbWrapper.inst.getHashKey();
}
if (GUI.Button(new Rect(10,120,100,100), "logout"))
{
fbWrapper.inst.logout();
queryresult =null;
myhash =null;
}
if (fbWrapper.inst.isSessionAvailable())
{
if (GUI.Button(new Rect(10,230,100,100), "invoke query"))
{
fbWrapper.inst.invokeQuery("SELECT uid, name, pic_square, is_app_user FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 =me())", queryCB);
}
if (GUI.Button(new Rect(10,340,100,100), "send request"))
{
fbWrapper.inst.sendRequest("check out my app !");
}
}
if (myhash !=null)
{
GUI.Label(new Rect(120, 10, Screen.width-120, 40), "Hash Key: "+myhash);
}
if (queryresult !=null)
{
GUI.Label(new Rect(120, 50, Screen.width-120, Screen.height-50), queryresult);
}
}
string queryresult =null;
void queryCB(bool ret, object data)
{
string tmpstr ="";
List<object> OBJARR =(List<object>)data;
for (int i=0;i<OBJARR.Count;++i)
{
Dictionary<string, object> items =(Dictionary<string, object>)OBJARR[i];
foreach(KeyValuePair<string, object> item in items)
{
tmpstr +=(item.Key+":"+item.Value+"\n");
}
}
queryresult =tmpstr;
}
}
將 Script 內的 FB_APP_ID 換成當初建立 FB APP 時,Facebook 給予的 App ID。此外,"com.macaronics.fbonandroid" 是本範例所設定之 Bundle Identifier 。另外,呼叫 invokeQuery 函數的第一個參數是 FQL 字串,第二個參數是作為回傳結果用的 Callback Function,回傳的結果字串屬於 JSON 格式,範例把字串內容解析並顯示出來。13. 設定 Untiy 載入 Scene 之後,各 Script 的執行順序,將 fbWrapper 設定早於預設的時間 (Edit --> Project Settings --> Script Execution Order)。
15. 編譯 R.java :
javac R.java -d .打包成為 jar 檔案 :
jar cvfM facebooksdk_R.jar com/將檔案放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下。
16. 重新編譯 apk 檔案。
此範例的原始碼可以從這裡 ( http://github.com/phardera/unity3d_fb_android.git ) 取得。







