DEV Community

KOGA Mitsuhiro
KOGA Mitsuhiro

Posted on • Originally published at qiita.com

Unreal C++はAndroidでアクセスできるフォルダが制限されていてつらい(回避策あり)

はじめに

Unreal C++でAndroidの任意のパスにアクセスするのが色々面倒だった話です。

Victory Pluginの導入でつらい

Unreal C++からAndroid API呼び出しのつらさを減らしてみた」でPicturesフォルダのパスをBlueprintから取得できるようになったので、次はそのフォルダにある画像のファイルリストを取ろうとしました。
幸いにも、Victory Pluginに丁度よさげな関数があります。

VictoryPlugin.png

ですが、Windowsでは問題なく動くのにAndroid用にビルドするとエラーになってしまいます。
これは公式フォーラムのRama's Extra Blueprint Nodes for You as a Plugin, No C++ Required!に解決策があり、Build.csに設定を追加するとビルドが通るようになりました。
他のプラグインは追加の設定なしでもビルドできるのに何故…

// 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_release_UPL.xml")));

            // ここから追加
            PrivateIncludePaths.AddRange(new string[] { "CppTest/Plugins/VictoryPlugin/Source/VictoryBPLibrary/Private" });
            DynamicallyLoadedModuleNames.AddRange(new string[] { "VictoryBPLibrary" });
            // ここまで追加
        }
        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

FAndroidPlatformFile::PathToAndroidPaths()の罠

Victory Pluginを導入できたので次は適当にpngファイルの一覧を取ってみました。

JoyFileIOGetFiles.png

ですが、Joy File Get Files関数の結果、Return Valueはtrueなのにファイル名の配列は0件です。
さすがに原因が分からなくてソースを追ってみました。Android依存部分のFAndroidPlatformFile::IterateDirectory()でも問題を見付けられなかったので最終手段のデバッグログ有効化です。

diff --git a/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp b/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp
index 104d4e6..8b8e737 100644
--- a/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp
+++ b/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp
@@ -20,7 +20,7 @@

 DEFINE_LOG_CATEGORY_STATIC(LogAndroidFile, Log, All);

-#define LOG_ANDROID_FILE 0
+#define LOG_ANDROID_FILE 1

 #define LOG_ANDROID_FILE_MANIFEST 0

上記だけ変更したソースからビルドしたUE4からAndroidアプリをビルドしてadb logcatでログを確認します。

D UE4     : FAndroidPlatformFile::IterateDirectory('/storage/emulated/0/Pictures')
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('/storage/emulated/0/Pictures') => AndroidPath = '/storage/emulated/0/Pictures'
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('/storage/emulated/0/Pictures') => LocalPath = '/storage/emulated/0/UE4Game/CppTest/storage/emulated/0/Pictures'
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('/storage/emulated/0/Pictures') => AssetPath = '/storage/emulated/0/Pictures'
D UE4     : [2016.11.03-09.15.36:314][  0]LogBlueprintUserMessages: [M_Default_C_1] 0
  • Picturesフォルダのフルパス: /storage/emulated/0/Pictures
  • FAndroidPlatformFile::PathToAndroidPathsで変換されたPicturesフォルダのフルパス: /storage/emulated/0/UE4Game/CppTest/storage/emulated/0/Pictures

えー、と思いながら再度ソースを調べると絶対パスを指定してもゲーム用フォルダをルートとしたパスに変換されてしまうようです…何てこったい…

更に相対パス指定しても以下のように削除されてしまいます。抜け目ない…

while (AndroidPath.StartsWith(TEXT("../")))
{
    AndroidPath = AndroidPath.RightChop(3);
}

しかも、IAndroidPlatformFileクラスから継承してオーバーライドしたメンバー関数は必ずこの変換が行われるのでFPathsやFFileHelperなどのプラットフォームの違いを吸収するクラスに影響があります。

小手先の回避策

絶対パスも相対パスも駄目なことが分かりましたが、1パターンだけ抜け道がありました。
カレントディレクトリを基準にした相対パスです。具体的に以下のようになります。

  • 参照したいパス: /storage/emulated/0/Pictures
  • 引数で渡す相対パス: ./../../Pictures

これでログを確認してみると…

D UE4     : FAndroidPlatformFile::IterateDirectory('./../../Pictures')
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('./../../Pictures') => AndroidPath = './../../Pictures'
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('./../../Pictures') => LocalPath = '/storage/emulated/0/UE4Game/CppTest/./../../Pictures'
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('./../../Pictures') => AssetPath = './../../Pictures'
D UE4     : FAndroidPlatformFile::IterateDirectory('./../../Pictures').. LOCAL Visit: './../../Pictures/Screenshot_20160610-201604.png'
D UE4     : FAndroidPlatformFile::IterateDirectory('./../../Pictures').. LOCAL Visit: './../../Pictures/0.png'
D UE4     : [2016.11.03-09.15.36:315][  0]LogBlueprintUserMessages: [M_Default_C_1] 2

取れた!Picturesフォルダのpngファイルのパスを取れました!

つまり、/storage/emulated/0/UE4Game/CppTestを起点にして、カレントディレクトリからの相対パスを作ればよいのです。
そしてこの為に用意されているかのようなFPaths::MakePathRelativeToを使えば簡単に相対パスを作ることができます。
(元のソースはMyBlueprintFunctionLibrary.cppです)

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

#include "CppTest_release.h"
#include "MyBlueprintFunctionLibrary.h"

#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#endif

FString UMyBlueprintFunctionLibrary::PicturesPath;

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

#if PLATFORM_ANDROID
/*
 * AndroidFile.cppに定義されている /storage/emulated/0/UE4Game/CppTest を返す関数
 * AndroidFile.hに定義されていないので、externで宣言しています。
 */
extern const FString &GetFileBasePath();

/*
 * 参照したい絶対パスを渡すと相対パスに変換する
 */
bool AndroidRelativePath(FString InPath, FString& OutPath)
{
    auto Source = InPath;
    auto InRelativeTo = GetFileBasePath();
    auto Result = FPaths::MakePathRelativeTo(Source, *InRelativeTo);
    OutPath = FPaths::Combine(TEXT("."), *Source);

    return Result;
}

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

        // Picturesの絶対パス
        auto RawPicturesPath = FString(UTF8_TO_TCHAR(javaChars));

        /*
         * ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆
         * ここでPicturesの絶対パスを相対パスに変換する
         * ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆
         */
        AndroidRelativePath(RawPicturesPath, UMyBlueprintFunctionLibrary::PicturesPath);

        jenv->ReleaseStringUTFChars(picturesPath, javaChars);
    }
}
#endif

ただし、この方法はAndroidFile.cppの実装依存の抜け道なので、今後のバージョンアップで使えなくなる可能性があります。

まとめ

何故このように制限されているのか、GitHubのコミットログにも特に記載されていないので分かりませんが、セーブファイル等を保存する都合上、SDカードのパーミッションが必要でその他のファイルを保護する仕組みがないので簡単にアクセスできないようにしたのではないかと考えられます。

UE4でAndroidアプリを作るのって色々つらい。

Sentry blog image

The countdown to March 31 is on.

Make the switch from app center suck less with Sentry.

Read more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay