Unreal Engine 4 SDK构建相关笔记

SDK构建相关阅读

[toc]

在Unreal项目开发的过程中,可能会用到第三方开发的库来集成一些第三方的功能,例如语音通话、播放视频等功能。虚幻引擎本身也引用了很多第三方的库,例如物理库PhysX、Steam平台相关的库SteamWorksGoogleVR等等。这些库可以在引擎的Engine\Source\ThirdParty目录下找到。Unreal官方推荐将第三方库集成到插件目录中,这样可以将游戏代码与第三方代码进行隔离。

使用插件引入第三方库

在Unreal的编辑器中,可以新建一个ThirdParty插件来引入SDK:在项目中点击编辑-插件-新插件-第三方库就可以创建一个使用已包含第三方库的插件。

1
1. 貌似只有C++项目中才可以创建第三方库插件,蓝图项目中貌似只能建立空白插件

新插件的目录结构如下:

其中SDKTest是插件本身逻辑的目录,ThirdParty是第三方库的目录,对于不同平台,所使用的SDK文件格式也有不同,这一点我们在后面再详细展开讲。

  • .uplugin文件是插件描述文件,虚幻引擎通过搜索这个格式的文件来查找插件,这个文件提供插件相关的基本信息。

  • XXX.Build.cs文件是模块描述文件,虚幻引擎使用UnrealBuildTool编译这个文件,并构造来确定整个编译环境。

  • Mac,x64文件夹内是不同平台下引入的库文件,x64平台下一般为.lib.dll文件,Mac平台下一般为dylib,IOS平台下一般为.a文件、.dylib文件和.framework文件,Android平台下一般为.aar.jar文件。这些文件在模块描述文件中进行引入,在实例项目中如图所示:

一个例子:

以GCloud的相关SDK接入为例(GCloud相关文档):

(由于没有SDK的下载权限所以只能用官网的图来意思意思)

下载之后将文件夹拷贝到插件的Public目录下,并参考AAInfo4Copy.txt,在xxx.Build.cs文件中引入相应的插件。

在官方文档中引入插件的方法是:

1
2
3
4
5
PrivateDependencyModuleNames.AddRange(new string[] { "GCloudCore" });
PrivateDependencyModuleNames.AddRange(new string[] { "GCloud" });
PrivateDependencyModuleNames.AddRange(new string[] { "TApm" });
PrivateDependencyModuleNames.AddRange(new string[] { "GVoice" });
PrivateDependencyModuleNames.AddRange(new string[] { "GEM_OneSDK" });

这部分的代码原理可以参考Engine\Source\Programs\UnrealBuildTool\Configuration\ModuleRules.cs文件中的相关定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 通过将插件中的Public文件夹下的相对路径添加进这个List中来暴露给其他模块
public List<string> PublicIncludePaths = new List<string>();
// 同理,如果不想暴露给其他模块,则将相对路径添加到这个List中
public List<string> PrivateIncludePaths = new List<string>();

// 公共依赖模块名称列表(不需要路径)(自动进行私有/公共包含)。这些模块是我们的公共源文件所需要的。
public List<string> PublicDependencyModuleNames = new List<string>();
// 私有依赖模块名称列表。 这些模块是我们的私有代码所依赖的模块,但在公共的include文件中没有任何模块可以依赖。
public List<string> PrivateDependencyModuleNames = new List<string>();

// 附加库列表(包括拓展名在内的.lib文件名),通常用于第三方模块
public List<string> PublicAdditionalLibraries = new List<string>();

//...除此之外还有很多列表,可以在上述路径中文件内查看源码和注释

// ReceiptProperty在\Engine\Source\Programs\UnrealBuildTool\System\TargetReceipt.cs中定义
// 任意属性名/值,来自构建脚本的元数据可以传递给下游任务。(这是一对键值对),在文件中存储着UPL格式写的XML文件,其中可以进行一些设置,并且可以插入代码到AndroidManifest.xml和GameActivity.java中,关于这两个文件的作用在后面会解释
public List<ReceiptProperty> AdditionalPropertiesForReceipt = new List<ReceiptProperty>();

总而言之,在xxx.Build.cs文件中,将对应的第三方库路径和设置文件的路径加入到了全局的List中,在之后的编译过程中,UBT会根据这个文件将对应的第三方库文件编译构建到指定的位置。

