从零开始手敲次世代游戏引擎(Android特别篇)-3

在文章从零开始手敲次世代游戏引擎(Android特别篇)-2当中我们完成了执行环境的部署,并打通了开发环境和执行环境。但是到目前为止我们只是完成了C/C++部分的交叉编译,并没有实现Android应用程序的开发。
事实上,Android是一套基于Linux上Java虚拟机的Java程序集团。也就是说,与之前我们开发macOS版类似,我们无法在C/C++当中完成与系统各项服务:如窗体管理/输入输出管理等的交互。在macOS当中,我们写了Object C++代码来桥接Cocoa和我们的引擎;在Android当中,我们需要通过JNI glue来桥接Java和C/C++代码。(JNI = Java Native Interface,Java本地代码接口)
参考引用*1为我们展示了在Java Code当中调用C/C++代码的方法。然而,采用这种方式的程序其主循环(Main Loop)仍然是在Java代码当中。这样的做法对于一般大多数应用程序来说是可以接受的,但是对于诸如游戏引擎这类对于性能要求比较高的场合,是不太合适的。
如果做过一些Android Java开发的人应该知道,Android的应用开发其实某种程度挺类似于网页开发,GUI开发的基本单位是Activity,每个Activity包括一个界面(可以通过基于XML的resource文件定义)和一组控制这个界面的程序(Java),Activity之间的迁移通过暴露Intent接口来实现。这与Web开发的网页(通过HTML编写)+脚本(JavaScript)+ URL参数的方式是十分类似的。
所以,为了实现高效的本地代码,最好是能够用C/C++直接写Activity,而不是在Java编写的Activity当中通过JNI去调用C/C++写的功能Function。
当然我们并不是第一个吃鸡的人。这类问题在早期的Android版本当中是不好解决的,但是在今天,由于商业引擎的推动,较新的Android版本当中已经有了比较好的解决方式:Native Activity。参考引用*2为我们展示了这个Native Activity的使用方法。
让我们将参考引用*2的代码下载下来,放到我们的代码树的Platform/Android目录(新建)下。然后我们启动我们的docker容器(切换到Android开发环境),在Platform/Android目录下执行:
就可以编译生成这个Sample的APK文件了。这证明了我们的docker环境是正常的。
(这里面有个小细节。Google Samples当中的代码似乎已经有一段时间没更新了。对于Native Activity这个Sample,其依赖的“com.android.support.constraint:constraint-layout:1.0.1”这个组件已经过期。如果编译的时候提示找不到这个包,请将其改为“com.android.support.constraint:constraint-layout:1.0.2”。具体位置是在app/build.gradle当中)
(可以从这里下载编译生成的APK安装包。注意是开发包,没有进行签名,系统会提示安装风险,请自行判断)
执行的效果如下:
 
 

视频封面

上传视频封面

 
 
