2013-12-28

Unity 3D + Local Notification for Android

在 Android 底下使用 Notification Service 配合 Alarm Service 即可使系統在指定時間產生通知訊息。底下敘述如何使用 Android 的 Notification 並且將其編譯成 Plugin 與 Unity 整合。

Plugin 主要只有一個檔案 ( AlarmReceiver.java ) :
package com.macaronics.notification;

import java.util.Calendar;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.util.Log;
import com.unity3d.player.UnityPlayer;

public class AlarmReceiver extends BroadcastReceiver {
    public static void startAlarm(String name, String title, String label, int secondsFromNow){
        Activity act =UnityPlayer.currentActivity;
        Log.i("Unity", "startAlarm...");
      
        Calendar c = Calendar.getInstance();
        c.add(Calendar.SECOND, secondsFromNow);
        long alarmTime = c.getTimeInMillis();
        Log.i("Unity", "alarm time +"+secondsFromNow);
        
        // Schedule the alarm!
        AlarmManager am = (AlarmManager)act.getSystemService(Context.ALARM_SERVICE);
        Intent ii =new Intent(act, AlarmReceiver.class);
        ii.putExtra("name", name);
        ii.putExtra("title", title);
        ii.putExtra("label", label);
        am.set(AlarmManager.RTC_WAKEUP, alarmTime, PendingIntent.getBroadcast(act, 0, ii, 0));
    }
    
    //<receiver android:process=":remote_notification" android:name="AlarmReceiver"></receiver>
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d("Unity", "Alarm Recieved!");
      
        NotificationManager mNM = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        
        Bundle bb =intent.getExtras();

        Class<?> cc = null;
        try {
          cc = context.getClassLoader().loadClass("com.unity3d.player.UnityPlayerProxyActivity");
        } catch (ClassNotFoundException e1) {
          e1.printStackTrace();
          return;
        }
            
        final PackageManager pm=context.getPackageManager();
        ApplicationInfo applicationInfo = null;
        try {
          applicationInfo = pm.getApplicationInfo(context.getPackageName(),PackageManager.GET_META_DATA);
        } catch (NameNotFoundException e) {
          e.printStackTrace();
          return;
        }
        final int appIconResId=applicationInfo.icon;
        Notification notification = new Notification(appIconResId, (String)bb.get("name"), System.currentTimeMillis());
        
        int id =(int)(Math.random()*10000.0f)+1;
        PendingIntent contentIntent = PendingIntent.getActivity(context, id, new Intent(context, cc), 0);
        notification.setLatestEventInfo(context, (String)bb.get("title"), (String)bb.get("label"), contentIntent);

        Log.i("Unity", "notify("+id+") with "+(String)bb.get("title")+", "+(String)bb.get("label"));
        mNM.notify(id, notification);
    }
}
其中函數 startAlarm 主要負責在 Alarm Service 設定 Alarm 事件的啟動時間,接收 Alarm 事件的對象 (com.macaronics.notification.AlarmReceiver ) ,傳遞的資料 (name, title, label),當 Alarm 事件呼叫啟動 onReceive 函數之後,onReceive 則是依照傳遞過來的資料利用 Notification Service 來產生 Notification。

編譯 AlarmReceiver.java (OSX 環境) :
#!/bin/sh

ANDROID_JAR=/Users/macaronics/Desktop/applications/adt-bundle-mac-x86_64-20130917/sdk/platforms/android-13/android.jar
UNITY_JAR=/Applications/Unity4.2.1/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar

