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的时候会先生成一个临时的安卓工程在IntermediateAndroid下,可以在打开工程看看里面的代码插得正确与否来提高做插入代码的效率
引用
https://docs.unrealengine.com/zh-CN/SharingAndReleasing/Mobile/UnrealPluginLanguage/index.html
https://imzlp.com/posts/27289/