Action disabled: source

안드로이드를 위한 플러그인 만들기 Building Plugins for Android

이 페이지는 안드로이드를 위한 네이티브 코드 플러그인 Native Code Plugins들을 설명합니다.

안드로이드 플러그인 빌드하기 Building a plugin for Android

안드로이드 플러그인을 시작하려면 Android NDK가 필요합니다. 어떻게 공유 라이브러리를 만드는지에 익숙해 지세요.

플러그인을 만드는데 C++ (.cpp)를 사용한다면 name mangling issues를 피하기 위해 함수가 C linkage와 선언되야 합니다.

extern "C" {
  float FooPluginFunction ();
} 

C#에서 플러그인 사용하기

일단 공유 라이브러리를 만들면 그것을 Assets→Plugins→Android 폴더에 복사합니다. 유니티는 아래와 같은 함수를 정의할때 이름으로 그것을 찾을 것입니다:

[DllImport ("PluginName")]
private static extern float FooPluginFunction (); 

PluginName은 ‘lib’로 시작하거나 ‘.so’ 로 끝이나면 안되는 것을 명심하세요. 모든 기본 코드 함수를 추가적인 C#코드 레이어로 감싸기를 권합니다. 이 코드는 Application.platform 를 체크할 수 있고 실제 장치에서 실행될 때만 기본 함수를 부르며 에이터에서 실행될 때는 모의 값을 리턴합니다. 사용자는 플랫폼에 의존하는 코드 컴파일을 제어하기 위해

platform defines 를 사용할 수 있습니다.

배치

크로스 플랫폼 플러그인을 위해서는 플러그인 폴더가 몇가지 다른 플랫폼(즉libPlugin.so 는Android, Plugin.bundle 는 Mac 그리고 Plugin.dll 는 Windows)을 위한 플러그인을 포함합니다. 유니티가 자동으로 개발 플랫폼에 맞는 플러그인을 선택하고 플레이어에 포함합니다.

자바 플러그인 사용하기

안드로이드 플러그인 메카니즘은 또한 안드로이드 OS와 상호작용을 가능하게 하는데 자바가 사용되는 것을 허용합니다. 자바 코드는 C#에서 직접 불릴 수 없으으로 사용자가 C#와 자바사이에서 그 콜을 해석하기 위해 기본 플러그인을 작성 합니다.

안드로이드에서 자바 플러그인 빌드하기

자바 플러그인을 만드는 것에는 몇가지 방법이 있습니다. 공통점은 필러그인에 필요한 .class 파일들을 포함하는 .jar파일로 끝난다는 것입니다. 한가지 방법은 JDK 를 받는 것으로 시작해서 사용자 .java파일을 명령 라인에서 javac로 .class파일을 만들기 위해 컴파일 하고 그들을 jar 명령어 라인 툴을 이용해 .jar로 패기지화합니다. 다른 방법은 Eclipse IDE를 ADT 와 함께 사용하는 것입니다.

기본 코드에서 자바 플러그인 사용하기

일단 자바 플러그인(.jar)을 만들었으면 Assets→Plugins→Android 폴더로 복사해야 합니다. 유니티는 .class파일과 나머지 자바 코드를 패키지화하고 Java Native Interface (JNI) 라는 것으로 그것을 부릅니다. JNI는 두가지 방법으로 동작하는데; 자바에서 기본 코드 부르기 그리고 기본 코드에서 자바(또는 JavaVM)와 상호작용하기.

기본 코드에서 사용자 자바 코드를 찾기 위해서는 자바 VM에 액세스 해야합니다. 다행히 그것은 매우 쉬운데 사용자의 C(++)코드에 다음과 같은 함수를 추가합니다:

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  JNIEnv* jni_env = 0;
  vm->AttachCurrentThread(&jni_env, 0);
} 