javac ./*.java -cp $ANDROID_JAR:$UNITY_JAR -d .
jar cvfM ../AlarmReceiver.jar com/
rm -rf ./com

其中 ANDROID_JAR 是 Android SDK 底下的 android.jar 路徑,UNITY_JAR 是 Unity 底下 classes.jar 的路徑,javac 指令將目前資料夾底下所有的 java 檔案 compile 成 class 檔案並置於相對應於 package name 的資料夾路徑底下,jar 指令則是將資料夾打包成 jar 檔案 (值得注意的是 package name 與資料夾路徑必須要一致,例如 package name 為 com.macaronics.notification,則其 class 檔案須置於 ./com/macaronics/notification/ 底下。 )

設定 AndroidManifest.xml,在 AndroidManifest.xml 的 application 底下新增 :
<receiver android:process=":remote" android:name="com.macaronics.notification.AlarmReceiver"></receiver>
底下為 AndroidManifest.xml 完整內容 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="preferExternal" android:theme="@android:style/Theme.NoTitleBar" android:versionName="1.0" android:versionCode="10">
  <supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true" />
  <application android:icon="@drawable/app_icon" android:label="@string/app_name" android:debuggable="false">  
    <receiver android:process=":remote" android:name="com.macaronics.notification.AlarmReceiver"></receiver>
    <activity android:name="com.unity3d.player.UnityPlayerProxyActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:name="com.unity3d.player.UnityPlayerActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" >
    </activity>
    <activity android:name="com.unity3d.player.UnityPlayerNativeActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" >
      <meta-data android:name="android.app.lib_name" android:value="unity" />
      <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" />
    </activity>
  </application>
  <uses-feature android:glEsVersion="0x00020000" />

  <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" />

</manifest>
將編譯完成的 AlarmReceiver.jar 以及 AndroidManifest.xml 置於 Unity 專案資料夾的 Assets/Plugins/Android/ 底下。

建立一個 C# Script 測試結果,其內容如下 :
using UnityEngine;
using System.Collections;

public class AlarmReceiver : MonoBehaviour {

  // Use this for initialization
  void Start () {

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

  AndroidJavaObject nativeObj =null;
  void OnGUI(){
    if (GUI.Button(new Rect(Screen.width*0.5f-90.0f, 100.0f, 180.0f, 100.0f), "Create Notification")){
      if (nativeObj ==null)
        nativeObj =new AndroidJavaObject("com.macaronics.notification.AlarmReceiver");

      nativeObj.CallStatic("startAlarm", new object[4]{"THIS IS NAME", "THIS IS TITLE", "THIS IS LABEL", 10});
    }
  }
}
當使用者按下 Create Notification 按鈕之後 Plugin 會建立一個 10 秒後顯示 Notification 的 Alarm 事件。

本範例的完整 Unity 程式碼可以在 http://github.com/phardera/unity3d_notification_android 下載。

2013-11-30

BeagleBone Black with AnTuTu Benchmark

底下敘述在 BeagleBone Black 上執行 Android AnTuTu App (4.1.1) 的流程 。

A. 製作 Android SDCard :
參考這裡提供的 Android 映象檔,只要將它放在 microSD 卡上就可以直接用了,將下載下來的壓縮檔解開,然後在該資料夾底下輸入 :
sudo./mkmmc-android.sh /dev/sdX MLO u-boot.img uImage uEnv_beagleboneblack.txt rootfs.tar.bz2
注意這行指令必須要在 Linux 環境下執行 ! 但本文要敘述在 mac osx mavericks 環境下 ( 利用 MacBook Air 3,2 內建之 SD Card Reader,但值得注意的是,目前 VirtualBox 尚不支援 USB 3.0,因此較新的 Macbook 可能不適用 ) 的製作流程 :
1. 安裝 VirtualBox,並在上面安裝 linux 作業系統,這裡使用的是 ubuntu server 13.10。
2. 安裝 VirtualBox Extension Pack。

3. 在 Settings --> Ports --> USB 底下將 Enable USB 2.0 (EHCI) Controller 打勾。
4. 新增 Empty Filter

5. 關閉 VirtualBox
5. 在 Terminal 底下輸入 :
sudo launchctl list | grep diskarbitrationd
6. 將前面顯示的數字 (如圖為 20) 代入至底下指令 :
sudo kill -SIGSTOP 20

7. 插入 SD 卡 !
8. 啟動 VirtualBox 之 Linux Virtual Machine
9. 點選底下 USB 圖示,然後選擇  Apple Internal Memory Card Reader。
若是安裝正確,在 /dev 底下會出多 sdb

10. 在 linux terminal 底下取得 這裡 提供的檔案 (可用 wget 指令),將檔案解壓縮後在該資料夾底下輸入 :
sudo./mkmmc-android.sh /dev/sdb MLO u-boot.img uImage uEnv_beagleboneblack.txt rootfs.tar.bz2

11. 恢復 mac osx sdcard reader 之運作,在 Terminal 底下輸入 :
sudo kill -SIGCONT 20

B. 將 sd card 安裝至 beaglebone black 然後按住 Boot 按鈕並插上電源後等待 android 字串出現到螢幕上為止。
C. 底下是 Android 4.2.2 畫面擷圖。

D. 系統資訊擷圖。


D. 底下是 Antutu 4.1.1 跑分結果。

E. 底下是 Antutu 4.1.1 顯示的系統資訊。



ref:http://www.ezequielaceto.com.ar/techblog/?page_id=910
ref:http://www.ezequielaceto.com.ar/techblog/?p=958
ref:http://superuser.com/questions/373463/how-to-access-an-sd-card-from-a-virtual-machine

2013-11-07

Cocos2d-x JNI 筆記

底下整理幾個常用的情況 :
  1. 新增 C++ 檔案
  2. 查看 Java Method Signature
  3. Java 呼叫 C++ 函數
  4. C++ 呼叫 Java 函數
  5. Java 呼叫 C++ 時傳遞數字變數
  6. C++ 呼叫 Java 時傳遞數字變數
  7. Java 呼叫 C++ 時傳遞字串
  8. C++ 呼叫 Java 時傳遞字串
  9. Java 呼叫 C++ 時傳遞矩陣
  10. C++ 呼叫 Java 時傳遞矩陣
  11. Java 呼叫 C++ 時傳遞回呼函數
  12. C++ 呼叫 Java 時傳遞回呼函數

1. C++ 新增 .h/.cpp 檔案 :
   i. 在 Classes 資料夾內新增 .h/.cpp 檔案
   ii. 編輯 proj.android/jni/Android.mk :
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := cocos2dcpp_shared

LOCAL_MODULE_FILENAME := libcocos2dcpp

#新增的檔案加在 LOCAL_SRC_FILES
LOCAL_SRC_FILES := hellocpp/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../Classes/HelloWorldScene.cpp \
                   ../../Classes/AMGamebarUtil.cpp

LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes

LOCAL_WHOLE_STATIC_LIBRARIES += cocos2dx_static
LOCAL_WHOLE_STATIC_LIBRARIES += cocosdenshion_static
LOCAL_WHOLE_STATIC_LIBRARIES += box2d_static
LOCAL_WHOLE_STATIC_LIBRARIES += chipmunk_static
LOCAL_WHOLE_STATIC_LIBRARIES += cocos_extension_static

include $(BUILD_SHARED_LIBRARY)

$(call import-module,cocos2dx)
$(call import-module,cocos2dx/platform/third_party/android/prebuilt/libcurl)
$(call import-module,CocosDenshion/android)
$(call import-module,extensions)
$(call import-module,external/Box2D)
$(call import-module,external/chipmunk)
2. 查看 Java Method Signature :
javap -s -classpath <path to jar file> <package name>.<class name>
javap -s -classpath ./AMGameBar.jar com.appmaster.AMCocos2dxInterface
javap -s com.appmaster.AMCocos2dxInterface (尚未壓縮成jar, 相對路徑 com/appmaster/AMCocos2dxInterface.class)
3. Java 呼叫 C++ 函數 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  //1.定義
  private static native void nativeLoginHandler();
  ...
  
  static public void callNative(){
    //2. 呼叫 c method
    nativeLoginHandler();
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  //c method 定義 :
  //Java_<package name (replace point with under line)>_<class>_<method>
  void Java_com_appmaster_AMCocos2dxInterface_nativeLoginHandler(JNIEnv* env, jobject thiz)
  {
    CCLOG("I HAVE BEEN TRIGGERED BY JAVA !");
  }

  ...
}
4. C++ 呼叫 Java 函數 :
   i. C++  檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "initGameBarLib", "()V");
  if (!methodid){
      return;
  }

  env->CallStaticObjectMethod(classid, methodid);
   ii. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  static public void initGameBarLib(){
    ...
  }
}

5. Java 呼叫 C++ 時傳遞數字變數 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  private static native void nativeMethod(int val);
  ...

  static public void run(){
    nativeMethod(9527);
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_ nativeMethod(JNIEnv* env, jobject thiz, jint val)
  {
    int nval =(int)val;
    CCLOG("val=%d", nval);

    ...

  }

  ...
}
6. C++ 呼叫 Java 時傳遞數字變數 :
   i. C++  檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "magicNumber", "(I)V");
  if (!methodid){
      return;
  }

  env->CallStaticObjectMethod(classid, methodid, (jint)9527);
   ii. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  static public void magicNumber(int val){
    ...
  }
}
7. Java 呼叫 C++ 時傳遞字串 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  private static native void nativeMethod(final string data);
  ...

  static public void run(){
    nativeMethod("hello C++");
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_ nativeMethod(JNIEnv* env, jobject thiz, jstring data)
  {
    const char* pData =env->GetStringUTFChars(data, 0);
    CCLOG("data=%s", pData);

    ...

    env->ReleaseStringUTFChars(data, pData);
  }

  ...
}
8. C++ 呼叫 Java 時傳遞字串 :
   i. C++  檔案如下 :
#include <jni/JniHelper.h>
#include <jni.h>

...

  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "initGameBarLib", "(Ljava/lang/String;Ljava/lang/String;)V");
  if (!methodid){
      return;
  }

  jstring arg0 =env->NewStringUTF(pArg0); //char* pArg0
  jstring arg1 =env->NewStringUTF(pArg1); //char* pArg1
  env->CallStaticObjectMethod(classid, methodid, arg0, arg1);
  env->DeleteLocalRef(arg0);
  env->DeleteLocalRef(arg1);
   ii. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  static public void initGameBarLib(String arg0, String arg1){
    ...
  }
}
9. Java 呼叫 C++ 時傳遞矩陣 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  private static native void nativeLoginHandler(int[] array);
  ...
  
  static public void run(){
    int[] array ={9, 5, 2, 7};
    nativeLoginHandler(array);
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_nativeLoginHandler(JNIEnv* env, jobject thiz, jintArray ptr)
  {
      jsize length =env->GetArrayLength(ptr);
      if (length>0){
          jint* intarr =env->GetIntArrayElements(ptr, 0);
          for (int i=0;i<length;++i){
              printf("val[%d] =%d", i, intarr[i]);
          }

          env->ReleaseIntArrayElements(ptr, intarr, 0); 
      }
  }

  ...
}
10. C++ 呼叫 Java 時傳遞矩陣 :
   i. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

  ...

  int* praw_int =new int[4];
  praw_int[0] =9;
  praw_int[1] =5;
  praw_int[2] =2;
  praw_int[3] =7;

  jintArray arrdata =env->NewIntArray(4);
  env->SetIntArrayRegion(arrdata, 0, 4, (jint*)praw_int);

  ...

  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "initGameBarLib", "([II)V");
  if (!methodid){
      return;
  }

  env->CallStaticObjectMethod(classid, methodid, arrdata, 4);

  delete [] praw_int;

}
   ii. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  
  static public void initGameBarLib(int[] data, int num){

    ...

  }

}
11. Java 呼叫 C++ 時傳遞回呼函數 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  //傳遞回呼函數名稱 (字串)
  private static native void nativeAsyncMethod(final string funcString);
  ...
  
  static public void callbackFunc(){
    //i will be callback from c++
  }

  static public void run(){
    nativeAsyncMethod("callbackFunc");
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_ nativeAsyncMethod(JNIEnv* env, jobject thiz, jstring funcString)
  {
    const char* pFuncString =env->GetStringUTFChars(funcString, 0);
    
    ...

    //callback
    JNIEnv *env  =NULL;
    jclass classid;
    jmethodID methodid;

    JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
    if (NULL == jvm) {
        env->ReleaseStringUTFChars(funcString, pFuncString);
        return;
    }

    //setup env
    jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
    if (ret !=JNI_OK) {
        env =NULL;
        env->ReleaseStringUTFChars(funcString, pFuncString);
        return;
    }

    classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
    if (!classid){
        env->ReleaseStringUTFChars(funcString, pFuncString);
        return;
    }

    methodid =env->GetStaticMethodID(classid, pFuncString, "()V");
    if (!methodid){
        env->ReleaseStringUTFChars(funcString, pFuncString);
        return;
    }

    env->CallStaticObjectMethod(classid, methodid);

    env->ReleaseStringUTFChars(funcString, pFuncString);
  }

  ...
}
12. C++ 呼叫 Java 時傳遞回呼函數 :
(ps. 底下的實作並未真正的把 C function pointer 傳遞到 java 環境)
   i. C++ (AMGamebarUtil.h)檔案如下 :
#include "cocos2d.h"

 ...

//定義回呼函數
typedef void (*onLoginCallback)(const cocos2d::CCString* pStrAccountID, const cocos2d::CCString* pStrNickname);

class AMGamebarUtil
{
public:
  static void initGameBarLib(onLoginCallback pCBFunc1);

}

 ...

   i. C++ (AMGamebarUtil.cpp)檔案如下 :
#include <jni/JniHelper.h>
#include <jni.h>
#include "AMGamebarUtil.h"

 ...

onLoginCallback pLC =NULL;

//預先建立 function 讓 java 回呼
extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_nativeLoginHandler(JNIEnv*  env, jobject thiz, jstring aid, jstring nickname)
  {
    const char* pAid =env->GetStringUTFChars(aid, 0);
    const char* pNick =env->GetStringUTFChars(nickname, 0);

    //若是回呼函數已指定則回呼之
    if (pLC !=NULL){
      pLC(
        cocos2d::CCString::createWithFormat("%s", pAid),
        cocos2d::CCString::createWithFormat("%s", pNick)
      );
    }

    env->ReleaseStringUTFChars(aid, pAid);
    env->ReleaseStringUTFChars(nickname, pNick);
  }
}

void AMGamebarUtil::initGameBarLib(onLoginCallback pCBFunc1)
{

  //呼叫 JAVA
  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      CCLOG("Unity - AMGamebarUtil.cpp, Failed to get JNIEnv. JniHelper::getJavaVM() is NULL");
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  if (env ==NULL){
    return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "initGameBarLib", "()V");
  if (!methodid){
      return;
  }

  //REGISTER CALLBACK FUNCTION
  pLC   =pCBFunc1;

  //CALLING METHOD
  env->CallStaticObjectMethod(classid, methodid);

}
   i. C++ (.cpp)執行如下 :
//實作回呼函數
void onLoginCallbackHandler(const cocos2d::CCString* pStrAccountID, const cocos2d::CCString* pStrNickname)
{
    CCLOG("onLoginCallbackHandler(), aid=%s, nickname=%s", pStrAccountID->getCString(), pStrNickname->getCString());
}

 ...

//實際呼叫 initGameBarLib 並傳遞回呼函數指標
AMGamebarUtil::initGameBarLib(&onLoginCallbackHandler);


   ii. JAVA 檔案如下 :

public class AMCocos2dxInterface
{
    //預先定義回呼之 c++ method
    private static native void nativeLoginHandler(final String aid, final String nickname);

    static public void initGameBarLib(){
       ...
       
       //回呼 c++ method
       nativeLoginHandler("THIS_IS_AID", "THIS_IS_NICKNAME");
    }
   
    ...
}

ref:http://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html
ref:http://stackoverflow.com/questions/1232236/retrieving-byte-array-of-unknow-length-from-java-store
ref:http://www.lyceemermoz.com/iris/jlp/util/java/jdk/graphic/graphic.cpp
ref:http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/types.html

2013-07-24

Create Android Live Wallpaper with Cocos2d-x

底下敘述使用 Cocos2d-x 製作 Android Live Wallpaper 的方法。(建構環境 : OSX ML,cocos2d-2.1rc0-x-2.1.3,Android NDK r8e,Android SDK r22。執行環境 : Sony Xperia TX with Android 4.1.2)

A. 程式架構 :
Cocos2d-x 本身提供一個便利的方法建立 Android Project ( 執行 Cocos2d-x 裡的 create-android-project.sh 即可 ),從自動建立的 Android Project 大概可以了解 App 模式之 Cocos2d-x 與 Android 作業系統間的關係,目前 Cocos2d-x 的程式架構在 Android 環境下,單一 Process 只能同時擁有及處理單一 GL Context 及 GLSurface,若要直接把 Cocos2dxRenderer 及 Cocos2dxGLSurfaceView 整合至 WallpaperService.Engine 內會衍生不少問題。因此這裡實作把它們整合在 WallpaperService。各別 Engine 在進行繪圖時 ( onDrawFrame ),只實作貼圖 ( Texture ) 的動作 ( 使用 Android 系統提供的 GL Function ),其中圖片的來源則是向 WallpaperService 的 Cocos2dxRenderer 取得 ( 此處的 Cocos2dxRenderer 是以 off-screen rendering 方式產生貼圖 )。

在不同的 GL Context 之間分享貼圖的方法,這裡選擇使用 EGL_KHR_gl_texture_2D_image Extension 的功能來實作,可避免因為使用 glReadPixles 及 glTexImage2D 造成效能低落的問題。

B. 實作程式 :

1. 建立 Android Project
從 cocos2d-x 網站下載檔案 cocos2d-2.1rc0-x-2.1.3.zip ,將壓縮檔解開並編輯檔案 create-android-project.sh,找到 :
# set environment paramters
NDK_ROOT_LOCAL="/home/laschweinski/android/android-ndk-r5"
ANDROID_SDK_ROOT_LOCAL="/home/laschweinski/android/android-sdk-linux_86"
將它們修改成自己電腦裡 NDK 及 Android SDK 的路徑,例如 :
# set environment paramters
NDK_ROOT_LOCAL="/Users/macaronics/Downloads/android-ndk-r8e"
ANDROID_SDK_ROOT_LOCAL="/Users/macaronics/android-sdks"
完成之後執行這個 sh 檔案,執行畫面如下 :
執行 create-android-project.sh

輸入 package path (com.macaronics.cclw) 之後,畫面會出現目前 Android SDK 已安裝的 API (依照各電腦情況不同),此範例選擇的是 id:1 (API Level 10),最後輸入專案名稱 (cclw),就完成了。

打開 eclipse 並匯入 cclw 這個專案,完成之後會發現幾個錯誤回報,那是因為找不到 cocos2dx package,此時匯入 cocos2dx 專案即可解決 ( cocos2dx 專案的路徑為 cocos2d-2.1rc0-x-2.1.3/cocos2dx/platform/android/java )。

匯入 cclw 及 cocos2dx 專案

本專案建立時選擇之 API Level 為 10,因此匯入完成之後檢查 AndroidManifest.xml 檔案,將 :
<uses-sdk android:minSdkVersion="8"/>
改為
<uses-sdk android:minSdkVersion="10"/>
到目前為止已經可以正常編譯並執行 cclw 專案,首先在資料夾 cocos2d-2.1rc0-x-2.1.3/cclw/proj.android 底下執行 build_native.sh,完成之後會看到專案裡的 lib 資料夾多出檔案 libgame.so,接著在 eclipse 點選 cclw 專案,並於功能表上選擇 Run --> Run As --> Android Application 即可實際編譯執行。此外也可直接在 Terminal 裡輸入 :
ant release -Dsdk.dir=/Users/macaronics/android-sdks/
編譯 apk 檔案 (/Users/macaronics/android-sdks/ 為 Android SDK 之路徑)。

2. 新增 PixelBuffer
如圖所示,在 cclw/src/com/macaronics/cclw 資料夾底下增加檔案 PixelBuffer.java :
新增檔案 PixelBuffer.java

PixelBuffer 的作用是向系統要求建立 GL Context 與 Surface Buffer ,它和 GLSurfaceView 的功能幾乎一樣,只是它所建立的 Surface Buffer 是 PBuffer (作為 off-screen rendering 使用),與 GLSurfaceView 建立的 Window Surface Buffer 不同。此段程式碼參考的來源在此,但 EGL Config Chooser 的部分參考 GLSurfaceView 做了一些修改,程式碼內容如下 :
public class PixelBuffer
{
    final static String TAG = "PixelBuffer";
    final static boolean LIST_CONFIGS = true;

    GLSurfaceView.Renderer mRenderer; // borrow this interface
    int mWidth, mHeight;
    Bitmap mBitmap;
        
    EGL10 mEGL; 
    EGLDisplay mEGLDisplay;
    EGLConfig[] mEGLConfigs;
    EGLConfig mEGLConfig;
    EGLContext mEGLContext;
    EGLSurface mEGLSurface;
    GL10 mGL;

    EGLConfigChooser mEGLConfigChooser;
    int mEGLContextClientVersion;
    
    String mThreadOwner;
    private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static int EGL_LARGEST_PBUFFER        = 0x3058;
    private static int EGL_OPENGL_ES2_BIT          = 4;


    public PixelBuffer(int width, int height) {
        mWidth = width;
        mHeight = height;
        mEGLContextClientVersion =2;
                
        int[] version = new int[2];
        int[] attribList = new int[] {
            EGL_WIDTH,              mWidth,
            EGL_HEIGHT,             mHeight,
            EGL_LARGEST_PBUFFER,    1,
            EGL_NONE
        };

        int [] context_attribList =new int[] {
            EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, 
            EGL_NONE
        };
                
        // No error checking performed, minimum required code to elucidate logic
        mEGL = (EGL10) EGLContext.getEGL();
        mEGLDisplay = mEGL.eglGetDisplay(EGL_DEFAULT_DISPLAY);
        mEGL.eglInitialize(mEGLDisplay, version);
        Log.i("cclw", "eglInitialized, version ="+version[0]+"."+version[1]);
        Log.i("cclw", "EGL Extension : "+mEGL.eglQueryString(mEGLDisplay, EGL_EXTENSIONS));

        mEGLConfigChooser =new SimpleEGLConfigChooser(8, 8, 8, 0, true);
        mEGLConfig =mEGLConfigChooser.chooseConfig(mEGL, mEGLDisplay);
        if (mEGLConfig ==null)
        {
            mEGLConfigChooser =new SimpleEGLConfigChooser(5, 8, 5, 0, true);
            mEGLConfig =mEGLConfigChooser.chooseConfig(mEGL, mEGLDisplay);
        }
        
        mEGLContext = mEGL.eglCreateContext(mEGLDisplay, mEGLConfig, EGL_NO_CONTEXT, context_attribList);
        mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, attribList);
        mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
        mGL = (GL10) mEGLContext.getGL();
        
        // Record thread owner of OpenGL context
        mThreadOwner = Thread.currentThread().getName();
        Log.i("cclw", "PixelBuffer created. mThreadOwner ="+mThreadOwner);

    }

    public void dispose()
    {
        if (mEGLDisplay !=EGL_NO_DISPLAY)
        {
            mEGL.eglMakeCurrent(mEGLDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
            if (mEGLContext !=EGL_NO_CONTEXT)
                mEGL.eglDestroyContext(mEGLDisplay, mEGLContext);

            if (mEGLSurface !=EGL_NO_SURFACE)
                mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface);
            mEGL.eglTerminate(mEGLDisplay);
        }
    }
    
    public void setRenderer(GLSurfaceView.Renderer renderer) {
        mRenderer = renderer;
        
        // Does this thread own the OpenGL context?
        if (!Thread.currentThread().getName().equals(mThreadOwner)) {
            Log.e(TAG, "setRenderer: This thread does not own the OpenGL context.");
            return;
        }
        
        // Call the renderer initialization routines
        mRenderer.onSurfaceCreated(mGL, mEGLConfig);
        mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
    }

    public void drawFrame()
    {
        makeCurrent();
        mRenderer.onDrawFrame(mGL);
    }

    public void makeCurrent()
    {
        // Do we have a renderer?
        if (mRenderer == null) {
            Log.e(TAG, "getBitmap: Renderer was not set.");
            return;
        }
        
        // Does this thread own the OpenGL context?
        if (!Thread.currentThread().getName().equals(mThreadOwner)) {
            Log.e(TAG, "getBitmap: This thread does not own the OpenGL context.");
            return;
        }
                
        // Call the renderer draw routine
        mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
    } 

    public interface EGLConfigChooser {
        EGLConfig chooseConfig(EGL10 egl, EGLDisplay display);
    }

    private abstract class BaseConfigChooser implements EGLConfigChooser {
        public BaseConfigChooser(int[] configSpec) {
            mConfigSpec =configSpec;
        }

        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
            int[] num_config = new int[1];
            if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config)) {
                throw new IllegalArgumentException("eglChooseConfig failed");
            }

            int numConfigs = num_config[0];

            if (numConfigs <= 0) {
                throw new IllegalArgumentException("No configs match configSpec");
            }

            EGLConfig[] configs = new EGLConfig[numConfigs];
            if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config)) {
                throw new IllegalArgumentException("eglChooseConfig#2 failed");
            }

            //list configs
            listConfig(configs);

            EGLConfig config = chooseConfig(egl, display, configs);
            if (config == null) {
                throw new IllegalArgumentException("No config chosen");
            }
            return config;
        }

        abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs);
        protected int[] mConfigSpec;
    }

    private class ComponentSizeChooser extends BaseConfigChooser {
        public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) {
            super(new int[] {
                    EGL10.EGL_RENDERABLE_TYPE,  EGL_OPENGL_ES2_BIT,
                    EGL10.EGL_SURFACE_TYPE,     EGL_WINDOW_BIT | EGL_PBUFFER_BIT,

                    EGL10.EGL_RED_SIZE,         redSize,
                    EGL10.EGL_GREEN_SIZE,       greenSize,
                    EGL10.EGL_BLUE_SIZE,        blueSize,
                    EGL10.EGL_ALPHA_SIZE,       alphaSize,
                    EGL10.EGL_DEPTH_SIZE,       depthSize,
                    EGL10.EGL_STENCIL_SIZE,     stencilSize,

                    EGL10.EGL_NONE});

            mValue = new int[1];
            mRedSize = redSize;
            mGreenSize = greenSize;
            mBlueSize = blueSize;
            mAlphaSize = alphaSize;
            mDepthSize = depthSize;
            mStencilSize = stencilSize;
        }

        @Override
        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
                EGLConfig[] configs) {
            for (EGLConfig config : configs) {
                int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0);
                int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0);
                if ((d >= mDepthSize) && (s >= mStencilSize)) {
                    int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0);
                    int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0);
                    int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0);
                    int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0);
                    if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize)) {
                        return config;
                    }
                }
            }
            return null;
        }

        private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) {

            if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
                return mValue[0];
            }
            return defaultValue;
        }

        private int[] mValue;
        // Subclasses can adjust these values:
        protected int mRedSize;
        protected int mGreenSize;
        protected int mBlueSize;
        protected int mAlphaSize;
        protected int mDepthSize;
        protected int mStencilSize;
        }

    private class SimpleEGLConfigChooser extends ComponentSizeChooser {
        public SimpleEGLConfigChooser(int red, int green, int blue, int alpha, boolean withDepthBuffer) {
            super(red, green, blue, alpha, withDepthBuffer ? 16 : 0, 0);
        }
    }    
    
    private void listConfig(EGLConfig[] tmpConfig) {
        Log.i("cclw", "Config List {");

        for (EGLConfig config : tmpConfig) {
            int d, s, r, g, b, a;
                    
            // Expand on this logic to dump other attributes        
            d = getConfigAttrib(config, EGL_DEPTH_SIZE);
            s = getConfigAttrib(config, EGL_STENCIL_SIZE);
            r = getConfigAttrib(config, EGL_RED_SIZE);
            g = getConfigAttrib(config, EGL_GREEN_SIZE);
            b = getConfigAttrib(config, EGL_BLUE_SIZE);
            a = getConfigAttrib(config, EGL_ALPHA_SIZE);
            Log.i("cclw", "    <d,s,r,g,b,a> = <" + d + "," + s + "," +  r + "," + g + "," + b + "," + a + ">");
        }

        Log.i("cclw", "}");
    }
        
    private int getConfigAttrib(EGLConfig config, int attribute) {
        int[] value = new int[1];
        return mEGL.eglGetConfigAttrib(mEGLDisplay, config,
                        attribute, value)? value[0] : 0;
    }
}
3. EGL_KHR_gl_texture_2D_image Extension 的部分 :
WallpaperService.Engine 命令 Cocos2d-x 以 off-screen rendering 的方式將圖案畫在 PBuffer 裡,然後將 PBuffer 裡的資料複製到自己的 GL Context,此處複製 PBuffer 資料的步驟使用 EGL_KHR_gl_texture_2D_image Extension 以 C 語言來實作,讓 WallpaperService.Engine 以 JNI 的方式呼叫。實作時,直接將函數寫在檔案 cclw/jni/hellocpp/main.cpp 內,底下列出修改後的結果 :
#include "AppDelegate.h"
#include "platform/android/jni/JniHelper.h"
#include <jni.h>
#include <android/log.h>

#include "cocos2d.h"
#include "HelloWorldScene.h"

#define EGL_EGLEXT_PROTOTYPES

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2ext.h>

#define  LOG_TAG    "main"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)

using namespace cocos2d;

extern "C"
{

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JniHelper::setJavaVM(vm);

    return JNI_VERSION_1_4;
}

void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv*  env, jobject thiz, jint w, jint h)
{
    if (!CCDirector::sharedDirector()->getOpenGLView())
    {
        CCEGLView *view = CCEGLView::sharedOpenGLView();
        view->setFrameSize(w, h);

        AppDelegate *pAppDelegate = new AppDelegate();
        CCApplication::sharedApplication()->run();
    }
    /*
    else
    {
        ccDrawInit();
        ccGLInvalidateStateCache();
        
        CCShaderCache::sharedShaderCache()->reloadDefaultShaders();
        CCTextureCache::reloadAllTextures();
        CCNotificationCenter::sharedNotificationCenter()->postNotification(EVNET_COME_TO_FOREGROUND, NULL);
        CCDirector::sharedDirector()->setGLDefaultValues(); 
    }
    */
}

