LoginSignup
9
10

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-10-05

はじめに

Unreal C++は機能が豊富でプラットフォーム間の差分もほとんど隠蔽されているのですが、どうしてもネイティブAPIが必要なときがあります。
そこでAndroid NDKを使ってAndroid APIを呼び出す方法をまとめてみました。
いくつか方法があるのですが、まずはベタにAndoird NDKを使う方法です。

前準備

Unreal Engine4のドキュメントの1.Android SDK をインストールするに従ってAndroid Worksをインストールします。

AndroidWorksのインストーラ
[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の詳しい使い方は最後の参考リンクをご覧ください。

MyBluePrintFunctionLibrary.h
// 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();
};
MyBluePrintFunctionLibrary.cpp
// 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変数に保持することができます。

MyBluePrintFunctionLibrary.h
// 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();
};
MyBluePrintFunctionLibrary.cpp
// 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の使い方を書きたいと思います。

参考リンク

9
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
10