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

参考リンク

Heroku

Build AI apps faster with Heroku.

Heroku makes it easy to build with AI, without the complexity of managing your own AI services. Access leading AI models and build faster with Managed Inference and Agents, and extend your AI with MCP.

Get Started

Top comments (0)

Android Malware: How It Works and How to Protect Your App Against It

Android Malware: How It Works and How to Protect Your App Against It

Your Android app is on the front line and malware is getting smarter using repackaging, dynamic instrumentation, and code injection. Learn how obfuscation and RASP deter attacks, protect code, and secure in-app defenses with real time insights.

Watch now

👋 Kindness is contagious

Explore this practical breakdown on DEV’s open platform, where developers from every background come together to push boundaries. No matter your experience, your viewpoint enriches the conversation.

Dropping a simple “thank you” or question in the comments goes a long way in supporting authors—your feedback helps ideas evolve.

At DEV, shared discovery drives progress and builds lasting bonds. If this post resonated, a quick nod of appreciation can make all the difference.

Okay