EGLImageKHR eglImage =NULL;
EGLDisplay eglDisplay =NULL;

void Java_com_macaronics_cclw_cclwservice_nativeAttachEGLImageKHR(JNIEnv*  env, jobject thiz)
{
    if (eglImage !=NULL)
    {
        glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage);
    }
}

void Java_com_macaronics_cclw_cclwservice_nativeDestroyEGLImageKHR(JNIEnv*  env, jobject thiz)
{
    if (eglImage !=NULL && eglDisplay !=NULL)
    {
        bool ret =eglDestroyImageKHR(eglDisplay, eglImage);
        LOGD("cclw, nativeDestroyEGLImageKHR ret val =%d", ret);

        eglImage =NULL;
        eglDisplay =NULL;
    }
}

void Java_com_macaronics_cclw_cclwservice_nativeRenderToTexture(JNIEnv*  env, jobject thiz)
{
    int textureID =HelloWorld::renderToTexture();

    if (eglImage ==NULL)
    {
        LOGD("cclw, nativeRenderToTexture - Generate EGLImageKHR ...");
        EGLint imageAttributes[] = {
            EGL_GL_TEXTURE_LEVEL_KHR,   0, // mip map level to reference
            EGL_IMAGE_PRESERVED_KHR,    EGL_FALSE,
            EGL_NONE,                   EGL_NONE
        };

        eglDisplay =eglGetCurrentDisplay();
        EGLContext eglContext =eglGetCurrentContext();
        LOGD("cclw, eglDisplay=%d, eglContext=%d, textureID=%d", eglDisplay, eglContext, textureID);

        eglImage =
            eglCreateImageKHR(
                eglDisplay,
                eglContext,
                EGL_GL_TEXTURE_2D_KHR,
                reinterpret_cast<EGLClientBuffer>(textureID),
                imageAttributes
            );
    }
}

}
其中值得注意的是,並不是所有的 GPU 都支援 EGL_GL_TEXTURE_2D_KHR (例如: Adreno 205 GPU 不支援),此外,函數 :
HelloWorld::renderToTexture();
利用 Cocos2d-x 提供的 CCRenderTexture 物件將圖案畫在 Texture 裡,實作內容如下 (檔案 HelloWorldScene.cpp) :
CCRenderTexture* g_RT =NULL;
int HelloWorld::renderToTexture()
{
    if (g_RT==NULL)
    {
        g_RT =CCRenderTexture::create(720, 1184, kCCTexture2DPixelFormat_RGBA8888);
        g_RT->retain();
        g_RT->setPosition(ccp(720 / 2, 1184 / 2));
        CCLOG("cclw, create RenderTexture (id=%d)...", g_RT->getSprite()->getTexture()->getName());
    }

    g_RT->beginWithClear(0.0f, 0.0f, 0.0f, 1.0f);
    CCDirector::sharedDirector()->mainLoop();
    g_RT->end();

    return g_RT->getSprite()->getTexture()->getName();
}
其中的 720 是畫面寬度,1184 是畫面高度。