虚幻插件语言(Unreal Plugin Language, UPL)

虚幻插件语言 (UPL)是一种基于XML的简单语言,用于操作XML和返回字符串。它包含一个分段,在计算任何其他分段之前,每个架构计算一次这个分段。状态被维护并推进到下个分段进行评估,因此分段的执行顺序很重要。虽然UPL是一个用于修改和查询XML的通用系统,但它专门用于允许插件影响其所属包的全局配置。例如,它允许插件修改Android APK AndroidManfiest.xml文件或IOS IPA plist文件。UBT还将查询插件的UPL xml文件,查找文件中要包含的字符串(对于包来说必须是常见的字符串),例如Android上的一些.java文件。

首先贴一个相关的文档在这里:https://docs.unrealengine.com/zh-CN/SharingAndReleasing/Mobile/UnrealPluginLanguage/index.html

在第三方库的引入过程中,可能会涉及到一些平台相关的功能,比如说权限获取等,这部分的设置文件,不同平台下有不同的规定,以Android和ios为例,在安卓平台下,使用应用清单概览文件对组件和权限进行控制,即AndroidManifest.xml文件;而在IOS下,使用Property List文件来存储用户设置。

对于Android平台,可能需要对AndroidManifest.xml文件进行一些修改,这时候就需要使用UPL来对该文件进行改动。

一个例子

以GCloudSDK中的MSDK为例:

这里的一些参数本质上是Android的一些参数,如果详细了解请阅读:Android开发者指南(有一说一Google这个文档写的是真滴好,什么是国际大厂啊)

以其中几个为例:

  • android:name : 实现 Activity 的类的名称,是 Activity 的子类。此属性值应为完全限定类名称(例如,“com.example.project.ExtracurricularActivity”)。不过,为简便起见,如果名称的第一个字符是句点(例如,“.ExtracurricularActivity”),则名称将追加至 <manifest> 元素中指定的软件包名称。
  • android:screenOrientation : Activity 在设备上的显示方向。如果 Activity 是在多窗口模式下运行,则系统会忽略该属性。

对GameActivity.java的拓展:

  1. 什么是GameActivity.java:

    在网上搜索了一下,大多数都是讲述这个文件是如何通过UPL插入修改的,没有描述这个文件本身是什么作用的。这个文件可以在\Engine\Build\Android\Java\src\com\epicgames\ue4\GameActivity.java下找到,在开头的注释中可以看到:

    Extending NativeActivity so that this Java class is instantiated from the beginning of the program. This will allow the user to instantiate other Java libraries from here, that the user can then use the functions from C++

    扩展NativeActivity,使这个Java类从程序开始就被实例化。 这将允许用户从这里实例化其他Java库,然后用户可以使用C++中的函数。

    NOTE — This class is not necessary for the UnrealEngine C++ code to startup, as this is handled through the base NativeActivity class. This class’s functionality is to provide a way to instantiate other Java libraries at the startup of the program and store references to them in this class.

    注意 — 这个类对于UnrealEngine C++代码的启动是不必要的,因为这是由基础NativeActivity类处理的。这个类的功能是提供一种方法,在程序启动时实例化其他Java库,并在这个类中存储对它们的引用。

    那么,在Android的文档中找到了关于NativeActivity类的描述,而GameActivity类继承自这个类。在文档内对NativeActivity的描述如下:

    Convenience for implementing an activity that will be implemented purely in native code. That is, a game (or game-like thing). There is no need to derive from this class; you can simply declare it in your manifest, and use the NDK APIs from there.

    方便实现一个将纯用本地代码实现的活动。也就是说,一个游戏(或类似游戏的东西)。不需要从这个类派生出来;你可以简单地在你的manifest中声明它,然后从那里使用NDK API。

    继续向上追溯,看看Acitvity类意在负责什么事:

    An activity is a single, focused thing that the user can do. Almost all activities interact with the user, so the Activity class takes care of creating a window for you in which you can place your UI with setContentView(View). While activities are often presented to the user as full-screen windows, they can also be used in other ways: as floating windows (via a theme with R.attr.windowIsFloating set), Multi-Window mode or embedded into other windows. There are two methods almost all subclasses of Activity will implement:

    一个Activity是用户可以做的独立的事情。几乎所有的Activity都可以与用户产生交互,因此Activity类负责为你创建一个窗口,你可以通过setContentView(View)来放置你的UI。虽然Activity通常以全屏窗口的方式呈现给用户,但是它们同样可以以其他的方式进行展示:例如浮动窗口(通过一个主题设置R.attr.windowIsFloating实现),多窗口模式或者嵌入到其他窗口中。几乎所有的Activity的子类都会实现两个方法:

    • onCreate(Bundle)
    • onPause()

    看到这两个函数命名就大概能知道这个Activity类是贯穿一个应用的生命周期的,那么NaviteActivity类以及GameActivity类中的内容就是用来在应用生命周期的不同阶段执行不同逻辑的。

  2. 如何拓展:

    UPL的源代码中可以看到全部的API,在SDK文档中应该会说明SDK的使用步骤,例如GCloud的文档中:

    这部分的初始化代码就可以使用UPL插入到GameActivity.java中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <gameActivityOnCreateAdditions>
    <insert>
    GCloud::InitializeInfo initInfo;
    initInfo.GameId = GCLOUD_GAME_ID;
    initInfo.GameKey = GCLOUD_GAME_KEY;

    IGCloud::GetInstance().Initialize(initInfo);
    </insert>
    </gameActivityOnCreateAdditions>

    当然具体的实现要参考不同SDK文档中提供的实现。