이것이 C(++)에서 자바를 이용하기 전부 입니다. JNI에 대한 완전히 설명하는 것은 이 문서의 내용을 넘어선 것이지만 클래스 정의 찾기, 생성자 (<init>) 함수 해결하기 그리고 아래와 같이 새로운 객체 인스턴스 만들기 등을 포함합니다:

jobject createJavaObject(JNIEnv* jni_env) {
  jclass cls_JavaClass = jni_env->FindClass("com/your/java/Class");			// find class definition
  jmethodID mid_JavaClass = jni_env->GetMethodID (cls_JavaClass, "<init>",  "()V");		// find constructor method
  jobject obj_JavaClass = jni_env->NewObject(cls_JavaClass, mid_JavaClass);		// create object instance
  return jni_env->NewGlobalRef(obj_JavaClass);						// return object with a global reference
} 

헬퍼 클래스와 사용자 자바 플러그인 만들기

AndroidJNIHelperAndroidJNI 는 JNI사용을 쉽게해 줍니다.

AndroidJavaObjectAndroidJavaClass 는 많은 것을 자동화해주고 캐쉬를 사용해서 자바 콜을 빠르게 해줍니다.

AndroidJavaObjectAndroidJavaClass 조합은 AndroidJNIAndroidJNIHelper 위에서 만들어 지고 그 안에 많은 로직이 있습니다(자동화를 다루는). 이런 클래스틀은 자바 클래스의 정적 멤버를 다루기 위해 ‘static’ 버전으로도 있습니다.

사용자가 원하는 어떤 방법을 사용해도 되지만JNI 와 AndroidJNI 클래스 멤버 또는 AndroidJNIHelperAndroidJNI 그리고 결국 최대 자동화와 편의를 위해 AndroidJavaObject/AndroidJavaClass 를 이용하세요.

의 인스턴스는 각각 java.lang.Object 와java.lang.Class (또는 서브클래스)의 인스턴스와 일대일 매핑이 됩니다. 그들은 자바와 3가지 타입의 상호작용을 제공합니다:

  • 함수 콜하기
  • 필드 값 얻기
  • 필드 값 지정하기

Call은 두가지 타입이 있습니다: 'void' Call 타입과 non-void return Call 타입이 있습니다. void타입이 아닌 콜은 리턴 타입으로 generic 타입이 사용됩니다. Get/Set 은 항상generic 타입만을 취합니다.

Example 1

//The comments is what you would need to do if you use raw JNI
 AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string"); 
 // jni.FindClass("java.lang.String"); 
 // jni.GetMethodID(classID, "<init>", "(Ljava/lang/String;)V"); 
 // jni.NewStringUTF("some_string"); 
 // jni.NewObject(classID, methodID, javaString); 
 int hash = jo.Call<int>("hashCode"); 
 // jni.GetMethodID(classID, "hashCode", "()I"); 
 // jni.CallIntMethod(objectID, methodID);

여기서는 java.lang.String의 인스턴스를 만들고 있으며 hhttp://developer.android.com/reference/java/lang/String.html#String(java.lang.StringBuilder)string 초기화하고 그 스트링을 위해

hash value를 되찾습니다.

AndroidJavaObject 생성자는 적어도 하나의 파라미터를 취합니다: 인스턴스를 생성하고자 하는 클래스의 이름. 클래스 이름 뒤에 있는 것은 모두 객체의 생성자 콜을 위한 파라미터이며 이 경우 스트링은 "some_string". 그러면 Call 함수에서 왜 generic타입을 파라미터로 사용하는 지에 대한 이유인 int를 리턴하는 hashCode()를 사용합니다.