4. WallpaperService 的部分 :
新增檔案 cclwservice.java 於資料夾 cclw/src/com/macaronics/cclw 。檔案的程式碼如底下所示,其中 WallpaperService 主要分為 Engine、Renderer 及 Service (負責創造 Engine Instance 、 Cocos2d-x Renderer 執行緒及收拾殘局) :
public class cclwservice extends WallpaperService {
 
    public final static long NANOSECONDSPERSECOND = 1000000000L;
    public final static long NANOSECONDSPERMICROSECOND = 1000000;
 
    public static native void nativeAttachEGLImageKHR();
    public static native void nativeDestroyEGLImageKHR();
    public static native void nativeRenderToTexture();

    //----------------------
    // LIBRARY
    //
        static {
            System.loadLibrary("game");
        }

    //----------------------
    // VARIABLES
    //
        public static cclwservice inst =null;

        public static PixelBuffer pb =null;
        public static Handler ph =null;
        public static HandlerThread mThread;

        private Cocos2dxRenderer pc2r =null;

    //----------------------
    // MAIN FUNCTION
    //
        @Override
        public Engine onCreateEngine() {

            Log.i("cclw", "cclw - onCreateEngine");
            Engine retEng =null;
            retEng =new GLEngine();

            return retEng;
        }

        @Override
        public void onCreate(){
            Log.i("cclw", "cclw - onCreate");
            super.onCreate();
            inst =this;

            mThread =new HandlerThread("Rendering Thread");
            mThread.start();
            ph =new Handler(mThread.getLooper());
            ph.post(new Runnable(){
                @Override
                public void run(){
                    Log.i("cclw", "cclw - calling Cocos2dxHelper.init...");
                    Cocos2dxHelper.init(cclwservice.inst, null);

                    //prepare offscreen buffer
                    Log.i("cclw", "cclw - prepare offscreen buffer...");
                    if (pb ==null)
                        pb =new PixelBuffer(720, 1184);

                    Log.i("cclw", "cclw - create native renderer...");

                    pc2r =new Cocos2dxRenderer();
                    pc2r.setScreenWidthAndHeight(720, 1184);
                    pb.setRenderer(pc2r);
                }
            });
                    
        }

