목차
안드로이드를 위한 플러그인 만들기 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 }
헬퍼 클래스와 사용자 자바 플러그인 만들기
AndroidJNIHelper
와 AndroidJNI
는 JNI사용을 쉽게해 줍니다.
AndroidJavaObject
와 AndroidJavaClass
는 많은 것을 자동화해주고 캐쉬를 사용해서 자바 콜을 빠르게 해줍니다.
AndroidJavaObject
와 AndroidJavaClass
조합은 AndroidJNI
와 AndroidJNIHelper
위에서 만들어 지고 그 안에 많은 로직이 있습니다(자동화를 다루는). 이런 클래스틀은 자바 클래스의 정적 멤버를 다루기 위해 ‘static’ 버전으로도 있습니다.
사용자가 원하는 어떤 방법을 사용해도 되지만JNI 와 AndroidJNI
클래스 멤버 또는 AndroidJNIHelper
와 AndroidJNI
그리고 결국 최대 자동화와 편의를 위해 AndroidJavaObject/AndroidJavaClass
를 이용하세요.
- UnityEngine.AndroidJNI는 C에 있는 JNI콜을 위한 wrapper입니다. 이 클래스의 모든 멤버는 static이며 Java Native Interface와 일대일 관계를 갖습니다.
- UnityEngine.AndroidJNIHelper는 다음 레벨에서 사용 되는 헬퍼 기능을 제공하지만 아마 어떤 특별한 이유로 유용할 수 있기 때문에 public 함수로 사용 됩니다.
의 인스턴스는 각각 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
.Dispose()$$ 함수를 직접 부를수 있습니다. 실제 C# 객체는 좀더 오래 존재할 수있는데 mono에 의해 결국 가비지 콜렉트될 것입니다.
AndroidJavaObject
대신 AndroidJavaClass
로 시작하는데 왜냐하면 사용자가 com.unity3d.player.UnityPlayer
의 static멤버의 액세스를 원하며 새로운 객체 생성을 윈치 않습니다(이미 Android UnityPlayer
에 하나가 만들어져 있습니다). 그러면 static필드인 “currentActivity”에 액세스하지만 이번에는 generic파라미터로 AndroidJavaObject
를 사용합니다. 왜냐하면 실제 필드 타입 (
android.app.Activity)이 java.lang.Object 위 하위 클래스이기 때문이고 어떤
non-primitive type도 AndroidJavaObject
(이 룰에 스트링은 예외인데 자바에서 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을 없게 하기위해
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 라이브러리에 기반한 예제가 있습니다.
- 출처: 유니티코리아위키 (CC BY-NC-SA 2.0)