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)

Playwright CLI Flags Tutorial

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • 0:56 --last-failed: Zero in on just the tests that failed in your previous run
  • 2:34 --only-changed: Test only the spec files you've modified in git
  • 4:27 --repeat-each: Run tests multiple times to catch flaky behavior before it reaches production
  • 5:15 --forbid-only: Prevent accidental test.only commits from breaking your CI pipeline
  • 5:51 --ui --headed --workers 1: Debug visually with browser windows and sequential test execution

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Click on any timestamp above to jump directly to that section in the tutorial!

Watch Full Video 📹️

AWS Security LIVE!

Hosted by security experts, AWS Security LIVE! showcases AWS Partners tackling real-world security challenges. Join live and get your security questions answered.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️