        @Override
        public void onDestroy(){
            Log.i("cclw", "cclw - onDestroy");
            super.onDestroy();

            //dispose handler
            ph =null;

            //dispose thread
            Log.i("cclw", "cclw - dispose Rendering Thread...");
            mThread.quit();

            cclwservice.nativeDestroyEGLImageKHR();

            Log.i("cclw", "cclw - dispose PixelBuffer");
            if (pb !=null){
                pb.dispose();
            }
            pb =null;

        }

        synchronized static public void renderToTexture(){
            pb.makeCurrent();
            cclwservice.nativeRenderToTexture();
        }

}
函數 renderToTexture 執行時,會透過 JNI 呼叫 cclw/jni/hellocpp/main.cpp 檔案裡的函數 Java_com_macaronics_cclw_cclwservice_nativeRenderToTexture 讓 Cocos2d-x 將圖案畫在 PixelBuffer 所建立之 PBuffer 的 Texture 裡,並將此 Texture 設定為 KHR_image。

底下為 Engine 部分的程式 ( Engine 負責建立自己的 Renderer、接收系統訊息,及乎如同一般的 Activity ) :
//----------------------
// GL Engine
//
private class GLEngine extends Engine
{

    //----------------------
    // VARIABLES
    //
        private WallpaperGLSurfaceView glSurfaceView;
        private boolean rendererHasBeenSet;

