はじめに
Unreal C++は機能が豊富でプラットフォーム間の差分もほとんど隠蔽されているのですが、どうしてもネイティブAPIが必要なときがあります。
そこでAndroid NDKを使ってAndroid APIを呼び出す方法をまとめてみました。
いくつか方法があるのですが、まずはベタにAndoird NDKを使う方法です。
前準備
Unreal Engine4のドキュメントの1.Android SDK をインストールするに従ってAndroid Worksをインストールします。
[ENGINE INSTALL LOCATION]\Engine\Extras\AndroidWorks\Win64\CodeWorksforAndroid-1R4-windows.exe
これを利用することでAndroid開発に必要なAndroid SDK, Java Development Kit, Ant Scripting Tool, Android NDKをまとめてインストールすることができます。
ここでNsight Tegra, Visual Studio EditionもインストールするとVisual Studioのプロジェクト構成が若干変化します。が、UE 4.13.1ではあまり違いはありません。
Android ゲームの開発のリファレンスによるとデバイス上でAndroidゲームをデバッグできるので必要に応じてインストールするとよいと思います。
呼びたいJavaのコード
以下のような外部ストレージのPicturesフォルダのパスを取得する処理をAndroid NDKで実装してみます。
import android.os.Environment;
import java.io.File;
public String getPicturesPath() {
File f = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
return f.getPath();
}
C++プロジェクトを作る
Picturesフォルダのパスは変化しないので関数ライブラリを作ります。
関数ライブラリの作り方はalweiさんが解説するUE4 C++コードをブループリントで使えるようにする(関数ライブラリー編)に全部載っていますので、
同じようにCppTestプロジェクトにBlueprintFunctionLibraryクラスを作ります。
GetPicturesPath()を実装する
まずはベタに実装してみます。
JNIの詳しい使い方は最後の参考リンクをご覧ください。
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"
/**
*
*/
UCLASS()
class CPPTEST_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "MyBPLibrary")
static FString GetPicturesPath();
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "CppTest.h"
#include "MyBlueprintFunctionLibrary.h"
#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#endif
FString UMyBlueprintFunctionLibrary::GetPicturesPath()
{
FString result;
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
if (nullptr != Env)
{
jclass EnvCls = Env->FindClass("android/os/Environment");
jfieldID DirectoryPicturesField = Env->GetStaticFieldID(EnvCls, "DIRECTORY_PICTURES", "Ljava/lang/String;");
jmethodID getExternalStoragePublicDirectoryMethod = Env->GetStaticMethodID(EnvCls, "getExternalStoragePublicDirectory", "(Ljava/lang/String;)Ljava/io/File;");
jstring DirectoryPictures = (jstring)Env->GetStaticObjectField(EnvCls, DirectoryPicturesField);
jobject externalStoragePublicDirectory = Env->CallStaticObjectMethod(EnvCls, getExternalStoragePublicDirectoryMethod, DirectoryPictures);
Env->DeleteLocalRef(DirectoryPictures);
Env->DeleteLocalRef(EnvCls);
jclass FileCls = Env->FindClass("java/io/File");
jmethodID getPathMethod = Env->GetMethodID(FileCls, "getPath", "()Ljava/lang/String;");
jstring pathString = (jstring)Env->CallObjectMethod(externalStoragePublicDirectory, getPathMethod, nullptr);
Env->DeleteLocalRef(externalStoragePublicDirectory);
Env->DeleteLocalRef(FileCls);
const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
result = FString(nativePathString);
Env->ReleaseStringUTFChars(pathString, nativePathString);
Env->DeleteLocalRef(pathString);
Env->DeleteLocalRef(externalStoragePublicPath);
}
else
{
#endif
result = FString("");
#if PLATFORM_ANDROID
}
#endif
return result;
}
ここではクラス、メソッド、フィールドを取り出すためにFindClass()、GetStaticFieldID()、GetStaticMethodID()を利用しています。
そしてjclass / jobject / jstringなどのオブジェクトはローカル参照が作成されるのですがデフォルトでは最大で16個までしか使えないので使い終わったらDeleteLocalRef()で削除しています。
ちょっと面倒すぎますね。。
しかもこのコードはJNIとしては駄目コードです。
FindClass()、GetStaticFieldID()、GetStaticMethodID()は内部でリフレクションするのでBlueprintのTickから呼び出すと負荷が大きくてアプリが落ちてしまいます。
JNI呼び出しを改良する
jclass / jfieldID / jmethodIDは一度特定してしまえば変わらないのでstatic変数などにキャッシュすることができます。
そしてNewGlobalRef()を使ってGCされないように保護するのがよくやる方法で、以下のソースが参考になりました。
他にもJNIの注意点がまとまったページを最後の参考リンクに記載しましたのでご覧ください。
[ENGINE INSTALL LOCATION]\Engine\Source\Runtime\Core\Private\Android\AndroidJavaMediaPlayer.cpp
[ENGINE INSTALL LOCATION]\Engine\Source\Runtime\Core\Private\Android\AndroidMisc.cpp
ですが、今回取得したいPicturesフォルダのパスは変化しないので次のように、JNIベタ呼び出し関数をprivateにしてしまい、publicな関数のローカルstatic変数に保持することができます。
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"
/**
*
*/
UCLASS()
class CPPTEST_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "MyBPLibrary")
static FString GetPicturesPath();
private:
static FString GetPicturesPathJNI();
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "CppTest.h"
#include "MyBlueprintFunctionLibrary.h"
#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#endif
FString UMyBlueprintFunctionLibrary::GetPicturesPath()
{
static FString PicturesPath = UMyBlueprintFunctionLibrary::GetPicturesPath();
return PicturesPath;
}
FString UMyBlueprintFunctionLibrary::GetPicturesPathJNI()
{
FString result;
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
if (nullptr != Env)
{
jclass EnvCls = Env->FindClass("android/os/Environment");
jfieldID DirectoryPicturesField = Env->GetStaticFieldID(EnvCls, "DIRECTORY_PICTURES", "Ljava/lang/String;");
jmethodID getExternalStoragePublicDirectoryMethod = Env->GetStaticMethodID(EnvCls, "getExternalStoragePublicDirectory", "(Ljava/lang/String;)Ljava/io/File;");
jstring DirectoryPictures = (jstring)Env->GetStaticObjectField(EnvCls, DirectoryPicturesField);
jobject externalStoragePublicDirectory = Env->CallStaticObjectMethod(EnvCls, getExternalStoragePublicDirectoryMethod, DirectoryPictures);
Env->DeleteLocalRef(DirectoryPictures);
Env->DeleteLocalRef(EnvCls);
jclass FileCls = Env->FindClass("java/io/File");
jmethodID getPathMethod = Env->GetMethodID(FileCls, "getPath", "()Ljava/lang/String;");
jstring pathString = (jstring)Env->CallObjectMethod(externalStoragePublicDirectory, getPathMethod, nullptr);
Env->DeleteLocalRef(externalStoragePublicDirectory);
Env->DeleteLocalRef(FileCls);
const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
result = FString(nativePathString);
Env->ReleaseStringUTFChars(pathString, nativePathString);
Env->DeleteLocalRef(pathString);
Env->DeleteLocalRef(externalStoragePublicPath);
}
else
{
#endif
result = FString("");
#if PLATFORM_ANDROID
}
#endif
return result;
}
まとめ
Javaではたった数行だったコードがNDKではかなり膨らんでしまい気軽に利用できるものではありません。
ですがちょっと待ってください。
UE4.10から追加された新しいAndroid plugin systemを使えばAndroid側のActivityにコードを追加することができるので本質的ではない部分のコードを大幅に減らす事ができます。
次はAndroid plugin systemの使い方を書きたいと思います。
参考リンク
- Unreal Engine4のドキュメント
- Android NDKの入門、注意点
- Android NDKを利用したプラグインプロジェクト