_주의:_ dotted notation 를 사용해서 중첩 자바 클래스를 인스턴스화 할 수 없습니다. 내부 클래스는 $ 분별자를 사용해야하며 이것은 점과 슬래스 형식에서 모두 작동할 것입니다. 그래서 LayoutParams 클래스가ViewGroup 클래스가 중첩되었을 때$$android.view.ViewGroup$LayoutParams 또는android/view/ViewGroup$LayoutParams 가 사용될 수 있습니다. ====Example 2==== 위 샘플 플러그인중 하나는 어떻게 현 프로그램을 위한 캐쉬 디렉토리를 얻을 수 있는지를 보여줍니다: <file csharp> AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); jni.FindClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity"); jni.GetStaticFieldID(classID, "Ljava/lang/Object;"); jni.GetStaticObjectField(classID, fieldID); jni.FindClass("java.lang.Object"); Debug.Log(jo.Call<AndroidJavaObject>("getCacheDir").Call<string>("getCanonicalPath")); jni.GetMethodID(classID, "getCacheDir", "()Ljava/io/File;"); or any baseclass thereof! jni.CallObjectMethod(objectID, methodID); jni.FindClass("java.io.File"); jni.GetMethodID(classID, "getCanonicalPath", "()Ljava/lang/String;"); jni.CallObjectMethod(objectID, methodID); jni.GetStringUTFChars(javaString); </file> 여기서는 of AndroidJavaObject 대신 AndroidJavaClass 로 시작하는데 왜냐하면 사용자가 com.unity3d.player.UnityPlayer의 static멤버의 액세스를 원하며 새로운 객체 생성을 윈치 않습니다(이미 Android UnityPlayer에 하나가 만들어져 있습니다). 그러면 static필드인 “currentActivity”에 액세스하지만 이번에는 generic파라미터로 AndroidJavaObject 를 사용합니다. 왜냐하면 실제 필드 타입 ( android.app.Activity)이 java.lang.Object 위 하위 클래스이기 때문이고 어떤 non-primitive typeAndroidJavaObject (이 룰에 스트링은 예외인데 자바에서 primitive타입이 아니라도 스트링이 직접 액세스 될수 있습니다)로 액세스 되어야 하기 때문입니다. 그런 후 캐쉬 디렉토리를 대표하는 File객체를 얻기위해 이제 getCacheDir()를 통해 간단히 Activity를 이동하고 스트링 표현을 얻기위해 getCanonicalPath()를 부르세요. 물론 요즘엔 캐쉬 디렉토리를 얻기위해 그럴 필요가 없습니다; 저희는 Application.temporaryCachePath, Application.persistentDataPath를 통해 프로그램의 캐쉬와 파일 디렉토리 액세스를 제공합니다. ====Example 3==== 마지막으로 UnitySendMessage를 사용해 어떻게 데이타를 자바에서 스크립트 코드로 보내는지에 대한 작은 트릭입닌다. <file csharp> using UnityEngine; public class NewBehaviourScript : MonoBehaviour { void Start () { JNIHelper.debug = true; using (JavaClass jc = new JavaClass("com.unity3d.player.UnityPlayer")) { jc.CallStatic("UnitySendMessage", "Main Camera", "JavaMessage", "whoowhoo"); } } void JavaMessage(string message) { Debug.Log("message from java: " + message); } } </file> 자바 클래스인 com.unity3d.player.UnityPlayer은 이제 static 함수인 UnitySendMessage를 가지며 이것은 iOS의 UnitySendMessage 와 같고 자바에서 스크립트 코드로 데이터를 보낼 때 쓸수 있습니다. 그러나 여기서 우리는 그것을 스크립트 코드에서 직접 부릅니다. 그것은 결국 자바쪽으로 메세지를 전달하며 자바쪽에서는 “JavaMessage”라는 함수가 있는 “Main Camera”이라는 이름의 객체에 메세지 전달을 위해 유니티에서 콜백 합니다. ====유니티에서 자바 플러그인을 사용하는 가장 좋은 방법들==== 이 섹션은 주로JNI, Java, 안드로이드 경험이 많지 않은 사람을 목표로 했기에 우리는AndroidJavaObject/AndroidJavaClass 접근이 유니티 자바 코드와 상호작용 한다고 가정합니다. 첫번째로 알아야 할것은 AndroidJavaObject / AndroidJavaClass에서의 모든작업은 비싸다는 것입니다(JNI접근 처럼). managed와 native/java사이의 변환을 최소화 하기를 권합니다. 이것은 성능과 복잡도 모두를 위한 것입니다. 기본적으로 자바 함수가 실제 모든 작업을 하게하고 AndroidJavaObject / AndroidJavaClass를 이용해 그 함수와의 대화를 통해 결과를 얻을 수 있습니다. JNI헬퍼 클래스로 가능한 많은 양을 캐쉬하려 한다는 것을 아는 것은 도움이 될 것입니다. <file csharp> The first time you call a Java function like AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string"); somewhat expensive int hash = jo.Call<int>("hashCode"); first time - expensive int hash = jo.Call<int>("hashCode"); second time - not as expensive as we already know the java method and can call it straight </file> 이것은 JIT와 같은 방법입니다: 그것을 처음에 콜할 때는 코드가 존재하지 않아 느릴 것입니다. 다음에는 빨라집니다. 다시 말해 모든 객체의 .Call /.Get / .Set마다 대가를 치뤄야하지만 처음으로 콜한 다음에는 대가가 적다는 말입니다. AndroidJavaClass / AndroidJavaObject를 생성하는 데도 대가가 따릅니다. Mono garbage collector는 AndroiJavaObject / AndroidJavaClass의 생성된 모든 인스턴스를 해제해야 하는데 그것을 using(){} 문에 두고 가능한 빨리 지워지도록 하기를 권합니다. 그렇지 않으면 그들이 언제 지워질지 장담할 수 없습니다. AndroidJNIHelper.debug = true; 를 설정하면 디버그 출력문이Garbage Collector의 행동도 보여줄 것입니다. <file csharp> Getting the system language with the safe approach void Start () { using (AndroidJavaClass cls = new AndroidJavaClass("java.util.Locale")) { using(AndroidJavaObject locale = cls.CallStatic<AndroidJavaObject>("getDefault")) { Debug.Log("current lang = " + locale.Call<string>("getDisplayLanguage")); } } } </file> 사용자는 또한 자바 객체lingering을 없게 하기위해 .Dispose()$$ 함수를 직접 부를수 있습니다. 실제 C# 객체는 좀더 오래 존재할 수있는데 mono에 의해 결국 가비지 콜렉트될 것입니다.