        private WallpaperGLRenderer glRenderer;
         
    //----------------------
    // FUNCTION
    //                 
        @Override
        public Bundle onCommand(String action, int x, int y, int z, Bundle extras, boolean resultRequested) {
            Log.i("cclw", "onCommand");
            return super.onCommand(action, x, y, z, extras, resultRequested);
        }

        @Override
        public void onCreate(SurfaceHolder surfaceHolder) {
            super.onCreate(surfaceHolder);

            setTouchEventsEnabled(true);

            Log.i("cclw", "onCreate");
            glSurfaceView = new WallpaperGLSurfaceView(cclwservice.this);

            setEGLContextClientVersion(2);
            glRenderer =new WallpaperGLRenderer();
            setRenderer(glRenderer);

        }

        @Override
        public void onDestroy() {
            super.onDestroy();

            glRenderer.onDestroy();
            glRenderer =null;

            Log.i("cclw", "onDestroy");
            glSurfaceView.onDestroy();
        }

        @Override
        public void onVisibilityChanged(boolean visible) {
            super.onVisibilityChanged(visible);
         
            Log.i("cclw", "onVisibilityChanged, visible ="+visible);
            if (rendererHasBeenSet) {
                if (visible) {
                    Log.i("cclw", "calling onResume...");
                    glSurfaceView.onResume();
                } else {
                    Log.i("cclw", "calling onPause...");
                    glSurfaceView.onPause();
                }
            }
        }

        @Override
        public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset)
        {
            super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);
            //Log.i("cclw", "onOffsetsChanged, xOffset="+xOffset+", yOffset="+yOffset+", xOffsetStep="+xOffsetStep+", yOffsetStep="+yOffsetStep+", xPixelOffset="+xPixelOffset+", yPixelOffset="+yPixelOffset);
        }

        protected void setRenderer(Renderer renderer){
            glSurfaceView.setRenderer(renderer);
            rendererHasBeenSet =true;
        }

        protected void setEGLContextClientVersion(int version) {
              glSurfaceView.setEGLContextClientVersion(version);
        }


    //------------------------
    // Custom SurfaceView for WallpaperService.engine
    //
        private class WallpaperGLSurfaceView extends GLSurfaceView {
         
            WallpaperGLSurfaceView(Context context) {
                super(context);
            }
         
            @Override
            public SurfaceHolder getHolder() {
                return getSurfaceHolder();
            }
        }

}
值得注意的是 WallpaperGLSurfaceView 類別裡的函數 getHolder,是透過 Engine 類別的函數 getSurfaceHolder 來取得 SurfaceHolder。此外,Engine 所建立的 Renderer 與 Cocos2d-x Renderer 無關,它擁有自己的 GL Context 及 Window Surface Buffer。

WallpaperGLRenderer 的部分 : (底下的程式如同一般的 GL 程式,使用 OpenGL ES 2.0 來顯示一張 Texture,只是這個 Texture 的資料是 PixelBuffer 的 PBuffer 的資料 )
//----------------------
// RENDERER
//
private class WallpaperGLRenderer implements GLSurfaceView.Renderer{

