DEV Community

KOGA Mitsuhiro
KOGA Mitsuhiro

Posted on • Originally published at qiita.com

UE4のC++からAndroid APIを呼んでみた

はじめに

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の使い方を書きたいと思います。

参考リンク

Top comments (0)