UnityPlayerActivity 자바 코드 확장하기

유니티 안드로이드는UnityPlayerActivity(안드로이드 유니티 플레이어 주요 자바 클래스, 유니티 iOS의AppController.mm와 비슷함)의 확장이 가능합니다.

UnityPlayerActivity (UnityPlayerActivity.java는 맥의

/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/src/com/unity3d/player 그리고 보통 C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\src\com\unity3d\player에서 찾을 수 있습니다)에서 유도된 새로운Activity 를 생성 함으로써 프로그램은 안드로이드 OS와 유니티 안드로이드의 어떤 또는 모든 기본 상호작용을 오버라이드할 수 있습니다.

그렇게 하기 위해서는 유니티 안드로이드에 있는 classes.jar를 찾습니다. 이것은 PlaybackEngines/AndroidPlayer/bin라고 불리는 설치 폴더(윈도우즈에서는 보통/Applications/Unity 그리고 맥에서는/Applications/Unity)에서 찾을 수 있습니다. 그러면 classes.jar 파일을 새로운 활동을 컴파일하는 classpath에 추가합니다. manifest이 어떤 활동이 시작되어야 하는지 나타내듯이 새로운 AndroidManifest.xml 의 생성 또한 필요합니다. 그 AndroidManifest.xml 또한Assets→Plugins→Android에 위치하여야 합니다.

그 새로운 활동은 _OverrideExample.java_처럼 보일 수 있습니다:

package com.company.product;
 
import com.unity3d.player.UnityPlayerActivity;
 
import android.os.Bundle;
import android.util.Log;
 
public class OverrideExample extends UnityPlayerActivity {
 
