DEV Community

KOGA Mitsuhiro
KOGA Mitsuhiro

Posted on • Originally published at qiita.com

Unreal C++からAndroid API呼び出しのつらさを減らしてみた

はじめに

以前、UE4のC++からAndroid APIを呼んでみたでUnreal C++からAndroid APIを呼んでみて、Android NDKがつらい感じでした。ですが、UE4はAndroid側を拡張する手段をUnreal Plugin Language (以下、UPLと省略する)として用意しています。今回はそれを使って、Unreal C++からAndroid API呼び出しのつらさを軽減します。

JavaのコードをGameActivity.javaに追い出す

Android NDKを使ったときのつらさはFindClass()GetMethodID()で各IDを取得したり、メソッド呼び出し後にDeleteLocalRef()で参照を削除しなければならないことです。それならばJavaのコードはJava側に追い出してしまうのが自然でしょう。

以下ではUPLで、GameActivity.javaにPicturesパスを取得するメソッドを追加して、C++からそのメソッドを呼び出すまでの設定およびコードを解説します。

CppTest.Build.cs

UE4でC++プロジェクトを作成すると必ずBuild.csが作成されます。
この中でUPLのXMLを読み込みます。

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;
// ここから追加
using System.IO;
// ここまで追加

public class CppTest : ModuleRules
{
    public CppTest(TargetInfo Target)
    {
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

        PrivateDependencyModuleNames.AddRange(new string[] {  });

        // ここから追加
        if (Target.Platform == UnrealTargetPlatform.Android)
        {
            string ProjectPath = Utils.MakePathRelativeTo(ModuleDirectory, BuildConfiguration.RelativeEnginePath);
            AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(ProjectPath, "CppTest_UPL.xml")));
        }
        // ここまで追加
    }
}

CppTest_UPL.xml

このファイルでGameActivity.javaにPicturesフォルダのパスを取得するメソッドを追加しています。XMLの中のJavaのコードの断片を書くのが気持ち悪いですが我慢です。
詳しくはUnreal Plugin Language リファレンスを参照してください。

<?xml version="1.0" encoding="utf-8"?>

<root xmlns:android="http://schemas.android.com/apk/res/android">

  <init>
    <!-- APK作成時にログを出力する -->
    <log text="CppTest init"/>
  </init>

  <!-- APK作成時にログ出力を有効にする -->
  <trace enable="true"/>

  <gameActivityImportAdditions>
    <insert>
      import android.os.Environment;
    </insert>
  </gameActivityImportAdditions>

  <gameActivityClassAdditions>
    <insert>
      public String AndroidThunkJava_GetPicturesPath() {
          File f = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
          return f.getPath();
      }
    </insert>
  </gameActivityClassAdditions>

</root>

UMyBlueprintFunctionLibrary.h

ヘッダファイルはUE4のC++からAndroid APIを呼んでみたと同じです。

// 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();
};

UMyBlueprintFunctionLibrary.cpp

やっとC++の本体です。
以前はJava側の複数のクラスやメンバー変数を参照していましたが、今回は追加したメソッド1つだけになったので幾分か楽になりました。
Javaのインスタンスに対してメソッドを呼びたい場合はこれが限界だと思います。

// 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 GameActivity = FAndroidApplication::GetGameActivityThis();
        jmethodID getPicturesPathMethod = Env->GetStaticMethodID(GameCls, "AndroidThunkJava_GetPicturesPath", "()Ljava/lang/String;");

        jstring pathString = (jstring)Env->CallObjectMethod(GameActivity, getPicturesPathMethod, nullptr);
        Env->DeleteLocalRef(getPicturesPathMethod);

        const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
        result = FString(nativePathString);

        Env->ReleaseStringUTFChars(pathString, nativePathString);
        Env->DeleteLocalRef(pathString);
    }
    else
    {
#endif
        result = FString("");
#if PLATFORM_ANDROID
    }
#endif

    return result;
}

GameActivity.javaからネイティブメソッドを呼び出す

今度はゲーム起動後、変更されない値をC++側で受け取る場合を解説します。

CppTest.Build.cs

Source/CppTest/CppTest.Build.csは同じなので割愛します。

CppTest_UPL.xml

今度はネイティブメソッドnativeSetPicturesPath()を定義して、AndroidのonCreate()メソッドの中でPicturesフォルダのパスを渡します。

<?xml version="1.0" encoding="utf-8"?>

<root xmlns:android="http://schemas.android.com/apk/res/android">

  <init>
    <!-- APK作成時にログを出力する -->
    <log text="CppTest init"/>
  </init>

  <!-- APK作成時にログ出力を有効にする -->
  <trace enable="true"/>

  <gameActivityImportAdditions>
    <insert>
      import android.os.Environment;
    </insert>
  </gameActivityImportAdditions>

  <gameActivityClassAdditions>
    <insert>
      public native void nativeSetPicturesPath(String PicturesPath);
    </insert>
  </gameActivityClassAdditions>

  <gameActivityReadMetadataAdditions>
    <insert>
      File f = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
      nativeSetPicturesPath(f.getPath());
    </insert>
  </gameActivityReadMetadataAdditions>

</root>

UMyBlueprintFunctionLibrary.h

Javaのネイティブメソッドの引数でPicturesフォルダのパスが渡されるのでそれを保存するための変数PicturesPathを追加して、JNI関数を使わなくなったのでGetPicturesPathJNI関数を削除しています。

// 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:
    static FString PicturesPath;

    UFUNCTION(BlueprintPure, Category = "MyBPLibrary")
    static FString GetPicturesPath();
};

UMyBlueprintFunctionLibrary.cpp

C++の本体はPicturesPathのgetter関数とJavaで宣言したネイティブメソッドの実装だけになり、最初と比べると随分と短くなりました。

// 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::PicturesPath;

FString UMyBlueprintFunctionLibrary::GetPicturesPath()
{
    return UMyBlueprintFunctionLibrary::PicturesPath;
}

#if PLATFORM_ANDROID
extern "C"
{
    JNIEXPORT void JNICALL Java_com_epicgames_ue4_GameActivity_nativeSetPicturesPath(JNIEnv* jenv, jobject thiz, jstring picturesPath)
    {
        const char* javaChars = jenv->GetStringUTFChars(picturesPath, 0);

        UMyBlueprintFunctionLibrary::PicturesPath = FString(UTF8_TO_TCHAR(javaChars));

        //Release the string
        jenv->ReleaseStringUTFChars(picturesPath, javaChars);
    }
}
#endif

まとめ

UPLを使って、Android API呼び出しのつらさを軽減できました!
Android NDKにも色々と関数が用意されているのでわざわざAndroid APIを呼ぶ機会はほとんどないと思いますが、誰かの助けになればよいかと思います。

Top comments (0)