不同的硬件厂商为 Android 用户带来了不同尺寸和体验的设备,因此,我们也一直努力地帮助开发者们将游戏呈现到尽多的 Android 设备并使得开发过程更加高效轻松。本文将向您介绍众多新的 Android 游戏开发工具以及游戏调试、打包和分发技巧。
更高效的游戏开发工具
工欲善其事,必先利其器,我们针对开发工具做出了大量优化。游戏引擎主要使用 C 和 C++ 语言编写,而大多数的 Android API 都被设计成由 Kotlin 这样的托管代码来调用。所以我们在 Android Studio 中实现了同时调试 C/C++ 代码和托管代码的能力,这样一来您就可以像下图那样在 Kotlin 及 C/C++ 代码中分别设置断点,然后在两种编程环境内分别跟踪执行情况。您甚至还可以在托管代码和原生 (native) 代码之间跳转观察执行情况。
△ 演示通过 Android Studio 同时调试托管代码和 C/C++
另一方面,Android Studio 可以通过自动代码补全功能来加快您编写代码的速度,也支持您快速插入 JNI 函数原型在两种不同环境之中编程。此外,构建过程中会使用 Cmake 和 Gradle,它们能让您更好地利用可移植 build。
对于那些在 Microsoft Windows 操作系统上用 Visual Studio 编写 C/C++ 跨平台游戏的开发者来说,我们在 2021 年 7 月推出的 Android 游戏开发套件 (Android Game Development Kit) 中提供了一个 Android 游戏开发扩展 (AGDE, Android Game Development Extension)。它能够进一步简化您的开发进程,并且使用一系列支持 C 和 C++ 的调试器及性能分析工具帮助您在 Visual Studio 环境中直接针对 Android 设备进行构建。并且 AGDE 可以很方便地与多种构建系统集成起来,还能非常方便地整合进您使用虚幻引擎的工作流,如此一来,您就不需要专门分别针对桌面设备、游戏主机、Android 设备各使用一套工具集和构建系统了。
配置 AGDE 的过程也非常简单。扩展安装完成后,请切换到您的 Visual Studio 项目,然后添加 Android APK 模板。您可以在工具栏访问到各种常用的 Android 开发工具,比如 SDK 和 NDK 管理器、虚拟设备管理器、设备文件管理器、Logcat 以及性能分析器等,如下图所示:
△ Visual Studio 中的 Android 工具栏
在项目中配置好 Android 构建目标后,您只需要像开发标准桌面 Visual Studio 目标那样操作就行了,所有关于构建、部署、调试的操作都没有差别。您可以很方便地在调试器中设置断点、切换到反汇编代码中查看寄存器和内存块中的值,还可以通过并行堆栈查看并发情况。
△ 使用 Visual Studio 调试 Android 构建目标
另外,您不仅可以在专门的 Logcat 面板搜索日志输出、按照类型过滤,还可以通过 AGDE 快速访问原本在 Android Studio 提供的独立 CPU、内存分析器,如下图右所示:
△ 可供搜索的 Logcat 面板 (左);独立的 CPU 和内存分析器 (右)
我们为这些工具设计了新的界面,增强了若干功能并支持原生内存采样。Android Studio 和 Android 游戏开发扩展工具的结合,为您提供了一整套丰富的工具来高效地开发游戏。
Android 游戏开发套件
俗话说,好马配好鞍。只有这些趁手的工具其实是不够的,我们还需要将这些工具集成到 Android 的托管代码 API 中。为此,我们在 Android 游戏开发套件 (AGDK) 中提供了全新的 C/C++ 库供您使用。
GameActivity
GameActivity 几乎就是用原生方式实现 (C/C++) 的 Android 标准 activity。它可以很好地与 Jetpack 库以及各种 Android 界面库结合使用。这样您就可以完全使用 C/C++ 编写游戏循环,同时充分利用基于 Jetpack 构建的各种库。另外,GameActivity 会被渲染到 SurfaceView 中,因此您可以很轻松地混用 Android 界面元素,比如 WebView、MapView 和广告 SDK 等各种服务所需要用到的视图。这样一来,您只需要在托管代码中通过一个简单加载 C/C++ 游戏模块的类,就能完成对游戏循环逻辑的整合了。有关代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content">
△ 需要显示的视图
public class MyGameActivity extends GameActivity {
static {
// 加载您的游戏库
System.loadLibrary("game");
}
}
△ 最少的托管代码
您需要修改 AndroidManifest.xml 文件中的 meta-data,用来告知 GameActivity 需要从哪个库开始执行游戏循环。
activity android:name=".MyGameActivity" android:label="@string/app_name">
<meta-data android:name="android.app.lib_name" android:value="game"/>
△ 修改 AndroidManifest.xml
GameActivity 为您提供了若干原生回调方法,它们与 Android 的生命周期事件是相匹配的,这些事件回调可以很方便地整合进您的游戏循环中。
△ 与 Android 生命周期对应的回调方法
下面是一段非常简单的代码,我们会向您展示一个基于 native_app_glue 库的案例。native_app_glue 库提供了一种异于寻常的执行模式。
void android_main(struct android_app *app) {
// 您的游戏引擎
NativeEngine *engine = new NativeEngine(app);
// 您的游戏循环
engine->GameLoop();
delete engine;
}
△ 基于 native_app_glue 的例子
在这里,android_main() 函数将在一个区别于主线程的新线程中被调用。您可以从与线程关联的 ALooper 中获取到 Android 的生命周期事件,就像这样:
while(1) {
int events;
struct android_poll_source *source;
// 如果没有动画发生,阻塞进程直到捕获某个事件
while ((ALooper_pollAll(IsAnimating() ? 0: -1, Null, &events, (void **) &source)) >= 0) {
// 处理事件
if (source != NULL) {
source->process(mApp, source);
}
}
}
△ 使用 ALooper 获取并处理 Android 生命周期事件
除了捕获生命周期事件,您还可以用这个方法监听文件描述符 (file descriptor)。
Frame Pacing API
帧数调步 (frame pacing) API 可以帮助开发者解决由较短游戏帧造成的拖延现象,以及避免较长游戏帧导致的拖延卡顿。如果设备支持选择刷新率功能,则可以为玩家提供更灵活和流畅的显示效果。
实现 GameActivity 后,您可以选择使用 OpenGL/ES 或者 Vulkan 将内容渲染到表层。不论您选择了哪种 API,Android Frame Pacing 库都会帮您妥善处理渲染过程,它会将游戏的逻辑、渲染循环与 Android 的显示子系统、底层显示相关的硬件进行同步,从而实现更流畅的渲染。
Oboe API
△ Oboe API
一款制作精良的游戏离不开出色的音效,这也是 Oboe 音频库所提供的能力。您可以在 Android 4.1 及更高版本的系统上使用它的 API。在 API 级别 27 的设备上,Oboe 会通过 AAudio 在设备上尽可能协调软硬件而达到最低的音频延迟。而对于较低版本的设备,Oboe 会使用 OpenSL ES 来尽可能保证兼容性。
Oboe 在引入重采样、格式转换、高性能的通道数转换等多项新功能的同时,还内置了一些已知音频问题的解决方法。
游戏输入
Android 游戏开发套件还提供了两个可以与 GameActivity 互操作的库,分别用于处理游戏中的软键盘和手柄输入信号。
游戏文本输入
GameTextInput 在底层进行了很多复杂的工作,它能将 Android 系统的软键盘连接到您游戏内的文本编辑器上,还包括了显示和隐藏软键盘的操作。
△ 游戏中的文本输入逻辑
如果您将这个库与 GameActivity 结合使用,那么无论有没有使用 native_app_glue,系统都能自动完成相关的配置。请看下面的代码,GameTextInput 库会将输入状态传递给您的游戏,这样您的文本编辑器就能正确反映 IME 的状态。
/**
* 获取最后收到的文本输入状态
*/
void GameActivity_getTextInputState(
GameActivity *activity,
GameTextInputGetStateCallback callback,
void* context);
△ 修改 AndroidManifest.xml 文件
游戏手柄输入
另一种游戏中常见的输入设备是游戏手柄 (game controller)。您可以通过使用 Game Controller 库来充分发挥实体游戏手柄的作用。这个库会在手柄连接到设备或断开连接时向您的游戏发送通知,并且提供了关于按钮布局、方向轴和其他按键控制器元数据的信息。
Game Controller 库也在底层进行了封装,让您不需要复杂的实现也能无缝与各种各样的手柄轻松连接,甚至还接受鼠标作为输入设备。
支持大屏幕游戏
在智能电视畅玩游戏
如果您的游戏可以通过方向键来控制,并在横屏模式下正常运行,那么它就可以运行在许多电视设备上。
要支持在智能电视上运行,您需要对 AndroidManifest.xml 文件做一些修改:
// 您需要在多个 uses-feature 标签内声明这些权限:
android.hardware.touchscreen
android.hardware.faketouch
android.hardware.telephony
android.hardware.camera
android.hardware.nfc
android.hardware.location.gps
android.hardware.microphone
android.hardware.sensor
// 如果有必要的话,请添加 android.required="false"
△ 修改 AndroidManifest.xml 文件
您需要在清单中声明这些权限是非必要的,即 required="false"。这是由于很多您在 AndroidManifest 中提到的权限、功能都不是必须的,智能电视上往往不支持这些功能,比如触摸屏、摄像头、加速度传感器等。您可以通过添加如下代码,声明游戏支持使用遥控器作为手柄:
<uses-feature
android:name="android.hardware.gamepad"
android:required="false"/>
△ 声明游戏支持使用遥控器作为手柄
在需要时,可以声明 android:required="true",来表示在启动应用前电视需要先检查遥控器是否可用。
在 Chrome OS 运行
Chrome OS 如今已经成为了第二大桌面操作系统,并且拥有非常多的游戏玩家。它可以直接运行 Android 游戏,并内置了 Google Play 商店。玩家可以在运行 Chrome OS 的设备上畅玩支持横屏的游戏。大多数设备提供了触摸屏供玩家操作,对于某些没有触摸屏的设备,通常会通过鼠标或触控板来模拟触摸屏操作。当然,直接支持鼠标和键盘输入的游戏往往会给玩家带来更好的游戏体验。
您可以在游戏中使用类似下面的代码捕捉鼠标或触控板事件:
fun onClick(view: View) {
view.requestPointerCapture()
}
override fun onCapturedPointerEvent(motionEvent: MotionEvent): Boolean {
// 捕获到的指针事件往往提供相对位置
val horizontalOffset: Float = motionEvent.x
val verticalOffset: Float = motionEvent.y
return true
}
△ 捕捉鼠标或触控板事件代码示例
您可以在 API 级别 26 或更高的 Android 上,使用上述代码捕获鼠标或触控板光标,来实现更精准的控制。比如,大多数场景下,您可能会希望能够让与用户交互的虚拟控件在尺寸更大的屏幕上也能保持合适的相对大小。这就需要您将不同的屏幕尺寸和屏幕显示密度纳入考虑,通过计算得到虚拟控件的大小。按照下面的代码,您可以获取当前显示屏的显示度量指标,然后获取 X 轴和 Y 轴的密度以及每英寸像素数,然后计算出虚拟控件的缩放比例。
val dm = resources.displayMetrics
// 用于精确缩放
val xdpi = dm.xdpi
val ydpi = dm.ydpi
// 用缩放因子计算 DPI
val densityDpi = dm.density * 160.0f
val scaledDensityDpi = dm.scaledDensity * 160.0f
△ 在托管代码中获取屏幕的 DisplayMetrics
您还可以使用 dm.density 和 dm.scaledDensity 获得近似的缩放系数。其中 1.0 对应着 DPI 值 160。ScaleDensity 用于根据用户偏好缩放 Android 应用的字体,所以,当您的游戏支持这一功能,必然会给用户带来更佳的体验。
有时出于性能考虑,您可能会想要设定一个显示密度的上限。如果您愿意,可以使用 surfaceHolder.setFixedSize 来自动且经济地缩放界面。这样不仅可以节省内存 (RAM),还可以减少需要着色的像素,并进一步节省电量和减少发热。具体参照下面这段代码:
var width = mSurfaceView.width
var height = mSurfaceView.height
val dm = resources.displayMetrics
if (dm.density > maxDensity) {
val newScaleFactor = maxDensity / dm.density
if (newScaleFactor != scaleFactor) {
width = (newScaleFactor * width).toInt()
height = (newScaleFactor * height).toInt()
mSurfaceView.holder.setFixedSize(width, height)
}
}
△ 使用 surfaceHolder.setFixedSize 缩放界面
您还可以使用 C/C++ 代码来自动实现缩放,比如下面的代码:
int32_t ret = ANativeWindow_setBuffersGeometry(
window, width, height, 0);
△ 使用 C/C++ 实现自动缩放
由于触控事件始终发生在屏幕坐标内,所以无论是用托管代码还是 C/C++ 实现,您都需要调整触控事件来匹配新的界面。
不同的游戏引擎有不同的后缓冲区缩放方式,如下图列举的 Unity、Unreal Engine、Godot 在这方面就有明显的区别。
△ 不同游戏引擎缩放后缓冲区的方法比较
Unity 可以通过多种方式缩放后缓冲区。您可以在 Android Player 设置中调整最大 DPI 值: 先选择 Fixed DPI,并拖动选择一个适当的 DPI 阈值。Unreal Engine 4 支持移动内容缩放系数,它提供了设备原生分辨率之外的几个额外选项。Godot 允许您通过多种方式缩放渲染的内容,同时您可以使用基本窗口宽度 (base window width) 来避免渲染时 DPI 过高。另外,Godot 还支持通过脚本的方式获得屏幕的 DPI 值。
如果您需要游戏支持运行在更多体系结构的 Chrome OS 设备,那么还需要在构建脚本中添加更多 ABI 项:
externalNativeBuild {
cmake {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
△ 在 Gradle 构建脚本中添加 x86/x86_64
这是由于很多运行 Chrome OS 的设备都采用了 x86 或者 x86_64 架构。如果您使用某一游戏引擎来开发 Android 游戏,那么您可以从图中查看目前已知的支持 x86/x86_64 的引擎版本:
△ 在支持 x86/x86_64 的引擎版本举例
当您准备好在 Google Play 商店分发游戏时,只需选择这些额外的架构,然后上传到同一个 Play 商品详情下即可。这种统一分发的方式能最大程度简化您分发游戏的流程。
Android App Bundle 是一种专门为 Android 平台设计的应用打包发布的文件格式。它允许您在不增加文件大小的情况下,在同一个打包中包含各种体系结构的二进制库文件。随后 Google Play 会自动为所包含的各种平台生成一个专门的 APK 文件用于分发下载。
处理更庞大的素材
使用 PAD 分发游戏资源
△ 使用基于 Android App Bundle 的 PAD 格式
很多游戏都需要加载巨量的资源、素材文件,比如 3D 模型、贴图、音效、视频过场等等。Play Asset Delivery (PAD) 是一种针对 Android App Bundle 的扩展格式 。有了它,您可以向 Google Play 商店发布单个工件,而其中同时包含了游戏的代码部分和资源部分。
△ 使用 Play Asset Delivery
PAD 这种格式经过大量的改进和优化,能为您提供更高效的游戏分发体验。当您使用 PAD 交付游戏资源时,它会确保代码和资源的版本一致,从而让用户一打开游戏就能获得最新的二进制文件和资源,不再需要等待资源更新。并且 Google Play 的自动更新功能还可以帮您自动处理增量更新,使得用户可以在现有游戏版本上直接下载更新产生变化的部分,而不需要重新下载整个游戏。使用 PAD 的另一好处是内容下载的优化,也就是用户可以在首次运行游戏时加载必要的资源,随后需要新内容时又继续按需加载。
使用纹理压缩区分设备
您可以使用纹理压缩格式 (texture compression format) 将这些内容进行划分,而 Google Play 会帮助您选择最合适的纹理分发到不同设备上,从而保证您的游戏在大部分设备上都能实现最高的渲染效率和最佳的渲染效果。
△ 使用纹理压缩划分内容分发
使用设备类别划分
△ 使用设备类别进行区分
我们当前正努力实现按照设备类别进行定向分发的功能,这样一来您只需在每一套资源中分别提供不同类别所需要的资源文件即可。您可以根据 RAM 大小、设备型号等信息,以及物理屏幕大小等硬件特征进行划分,实现用更小的安装包覆盖更庞大的设备群体。
总结
提高 Android 游戏开发的效率不仅是每个开发者的期望,也是我们这些年来坚持不懈的理想。通过这篇文章我们分享了高效开发 Android 游戏的一些工具和技巧: Android 游戏开发套件中新增的 Android 游戏开发扩展、Android GPU 检查器、GameActivity、软键盘、游戏手柄和高性能音频库及 Android 性能调优工具;另外向您展示了 Play Asset Delivery 格式在分发游戏资源方面展示出的强大能力。希望这些内容能加深您对 Android 开发的理解,并帮助您开发出广受欢迎的游戏作品!