    private String TAG ="cclw";
    
    int[] mTextureNameWorkspace =new int[] {0};

    private final float[] mTriangleVerticesData = { -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f };
    private final float[] mTriangleNormalData ={0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f};
    private final float[] mTriangleTexCoordData ={0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f };
    private final float[] mTriangleTexCoordData2 ={1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f };

    private FloatBuffer mTriangleVertices;
    private FloatBuffer mTriangleNormal;
    private FloatBuffer mTriangleTexCoord;
    private FloatBuffer mTriangleTexCoord2;

    private final String mVertexShader = "attribute vec4 a_position;   \n"+
        "attribute vec3 a_normal;     \n"+
        "attribute vec2 a_texCoord;   \n"+
        "varying vec2 v_texCoord;     \n"+
        "varying vec3 v_normal;       \n"+
        "void main()                  \n"+
        "{                            \n"+
        "   gl_Position =a_position;  \n"+
        "   v_normal = a_normal;      \n"+
        "   v_texCoord = a_texCoord;  \n"+
        "}                            \n";

    private final String mFragmentShader = "precision mediump float; \n"+
                "varying vec2 v_texCoord;                            \n"+
                "varying vec3 v_normal;                              \n"+
                "uniform sampler2D s_texture;                        \n"+
                "void main()                                         \n"+
                "{                                                   \n"+
                "  gl_FragColor = texture2D( s_texture, v_texCoord );\n"+
                "}                                                   \n";
    
    private int mProgram;
    private int mvPositionHandle;
    private int mvNormalHandle;
    private int mvTexCoordHandle;
    private int mvSamplerHandle;

    private long mLastTickInNanoSeconds;
    public long aniInterval;

    public int mOrientation =0;

    public boolean onDestroyCalled =false;

    public WallpaperGLRenderer() {
        mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleVertices.put(mTriangleVerticesData).position(0);

        mTriangleNormal = ByteBuffer.allocateDirect(mTriangleNormalData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleNormal.put(mTriangleNormalData).position(0);

        mTriangleTexCoord = ByteBuffer.allocateDirect(mTriangleTexCoordData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleTexCoord.put(mTriangleTexCoordData).position(0);

        mTriangleTexCoord2 =ByteBuffer.allocateDirect(mTriangleTexCoordData2.length *4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleTexCoord2.put(mTriangleTexCoordData2).position(0);

        this.mLastTickInNanoSeconds =System.nanoTime();
        aniInterval =(long) (1.0 / 30 * NANOSECONDSPERSECOND);
    }

    public void onDestroy(){
        Log.i("cclw", "onDestroy called...");
        onDestroyCalled =true;
    }
    
    protected class WorkerRunnable implements Runnable{
        private final CountDownLatch doneSignal;
        WorkerRunnable(CountDownLatch doneSignal){
            this.doneSignal =doneSignal;
        }

        public void run(){
            try{
                cclwservice.renderToTexture();
                this.doneSignal.countDown();
            }catch(Exception ex){
                Log.i("cclw", "Error : Runnable return exception : "+ex);

            }                    
        }
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        try{
                final long nowInNanoSeconds = System.nanoTime();
                final long interval = nowInNanoSeconds - this.mLastTickInNanoSeconds;

                //------------------------
                // FETCH DATA
                //
                    EGL10 mEGL = (EGL10) EGLContext.getEGL();
                    EGLSurface mEGLSurface =mEGL.eglGetCurrentSurface(EGL10.EGL_DRAW);
                    EGLDisplay mEGLDisplay =mEGL.eglGetCurrentDisplay();
                    EGLContext mEGLContext =mEGL.eglGetCurrentContext();

                    CountDownLatch doneSignal =new CountDownLatch(1);
                    cclwservice.ph.post(new WorkerRunnable(doneSignal));

                    doneSignal.await();

                //------------------------
                // SETUP BASIC ENVIRONMENT
                //
                    mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
                    if (onDestroyCalled==true)
                    {
                        Log.i("cclw", "onDestroyCalled==true, ignore drawing...");
                        return;
                    }

                //------------------------
                // UPDATE TEXTURE
                //
                    if (mTextureNameWorkspace[0]==0){                                                            
                        //Load texture
                        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                        gl.glGenTextures(1, mTextureNameWorkspace, 0);
                        Log.i("cclw", "mTextureNameWorkspace[0]="+mTextureNameWorkspace[0]);

                    }

                    gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureNameWorkspace[0]);

                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);

                    cclwservice.nativeAttachEGLImageKHR();

                    gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);


                //------------------------
                // RENDER SCENE
                //
                    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                    GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
                    GLES20.glUseProgram(mProgram);

                    GLES20.glVertexAttribPointer(mvPositionHandle, 3, GLES20.GL_FLOAT, false, 0, mTriangleVertices);
                    GLES20.glVertexAttribPointer(mvNormalHandle, 3, GLES20.GL_FLOAT, false, 0, mTriangleNormal);

                    if (mOrientation ==1)
                        GLES20.glVertexAttribPointer(mvTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTriangleTexCoord);
                    else if(mOrientation ==0)
                        GLES20.glVertexAttribPointer(mvTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTriangleTexCoord2);

                    GLES20.glEnableVertexAttribArray(mvPositionHandle);
                    GLES20.glEnableVertexAttribArray(mvNormalHandle);
                    GLES20.glEnableVertexAttribArray(mvTexCoordHandle);

                    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureNameWorkspace[0]);
                    GLES20.glUniform1i(mvSamplerHandle, 0);

                    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);

                //------------------------
                // FPS Limitation
                //
                    final long val =(aniInterval - interval) / NANOSECONDSPERMICROSECOND;
                    if (val >0)
                    {
                        try{
                            Thread.sleep(val);
                        }catch(final Exception e){}
                    }

                    this.mLastTickInNanoSeconds = nowInNanoSeconds;

        }
        catch(Exception e){

        }
    }

    @Override
    public void onSurfaceChanged(GL10 arg0, int arg1, int arg2) { //arg1:width, arg2:height
        // TODO Auto-generated method stub
        Log.i("cclw", "WallpaperGLRenderer::onSurfaceChanged, arg1="+arg1+", arg2="+arg2);

        float myRatio =1184.0f/720.0f;
        mOrientation =0;
        if (arg2>=arg1)
        {
            myRatio =720.0f/1184.0f;
            mOrientation =1;
        }

        int targetHeight =arg2;
        int targetWidth =(int)((float)arg2*myRatio);

        if (targetWidth >arg1)
        {
            targetWidth =arg1;
            targetHeight =(int)((float)arg1/myRatio);
        }

        Log.i("cclw", "WallpaperGLRenderer::onSurfaceChanged, fit targetWidth="+targetWidth+", targetHeight="+targetHeight);
        GLES20.glViewport((int)((arg1-targetWidth)*0.5f), (int)((arg2-targetHeight)*0.5f), targetWidth, targetHeight);
    }

    @Override
    public void onSurfaceCreated(GL10 arg0, EGLConfig arg1) {
        // TODO Auto-generated method stub
        Log.i("cclw", "WallpaperGLRenderer::onSurfaceCreated");

        mProgram =createProgram(mVertexShader, mFragmentShader);
        if (mProgram ==0)
            return;

        mvPositionHandle =GLES20.glGetAttribLocation(mProgram, "a_position");
        if (mvPositionHandle ==-1)
            return;
        
        mvNormalHandle =GLES20.glGetAttribLocation(mProgram, "a_normal");
        mvTexCoordHandle =GLES20.glGetAttribLocation(mProgram, "a_texCoord");
        mvSamplerHandle =GLES20.glGetUniformLocation(mProgram, "s_texture");

        GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
    }

    ...

其中值得注意的是函數 onDrawFrame 被系統呼叫之後,會產生一個實體 WorkerRunnable ,讓負責 cocos2d-x 繪圖的執行緒去執行,目的是讓 cocos2d-x 更新 PBuffer 裡的資料,更新完成之後,onDrawFrame 才繼續執行貼圖的動作。

5. 設定 AndroidManifest.xml
為 WallpaperService 新增 service 標籤,如底下所示 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.macaronics.cclw"
      android:versionCode="1"
      android:versionName="1.0">

    <uses-sdk android:minSdkVersion="10"/>
    <uses-feature android:glEsVersion="0x00020000" />

    <application android:label="@string/app_name"
        android:icon="@drawable/icon">

        <activity android:name=".cclw"
                  android:label="@string/app_name"
                  android:screenOrientation="landscape"
                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
                  android:process="com.macaronics.cclw.cclw"
                  android:configChanges="orientation">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <service
            android:name=".cclwservice"
            android:label="@string/app_name"
            android:screenOrientation="portrait"
            android:permission="android.permission.BIND_WALLPAPER"
            android:process="com.macaronics.cclw.cclwservice">
            <intent-filter>
                <action android:name="android.service.wallpaper.WallpaperService" />
            </intent-filter>
            <meta-data android:name="android.service.wallpaper" android:resource="@xml/cclw_res" />
        </service>
                
    </application>
    <supports-screens android:largeScreens="true"
                      android:smallScreens="true"
                      android:anyDensity="true"
                      android:normalScreens="true"/>
</manifest> 
其中的 xml/cclw_res.xml 表示 Live Wallpaper 選單列表時所顯示之資訊,需要額外手動建立,步驟是首先建立資料夾 xml 於 cclw/res,然後建立檔案 cclw_res.xml,並設定內容如下 :
<?xml version="1.0" encoding="utf-8"?>
<wallpaper 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:thumbnail="@drawable/icon" 
/>

6. 設定 jni/Android.mk :
此時若執行 native_build.sh 會顯示一些錯誤,那是因為尚未設定連結 EGL Library,將 jni/Android.mk 修改如下即可正常編譯 :
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := game_shared

LOCAL_MODULE_FILENAME := libgame

LOCAL_SRC_FILES := hellocpp/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../Classes/HelloWorldScene.cpp
                   
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes                   

LOCAL_LDLIBS := -lEGL

LOCAL_WHOLE_STATIC_LIBRARIES := cocos2dx_static cocosdenshion_static cocos_extension_static
            
include $(BUILD_SHARED_LIBRARY)

$(call import-module,CocosDenshion/android) \
$(call import-module,cocos2dx) \
$(call import-module,extensions)

7. 執行結果 :
動態桌布選單多出了 cocos2d-x 建立的 cclw


將 cclw 設定成動態桌布

完整範例程式碼可以到 Git 下載: http://github.com/phardera/cocos2dx_android_livewallpaper

2013-06-27

BeagleBone on OSX Mountain Lion

依照官方提供的 Getting Started 步驟來執行的結果就是卡在 Step3,... 無法 Launch USB-to-Ethernet interface 因此沒辦法開啟 http://192.168.7.2 的網頁 (使用的 BeagleBone 版本是 A6a)。
底下是更詳細的步驟:
  1. 安裝 Driver : FTDI USB to serial。
  2. 將 BeagleBorad 接上 mac,此時在 Finder 會看到一個 BEAGLE_BONE 的 USB Disk,然後 Eject 這個 Disk。 
  3. 此時 System Preferences 的 Network 裡應該會自動多出 interface : RNDIS/Ethernet Gadget,如圖所示 :
    左方有 RNDIS/Ethernet Gadget

  4. 若沒發現 RNDIS/Ethernet Gadget 的話是正常的,因為我的 Mountain Lion 也沒有。
  5. 開啟 Terminal,輸入  screen `ls /dev/{tty.usb*B,beaglebone-serial}` 115200
  6. Terminal 全黑之後再按下 Enter,出現 Login 畫面,如圖所示 :
    登入頁面

  7. 輸入 root 後登入,再輸入 systemctl start network-gadget-init.service
  8. 完成!RNDIS/Ethernet Gadget 出現!
  9. 開啟瀏覽器並輸入 http://192.168.7.2
在 BeagleBone 內建的作業系統上編譯新版 node.js :
  1. 執行 opkg install python python-compiler python-modules python-distutils python-misc
  2. 執行 opkg install openssl openssl-dev
  3. 使用 wget 下載新版 nodejs 然後使用 tar xf 解壓縮之
  4. 修改檔案 deps/v8/SConstruct 內的 :
    'CCFLAGS' : ['$DIALECTFLAGS', '$WARNINGFLAGS'],
    
    'CCFLAGS' : ['$DIALECTFLAGS', '$WARNINGFLAGS', '-march=armv7-a'],
    
  5. 執行 export CC='gcc -march=armv7-a -mtune=cortex-a8'
  6. 執行 export CXX='g++ -march=armv7-a -mtune=cortex-a8'
  7. 執行 ./configure
  8. 執行 make
ps.
1. Eject USB Disk 的指令是 systemctl stop storage-gadget-init.service
2. 在 OSX 上設定好 Internet Sharing 之後鍵入 udhcpc -i usb0 則可透過 OSX 連結上 Internet
3. 資料參考來源 https://groups.google.com/forum/#!topic/beagleboard/gKQtFpdpB6k
4. 資料參考來源 http://brachestudios.com/2012/03/13/installing-the-latest-version-of-node-js-on-the-beaglebone/
5. 資料參考來源 http://fastr.github.io/articles/Node.js-on-OpenEmbedded.html

2013-04-08

Unity 3D + Facebook SDK for Android


底下是將 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) 了。

5. 建立一個 Unity Project,並將 facebooksdk.jar 及 res 資料夾複製到 Unity 專案的 Assets/Plugins/Android 資料夾底下。(ps. res 資料夾是指 facebook-android-sdk-3.0.1/facebook/res)

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/ 資料夾底下。直至目前為止,資料夾底下應該有這些檔案 :

11. 建立 fbWrapper.cs ,讓 fbWrapper class 更適用於 Unity 的 Component 架構之環境,程式碼如下 :
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 所提供,如圖所示 :

函數 getHashKey 所回傳的字串必須設定至 Native Android App 之 Key Hash 欄位 ,如圖所示 :

12. 底下是簡單的 fbWrapper 應用範例,建立一個  C# script 名稱為 test.cs 並設定成為 GameObject "Main Camera" 之 Component。
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)。

14. 當一切設定就緒之後 (記得設定 Player Settings 之 Other Settings 的 Bundle Identifier),嘗試 Build apk 檔案,Build 完成之後,在 Unity 專案資料夾下會多出一個 Temp 資料夾,在路徑 Temp/StagingArea/gen/com/macaronics/fbonandroid/ 找到檔案 R.java。

打開此檔案,將 package com.macaronics.fbonandroid; 改為 package com.facebook.android; 之後另存成 R.java 在別的資料夾。

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