有了Google官方的Sample,接下来我们需要解决的问题主要就是如何将Android标准的基于Gradle的项目与我们的基于CMake的项目统合起来。
一种首先可能会想到的方法是,我们采用从零开始手敲次世代游戏引擎(Android特别篇)-1当中介绍的方法,先将我们的引擎代码编译称为动态库(libMyGameEngine.so),然后在Android应用当中引用这个库。也就是说,我们需要分开的两个步骤,第一次编译引擎,第二次编译应用。
其实在参考引用*2当中,已经为我们展示了直接在Gradle当中进行CMake的调用,于应用构建的过程当中自动构建本地代码模块的方法。如果我们打开参考引用*2的app/build.gradle文件,可以看到如下的内容:
红框所示部分,就是通过Gradle调用CMake的关键。
不仅如此,Gradle还会自动将CMake生成的生成物保存到Gradle的编译目录,所以我们只需要在我们的AndroidManifest.xml当中直接引用我们的引擎动态库就可以,而不必关心它会被放在什么地方。
上图最后一行的MyGameEngine,就是我们动态库的名字。注意在Linux环境当中,编译出来的实际文件名是libMyGameEngine.so。但是这个前缀“lib”和扩展名“.so”是不需要指定的。
为了生成这个动态库,我们需要在Platform目录下面新建Android目录,并且参照参考引用*2当中的写法,如下书写CMakeLists.txt
其中,AndroidApplication.*,OpenGLESApplication.*是将参考引用*2当中的代码,按照我们的架构和继承关系进行分解之后的产物。而AndroidAssetLoader.*,是从AssetLoader派生出的子类,原因是当我们最终在打包APK(APK是Android上面的安装包)的时候,如果将Asset目录下的资源打包进去,那么这些资源其实并不会在安装之后展开到安装对象的文件系统当中,而是继续以压缩文件的方式存在(APK文件其实就是一个ZIP压缩包。Java编译之后的jar文件也是一个ZIP压缩包)
这就是说,在这种情况下,通过通常的fopen/fclose/fread系列的API我们是无法读取到资源文件到,而是需要通过Android NDK提供的特殊接口,AAssetManager去读写这些文件。所以我们这里单独派生出了一个类来对应这个特别的需求。
将资源文件打包进APK的方法是,在app/build.gradle当中添加sourceSets,然后指定从哪里拷贝资源文件:
好了,在进行完这些改造和代码的重组之后,我们可以通过在Platform/Android目录下执行./gradlew assembleDebug来完成整个项目的编译构建工作。(需要在安装了Android SDK/NDK的环境当中进行)
代码在下面这个链接当中。目前在模拟器上运行是OK的,在我的Huawei P9上面仍然有问题,尚在Debug当中。
图标
本地代码(C/C++)调试方法:
与PC本地开发不同,嵌入式开发由于代码执行环境与开发环境是分离的两个环境,无法直接使用调试器(gdb / lldb)启动并调试可执行文件。这里需要用到的就是调试服务(如gdbserver)
方法是,首先将预先交叉编译后的调试服务程序推送到目标机器。对于Android,在NDK的prebuild目录当中提供了预先编译好的gdbserver。使用adb push命令推送过去就可以了。注意需要根据目标机器的CPU选择正确的版本:
(注意推送之后需要添加“x”属性)
然后,通过adb shell命令启动gdbserver,并指定需要调试的程序,以及gdbserver侦听的TCP端口号(用于接收来自开发环境的调试命令)。因为这个命令不会自动退出,需要在最后指定“&”参数将其置于后台,以便我们继续输入命令:
然后,通过adb forward命令,将运行环境当中gdbserver侦听的端口映射到开发环境当中:(实质上是ssh port forwarding)
然后在开发环境当中启动gdb。NDK当中有提供这个客户端,在下面这个目录当中
$ANDROID_NDK_HOME/prebuilt/linux-x86_64/bin/
启动之后,输入下面这个命令,连接运行环境当中的gdbserver:
之后就如同在本地使用GDB一样,通过b命令设置断点,s命令单步等。不再赘述。
对于被打包在APK当中的C/C++代码,因为该代码由Java启动(即使使用Native Activity,其实程序仍然是从Java层启动的)。也就是说,在运行环境里面并没有可以启动这个应用的可执行文件。对于这种情况,首先需要通过手点击主菜单应用程序的图标,将程序启动起来,或者(对于我们这种使用docker没有window的情况)使用如下命令启动应用:
然后使用
列出执行当中的应用进程,查找我们的应用程序的进程号码(下图星号所示):
然后执行:
来启动gdbserver。其它的相同。
如果程序一启动就崩溃,那么这种方式显然赶不及,那么还需要在“设置 > 开发者选项”当中打开“等待调试器”。不过这一项通常是无效状态,需要在“选择调试应用”当中首先指定被调试应用,这一项就可以选择了。打开之后,应用程序启动会立即中断,等待我们将gdb连通之后,输入“c”命令,就可以继续执行了。
 
Circle CI的自动化测试
采用Docker环境的好处是我们可以直接在Circle CI当中完成大多数不需要人工参与的自动化测试。我们可以将到此为止所写的整个编译以及构建执行环境、打通与开发环境的连接的操作全部写到Circle CI的YAML脚本当中,从而完成程序的自动测试。相关脚本请直接参考GitHub上的源代码当中的.circleci/config.yml文件。
并且,Circle CI支持将编译结果保存提供下载。上文当中提供的APK下载就是采用的这种方式。具体做法同样参考上述配置文件。
 
关于Android环境下的调试输出
我们的引擎目前是直接将调试信息输出到stdout / stderr,但是在以APK形式安装的程序当中,stdout / stderr是被重定向到/dev/null,也就是被丢弃的。因此,为了能够看到我们的Log,我们需要将Log重新定向到NDK所提供的机制当中。具体的做法有三种:
  1. 修改我们的代码,将相关代码替换成Android NDK的接口。但是这显然对于跨平台的代码来说不可接受;
  2. 编写专门的调试模块,并且对于不同的平台采用不同的输出方法。这个在我们后续的文章当中会有介绍;
  3. 利用Unix/Linux的管道和重定向机制,在应用程序里面另外启动一个线程,从我们的主线程接收Log并且将Log输出到NDK的接口当中。这个是本篇采用的方法。具体实现在Platform/Android/AndroidApplication.cpp当中。
另外,查看这个Log输出的方法是使用下面的命令:
但是这个命令会输出所有的log,包括我们的,包括系统和其它应用的。NDK提供的Log接口当中可以指定一个Tag,在我们的例子当中,我们指定了“MyGameEngine”作为我们的Tag,所以我们可以通过下面使用这个命令这样来过滤:
这样我们就可以看到大量我们的程序输出的关于重力加速度传感器的读数:(下面的数据因为是在模拟器当中采集的,所以不变)
参考引用
图标
图标
图标
图标
图标
图标
图标
图标
图标

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 注销 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 注销 /  更改 )

Connecting to %s

%d 博主赞过: