UPL
UPL 全稱 Unreal Plugin Language,是一個 XML-Based 的結構化語言,用於介入 UE 的打包過程(如拷貝 so / 編輯 AndroidManifest.xml,添加 IOS 的 framework / 操作 plist 等)。
簡單的說就是使用XML的格式往我們UE的安卓GameActivity.java 里添加代碼,給添加gradle構建指令等等
首先他有一個固定的頭尾部
<?xml version="1.0" encoding="utf-8"?>
<!--Unreal Plugin Example-->
<root xmlns:android="http://schemas.android.com/apk/res/android">
</root>
在 <root></root>
中可以使用 UPL 提供的節點來編寫邏輯(不過太麻煩了一般不用),以添加 AndroidManifest.xml
中權限請求為例(以下代碼均位於 <root></root>
中)
<androidManifestUpdates>
<addPermission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<addPermission android:name="android.permission.CAMERA"/>
</androidManifestUpdates>
UE調用安卓
常用xml節點
對於安卓來說,引擎里面有一份GameActivity.java.template的模板文件,這是用於生成安卓打包的類的固定模板,可以自己看看里面有些啥東西,UE也提供了一個可以替換這個模板的節點,這個節點的執行會先於其他的替換代碼的節點(建議只讓一份UPL代碼使用這個Replacement節點)
<gameActivityReplacement>
<log text="Use Customize GameActivity instead of UE's GameActivity.java.template" />
<setString result="Output" value="$S(PluginDir)/Android/GameActivity.java.template" /
</gameActivityReplacement>
可以通過給的GameActivity使用UPL里添加一些函數,比如,你想獲取機器型號,讓他返回一個java的string對象,比如你想打開相冊,它就會打開一個系統內置的activity然后等待這邊選擇一張圖片,異步的返回到這邊來GameActivity(這個數據還是在安卓層,后續還要通過java調用C++來將圖片數據傳遞到C++層)
這里展示一個簡單的返回一個普通字符串
<gameActivityClassAdditions>
<insert>
public String AndroidThunkJava_GetPackageName()
{
Context context = getApplicationContext();
return context.getPackageName();
}
</insert>
</gameActivityClassAdditions>
需要import包也有對應的節點
<gameActivityImportAdditions>
<insert>
import xxx;
</insert>
</gameActivityImportAdditions>
給特定的某個GameActivity的函數添加函數,這里給處理ActivityResult的的函數添加東西
<gameActivityOnActivityResultAdditions>
<insert>
switch (requestCode)
{
case CHOOSE_FROM_ALBUM_RESULT:
if(resultCode == RESULT_OK)
{
if(Build.VERSION.SDK_INT>=19)
handleImageOnKitKat(data);
else
handleImageBeforeKitKat(data);
}
default:
break;
}
</insert>>
</gameActivityOnActivityResultAdditions>
所有相關的添加代碼的節點可以在UnrealPluginLanguage.cs的注釋部分找到
/ * <!-- optional additions to the GameActivity imports in GameActivity.java -->
* <gameActivityImportAdditions> </gameActivityImportAdditions>
*
* <!-- optional additions to the GameActivity after imports in GameActivity.java -->
* <gameActivityPostImportAdditions> </gameActivityPostImportAdditions>
*
* <!-- optional additions to the GameActivity class implements in GameActivity.java (end each line with a comma) -->
* <gameActivityImplementsAdditions> </gameActivityImplementsAdditions>
*
* <!-- optional additions to the GameActivity class body in GameActivity.java -->
* <gameActivityClassAdditions> </gameActivityOnClassAdditions>
*
* <!-- optional additions to GameActivity onCreate metadata reading in GameActivity.java -->
* <gameActivityReadMetadata> </gameActivityReadMetadata>
*
* <!-- optional additions to GameActivity onCreate in GameActivity.java -->
* <gameActivityOnCreateAdditions> </gameActivityOnCreateAdditions>
*
* <!-- optional additions to GameActivity onDestroy in GameActivity.java -->
* <gameActivityOnDestroyAdditions> </gameActivityOnDestroyAdditions>
*
* <!-- optional additions to GameActivity onStart in GameActivity.java -->
* <gameActivityOnStartAdditions> </gameActivityOnStartAdditions>
*
* <!-- optional additions to GameActivity onStop in GameActivity.java -->
* <gameActivityOnStopAdditions> </gameActivityOnStopAdditions>
*
* <!-- optional additions to GameActivity onPause in GameActivity.java -->
* <gameActivityOnPauseAdditions> </gameActivityOnPauseAdditions>
*
* <!-- optional additions to GameActivity onResume in GameActivity.java -->
* <gameActivityOnResumeAdditions> </gameActivityOnResumeAdditions>
*
* <!-- optional additions to GameActivity onNewIntent in GameActivity.java -->
* <gameActivityOnNewIntentAdditions> </gameActivityOnNewIntentAdditions>
*
* <!-- optional additions to GameActivity onActivityResult in GameActivity.java -->
* <gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>
*
* <!-- optional libraries to load in GameActivity.java before libUE4.so -->
* <soLoadLibrary> </soLoadLibrary>*/
好了現在我們寫完了想要插入到GameActivity的java代碼了
使用這個代碼需要在一個Module里,這里推薦閱讀 UE4Module,推薦是使用一個插件來管理這邊所有的平台代碼
在Module的Build.cs文件添加。路徑自己拼,其實打包的時候就可以發現路徑是否正確,給到的提示還是比較明確的
// for Android
if (Target.Platform == UnrealTargetPlatform.Android)
{
PrivateDependencyModuleNames.Add("Launch");
AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(ModuleDirectory, "UPL/Android/FGame_Android_UPL.xml"));
Console.WriteLine("AndroidPlugin ModuleDirectory:"+ModuleDirectory);
}
// // f
JNI
在調用java之前還得了解C++調用java的方法 JNI
JNI 是什么?JNI 全稱 Java Native Interface,即 Java 原生接口。主要用來從 Java 調用其他語言代碼、其他語言來調用 Java 的代碼。
通過 C++ 去調用 Java,首先需要知道,所要調用的 Java 函數的簽名。簽名是描述一個函數的參數和返回值類型的信息。
以該函數為例:
public String AndroidThunkJava_GetPackageName(){ return ""; }
以這個函數為例,它不接受參數,返回一個 Java 的 String 值,那么它的簽名是什么呢?
()Ljava/lang/String;
JDK 提供的 javac
具有一個參數可以給 Java 代碼生成 C++ 的頭文件,用來方便 JNI 調用,其中就包含了簽名。
寫一個測試的 Java 代碼,用來生成 JNI 調用的.h:
public class GameActivity {
public static native String SingnatureTester();
}
生成命令:
javac -h . GameActivity.java
會在當前目錄下生成.class
和.h
文件,.h
中的內容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class GameActivity */
#ifndef _Included_GameActivity
#define _Included_GameActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: GameActivity
* Method: SingnatureTester
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_GameActivity_SingnatureTester
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
里面導出了 GameActivity
類成員 SingnatureTester
JNI 調用的符號信息,在注釋中包含了它的簽名 ()Ljava/lang/String;
。
Java_ue4game_GameActivity_SingnatureTester
是當前函數可以在 C/C++ 中實現的函數名,當我們在 C++ 中實現了這個名字的函數,在 Java 中調用到 GameActivity
的 SingnatureTester
時,就會調用到我們 C++ 中的實現。
可以由此來確定需要調用的java接口的簽名,當然這里也有一個簽名生成規則
它的簽名則是:
/*
* Class: GameActivity
* Method: SingnatureTester
* Signature: (IDLjava/lang/String;)Ljava/lang/String;
*/
經過上面的例子,其實就可以看出來 Java 函數的簽名規則:簽名包含兩部分 —— 參數、返回值。
其中,()
中的是參數的類型簽名,按照參數順序排列,()
后面的是返回值的類型簽名。
那么 Java 中的類型簽名規則是怎么樣的呢?可以依據下面的 Java 簽名對照表:JNI 調用簽名對照表。
Java 中的基礎類型和簽名對照表:
Java | Native | Signature |
---|---|---|
byte | jbyte | B |
char | jchar | C |
double | jdouble | D |
float | jfloat | F |
int | jint | I |
short | jshort | S |
long | jlong | J |
boolean | jboolean | Z |
void | void | V |
根據上面的規則,void EmptyFunc(int)
的簽名為 (I)V
。
非內置基礎類型的簽名規則為:
- 以
L
開頭 - 以
;
結尾 - 中間用
/
隔開包和類名
如 Java 中類類型:
- String:
Ljava/lang/String;
- Object:
Ljava/lang/Object;
給上面的例子加上 package 時候再測試下:
package ue4game;
public class GameActivity {
public static native String SingnatureTester(GameActivity activity);
}
則得到的簽名為:
/*
* Class: ue4game_GameActivity
* Method: SingnatureTester
* Signature: (Lue4game/GameActivity;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_ue4game_GameActivity_SingnatureTester
(JNIEnv *, jclass, jobject);
調用java代碼
注意基本所有安卓有關的代碼都需要定義了PLATFORM_ANDROID這個宏才能生效,避免PC使用的時候編譯錯誤,這里給他們都用#if #endif包裹,相關調用的位置也應該用這個包裹
頭文件
#if PLATFORM_ANDROID
#include "Android/AndroidJNI.h"
#include "Android/AndroidApplication.h"
#include "Android/AndroidJavaEnv.h"
#endif
接下來就是實際的UE4調用安卓的代碼
想要在 UE 中調用到它,首先要獲取它的 jmethodID
,需要通過函數所屬的類、函數名字,簽名三種信息來獲取:
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
jmethodID GetPackageNameMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetPackageName", "()Ljava/lang/String;", false);
}
因為我們的代碼是插入到 GameActivity
類中的,而 UE 對 GameActivity
做了封裝,所以可以通過 FJavaWrapper
來獲取,FJavaWrapper
定義位於 Runtime/Launch/Public/Android
。
得到的這個 methodID
,有點類似於 C++ 的成員函數指針,想要調用到它,需要通過某個對象來執行調用,UE 也做了封裝:
jstring JstringResult = (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis,GetPackageNameMethodID);
通過 CallObjectMethod
來在 GameActivity
的實例上調用 GetPackageNameMethodID
,得到的值是 java 中的對象,這個值還不能直接轉換為 UE 中的字符串使用,需要進行轉換的流程:
namespace FJavaHelperEx
{
FString FStringFromParam(JNIEnv* Env, jstring JavaString)
{
if (!Env || !JavaString || Env->IsSameObject(JavaString, NULL))
{
return {};
}
const auto chars = Env->GetStringUTFChars(JavaString, 0);
FString ReturnString(UTF8_TO_TCHAR(chars));
Env->ReleaseStringUTFChars(JavaString, chars);
return ReturnString;
}
FString FStringFromLocalRef(JNIEnv* Env, jstring JavaString)
{
FString ReturnString = FStringFromParam(Env, JavaString);
if (Env && JavaString)
{
Env->DeleteLocalRef(JavaString);
}
return ReturnString;
}
}
通過上面定義的 FJavaHelperEx::FStringFromLocalRef
可以把 jstring
轉換為 UE 的 FString:
FString FinalResult = FJavaHelperEx::FStringFromLocalRef(Env,JstringResult);
到這里,整個 JNI 調用的流程就結束了,能夠通過 C++ 去調用 Java 並獲取返回值了。
關於類型
語言之間互相調用一般只處理基礎的類型轉換,像String這樣的是UE自己有處理
上面的類型簽名也並沒有提到數組的轉換,數組的轉換在JNI的使用中可以找到一些,訪問基本類型數組和訪問引用類型數組又有所不同,這點在UE里一些java調用C++里可以看到很多案例比如GetIntArrayElements、GetFloatArrayElements(這種就是得到一個數組的頭部的指針,然后可以GetArrayLength得到長度)
推薦閱讀JNI數組數據處理
以及獲取String對象引的數組,這是UE官方寫的一個java調用C++的C++函數定義
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeSetConfigRulesVariables(JNIEnv* jenv, jobject thiz, jobjectArray KeyValuePairs)
{
int32 Count = jenv->GetArrayLength(KeyValuePairs);
int32 Index = 0;
while (Index < Count)
{
auto javaKey = FJavaHelper::FStringFromLocalRef(jenv, (jstring)(jenv->GetObjectArrayElement(KeyValuePairs, Index++)));
auto javaValue = FJavaHelper::FStringFromLocalRef(jenv, (jstring)(jenv->GetObjectArrayElement(KeyValuePairs, Index++)));
FAndroidMisc::ConfigRulesVariables.Add(javaKey, javaValue);
}
}
安卓調用UE
安卓調用UE非常的簡單,只需要在java層寫一個native函數的聲明,然后C++里寫上對應函數名的一個定義就可以了
比如我這里想講一個圖片的數據傳遞給C++層,我在java層添加一個函數聲明
public native void nativeSetImageByByteArray(byte[] bytes);
在C++我就需要一個對應的實現,這個實現其實好像寫在哪里都無所謂
#if PLATFORM_ANDROID
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeSetImageByByteArray
(JNIEnv* jenv, jobject thiz, jbyteArray j_array)
{
UE_LOG(LogTemp, Log, TEXT("Java_com_epicgames_ue4_GameActivity_nativeSetImageByByteArray"));
jbyte *c_array = jenv->GetByteArrayElements(j_array, 0);
int len_arr = jenv->GetArrayLength(j_array);
UE_LOG(LogTemp, Log, TEXT("nativeSetImageByByteArray byteArray Len = %d"),len_arr);
}
#endif
然后java在調到這個函數的時候就可以調用到對應的C++實現了
需要注意的點
UE有一個線程的概念,安卓這邊的調用實際上是在安卓線程,讓其直接去操作UE的一些類比如UMG會引發報錯,因為UE的UMG只能在gamethread進行使用,UE這邊也提供了AsyncTask函數來將一調用作為任務的形式添加到某個線程去做
采坑記錄
調用安卓打開相冊功能時遇到了包沖突的問題,UE自帶的GameActivity模板import了很多android.support的包,而我所需要的功能需要引用androidx的包,經我調查發現這兩者不能同時存在,於是索性將所有的android.support包換成新版的androidx。
直接改模板解決不了問題,因為除了GameActivity外還有很多其他的Activity同樣引入了android.support包
這里找到一個解決方案是使用gradle的指令將做一個字符串映射表,將對應的android.support和androidx對應起來進行替換,gradle構建指令也可通過使用UPL來進行插入
引入androidx包好像還需要這種依賴,我是將自己寫的安卓工程里的build.gradle里缺少什么就補充什么
<buildGradleAdditions>
<insert>dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
}
</insert>
</buildGradleAdditions>
映射表
<baseBuildGradleAdditions>
<insert>
allprojects {
def mappings = [
'android.support.annotation': 'androidx.annotation',
'android.arch.lifecycle': 'androidx.lifecycle',
'android.support.v4.content.FileProvider':'androidx.core.content.FileProvider',
'android.support.v4.app.NotificationManagerCompat':'androidx.core.app.NotificationManagerCompat',
'android.support.v4.app.NotificationCompat': 'androidx.core.app.NotificationCompat',
'android.support.v4.app.ActivityCompat': 'androidx.core.app.ActivityCompat',
'android.support.v4.content.ContextCompat': 'androidx.core.content.ContextCompat',
'android.support.v13.app.FragmentCompat': 'androidx.legacy.app.FragmentCompat',
'android.arch.lifecycle.Lifecycle': 'androidx.lifecycle.Lifecycle',
'android.arch.lifecycle.LifecycleObserver': 'androidx.lifecycle.LifecycleObserver',
'android.arch.lifecycle.OnLifecycleEvent': 'androidx.lifecycle.OnLifecycleEvent',
'android.arch.lifecycle.ProcessLifecycleOwner': 'androidx.lifecycle.ProcessLifecycleOwner',
]
beforeEvaluate { project ->
project.rootProject.projectDir.traverse(type: groovy.io.FileType.FILES, nameFilter: ~/.*\.java$/) { f ->
mappings.each { entry ->
if (f.getText('UTF-8').contains(entry.key)) {
println "Updating ${entry.key} to ${entry.value} in file ${f}"
ant.replace(file: f, token: entry.key, value: entry.value)
}
}
}
}
}
</insert>
</baseBuildGradleAdditions>
具體可以查看 強制開啟UE4的androidx
小技巧
UE構建apk的時候會先生成一個臨時的安卓工程在Intermediate\Android下,可以在打開工程看看里面的代碼插得正確與否來提高做插入代碼的效率
引用
https://docs.unrealengine.com/zh-CN/SharingAndReleasing/Mobile/UnrealPluginLanguage/index.html
https://imzlp.com/posts/27289/