IOS平台设置相关:

  1. info.plist文件

    这个文件的作用就是提供应用在运行期间的一些设置,是一个XML格式组织起来的文件。

  2. 在虚幻引擎中的设置:

    Project Settings - iOS - Extra Plist Data - Aditional Plist Data - 中添加相关的配置信息中添加相关的渠道信息。

一些详细的解读可以参考查利鹏大佬的这篇博文:UE4 开发笔记:Mac/iOS 篇

在虚幻中调用Java代码

JNI:Java Native Interface

Java Native Interface , Java原生接口,一种让其他语言或程序调用Java代码的方法,具体实现细节笔者并不清楚,可以参考官方文档,此处仅对项目中遇到的问题进行总结和梳理。

对于Android平台,SDK提供方可能会提供一些Java版本的函数接口,这些Java函数接口需要我们在虚幻引擎中调用,我们假定前置的接入插件的步骤已经完成。

SlugSDK为例,它提供了一个初始化SDK组件的接口:

1
2
3
4
5
6
7
8
9
10
/*
* @url 打开的页面url
* @supportedOrientations 设置屏幕方向类型(1:只支持横屏,2:只支持竖屏,3:横竖屏都支持)
* @qqAppid 设置游戏的qq Appid(用来分享到qq)
* @wxAppid 设置游戏的微信Appid(用来分享到微信)
* @className: String类型,比如"com.tencent.ingame.TestActivity",从H5页面跳转到游戏的某个界面,如果不需要可以设置为"",H5页面跳转到游戏的某个界面时,会先跳转到游戏的该Activity(Activity声明需设置singleTask或singleTop)再由该Activity作为中转跳转到具体的游戏界面,该Activity先在onNewIntent方法setIntent(intent),然后可以通过getIntent().getStringExtra("routeInfo")获取跳转的路由信息,比如关闭浏览器时获取的routeInfo值为"close"
* @finishShouldSendMsgToGame 浏览器webview finish时向游戏发送close消息,不需要可以传false,如果需要则必须先设置@className参数
* @webviewBackground 设置打开的浏览器背景颜色,比如#ffffffff
* */
public static void openIngameCommunityByUrl(Activity currentActivity, String url, int supportedOrientations, String qqAppid, String wxAppid, String className, boolean finishShouldSendMsgToGame, String webviewBackground)

该函数的函数签名如下:

  • 参数 : Activity currentActivity, String url, int supportedOrientations, String qqAppid, String wxAppid, String className, boolean finishShouldSendMsgToGame, String webviewBackground
  • 返回值 :void

我们可以用命令行工具 : javap -s输出函数签名 , 具体可以参考博文 : Java dos命令窗口获取方法的签名 ; 也可以根据JNI函数签名的转换规则(详见Type Signatures部分)进行转换 , 以上面的函数为例 :

1
2
3
4
5
6
7
8
9
Activity	-- Landroid/app/Activity
string -- Ljava/lang/String;
int -- I
string -- Ljava/lang/String;
string -- Ljava/lang/String;
string -- Ljava/lang/String;
bool -- Z
string -- Ljava/lang/String;
void -- V

需要注意的是 : 内置基础类型对应的签名后面没有分号 , 不要擅自加上分号 , 这样会导致搜索不到对应的方法!

因此最后该函数我们导出的函数签名为(注意为ILjava/lang/String) :

1
(Landroid/app/Activity;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)V

至此 , 我们完成了调用Java代码的准备工作。

在虚幻中调用 :

我们需要JNIEnv环境 , 在虚幻中我们可以使用FAndroidApplication::GetJavaEnv();

我们需要对应的jclass , 在虚幻中我们可以使用FAndroidApplication::FindJavaClass(ClassName)进行获取

我们需要对应的函数 , 此时我们就用上了刚刚推导的函数签名 , 同样在虚幻中我们可以使用env->GetStaticMethodID(signature)获得

最后我们需要调用函数 , 还是通过env完成 , 可以调用env->CallStaticVoidMethod()完成

整个过程的伪代码如下(对于env , cls , jmethodID必要时可以做一些防御性检查):

1
2
3
4
5
6
7
8
9
10
11
void AndroidSlugSDK::OpenIngameCommunity(FString url, int supportedOrientations, FString qqAppid, FString wxAppid, FString className, bool finishShouldSendMsgToGame, FString webviewBackground){
JNIEnv *env = FAndroidApplication::GetJavaEnv();
jclass cls = FAndroidApplication::FindJavaClass("com/tencent/slugsdk/SlugWebAdapter");
jmethodID jOpenCommunityMthod = env->GetStaticMethodID(cls, "openIngameCommunityByUrl", "(Landroid/app/Activity;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)V");
jstring jUrl = env->NewStringUTF(TCHAR_TO_UTF8(*url));
jstring jqqAppID = env->NewStringUTF(TCHAR_TO_UTF8(*qqAppID));
jstring jwxAppID = env->NewStringUTF(TCHAR_TO_UTF8(*wxAppID));
jstring jclassName = env->NewStringUTF(TCHAR_TO_UTF8(*className));
jstring jwebColor = env->NewStringUTF(TCHAR_TO_UTF8(*WebViewBackgroundColor));
env->CallStaticVoidMethod(cls, jOpenCommunityMthod, FAndroidApplication::GetGameActivityThis(), jUrl, supportedOrientations, jqqAppID, jwxAppID, jclassName, finishShouldSendMsgToGame, jwebColor);
}

一些其他的坑 :

  1. 要明确项目是否支持aar包 , 貌似用gradle构建才可以使用aar包
  2. ios的代码调用要在主线程 , 防止崩溃

参考

插件

  1. 虚幻插件语言(Unreal Plugin Language) : https://docs.unrealengine.com/en-US/SharingAndReleasing/Mobile/UnrealPluginLanguage/index.html
  2. 虚幻插件语言参考:https://qiita.com/shiena/items/fe0e4cc1de4ddbaa60f0
  3. UE4:UPL与JNI调用的最佳实践:https://imzlp.me/posts/27289/

模块

  1. 模块的官方文档 : https://docs.unrealengine.com/en-US/ProductionPipelines/BuildTools/UnrealBuildTool/ModuleFiles/index.html
  2. 详解UE4静态库与动态库的导入与使用:https://www.cnblogs.com/sevenyuan/p/7161516.html

平台相关

Android

  1. 应用清单 (AndroidManifest) : https://developer.android.com/guide/topics/manifest/manifest-intro?hl=zh-cn
  2. 权限 (Permission) : https://developer.android.com/guide/topics/permissions/overview?hl=zh-cn
  3. 应用二进制接口 (Android ABI, Application Binary Interface) : https://developer.android.com/ndk/guides/abis

    1. Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题:https://zhuanlan.zhihu.com/p/23102158
  4. aar与jar : https://developer.android.com/studio/projects/android-library?hl=zh-cn

IOS

  1. .bundle资源文件 : https://www.jianshu.com/p/55038871e7de
  2. framework库文件 : https://blog.csdn.net/lvxiangan/article/details/43115131
  3. info.plist:https://my.oschina.net/hmj/blog/104196