  protected void onCreate(Bundle savedInstanceState) {
 
    // call UnityPlayerActivity.onCreate()
    super.onCreate(savedInstanceState);
 
    // print debug message to logcat
    Log.d("OverrideActivity", "onCreate called!");
  }
 
  public void onBackPressed()
  {
    // instead of calling UnityPlayerActivity.onBackPressed() we just ignore the back button event
    // super.onBackPressed();
  }
} 

그리고 이것은 매치하는 _AndroidManifest.xml_ 을 보여줍니다:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.company.product">
  <application android:icon="@drawable/app_icon" android:label="@string/app_name">
	<activity android:name=".OverrideExample"
			  android:label="@string/app_name">
        <intent-filter>
			<action android:name="android.intent.action.MAIN" />
			<category android:name="android.intent.category.LAUNCHER" />
		</intent-filter>
	</activity>
  </application>
</manifest> 

유니티 플레이어 네이티브 활동 UnityPlayerNativeActivity

물론 사용자만의 UnityPlayerNativeActivity의 서브클래스를 만드는 것도 가능합니다. 이것은 UnityPlayerNativeActivity 를 서브클래스화하는 것과 비슷한 효과를 가질 것이나 더 개선된 인풋 대기 시간을 보여줄 것입니다. 다만 NativeActivity는 Gingerbread에서 도입되었기 때문에 다른 이전 기기들에서는 작동하지 않습니다. 터치/모션 이벤트들이 네이티브 코드에서 처리되기 때문에, 자바 뷰는 일반적으로 그 이벤트들을 보지 못할 것입니다. 하지만 유니티에는 forwarding 하는 기능이 잇어 이벤트들이 DalvikVM으로 보급 (propagate) 될 수 있게 해줍니다. 이 메카니즘을 사용하려면, manifest 파일을 다음과 같이 수정해야 합니다:-

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.company.product">
  <application android:icon="@drawable/app_icon" android:label="@string/app_name">
	<activity android:name=".OverrideExampleNative"
			  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="false" />
        <intent-filter>
			<action android:name="android.intent.action.MAIN" />
			<category android:name="android.intent.category.LAUNCHER" />
		</intent-filter>
	</activity>
  </application>
</manifest> 

Activity 요소와 두개의 추가적인 메타-데이타 요소안의 ".OverrideExampleNative" 속성을 눈여겨 보세요. 처음의 메타데이타는 유니티 라이브러리 _libunity.so_를 사용하기 위한 지시 사항이고 두번째는 이벤트들이 UnityPlayerNativeActivity의 사용자화된 서브클래스로 보내지는걸 가능하게 해줍니다.

예제

네이티브 플러그인 샘플 Native Plugin Sample

네이티브 코드 플러그인 사용의 간단한 예제는 여기서 찾을 수 있습니다.

이 샘플은 어떻게 C 코드가 유니티 안드로이드 어플리케이션에서 호출될 수 있는지 보여줍니다. 이 팩키지는 네이티브 플러그인이 계산한 두 값의 합을 보여주는 씬을 포함합니다. 한가지 알아두실 것은 플러그인 컴파일하려면 Android NDK가 필요합니다.

자바 플러그인 샘플 Java Plugin Sample

자바 코드의 예제는 여기서 찾을 수 있습니다.

이 샘플은 어떻게 자바 코드가 안드로이드 OS와 상호작용하기 위해 사용되고 C++ 이 어떻게 C#과 Java 를 연결하기 위하여 사용되는지 보여줍니다. 이 팩키지 안에 씬은 눌렀을때 안드로이드 OS가 정의한 어플리케이션 캐시 디렉토리를 잡는 버튼을 보여줍니다. 이 플러그인을 컴파일하려면 JDK 와Android NDK가 둘 다 필요합니다.

여기에 비슷하지만 네이티브 코드를 C#으로 Wrap 하기 위하여 미리 만들어진 JNI 라이브러리에 기반한 예제가 있습니다.

역링크