Android-游戏开发入门指南-全-

Android 游戏开发入门指南(全)

原文:Beginning Android Games Development

协议:CC BY-NC-SA 4.0

一、设置

  • 获得 Android Studio

  • 设置 IDE

  • 基本配置

构建 Android 应用并不总是像今天这样方便。回到 2008 年,当 Android 第一次发布时,我们通过开发工具包获得的只是一堆命令行工具和 Ant 构建脚本。如果你习惯了这类东西,用简单的编辑器、Android CLI 工具和 Ant 来构建应用并不坏,但许多开发人员并不习惯。缺乏现代 IDE 的功能,如代码提示、完成、项目设置/搭建和集成调试,在某种程度上是入门的障碍。

幸运的是,用于 Eclipse IDE 的 Android 开发工具(ADT) 也是在 2008 年发布的。对于许多 Java 开发人员来说,Eclipse 曾经是(现在仍然是)最受欢迎的 IDE 首选。很自然地,它也将成为 Android 开发者的首选 IDE。

从 2009 年到 2012 年,Eclipse 一直是开发的首选 IDE。Android SDK 在结构和范围上经历了重大和渐进的变化。2009 年,SDK 管理器发布;我们用它来下载工具、单个 SDK 版本和可以用于模拟器的 Android 图像。2010 年,发布了针对 ARM 处理器和 x86 CPUs 的附加映像。

2012 年是重要的一年,因为 Eclipse 和 ADT 终于捆绑在一起了。这是一件大事,因为在那之前,开发人员必须分别安装 Eclipse 和 ADT 安装过程并不总是一帆风顺。因此,将两者捆绑在一起使得开始 Android 开发变得更加容易。2012 年也值得纪念,因为它标志着 Eclipse 成为 Android 主流 IDE 的最后一年。

2013 年 Android Studio 发布。可以肯定的是,它仍然处于测试阶段,但是不祥之兆已经很明显了。它将成为 Android 开发的官方 IDE。Android Studio 基于 JetBrains 的 IntelliJ。IntelliJ 是一个商业 Java IDE,它也有一个社区(非付费)版本。这将是社区版本,将作为 Android Studio 的基础。

安装 Android Studio

撰写本文时, Android Studio 在 3.5 版本;希望在你读到这本书的时候,这个版本不会太遥远。可以从【https://developer.android.com/studio】下载。它适用于 Windows(32 位和 64 位)、macOS 和 Linux。我在 macOS (Catalina)、Windows 10 64 位和 Ubuntu 18 上运行了安装说明。我主要在 macOS 环境中工作,这解释了为什么这本书的大部分截图看起来像 macOS。Android Studio 在所有三个平台上的外观、运行和感觉(大部分)都是一样的,只有非常小的差异,比如 macOS 中的按键绑定和主菜单栏。

Before we go further, let’s look at the system requirements for Android Studio. At a minimum, you’ll need either of the following:

  • 微软 Windows 7/8/10 (32 位或 64 位)

  • macOS 10.10 (Yosemite 或更高版本)

  • Linux (Gnome 或 KDE 桌面),Ubuntu 14.04 以上;64 位能够运行 32 位应用

  • 如果你在 Linux 上,GNU C 库(glibc 2.19 或更高版本)

For the hardware, your workstation needs to be at least

  • 3GB 内存(建议 8GB 或更多)

  • 2GB 可用硬盘空间

  • 1280 x 800 最小屏幕分辨率

这些需求来自 Android 官方网站;当然越多越好。如果你能抢到 32GB 内存、1TB 固态硬盘和全高清(或 UHD)显示器,那就不错了;一点也不。

现在我们来谈谈 Java 开发工具包(JDK) 需求 。从 Android Studio 2.2 开始,安装程序自带 OpenJDK embedded。这样,一个初学者就不必为安装一个单独的 JDK 而烦恼;但是如果你愿意,你仍然可以安装一个单独的 JDK。在本书中,我将假设您将使用 Android Studio 附带的嵌入式 OpenJDK。

【https://developer.android.com/studio/】下载安装程序;获取适合您平台的二进制文件。

If you have a Mac, do the following:

  1. 1.

    Decompress the compressed file of the installer.

  2. 2.

    Drag the application file to the application folder.

  3. 3 .

    唉哟 Android Studio。

  4. 4.

    If you have installed it before, Android Studio will prompt you to import some settings. You can import it, which is the default option.

If you’re using Windows, do the following:

  1. 1.

    Unzip the installer file.

  2. 2.

    Move the decompressed directory to the location of your choice, for example: C: \ users \ my name \ Android studio

  3. 3.

    Go deep into the "AndroidStudio" folder; Inside, you will find "studio64.exe". This is the file that you need to start. It's a good idea to create a shortcut for this file-if you right-click studio64.exe and select "Pin to Start Menu", you can use Android Studio from the Windows Start Menu or you can pin it to the taskbar.

Linux 安装比简单地双击并遵循安装程序提示需要更多的工作。在 Ubuntu(及其衍生产品)的未来版本中,这可能会改变,变得像 Windows 和 macOS 一样简单、流畅,但现在,我们需要做一些调整。Linux 上的额外活动大多是因为 Android Studio 需要一些 32 位库和硬件加速。

Note

本节中的安装说明适用于 64 位 Ubuntu 和其他 Ubuntu 衍生产品,例如 Linux Mint、Lubuntu、Xubuntu 和 Ubuntu MATE。我选择这个发行版是因为我认为对于本书的读者来说,它是一个非常常见的 Linux 版本。如果你运行的是 64 位版本的 Ubuntu,你将需要一些 32 位的库才能让 Android Studio 正常运行。

To start pulling the 32-bit libraries for Linux, run the following commands on a terminal window:sudo apt-get update && sudo apt-get upgrade -ysudo dpkg --add-architecture i386sudo apt-get install libncurses5:i386 libstdc++6:i386 zlib1g:i386When all the prep work is done, you need to do the following:

  • 解压下载的安装文件。您可以使用命令行工具或 GUI 工具解压缩文件,例如,您可以右键单击文件并选择“在此解压缩”选项,如果您的文件管理器有该选项的话。

  • 解压文件后,将文件夹重命名为“AndroidStudio”。

  • 将文件夹移动到您拥有读取、写入和执行权限的位置。或者,您也可以将其移动到/usr/local/AndroidStudio。

  • 打开一个终端窗口,进入 AndroidStudio/bin 文件夹,然后运行。/studio.sh 。

  • 首次启动时,Android Studio 会问你是否要导入一些设置;如果您已经安装了以前版本的 Android Studio,您可能需要导入这些设置。

配置 Android Studio

If this is the first time you’ve installed Android Studio, you might want to configure a couple of things first before diving into coding work. In this section, I’ll walk you through the following:

  • 获取更多我们需要的软件,以便我们可以创建针对特定版本 Android 的程序。

  • 确保我们拥有所有需要的 SDK 工具。

Launch the IDE if you haven’t done so yet, then click “Configure,” as shown in Figure 1-1. Choose “Preferences” from the drop-down list.

![img/340874_4_En_1_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_1_Fig1_HTML.jpg)
Figure 1-1

从 Android Studio 的打开对话框进入“首选项”

When you click the “Preferences” option, it will open the Preferences dialog, as shown in Figure 1-2. On the left-hand side of the dialog, select the “Android SDK” section.

![img/340874_4_En_1_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_1_Fig2_HTML.jpg)
Figure 1-2

SDK 平台

“Android SDK ”部分有三个选项卡:“SDK 平台”、“SDK 工具”和“SDK 更新站点”;它们的标题不言自明。

当您到达“SDK 平台”部分时,启用“显示包细节”选项,以便您可以看到每个 API 级别的更详细视图。我们不需要下载 SDK 窗口中的所有内容。我们将只得到我们需要的物品。

SDK 等级或者平台号都是 Android 的特定版本。Android 9 或“派”是 API 等级 28,Android 8 或“奥利奥”是 API 等级 26 和 27,牛轧糖是 API 等级 24 和 25。您不需要记住平台号,至少不再需要,因为 IDE 会显示平台号和相应的 Android 昵称。

你会注意到在我的设置中只选择了 Android 9 (Pie)。你可以选择安装尽可能多的 SDK 平台,但出于本书的目的,我将使用 Android 9 或 10,因为在撰写本文时这些版本是最新的。这就是我们将用于示例项目的内容。请确保在下载平台的同时,您还将下载“Google APIs 英特尔 x86 Atom_64 系统映像”当我们测试运行我们的应用时,我们将需要这些。

选择一个 API 级别现在可能没什么大不了的,因为在这一点上,我们只是在练习应用。当您计划向公众发布您的应用时,您可能不会轻易做出这个选择。为你的应用选择一个最低的 SDK 或 API 级别将决定有多少人能够使用你的应用。在撰写本文时,25%的安卓设备使用“棉花糖”,22%使用“牛轧糖”,4%使用“奥利奥”。这些统计数据来自 developer 的仪表盘页面。安卓。com 。不时查看这些统计数据是个不错的主意,你可以在这里找到:developer.android.com/about/dashboards/

Our next stop is the “SDK Tools” section, which is shown in Figure 1-3.

![img/340874_4_En_1_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_1_Fig3_HTML.jpg)
Figure 1-3

SDK 工具

You don’t need to change anything on this window, but it wouldn’t hurt to check if you have the tools, as shown in the following list, marked as “Installed.”

  • Android SDK 构建工具

  • Android SDK 平台工具

  • Android SDK 工具

  • 安卓模拟器

  • 支持知识库

  • HAXM 安装程序

检查这些工具可以确保我们得到类似于 adbsqliteaaptzipalign 等工具。这些工具帮助我们调试、创建构建、使用数据库、运行仿真等等。

Note

如果你在 Linux 平台上,即使你有 Intel 处理器,你也不能使用 HAXM。KVM 将用于 Linux,而不是 HAXM。

一旦你对你的选择满意,点击“确定”按钮开始下载软件包。

硬件加速

在你编写应用的时候,不时地测试和运行它是很有用的,这样可以得到即时的反馈,并发现它是否像预期的那样运行,或者它是否正在运行。为此,您将使用物理或虚拟设备。每个选项都有其利弊,你不必选择一个而不是另一个;事实上,你最终将不得不使用这两个选项。

Android 虚拟设备或 AVD 是一个仿真器,你可以在其中运行你的应用。在模拟器上运行有时会很慢;这就是谷歌和英特尔想出 HAXM 的原因。这是一个模拟器加速工具,让测试你的应用变得更容易忍受。这对开发者来说是个福音。也就是说,如果您使用的机器配备了支持虚拟化的英特尔处理器,并且您没有使用 Linux。但是,如果您不够幸运,不要担心,有一些方法可以在 Linux 中实现模拟器加速,我们将在后面看到。

macOS 用户可能最容易拥有它,因为 HAXM 是自动随 Android Studio 安装的。他们不需要做任何事情就可以得到它,安装人员会为他们处理好的。

Windows users can get HAXM either by

对于 Linux 用户,推荐的软件是 KVM(基于内核的虚拟机);这是针对 Linux 的虚拟化解决方案。它包含虚拟化扩展(英特尔 VT 或 AMD-V)。

To get KVM, we need to pull some software from the repos; but even before you can do that, you need to do the following first:

  • 确保在 BIOS 或 UEFI 设置中启用了虚拟化。关于如何获得这些设置,请查阅您的硬件手册。它通常包括关闭电脑,重新启动电脑,并在听到系统扬声器的声音时按下中断键,如 F2 或 DEL,但正如我所说的,请查阅您的硬件手册。

  • 完成更改并重启到 Linux 后,看看您的系统是否可以运行虚拟化。这可以通过从终端运行以下命令来实现:egrep–c '(vmx | SVM)'/proc/CPU info。如果结果是一个大于零的数字,这意味着您可以继续安装。

To install KVM, type the commands, as shown in Listing 1-1, in a terminal window.sudo apt-get install qemu-kvm libvirt-bin ubuntu-vm-builder bridge-utilssudo adduser your_user_name kvmsudo adduser your_user_name libvirtdListing 1-1

安装 KVM 的命令

您可能需要重新启动系统才能完成安装。

希望一切顺利,您现在有了一个合适的开发环境。

关键要点

  • 可以获得 macOS、Windows、Linux 的 Android 和 Android Studio。在 Android 网站上,每个平台都有一个可用的预编译二进制文件。

  • HAXM 为我们提供了一种在 Android 虚拟设备上加速仿真的方法。当你在 macOS 或 Windows 上时(使用 Intel 处理器),你将自动获得 HAXM。如果你在 Linux 上,你可以用 KVM 代替 HAXM。

二、项目基础

  • 创建一个简单的项目。

  • 创建一个 Android 虚拟设备(模拟器),这样我们就可以运行和测试项目。

创建项目

Launch Android Studio, if you haven’t done so yet. Click “Start a new Android Studio project,” as shown in Figure 2-1. You need to be online when you do this because Android Studio’s Gradle (a project build tool) pulls quite a few files from online repositories when starting a new project.

![img/340874_4_En_2_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig1_HTML.jpg)
Figure 2-1

欢迎来到安卓工作室

During the creation process, Android prompts for what kind of project we want to build; choose “Phone and Tablet” ➤ “Empty Activity,” as shown in Figure 2-2—we’ll discuss Activities in the coming chapters, but for now, think of an Activity as a screen or form; it’s something that the user sees and interacts with.

![img/340874_4_En_2_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig2_HTML.jpg)
Figure 2-2

创建一个新项目,选择一个活动类型

In the next screen, we get to configure the project. We set the app’s name, package name (domain), and the target Android version. Figure 2-3 shows the annotated picture of the “Create New Project” screen.

![img/340874_4_En_2_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig3_HTML.jpg)
Figure 2-3Create New Project | -好的 | **名称**—这是您想要调用的应用;这也称为项目名称。该名称成为包含所有项目文件的顶级文件夹的名称。如果您在 Play Store 中发布应用,该名称也将成为您的应用标识的一部分。 | | ❷ | **包名**—这是您的组织或公司的域名,采用反向 DNS 表示法。如果您没有公司名称,您可以使用任何类似于 web 域的名称。目前,我们是否使用真实的公司名称并不重要,因为我们不会将这个发布到 Play Store。 | | -你好 | **保存位置**—这是本地目录中保存项目文件的位置。 | | (a) | **语言**—可以用 Kotlin,也可以用 Java 对于这个项目,我们将使用 Java。 | | (一) | **最低 API 级别**—最低 API 级别将决定您的应用可以运行的最低 Android 版本。你需要明智而谨慎地选择,因为这会严重限制你的应用的潜在受众。 | | ❻ | **帮我选择**—这显示了你的应用可以在 Android 设备上运行的百分比。如果你点击“帮我选择”链接,它会打开一个窗口,显示 Android 设备的分布,每个 Android 版本。 | | ❼ | **即时应用**—如果您希望您的应用可以播放,而无需用户安装您的应用,请启用此复选框。即时应用允许用户在 Google Play 中浏览和“试用”您的应用,而无需下载和安装应用。 | | ❽ | **安卓。x**—这些是支持库。包含它们是为了让你可以使用现代的 Android 库(比如 Android 9 中包含的那些),但仍然允许你的应用在较低版本的 Android 设备上运行。 |

完成后,单击“Finish”开始创建项目。Android Studio 搭建项目并创建启动文件,如主活动文件、Android 清单和其他文件,以支持项目。构建工具(Gradle)将从在线回购中提取相当多的文件——这可能需要一些时间。

After all that, hopefully the project is created, and you get to see Android Studio’s main editor window, as shown in Figure 2-4.

![img/340874_4_En_2_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig4_HTML.jpg)
Figure 2-4

主编辑窗口

Android Studio 的屏幕由几个部分组成,可以根据你的需要折叠和展开。左边部分(图 2-4 )是项目面板;它是一个树状结构,显示了项目中的所有(相关)文件。如果你想编辑一个特定的文件,只需在项目面板中选择它并双击;此时,它将在主编辑器窗口中打开进行编辑。在图 2-4 中,可以看到主活动java 文件可供编辑。随着时间的推移,我们将花费大量的时间在主编辑器窗口中涂鸦,但现在,我们只想简单地经历应用开发的基本过程。我们不会添加或修改这个 Java 文件或项目中的任何其他文件。我们会让它保持原样。

创建一个 AVD

我们可以通过在仿真器中运行应用或者将物理 Android 设备插入工作站来测试应用。本节介绍如何设置模拟器。

From Android Studio’s main menu bar, go to ToolsAVD Manager, as shown in Figure 2-5.

![img/340874_4_En_2_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig5_HTML.jpg)
Figure 2-5

菜单栏、工具、AVD 管理器

The AVD manager window will launch. AVD stands for Android Virtual Device; it’s an emulator that runs a specific version of the Android OS which we can use to run the apps on. The AVD manager (shown in Figure 2-6) shows all the defined emulators in our local development environment.

![img/340874_4_En_2_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig6_HTML.jpg)
Figure 2-6

AVD 管理器

As you can see, I already have a couple of emulators; but let’s create another one; to do that, click the “+ Create Virtual Device” button, as shown in Figure 2-6. That action will launch the “Virtual Device Configuration” screen, as shown in Figure 2-7.

![img/340874_4_En_2_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig7_HTML.jpg)
Figure 2-7

虚拟设备配置

Choose the “Phone” category, then choose the device resolution. I chose the Pixel 5.0” 420dpi screen. Click the “Next” button, and we get to choose the Android version we want to run on the emulator; we can do this on the “System Image” screen, shown in Figure 2-8.

![img/340874_4_En_2_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig8_HTML.jpg)
Figure 2-8

虚拟设备配置

I want to use Android 9 (API level 28) or Pie, as some may call it; but as you can see, I don’t have the Pie system image in my machine just yet—when you can see the “download” link next to the Android version, that means you don’t have that system image yet. I need to get the system image for Pie first before I can use it for the AVD; so, click the “download” link. You’ll need to agree to the license agreement before you can proceed. Click “Accept,” then click “Next,” as shown in Figure 2-9.

![img/340874_4_En_2_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig9_HTML.jpg)
Figure 2-9

SDK Quickfix 安装

The download process can take some time, depending on your Internet speed; when it’s done, you’ll get back to the “System Image” selection screen, as shown in Figure 2-10.

![img/340874_4_En_2_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig10_HTML.jpg)
Figure 2-10

虚拟设备配置

As you can see, we can now use Pie as a system image for our emulator. Select Pie, then click “Next.” The next screen shows a summary of our past choices for creating the emulator; the “Verify Configuration” screen is shown next (Figure 2-11).

![img/340874_4_En_2_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig11_HTML.jpg)
Figure 2-11

验证配置

The “Verify Configuration” screen not only shows the summary of our past choices, you can configure some additional functionalities here. If you click the “Show Advanced Settings” button, you can also configure the following:

  • 前后摄像头

  • 仿真网络速度

  • 模拟性能

  • 内部存储的大小

  • 键盘输入(无论启用还是禁用)

When you’re done, click the “Finish” button. When Android Studio finishes provisioning the newly created AVD, we’ll be back in the “Android Virtual Device Manager” screen, as shown in Figure 2-12.

![img/340874_4_En_2_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig12_HTML.jpg)
Figure 2-12

Android 虚拟设备管理器

现在我们可以看到新创建的模拟器(Pixel API 28)。您可以通过单击“Actions”列上的绿色小箭头来启动它——铅笔图标编辑模拟器的配置,绿色箭头启动它。

当模拟器启动时,你会看到 Pixel 手机的图像在桌面上弹出;完全启动需要时间。回到 Android Studio 的主编辑器窗口运行应用。

From the main menu bar, go to RunRun ‘app’, as shown in Figure 2-13.

![img/340874_4_En_2_Fig13_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig13_HTML.jpg)
Figure 2-13

主菜单栏,运行

Android Studio 编译项目;然后它寻找一个连接的(物理的)Android 设备或者一个正在运行的模拟器。我们不久前已经启动了模拟器,所以 Android Studio 应该可以找到它并在模拟器实例中安装应用。

If all went well, you should see the Hello World app that Android Studio scaffolded for us, as shown in Figure 2-14.

![img/340874_4_En_2_Fig14_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_2_Fig14_HTML.jpg)
Figure 2-14

你好世界

关键要点

  • Android 项目几乎总是有一个活动。如果你想从一个基础项目开始,选择一个有“空活动”的项目,然后从那里开始构建。

  • 在创建过程中,请注意您在项目细节中放入的内容;如果您将项目发布到 Google Play,这些项目信息将成为您的应用的一部分,许多人都会看到。

  • 谨慎选择最低 SDK 它会限制你的应用潜在用户的数量。

  • 你可以使用模拟器来运行你的应用,看看它是如何形成的。如果您的系统上启用了 HAXM(模拟器加速器),那么使用模拟器进行测试会好得多;如果您使用的是 Linux,可以使用 KVM 实现加速。

三、Android Studio

  • 在 Android Studio 中处理文件

  • 主编辑器的各个部分

  • 编辑布局文件

  • 项目工具窗口

IDE

From the opening dialog of Android Studio, you can launch the previous project we created. Links to existing projects appear on the left panel of the opening dialog, as shown in Figure 3-1.

![img/340874_4_En_3_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig1_HTML.jpg)
Figure 3-1

欢迎来到安卓 工作室

When you open a project, you’ll see the main editor window, the project panel, and other panels that Android Studio opens by default. An annotated picture of an opened project is shown in Figure 3-2.

| -什么 | **主菜单栏**—你可以通过多种方式导航 Android Studio。通常,完成一项任务有多种方法,但是主要的导航是在主菜单栏中完成的。如果你在 Linux 或 Windows 上,主菜单栏直接位于 IDE 的顶部;如果你在 macOS 上,主菜单栏与 IDE 断开连接(这是所有 macOS 软件的工作方式)。 | | ➋ | **导航条**—该导航条允许您导航项目文件。这是一个水平排列的人字形集合,类似于一些网站上可以找到的面包屑导航。您可以通过导航栏或项目工具窗口打开您的项目文件。 | | ➌ | **工具栏**—这让您可以执行各种操作(例如,保存文件、运行应用、打开 AVD 管理器、打开 SDK 管理器、撤销、重做操作等。). | | -你好 | **主编辑器窗口**—这是最突出的窗口,拥有最多的屏幕空间。在编辑器窗口中,您可以创建和修改项目文件。它会根据您正在编辑的内容改变外观。如果您正在处理程序源文件,此窗口将只显示源文件。当您在编辑布局文件时,您可能会看到原始的 XML 文件或布局的可视化呈现。 | | ➎ | **项目工具窗口**—该窗口显示项目文件夹的内容;您将能够看到并启动您的所有项目资产(源代码、XML 文件、图形等。)从这里。 | | ➏ | **工具窗口条**—工具窗口条沿着 IDE 窗口的周边运行。它包含激活特定工具窗口所需的各个按钮,例如 TODO、Logcat、项目窗口、连接的设备等。 | | -好的 | **显示/隐藏工具窗口**—显示(或隐藏)工具窗口条**。这是个开关。** | | -好的 | **工具窗口**—你会在 Android Studio 工作区的侧面和底部找到工具窗口。它们是二级窗口,让你从不同的角度看项目。它们还允许您访问开发任务所需的典型工具,例如,调试、与版本控制的集成、查看构建日志、检查 Logcat 转储、查看 TODO 项目等等。以下是您可以使用工具窗口做的几件事:您可以通过单击工具窗口栏中的工具名称来展开或折叠它们。还可以拖动、固定、取消固定、附加和分离工具窗口。您可以重新排列工具窗口,但是如果您觉得需要将工具窗口恢复到默认布局,您可以从主菜单栏执行此操作。点击**窗口** ➤ **恢复默认布局**。另外,如果你想自定义“默认布局”,你可以根据自己的喜好重新排列窗口,然后在主菜单栏中点击**窗口** ➤ **将当前布局保存为默认布局**。 |
![img/340874_4_En_3_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig2_HTML.jpg)
Figure 3-2

Android Studio 的主要部分

主编辑

Like in most IDEs, the main editor window lets you modify and work with source files. What makes it stand out is how well it understands Android development assets. Android Studio lets you work with a variety of file types, but you’ll probably spend most of your time editing these types of files:

  • Java 源文件

  • XML 文件

  • 用户界面布局文件

When you’re working with Java source files, you get all the code hinting and completions that you’ve come to expect from a modern editor. What’s more, it gives you plenty of early warnings when something is wrong with your code. Figure 3-3 shows a Java class file opened in the main editor. The class file is an Activity, and it’s missing a semicolon on one of its statements. You could see Android Studio peppering the IDE with (red) squiggly lines which indicates that the class won’t compile.

![img/340874_4_En_3_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig3_HTML.jpg)
Figure 3-3

显示错误指示器的主编辑器

Android Studio 将弯曲的线条放在非常靠近违规代码的地方。正如你在图 3-3 中所看到的,弯弯曲曲的线条被放置在分号应该出现的地方。

编辑布局文件

The screens that the user sees are made up of Activity source files and layout files. The layout files are written in XML. Android Studio, undoubtedly, can edit XML files, but what sets it apart is how intuitively it can render the XML files in a WYSIWYG mode (what you see is what you get). Figure 3-4 shows the two ways you can work with layout files.

![img/340874_4_En_3_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig4_HTML.jpg)
Figure 3-4

设计模式和文本模式编辑布局文件

Figure 3-5 shows the various parts of Android Studio that are relevant when working on a layout file during design mode.

![img/340874_4_En_3_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig5_HTML.jpg)
Figure 3-5

Android Studio 的布局设计工具

  • 视图面板—视图面板包含视图(小部件),您可以将其拖放到设计图面或蓝图面上。

  • 设计表面(Design surface)—它就像是你的屏幕的真实预览。

  • 蓝图面—类似于设计图面,但它只包含 UI 元素的轮廓。

  • 属性窗口—您可以在这里更改 UI 元素(视图)的属性。当您使用属性窗口更改视图的属性时,该更改将自动反映在布局的 XML 文件中。同样,当您对 XML 文件进行更改时,这将自动反映在属性窗口中。

插入待办事项

这可能看起来像是一个微不足道的特性,但是我希望有些人会发现这很有用——这就是我挤在这一节的原因。我们每个人都有一种方法来为我们正在开发的任何应用编写待办事项。写 TODO 项没有太多的麻烦;难的是巩固它们。

In Android Studio, you don’t have to create a separate file to keep track of your TODO list for the app. Whenever you create a comment followed by a “TODO” text, like this:// TODO This is a sample todoAndroid Studio will keep track of all the TODO comments in all of your source files. See Figure 3-6.

![img/340874_4_En_3_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig6_HTML.jpg)
Figure 3-6

所有项目

要查看所有待办事项,请单击工具窗口栏中的“待办事项”选项卡。

如何为代码获得更多的屏幕空间

You can have more screen real estate by closing all Tool Windows. Figure 3-7 shows a Java source file opened in the main editor window while all the Tool Windows are closed. You can collapse any tool window by simply clicking its name, for example, to collapse the Project tool window, click “Project.”

![img/340874_4_En_3_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig7_HTML.jpg)
Figure 3-7

所有工具窗口关闭的主编辑器

You can even get more screen real estate by hiding all the tool window bars, as shown in Figure 3-8.

![img/340874_4_En_3_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig8_HTML.jpg)
Figure 3-8

关闭所有工具窗口并隐藏工具栏的主编辑器

You can get even more screen space by entering “Distraction Free Mode,” as shown in Figure 3-9. You can enter distraction free mode from the main menu bar; click ViewEnter Distraction Free Mode. To exit the mode, click View from the main menu bar, then Exit Distraction Free Mode.

![img/340874_4_En_3_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig9_HTML.jpg)
Figure 3-9

无分心模式

You may also try two other modes that can increase the screen real estate. They’re also found on the View menu from the main menu bar.

  • 呈现方式

  • 全屏幕

项目工具窗口

You can get to your project’s files and assets via the Project tool window, shown in Figure 3-10. It has a tree-like structure, and the sections are collapsible. You can launch any file from this window. If you want to open a file, you simply need to double-click that file from this window.

![img/340874_4_En_3_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig10_HTML.jpg)
Figure 3-10

项目工具窗口

By default, Android Studio displays the Project Files in Android View, as shown in Figure 3-10. The “Android View” is organized by modules to provide quick access to the project’s most relevant files. You change how you view the project files by clicking the down arrow on top of the Project window, as shown in Figure 3-11.

![img/340874_4_En_3_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig11_HTML.jpg)
Figure 3-11

如何在项目工具窗口中改变视图

![img/340874_4_En_3_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_3_Fig12_HTML.jpg)
Figure 3-12

设置/首选项窗口

首选项/设置

如果你想自定义 Android Studio 的行为或外观,可以在它的设置或首选项窗口中进行;如果你在 Windows 或 Linux 上,它被称为设置,如果你在 macOS 上,它被称为偏好设置

For Windows and Linux users, you can get to the Settings window in one of two ways:

  • 从主菜单栏中点击文件设置

  • 使用键盘快捷键Ctrl+Alt+S

For macOS users, you can do it this way:

  • 从主菜单栏中,点击 Android Studio首选项

  • 使用键盘快捷键命令 +

您可以在该窗口中访问各种设置,包括 Android Studio 的外观、在编辑器上使用空格还是制表符、制表符使用多少空格、使用哪个版本控制、下载什么 API、AVD 使用什么系统映像等等。

关键要点

  • 通过增加主编辑器的屏幕空间,你可以看到更多的代码。你可以通过

    • 来做到这一点折叠所有工具窗口

    • 隐藏工具窗口栏

    • 进入无干扰模式

    • 进入全屏模式

  • 您可以通过在项目工具窗口中切换视图来更改查看项目文件的方式。

  • 在 Android Studio 中添加 TODO 项目很容易;只需添加一行注释,后跟一个待办事项文本,如下所示:// TODO 这是我的待办事项列表

四、Android 应用中有什么

我们已经知道如何创建一个基本的项目,我们参观了 Android Studio。在这一章,我们将看看 Android 应用是由什么组成的。

The Android application framework is vast and can be confusing to navigate. Its architecture is different than a desktop or web app, if you’re coming from that background. Learning the Android framework can take a long time; fortunately, we don’t have to learn all of it. We only need a few, and that’s what this chapter is about, those few knowledge areas that we need to absorb so we can build an Android game:

  • Android 项目是由什么组成的

  • Android 组件概述

  • Android 清单文件

  • 意图

Android 项目是由什么组成的

An Android app may look a lot like a desktop app; some may even think of them as miniature desktop apps, but that wouldn’t be correct. Android apps are structurally different from their desktop or web counterparts. A desktop app generally contains all the routines and subroutines it needs in order to function; occasionally, it may rely on dynamically loaded libraries, but the executable file is self-contained. An Android app, on the other hand, is made up of loosely coupled components that communicate to each other using a message-passing mechanism. Figure 4-1 shows the logical structure of an Android app.

![img/340874_4_En_4_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_4_Fig1_HTML.jpg)
Figure 4-1

Android 应用的逻辑表示

图 4-1 中显示的应用是一个大应用——它拥有一切。我们的 app 不会那么大;我们不需要在 Android 中使用所有种类的组件,但是我们需要学习如何使用其中的一些,比如活动和意图。

活动、服务、广播接收者和内容提供者被称为 Android 组件。它们是应用的关键组成部分。它们是对有用事物的高级抽象,如向用户显示屏幕、在后台运行任务、广播事件以便感兴趣的应用可以响应它们,等等。组件是具有非常特定行为的预编码或预构建的类,我们通过扩展它们在应用中使用它们,以便我们可以添加我们的应用特有的行为。

构建一个 Android 应用很像建造一座房子。有些人用传统方式建造房屋;他们组装横梁、支柱、地板等等。他们像工匠一样,用原材料手工制作门和其他配件。如果我们以这种方式构建 android 应用,可能会花费我们很长时间,而且可能会相当困难。对于一些程序员来说,从头构建应用所需的技能可能遥不可及。在 Android 中,应用是使用组件构建的。把它想象成房子的预制构件。零件是预先制造好的,只需要组装就可以了。

一个活动是我们把用户可以看到的东西放在一起的地方。这是一个用户可以专注做的事情。例如,一个活动可以被有目的地使用户能够查看单个电子邮件或填写表单。它是用户界面元素粘合在一起的地方。如图 4-1 所示,在活动内部,有视图片段。视图是用于将内容绘制到屏幕中的类;视图对象的一些例子有按钮文本视图。片段类似于活动,因为它也是一个组合单元,但是更小。像活动一样,它们也可以持有视图对象。大多数现代应用使用片段来解决在多种外形上部署应用的问题。片段可以根据可用的屏幕空间和/或方向打开或关闭。

服务 是允许我们运行程序逻辑而不冻结用户界面的类。服务是在后台运行的代码;当你的应用需要从网上下载文件或者播放音乐时,它们会非常有用。

BroadcastReceivers 允许我们的应用监听来自 Android 系统或其他应用的特定消息——是的,我们的应用可以发送消息并在系统范围内广播。例如,如果你想在电池电量下降到 10%以下时显示警告信息,你可能想使用广播接收器。

ContentProviders 允许我们创建能够与其他应用共享数据的应用。它管理对某种中央数据存储库的访问。一些内容供应器有自己的用户界面,但一些没有。使用这个组件的主要目的是让其他应用能够访问你的应用的数据,而不需要通过一些 SQL 技巧。数据库访问的细节对他们是完全隐藏的(客户端应用)。Android 中的“ContentProvider”应用就是一个预构建应用的例子。

您的应用可能需要一些视觉或听觉资产;这些就是我们在图 4-1 中所说的“资源”的种类。

The AndroidManifest is exactly what its name implies; it’s a manifest and it’s in XML format. It declares quite a few things about the application, like

  • 应用的名称。

  • 当用户启动应用时,哪个活动将首先显示。

  • app 里有什么样的组件。如果它有活动,清单会声明它们——类名和所有的名称。如果应用有服务,它们的类名也将在 manifest 中声明。

  • 这个应用可以做哪些事情?它的权限是什么?允许上网还是相机?它能记录 GPS 位置之类的吗?

  • 它使用外部库吗?

  • 它支持特定类型的输入设备吗?

  • 这种应用需要特定的屏幕密度吗?

正如你所看到的,清单是一个繁忙的地方;有很多事情需要关注。不过这个文件不用太担心。这里的大部分条目都是由 Android Studio 的创建向导自动处理的。为数不多的几个与它交互的场合之一可能是当你需要给你的应用添加权限的时候。

Note

Google Play 从特定设备的可用应用列表中过滤掉不兼容的应用。它使用项目的清单文件来进行过滤。无法满足清单文件中规定的要求的设备将看不到你的应用。

应用入口点

An app typically interacts with a user, and it does so using Activity components. These apps usually have at least these three things:

  1. 1.

    As the activity class of the first screen that users will see

  2. 2.

    The layout file of activity class contains all UI definitions, such as text views and buttons

  3. 3.

    Android Manifest file, which links all project resources and components together

When an application is launched, the Android runtime creates an Intent object and inspects the manifest file. It’s looking for a specific value of the intent-filter node (in the xml file). The runtime is trying to see if the application has a defined entry point, something like a main function. Listing 4-1 shows an excerpt from the Android manifest file.Listing 4-1

AndroidManifest.xml 摘录

如果应用有多个活动,您将在清单文件中看到几个活动节点,每个活动一个节点。定义的第一行有一个名为 android:name 的属性。该属性指向活动的类名。在这个例子中,类的名称是“MainActivity”。

第二行声明了意图过滤器;当你在 intent-filter 节点上看到类似于 Android . intent . action . main 的东西时,这意味着该活动是应用的入口点。当应用启动时,这是将与用户交互的活动。

活动

你可以把一个活动想象成一个屏幕或者一个窗口。这是用户可以与之互动的东西。这是 app 的 UI。Activity 是一个从 android.app.Activity 继承而来的类(以某种方式),但我们通常会扩展 AppCompatActivity 类(而不是 Activity ),这样我们可以使用现代的 UI 元素,但仍然可以让应用在旧版本的 android 上运行;因此,AppCompatActivity 名称中的“Compat”代表“兼容性”

Activity 组件有两个部分,一个 Java 类(或者 Kotlin,如果您选择的是 kot Lin 语言)和一个 XML 格式的布局文件。布局文件是放置所有 UI 定义的地方,例如,文本框、按钮、标签等等。Java 类是您编写 UI 的所有行为部分的地方,例如,当按钮被单击时,当文本被输入到字段中时,当用户改变设备的方向时,当另一个组件向活动发送消息时,等等。

An Activity, like any other component in Android, has a life cycle. Each lifecycle event has an associated method in the Activity’s Java class; we can use these methods to customize the behavior of the application. Figure 4-2 shows the Activity life cycle.

![img/340874_4_En_4_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_4_Fig2_HTML.jpg)
Figure 4-2

活动生命周期

在图 4-2 中,方框显示了活动在特定存在阶段的状态。方法调用的名称嵌入在连接阶段的方向箭头中。

当运行时启动应用时,它调用主活动的 onCreate() 方法,将活动的状态变为“已创建”您可以使用此方法执行初始化例程,如准备事件处理代码等。

活动进行到下一个状态“开始”;此时,用户可以看到活动,但是还不能进行交互。下一个状态是“恢复”;这是应用与用户交互的状态。

如果用户单击任何可能启动另一个活动的东西,运行时将暂停当前活动,并进入“暂停”状态。从那里,如果用户返回到活动,调用 onResume() 函数,活动再次运行。另一方面,如果用户决定打开一个不同的应用,Android 运行时可能会“停止”并最终“破坏”该应用。

意图

If you have an experience with object-oriented programming, you might be used to the idiom of activating an object’s behavior by simply creating an instance of the object and calling its methods—that’s a straightforward and simple way of making objects communicate to each other; unfortunately, Android’s components don’t follow that idiom. The code shown in Listing 4-2, while idiomatically object oriented, isn’t going to work in Android.public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Button b = (Button) findViewById(R.id.button);b.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {new SecondActivity(); // WON****'T WORK}});}}Listing 4-2

激活另一个活动的方式错误

Android 的架构在构建应用的方式上非常独特。它有组件的概念,而不仅仅是简单的对象。Android 使用 Intents 作为其组件通信的方式;它还使用意图在组件之间传递消息。

列表 4-2 不起作用的原因是因为 Android 活动不是一个简单的对象;它是一个组件。您不能为了激活一个组件而简单地实例化它。Android 中的组件激活是通过创建一个 Intent 对象,然后将其传递给想要激活的组件来完成的,在我们现在的例子中,这是一个活动。

There are two kinds of Intents, an explicit Intent and an implicit Intent. For our purposes, we will only need the explicit Intent. Listing 4-3 shows a sample code on how to create an explicit Intent and how to use it to activate another Activity.public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Button b = findViewById(R.id.button);b.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Intent i = new Intent(v.getContext(), SecondActivity.class);****v.getContext().startActivity(i);}});}}Listing 4-3

如何激活另一个活动

看起来我们的示例代码中有很多东西需要解开,但是不要担心,在接下来的章节中,我会用更多的上下文来解释代码。

关键要点

  • Android 应用由松散耦合的组件组成。这些组件通过意图对象进行通信。

  • 一个应用的入口点通常是一个启动器活动。这个启动器活动在应用的 AndroidManifest 文件中指定。

  • 清单文件就像胶水一样将应用的组件粘在一起;应用拥有的、能做的或不能做的一切都反映在清单中。

五、游戏开发入门

据估计,Google Play 上有 280 万个应用(在撰写本文时),其中 30 万个是游戏。那是很多游戏;而且还会增长。考虑到程序员现在已经写了很长时间的游戏,任何想写小说游戏的人都会很难。如果你在寻找新游戏的创意,最好调查一下现有的游戏;看看你能挑选和组合什么样的想法。

In this chapter, we’ll look at some of the popular games in Google Play. We’ll also discuss a high-level overview of what kind of functionalities we’ll need to bake into our game code. We’ll cover the following areas:

  • 游戏性别

  • 游戏引擎

  • 游戏循环

游戏类型快速浏览

如果你在维基百科页面上查看游戏种类,你会看到很多(并且还在增加)游戏种类。游戏类型是一个特定的游戏类别,与游戏的游戏性特征相关。在这里我们不会描述所有的游戏,但是让我们看看一些流行的游戏。

休闲游戏

休闲游戏正迅速成为有经验和无经验玩家的最爱。这些游戏通常有非常简单的规则、玩法和策略程度。你不需要为这些游戏投入额外的时间,也不需要特殊的技能来享受它们;这可能是这些游戏非常受欢迎的原因,因为它们容易学习,并且可以作为一种消遣来玩。

I’m sure you’ve seen some of these games already; you might have played a couple of them. Minion Rush (Figure 5-1) is a runner game, based loosely on the very popular Temple Run, where you guide a figure—in this case, a minion—through hoops and obstacles. Swipe left and the minion goes left, swipe right and it goes right, swipe down to slide, and swipe up to jump; it really is simple. There are many derivatives of this game, but the mechanics rarely changes. Usually, the objective is to run for as long as possible and collect some tokens along the way.

![img/340874_4_En_5_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_5_Fig1_HTML.jpg)
Figure 5-1

宠臣闯

Another example of a casual game is Candy Crush Saga (Figure 5-2). It’s a “match three” game. The gameplay revolves around swapping two adjacent candies among several on the game board so that you can make a row or column of three matching colored candies. By the way, while Candy Crush Saga is considered a casual game, it also belongs to another category called puzzle games; sometimes, a game may belong to more than one category.

![img/340874_4_En_5_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_5_Fig2_HTML.jpg)
Figure 5-2

糖果粉碎传奇

益智游戏

Puzzle or logic games require the player to solve logic puzzles or navigate challenging locations such as mazes. This genre frequently crosses over with adventure, educational, or even casual games. I’m sure you’ve heard of Tetris (Figure 5-3) or Bejeweled; these two are the best examples I can think of for puzzle games.

![img/340874_4_En_5_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_5_Fig3_HTML.jpg)
Figure 5-3

俄罗斯方块

Tetris is largely credited for popularizing the puzzler genre. Tetris, originally, came from the Soviet Union and came to life sometime in 1984. The goal in this game is simple; the player must destroy lines of block before the blocks pile up and reaches the top. A tetromino is the shape of the four connected blocks that falls from the top of the screen and settles at the bottom. There are generally seven kinds of tetrominoes (Figure 5-4). You can guide the tetrominoes as they fall; swiping left or right guides the blocks to the desired location, and (usually) double tapping rotates the tetrominoes.

![img/340874_4_En_5_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_5_Fig4_HTML.jpg)
Figure 5-4

三聚氰胺

Bejeweled (Figure 5-5) is another popular puzzler. The goal is to clear gems of the same color, potentially causing a chain reaction; this is done by swapping one gem with an adjacent gem to form a horizontal or vertical chain of three or more gems of the same color. When chains are formed, the gems disappear and some other gems fall from the top to fill in the gaps—sometimes, “cascades” are triggered when chains are formed by the falling gems.

![img/340874_4_En_5_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_5_Fig5_HTML.jpg)
Figure 5-5

珠光宝气

As you can see from the Tetris and Bejeweled examples, matchers make for good puzzle gameplay; but there are other kinds of puzzlers. Take “Cut the Rope” (Figure 5-6) by ZeptoLab as an example; it’s a physics puzzler. The goal of the game is to feed the candy to “Om Nom” (the little green creature). The candy must be guided toward Om Nom by cutting ropes the candy is attached to; the candy may be blown or put inside bubbles, so it avoids obstacles. Every game object is physically simulated to some degree. The game is powered by Box2D, a 2D physics engine.

![img/340874_4_En_5_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_5_Fig6_HTML.jpg)
Figure 5-6

割断绳子

动作游戏

动作游戏通常需要手眼协调和运动技能。这些游戏以一个控制大部分行动的玩家为中心。这种类型有许多子类别,如平台游戏、射击游戏、战斗游戏、潜行、生存游戏、皇家战役和节奏游戏。

平台玩家通常会有一个角色在环境中跳跃和攀爬。角色通常必须避开敌人和障碍。最受欢迎的平台游戏通常要么在游戏机上发布,要么在个人电脑上发布(马里奥兄弟、大金刚、速成乐队、索尼克狂热、地狱边缘等)。),但一些平台正在进军 Google Play(冒险岛、Blackmoor 2、Dandara 等)。).

射击游戏是动作游戏的另一个流行分支。流派是非常描述性的,你可以从他们的流派中猜出这些游戏是关于什么的,你是对的;你拍东西,人,外星人,怪物,僵尸,等等。玩家使用一系列武器参与行动,行动发生在远处。这种类型通常以暴力游戏和致命武器为特征(有一些明显的例外,如 Splatoon,它有一个非暴力的目标和游戏)。Google Play 中一些受欢迎的射击游戏是《使命召唤移动版》、《堡垒之夜》、《杀手狙击手》、《PUBG 移动版》、《关键行动》、《死亡效果 2》和《巨人 X》,等等。

塔防小游戏

塔防是策略游戏的一个子类。战略游戏注重游戏性,这需要技巧性和仔细的思考和计划,以取得胜利。在大多数策略游戏中,玩家被赋予了“上帝般”的游戏世界视角,因此他们可以直接或间接地控制他们指挥的单位。

塔防游戏 游戏的典型特征是一股邪恶的力量散发出一波又一波的生物、僵尸、气球等等。你的任务是通过建立防御来保卫游戏世界中的一些战略区域(你的塔),无论是炮塔,猴子,枪,等等。这些防御将射击敌人来袭的电波,每杀死一个敌人你就得到一分。这些点数被转换成游戏币,你可以用它来升级你的武器或者购买新武器。

在撰写本文时,Google Play 中流行的塔防游戏有 Bloons TD 6、Defenders 2、Defense Zone 3、Digfender、Element TD、Kingdom Rush 和 Grow Castle 等。

这绝不是游戏类型的概要;这是你能在 Google Play 中找到的游戏种类的一个小列表。如果你在为你的下一个游戏(或第一个游戏)寻找灵感,试着分析性地玩游戏,把娱乐部分放在一边。临床上做。试着感受一下游戏是如何流动的,并试着在脑海中解构它。这可能会给你的游戏一些想法。

游戏引擎

一旦你有了想要制作什么游戏的想法,并且假设你已经通过故事板、模拟图形和绘制一些屏幕线框(你知道,规划阶段)完成了设计游戏的练习,你可能会想花一些时间来组织代码。代码的组织构成了游戏引擎和游戏循环。

At the core of every game is the game engine . This is the code that powers the game; this is the one that handles all the grunt work. A typical game engine will handle the following tasks:

  • 窗口管理

  • 图形渲染

  • 动画

  • 声音的

  • 冲突检出

  • 物理学

  • 线程和内存

  • 建立关系网

  • 输入/输出

  • 仓库

游戏循环是游戏引擎中的一段代码。顾名思义,它是循环的。它重复而永恒地运行;直到玩家退出才会停止。你可能以前听过游戏玩家谈论帧率;你的游戏循环运行的速度会影响游戏的帧率。你的代码在循环中执行的越快,它的响应就越快,游戏就越流畅。

A typical game loop does the following:

  • 获取用户的输入—这是命令解释器;您需要设置代码来监听用户输入,无论是双击、长时间点击、按钮点击、滑动、手势、键盘输入还是其他。这些输入会影响角色和整个游戏,例如,如果游戏是奴才狂奔神庙逃亡,向左、向右、向上或向下滑动会移动逃跑者。

  • 碰撞检测(Collision detection)——这是你追踪角色在游戏世界中移动的地方。当他们到达游戏世界的边缘时,你决定如何处理这个角色。碰撞检测也是测试角色是否撞到障碍物的地方。

  • 绘制并移动背景——这是你绘制游戏世界的地方,至少玩家可以看到其中的一部分。

  • 移动字符作为对用户输入的响应。

  • 当角色或游戏世界中发生有趣的事件时,播放音效。

  • 播放背景音乐—这和播放音效不一样。背景音乐贯穿整个关卡,所以它需要是连续的。这就是你的线程知识派上用场的地方。

  • 追踪玩家的分数—随着游戏的进行,玩家会累积分数。您可以使用本地存储器在本地存储游戏统计数据。如果你需要在云中更新排行榜,你需要使用 Android 的网络 API。跟踪玩家的分数可能还包括显示一个专门的屏幕(Android 中的一个活动或一帧),在那里记录分数。

这不是您需要在代码中解决的问题的详尽或确定的列表,但这是一个开始。你在游戏循环和游戏引擎中需要做的事情的数量会根据游戏的复杂程度而增减。

关键要点

  • 已经有无数的游戏了。你的下一个游戏灵感可能来自现有的游戏。尝试分析性地、临床地、脱离娱乐性地玩游戏。解剖它们以了解它们是如何流动的。

  • 游戏体验的流畅程度在很大程度上取决于你在游戏循环中所做的事情。循环执行得越快,你的游戏就越快。

六、构建 Crazy8 游戏

学习游戏编程的最好方法是开始编写一个。在这一章,我们将建立一个简单的纸牌游戏,Crazy8。Crazy8 是一个受欢迎的游戏,无论是实际的纸牌游戏还是电子游戏。如果你在 Google Play 上搜索疯狂 8,会出现很多选择。

We’ll walk through the process of how to build a simple turn-based card game like Crazy Eights. The rules of this game are simple, and it doesn’t involve a lot of moving parts; that’s not to say it won’t be challenging to build. There are plenty of challenges ahead, especially if this is the first time you’ll build a game. In this chapter, we’ll discuss the following:

  • 如何使用自定义视图

  • 如何构建闪屏

  • 绘制图形

  • 处理屏幕方向

  • 全屏显示

  • 从图形绘制按钮

  • 处理触摸事件

  • Crazy8 分游戏的机制

  • Crazy8 分游戏所需的所有逻辑

在这一章中,我将展示构建游戏所需的代码片段,以及程序在特定开发阶段的样子。理解和学习本章中的编程技术的最好方法是下载游戏的源代码,并在阅读本章的时候保持它在 Android Studio 中打开。如果您想继续学习并自己构建项目,最好将本章的源代码放在手边,这样您就可以根据需要复制和粘贴特定的代码片段。

基本游戏

一副 52 张牌,两个最多五个玩家可以玩 Crazy8;在我们的例子中,只有两个玩家——一个人类玩家和一个电脑玩家。当然,您可以构建这个游戏来容纳更多的玩家,但是将玩家限制为一个人类玩家会使编程简单很多。

七张牌分发给两个玩家,一次一张;剩余牌组的顶牌面朝上放置,开始弃牌堆。

这个游戏的目标是成为第一个扔掉手中牌的玩家。有相同花色或号码的牌可以打到中间。按照惯例,庄家左边的玩家先走,但是在我们的例子中,人类玩家将简单地开始。因此,人类玩家(我们)看我们的牌,如果我们有一张牌与花色或弃牌堆中顶牌的号码匹配,我们就可以出那张牌。如果我们不能出任何一张牌,我们将从剩余的一副牌中抽取(最多三张牌);如果我们仍然不能玩,我们通过。如果我们抽到了一张可以用的牌,那就用这张牌。8(任何花色)都是百搭牌,可以在任何牌上使用。一个 8 的玩家将陈述或选择一个花色,下一个玩家必须在所选的花色中出一张牌。当其中一名玩家能将最后一张牌打到中间时,这一轮就结束了。如果没有玩家可以玩一手牌,这一轮也可以结束。

分数的计算方法是,在一轮结束时,奖励玩家手中剩余牌的点数;例如,如果计算机在这一轮赢了我们,我们剩下红心 9 和黑桃 3,我们的得分将是 12。

当其中一名玩家达到 100 分或更多时,游戏结束。得分最低的玩家获胜。

计划的关键部分

To build the game, the key things to figure out are the following:

  • 如何抽卡 —Android 没有内置可以显示卡片的视图对象;我们得自己画。

  • 如何处理事件—在程序的某些部分,我们可以使用 Android 的传统事件处理,我们只需将一个侦听器附加到视图对象,但也有一些部分,我们需要判断用户操作是否落在我们绘制按钮的区域。

  • 让游戏全屏。

还有其他技术挑战,但是前面的列表是一个很好的起点。

准确地说,我们将主要用两个活动和两个视图,两个自定义视图来构建游戏应用。为了说明个别卡,卡牌组,和弃牌堆,我们需要做 2D 图纸。Android SDK 没有现成的视图对象来满足我们的需求。这不像我们可以从调色板中拖放一个卡对象,然后从那里开始;因此,我们必须构建自己的自定义视图对象。 android.view.View 是绘图和处理输入的基础类;我们将使用这个类来绘制卡片、甲板和游戏所需的其他东西,比如记分牌。我们可以使用 SurfaceView 类作为我们的 2D 绘图的基类,由于性能优势(它与 SurfaceView 处理线程的方式有关),这将是一个更好的选择,但是 SurfaceView 需要更多的编程工作。因此,让我们使用更简单的视图对象。我们的游戏反正不需要在动画上发疯。我们应该对自己的选择满意。

自定义视图和活动

在我们过去的项目中,您可能还记得 Activity 组件用于显示 UI,它有两个部分——代码隐藏的 Java 程序和 XML 文件,在 XML 文件中,UI 被构造为 XML 中定义的视图对象的嵌套排列。这对于应用来说没问题,但是我们需要从图像文件中渲染自定义绘图,所以这种技术行不通。我们要做的是创建一个自定义视图对象,我们将在其中绘制我们需要的所有内容,然后我们将活动的内容视图设置为该自定义视图。我们可以通过创建一个扩展 android.view.View 的 Java 类来创建自定义视图。

Assuming you’ve already created a project with an empty Activity, like how we did it in the previous chapters, you can add a class to your project by using the context menu in the Project tool window. Right-click the package name, then click New ➤ Java, as shown in Figure 6-1.

![img/340874_4_En_6_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig1_HTML.jpg)
Figure 6-1

向项目中添加一个类

Type the name of the class, then hit ENTER. I named the class SplashScreen, and its contents are shown in Listing 6-1.import android.content.Context;import android.view.View;public class SplashScreen extends View {public SplashScreen(Context context) {super(context);}}Listing 6-1

SplashScreen.java

This is the starting point on how to create a custom View object. We can associate this View to our MainActivity by setting the MainActivity’s View to SplashScreen, as shown in Listing 6-2.import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);SplashScreen splash = new SplashScreen(this);setContentView(splash);}}Listing 6-2

主要活动

在屏幕上画画

To draw on the screen, we can override the onDraw() method of the View object. Let’s modify the SplashScreen class to draw a simple circle on the screen. The code is shown in Listing 6-3.import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.view.View;import android.graphics.Color;public class SplashScreen extends View {private Paint paint;private int cx;private int cy;private float radius;public SplashScreen(Context context) {super(context);paint = new Paint(); ❶paint.setColor(Color.GREEN);paint.setAntiAlias(true);cx = 200; cy = 200; radius = 50; ❷❸❹}@Overrideprotected void onDraw(Canvas canvas) { ❺super.onDraw(canvas);canvas.drawCircle(cx,cy,radius,paint); ❻}}Listing 6-3

在屏幕上画画

| -好的 | Paint 对象决定了圆形在画布上的外观。 | | 我的心脏 | cx,cy,和半径变量保存我们将要画圆的大小和位置。 | | (一) | 当 Android 运行时调用 **onDraw** 方法时,一个画布对象被传递给该方法,我们可以用它在屏幕上绘制一些东西。 | | ❻ | **drawCircle** 是 Canvas 对象可用的绘图方法之一。 |

这里重要的一点是要记住,如果你想在屏幕上画东西,你需要在视图对象的 onDraw() 方法上完成。onDraw()的参数是一个画布对象,视图可以用它来绘制自己。画布定义了画线、位图、圆(如我们这里的例子)和许多其他图形元素的方法。覆盖 onDraw()是创建自定义用户界面的关键。

此时,您可以运行该示例。我不会再截屏了,因为这只是一个不起眼的圈子。

处理事件

The touchscreen is the most common type of input for game apps, so that’s what we’ll use. To handle touch events, we will override the onTouchEvent() callback of our SplashScreen class. Listing 6-4 shows the basic structure and a typical code for handling touch events. You can put the onTouchEvent() callback anywhere inside the SplashScreen program.public boolean onTouchEvent(MotionEvent evt) { ❶int action = evt.getAction(); ❷switch(action) { ❸case MotionEvent.ACTION_DOWN:Log.d(TAG, "Down"); ❹break;case MotionEvent.ACTION_UP:Log.d(TAG, "Up");break;case MotionEvent.ACTION_MOVE:Log.d(TAG, "Move");cx = (int) evt.getX(); ❺cy = (int) evt.getY(); ❻break;}invalidate(); ❼return true;}Listing 6-4

处理触摸事件

| -好的 | 当触摸、拖动或滑动屏幕时,Android 运行时调用 **onTouchEvent** 方法。 | | ❷ | **evt.getAction()** 返回一个 int 值,告诉我们用户采取的动作,是向下滑动、向上滑动,还是只是触摸。在我们的例子中,我们只是观察任何移动。 | | -你好 | 我们可以在动作上使用一个简单的开关结构来路由程序逻辑。 | | (a) | 我们现在不需要处理 down 操作,但是我正在记录它。 | | (一) | 这将获得触摸发生位置的 x 坐标。 | | ❻ | 这得到了 y 坐标。我们正在更新我们的 **cx** 和 **cy** 变量(圆圈的位置)的值。 | | ❼ | 这将导致 Android 运行时调用 **onDraw** 方法。 |

In Listing 6-4, all we did was capture the location where the touch happened. Once we extracted the x and y coordinates of the touch, we assigned those coordinates to our cx and cy member variables, then we called invalidate(), which forced a redraw of the View class. Each time a redraw is forced, the runtime will call the onDraw() method, which then draws the circle (again), but this time using the updated location of cx and cy (variables that hold the location of our small circle drawing). Listing 6-5 shows the completed code for SplashScreen.java.import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.graphics.Color;public class SplashScreen extends View {private Paint paint;private int cx;private int cy;private float radius;private String TAG = getContext().getClass().getName();public SplashScreen(Context context) {super(context);paint = new Paint();paint.setColor(Color.GREEN);paint.setAntiAlias(true);cx = 200;cy = 200;radius = 50;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);cx = cx + 50;cy = cy + 25;canvas.drawCircle(cx,cy,radius,paint);}public boolean onTouchEvent(MotionEvent evt) {int action = evt.getAction();switch(action) {case MotionEvent.ACTION_DOWN:Log.d(TAG, "Down");break;case MotionEvent.ACTION_UP:Log.d(TAG, "Up");break;case MotionEvent.ACTION_MOVE:Log.d(TAG, "Move");cx = (int) evt.getX();cy = (int) evt.getY();break;}invalidate();return true;}}Listing 6-5

闪屏完成代码

如果你运行这段代码,它所做的就是在屏幕上画一个绿色的小圆圈,等待你触摸屏幕。每触摸一次屏幕,圆圈就会移动到触摸过的位置。

这不是我们游戏的一部分。这是某种练习代码,所以我们可以热身到实际的游戏代码。既然我们对如何在屏幕上绘制东西以及如何处理基本的触摸事件有了一些了解,让我们继续游戏代码。

带有标题图形的闪屏

We don’t want to show just a small dot to the user when the game is launched; instead, we want to display some title graphic. Some games probably will show credits and some other info, but we’ll keep ours simple. We’ll display the title of the game using a simple bitmap. Before you can do this, you need to put the graphic file in the app/res/drawable folder of the project. A simple way to do that is to use the context menu; right-click the app/res/drawableReveal in Finder (on macOS); if you’re on Windows, this will read Show in Explorer. The dialog window in macOS is shown in Figure 6-2.

![img/340874_4_En_6_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig2_HTML.jpg)
Figure 6-2

res ➤ drawable ➤揭示在寻找

当您启动文件管理器时,您可以将图形文件放在那里。drawable 文件夹是图形资源通常存储的地方。

To load the bitmapimport android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.content.Context;import android.graphics.Canvas;import android.view.View;public class SplashScreen extends View {private Bitmap titleG;public SplashScreen(Context context) {super(context);titleG = BitmapFactory.decodeResource(getResources(),R.drawable.splash_graphic); ❶}protected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawBitmap(titleG, 100, 100, null); ❷}}Listing 6-6

加载位图

| -好的 | 使用 BitmapFactory 从 drawable 文件夹中解码图形资源。这将位图加载到内存中,稍后我们将使用它在屏幕上绘制图形。 | | ❷ | Canvas 的 **drawBitmap** 方法将位图绘制到屏幕上。 |

Our splash screen is shown in Figure 6-3.

![img/340874_4_En_6_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig3_HTML.jpg)
Figure 6-3

启动画面

The screen doesn’t look bad, but it’s skewed to the left. That’s because we hardcoded the drawing coordinates for the bitmap. We’ll fix that in a little while; first, let’s take care of that application title and the other widgets on top of the screen. Let’s maximize the screen space for our game. Open MainActivity.java and make the changes shown in Listing 6-7.public class MainActivity extends AppCompatActivity {private View splash;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);splash = new SplashScreen(this);splash.setKeepScreenOn(true);setContentView(splash);}private void setToFullScreen() { ❶splash.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE| View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);}@Overrideprotected void onResume() {super.onResume();setToFullScreen(); ❷}}Listing 6-7

全屏显示应用

| -好的 | 创建一个新的方法,我们可以把必要的代码,使应用全屏。 | | ❷ | 在 **onResume** 回调上调用 **setFullScreen** 方法。onResume()在 UI 对用户可见之前被调用;所以,这是一个放置全屏代码的好地方。在应用的生命周期中,可能会多次调用这个生命周期方法。 |

视图对象的setSystemUiVisibility方法是向用户显示更加身临其境的屏幕体验的关键。您可以尝试系统 UI 标志的多种组合。你可以在这里的文档页面上阅读更多关于它们的信息:【https://bit.ly/androidfullscreen】

Next, we take care of the orientation. We can choose to let users play the game either in portrait or landscape mode, but that means we need to write more code to handle the orientation change; we won’t do that here. Instead, we will fix our game in portrait mode. This can be done in the AndroidManifest file. You need to edit the manifest file to reflect the modifications shown in Listing 6-8. To open the manifest file, double-click the file from the Project tool window, as shown in Figure 6-4.

![img/340874_4_En_6_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig4_HTML.jpg)
Figure 6-4

雄激素类化合物

Listing 6-8

文件

| -好的 | 这会将屏幕方向固定为纵向。 | | ❷ | 当切换软件键盘时,这条线防止屏幕方向改变。 |

现在,我们已经确定了方向,全屏排序,我们可以将图形居中。

To center the title graphic, we need the actual width of the screen and the actual width of the title graphic. The width of the screen minus the width of the title graphic divided by two should give us the location where we can start drawing the title graphic such that it’s centered on the screen. Listing 6-9 shows the changes we need to make in SplashScreen to make all these happen.public class SplashScreen extends View {private Bitmap titleG;private int scrW; private int scrH; ❶public SplashScreen(Context context) {super(context);titleG = BitmapFactory.decodeResource(getResources(),R.drawable.splash_graphic);}@Overridepublic void onSizeChanged (int w, int h, int oldw, int oldh){super.onSizeChanged(w, h, oldw, oldh);scrW = w; scrH = h; ❷}protected void onDraw(Canvas canvas) {super.onDraw(canvas);int titleGLeftPos = (scrW - titleG.getWidth())/2; ❸canvas.drawBitmap(titleG, titleGLeftPos, 100, null); ❹}}Listing 6-9

将标题图形居中

| -好的 | 让我们声明一些变量来保存屏幕的尺寸。 | | ❷ | 一旦 Android 运行时能够计算出屏幕的实际尺寸,就会调用 **onSizeChanged()** 方法。我们可以从这里获取屏幕的实际宽度和高度,并将它们分配给我们的成员变量,这些变量将保存屏幕高度和屏幕宽度的值。 | | -你好 | **title.getWidth()** 得到我们的标题图形的宽度,从屏幕宽度(在 onSizeChanged 期间获取的)中减去它,然后除以 2。这应该使图形居中。 | | (a) | 现在我们可以画出适当居中的图形。 |

Figure 6-5 shows our app, as it currently stands.

![img/340874_4_En_6_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig5_HTML.jpg)
Figure 6-5

居中图形和全屏屏幕

添加播放按钮

我们将在闪屏上添加一个按钮,这样用户就可以开始游戏了。我们将只添加一个“播放”按钮;我们不会添加“退出”按钮。我们可以添加一个退出按钮,但我们没有这样做,因为它不符合 Android 应用的惯例。毕竟,我们的游戏还是一个安卓应用。它需要像大多数 Android 应用一样运行,并且大多数 Android 应用没有退出按钮。一个应用通常会被启动、使用、暂停和终止,而 Android 操作系统已经有了终止应用的方法。

我们不能从面板中拖放按钮视图对象,因为我们正在使用自定义视图。我们必须像绘制标题图形一样绘制按钮。因此,在 SplashScreen 类中,添加按钮的声明语句,然后通过使用 SplashScreen 的构造函数中的 BitmapFactory 加载图像来初始化它。

我为按钮准备了两个图形;一个图形显示按钮未被按下时的常规外观,另一个图形显示按钮被按下时的图像。这只是给用户的一个小的视觉提示,这样当他们点击按钮时,就会发生一些事情。这也意味着我们需要处理按钮状态。绘制按钮的实际图像将发生在 onDraw() 方法中;我们需要一种方法来路由程序逻辑是绘制按钮的常规状态还是按下状态。

Another task we need to manage is detecting the button click. Our button isn’t the regular button; it’s a drawn bitmap on the screen. We cannot use findViewbyId then bind the reference to an event listener. Instead, we need to detect if a touch happens within the bounds of the drawn button and write the appropriate code. Listing 6-10 shows the annotated code for loading, drawing, and managing the state of the Play button. The other code related to the display and centering of the title graphic has been removed, so only the code relevant for the button is displayed.import android.view.MotionEvent;public class SplashScreen extends View {private Bitmap playBtnUp; ❶private Bitmap playBtnDn;private boolean playBtnPressed; ❷public SplashScreen(Context context) {super(context);playBtnUp = BitmapFactory.decodeResource(getResources(), R.drawable.btn_up); ❸playBtnDn = BitmapFactory.decodeResource(getResources(), R.drawable.btn_down);}@Overridepublic void onSizeChanged (int w, int h, int oldw, int oldh){super.onSizeChanged(w, h, oldw, oldh);scrW = w;scrH = h;}public boolean onTouchEvent(MotionEvent event) {int evtAction = event.getAction();int X = (int)event.getX();int Y = (int)event.getY();switch (evtAction ) {case MotionEvent.ACTION_DOWN:int btnLeft = (scrW - playBtnUp.getWidth())/2; ❹int btnRight = btnLeft + playBtnUp.getWidth();int btnTop = (int) (scrH * 0.5);int btnBottom = btnTop + playBtnUp.getHeight();boolean withinBtnBounds = X > btnLeft && X < btnRight &&Y > btnTop &&Y < btnBottom; ❺if (withinBtnBounds) {playBtnPressed = true; ❻}break;case MotionEvent.ACTION_MOVE:break;case MotionEvent.ACTION_UP:if (playBtnPressed) {// Launch main game screen}playBtnPressed = false;break;}invalidate();return true;}protected void onDraw(Canvas canvas) {super.onDraw(canvas);int playBtnLeftPos = (scrW - playBtnUp.getWidth())/2;if (playBtnPressed) { ❼canvas.drawBitmap(playBtnDn, playBtnLeftPos, (int)(scrH *0.5), null);} else {canvas.drawBitmap(playBtnUp, playBtnLeftPos, (int)(scrH *0.5), null);}}}Listing 6-10

显示和管理播放按钮状态

| -好的 | 它定义变量来保存按钮图像的位图。 | | ❷ | 我们将使用被压缩的布尔变量作为开关;如果这是假的,这意味着按钮没有被按下,我们将显示常规的按钮图形。如果为真,我们将显示处于按下状态的按钮图形。 | | -你好 | 让我们从图形文件中加载按钮位图,就像我们对标题图形所做的那样。 | | (a) | 变量 **btnLeft、btnTop、btnBottom** 和 **btnRight** 是按钮边界的屏幕坐标。 | | (一) | 如果触摸动作的 X 和 Y 坐标在按钮边界内,该表达式将返回 **true** 。 | | ❻ | 如果按钮在边界内,我们将 **btnPressed** 变量设置为 true。 | | ❼ | 在 **onDraw** 期间,我们可以根据**Bt pressed**变量的值显示适当的按钮图形。 |

Figure 6-6 shows our app with the centered title graphic and Play button.

![img/340874_4_En_6_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig6_HTML.jpg)
Figure 6-6

带播放按钮的闪屏

The play button is centered vertically on the screen; if you want to adjust the vertical location of the button, you can change it in the onDraw method; it’s the third parameter of the drawBitmap method, as shown in the following snippet.canvas.drawBitmap(playBtnUp, playBtnLeftPos, (int)(scrH *0.5), null);

表达式(int)(scrrh * 0.5)的意思是得到检测到的屏幕高度的中点值;将屏幕高度乘以 50%得到中点。

启动游戏屏幕

我们将启动游戏屏幕作为另一个活动,这意味着我们需要创建另一个活动和另一个视图类。

To add another Activity, right-click the package name in the Project tool window, then click NewActivityEmpty Activity, as shown in Figure 6-7.

![img/340874_4_En_6_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig7_HTML.jpg)
Figure 6-7

新空活动

Then, fill up the Activity name, as shown in Figure 6-8.

![img/340874_4_En_6_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig8_HTML.jpg)
Figure 6-8

配置活动

Next, add a new class to the project. You can do this by right-clicking the package name and choosing New ➤ Java Class, as shown in Figure 6-9.

![img/340874_4_En_6_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig9_HTML.jpg)
Figure 6-9

新的 Java 类

Name the class CrazyEightView, edit it, and make it extend the View class, just like our SplashScreen class. Listing 6-11 shows the code for CrazyEightView.import android.content.Context;import android.graphics.Canvas;import android.view.View;public class CrazyEightView extends View {public CrazyEightView(Context context) {super(context);}protected void onDraw(Canvas canvas) {super.onDraw(canvas);}}Listing 6-11

crazy wiec . Java 版

Next, we fix the second Activity class (CrazyEight class) to occupy the whole screen, much like our MainActivity class. Listing 6-12 shows the code for CrazyEightActivity.public class CrazyEight extends AppCompatActivity {private View gameView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);gameView = new CrazyEightView(this); ❶gameView.setKeepScreenOn(true);setContentView(gameView); ❷}private void setToFullScreen() { ❸gameView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE| View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);}@Overrideprotected void onResume() {super.onResume();setToFullScreen(); ❹}}Listing 6-12

CrazyEightActivity

| -好的 | 创建 CrazyEightView 类的实例并传递当前上下文。 | | ❷ | 将此活动的视图设置为我们的自定义视图(CrazyEightView)。 | | -你好 | 下面是让整个视图占据整个屏幕的代码,和我们之前做的一样。 | | (a) | 我们在 **onResume** 回调中调用 **setFullScreen** ,因为我们希望它正好在用户看到屏幕之前运行。 |

现在我们已经有了一个实际游戏将要进行的活动,我们可以在 SplashScreen 中放入代码来启动我们的第二个活动(CrazyEight)。

Android 使用 Intent 对象进行组件激活,发起一个活动需要组件激活。Intents 还有许多其他用途,但我们不会在这里介绍。我们只需输入必要的代码来启动我们的 CrazyEight 活动。

Go back to SplashScreen’s onTouchEvent, specifically the MotionEvent.ACTION_UP branch. In Listing 6-10, find the code where we made the comment // Launch main game screen, as shown in the snippet in Listing 6-13.case MotionEvent.ACTION_UP:if (playBtnPressed) {// Launch main game screen}playBtnPressed = false;break;Listing 6-13

代码片段 MotionEvent。ACTION_UP

We will replace that comment with the code that will actually launch the CrazyEight Activity, but first, we’ll need to add a member variable to SplashScreen that will hold the current Context object. Just add a variable to the SplashScreen class like this:private Context ctx;Then, in SplashScreen’s constructor, add this line:ctx = context;

我们需要一个对当前上下文的引用,因为我们需要将它作为参数传递给 Intent 对象。

Now, write the Intent code inside the ACTION_UP branch of SplashScreen’s onTouchEvent handler so that it reads like Listing 6-14.case MotionEvent.ACTION_UP:if (playBtnPressed) {Intent gameIntent = new Intent(ctx, CrazyEight.class);ctx.startActivity(gameIntent);}playBtnPressed = false;break;Listing 6-14

意图发起疯狂活动

开始游戏

游戏从洗牌开始,给我们的对手(计算机)和用户发七张牌。之后,我们将剩余牌组的顶牌面朝上,开始弃牌。

对于这些东西,我们需要一些东西来表示一张卡(我们将为此使用一个类);我们需要表示人类玩家手里和计算机手里的牌的集合;我们还需要表示弃牌堆。

To represent a single card, let’s create a new class and add it to the project. Right-click the project’s package name in the Project tool window, as shown in Figure 6-10.

![img/340874_4_En_6_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig10_HTML.jpg)
Figure 6-10

添加新类别

Name the new class “Card” and modify the contents, as shown in Listing 6-15.import android.graphics.Bitmap;public class Card {private int id;private int suit;private int rank;private Bitmap bmp;private int scoreValue;public Card(int newId) {id = newId;}public void setBitmap(Bitmap newBitmap) {bmp = newBitmap;}public Bitmap getBitmap() {return bmp;}public int getId() {return id;}}Listing 6-15

卡类

Our Card class is a basic POJO. It’s meant to represent a single card in the deck. The constructor takes an int parameter, which represents a unique id for the card. We’ve assigned an id to all the cards, from the deuce of Diamonds to the Ace of Spades. The four suits (Diamonds, Clubs, Hearts, and Spades) are given base values, as follows:

  • 钻石(100)

  • 俱乐部(200)

  • 红心大战(300)

  • 黑桃(400)

花色中的每张牌都有一个等级,即该牌的数值。最低等级是 2(平手),最高等级是 14(王牌)。卡对象的 id 将被计算为套装的基础值加上卡的等级;因此,方块 2 是 102,梅花 3 是 203,以此类推。

你可以从各种地方获得你的卡片图像,比如【www.shutterstock.com】【www.acbl.mybigcommerce.com】(美国契约桥牌联盟),如果你愿意,甚至可以自己创建图像。无论你从哪里得到你的卡片图像文件,你必须根据我们如何分配基础值和等级来命名它们。所以,方块 2 是“牌 102”,方块 a 是“牌 114”,黑桃 a 是“牌 414”。

Card 类也有针对图像文件的 get()和 set()方法,这样我们就可以获取和设置特定卡片的位图图像。

Now that we have a POJO for the Card, we need to build a deck of 52 cards; to do this, let’s create a new method in the CrazyEightView class and call it initializeDeck() ; the annotated code is shown in Listing 6-16.private void initializeDeck() {for (int i = 0; i < 4; i++) { ❶for (int j = 102; j < 115; j++) { ❷int tempId = j + (i*100); ❸Card tempCard = new Card(tempId); ❹int resourceId = getResources().getIdentifier("card" + tempId, "drawable",ctx.getPackageName()); ❺Bitmap tempBitmap = BitmapFactory.decodeResource(ctx.getResources(),resourceId);scaledCW = (int) (scrW /8); ❻scaledCH = (int) (scaledCW *1.28);Bitmap scaledBitmap = Bitmap.createScaledBitmap(tempBitmap,scaledCW, scaledCH, false);tempCard.setBitmap(scaledBitmap);deck.add(tempCard); ❼}}}Listing 6-16

初始化甲板

| -好的 | 我们循环看花色(方块、梅花、红心和黑桃)。 | | ❷ | 然后,我们遍历当前套装中的每个等级。 | | -你好 | 让我们得到一个唯一的身份。这个 id 现在将是 j 的当前值**+I 的当前值**乘以 100。由于我们将我们的卡片图像命名为 card102.png 的**直到 card413.png 的**,我们应该能够使用 j + (i * 100) 表达式遍历所有的图像文件。******** | | (a) | 我们创建一个 Card 对象的实例,将一个惟一的 id 作为参数传入。这个唯一的 id 与我们对卡片图像文件的命名约定一致。 | | (一) | 让我们基于 **tempId** 为图像创建一个资源 id。 | | ❻ | 我们将卡片的宽度缩放到屏幕宽度的 1/8,这样我们可以水平放置七张卡片。变量 **scaledCW** 和 **scaledCH** 应该被声明为卡类中的成员变量。 | | ❼ | 现在,我们将 Card 对象添加到 **dec** 对象中,这是一个应该声明为成员变量的 ArrayList 对象。可以这样为卡牌添加一个声明:Listdeck = new ArrayList(); |

现在我们有了一副牌,我们需要想办法把牌发给玩家。我们需要代表人类玩家的手和电脑玩家的手。因为我们已经使用了数组列表来表示卡片组,所以让我们也使用数组列表来表示双手(人类玩家和计算机)。我们还将使用一个数组列表来表示丢弃堆。

Add the following member variable declarations to the CrazyEightView class:private List playerHand = new ArrayList<>();private List computerHand = new ArrayList<>();private List discardPile = new ArrayList<>();Now let’s add the method to deal the cards to the human player and the computer player; Listing 6-17 shows the code for the method dealCards() .private void dealCards() {Collections.shuffle(deck,new Random());for (int i = 0; i < 7; i++) {drawCard(playerHand);drawCard(computerHand);}}Listing 6-17

将牌发给双方玩家

该方法中的第一条语句是一个 Java 实用函数,用于随机化列表中元素的顺序;这应该满足我们洗牌的要求。

The for-loop comes around seven times (we want to give each hand seven cards), and inside the loop, we call the drawCard() method twice, once for each of the players; the code for this method is shown in Listing 6-18.private void drawCard(List hand) { ❶hand.add(0, deck.get(0)); ❷deck.remove(0); ❸if (deck.isEmpty()) { ❹for (int i = discardPile.size()-1; i > 0 ; i--) {deck.add(discardPile.get(i));discardPile.remove(i);Collections.shuffle(deck,new Random());}}}Listing 6-18

drawCard()方法

| -好的 | 人类玩家和计算机都会调用 **drawCard()** 方法。为了调用该方法,我们传递一个列表对象作为参数;这个论点代表了我们应该把牌发给哪一手。 | | ❷ | 我们在**牌组**的顶部拿到卡片,并将其添加到**手**对象中。 | | -你好 | 接下来,拿到卡顶的卡并不会自动移除;所以,我们把它从甲板上拿走。当一张牌发给一个玩家时,它应该被从牌堆中取出。 | | (a) | 当这副牌是空的,我们从弃牌堆里拿回牌,然后重新洗牌。 |

初始化牌组和发牌的方法应该放在 onSizeChanged() 方法中。一旦运行时计算出屏幕尺寸,就调用此方法,如果由于某种原因,屏幕尺寸发生变化,随后可能会调用此方法。屏幕的方向总是从纵向开始,由于我们对清单文件进行了修改,使方向总是保持纵向,因此很有可能只调用一次 onSizeChanged() 方法(至少在应用的生命周期内)。因此,这似乎是放置游戏初始化方法的好地方,比如 initializeDeck()drawCard()

展示卡片

Our next tasks are to display the cards in the game, namely:

  • 我们手中的牌

  • 电脑的手

  • 废弃堆

  • 面朝上的卡片

  • 分数

Figure 6-11 shows the layout of cards in the game.

![img/340874_4_En_6_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig11_HTML.jpg)
Figure 6-11

游戏应该是什么样子

电脑的手朝下;我们不应该看到他们;所以,我们需要做的就是抽出卡片的背面。我们可以通过迭代计算机的手来做到这一点,对于列表中的每一项,我们绘制卡片的背面。我们在卡片的背面有一个图形文件。我们将简单地用绘制其他图形的方法来绘制它。

Before we do any further drawing, we’ll need to establish some scale and get the density of the device’s screen. We can do that with the following code:scale = ctx.getResources().getDisplayMetrics().density;We’ll put that in the constructor of the CrazyEightView class. We need to define the scale as a member variable as well. So, somewhere in the top level of the class, define the scale as a variable, like this:private float scale;

我们将使用比例变量作为我们绘图的比例因子;这样,如果移动设备的密度发生变化,我们的显卡仍将保持比例。

Now we can draw the computer’s hand. Listing 6-19 shows that code.public void onSizeChanged (int w, int h, int oldw, int oldh){// other statementsscaledCW = (int) (scrW /8); ❶scaledCH = (int) (scaledCW 1.28); ❷Bitmap tempBitmap = BitmapFactory.decodeResource(ctx.getResources(),R.drawable.card_back); ❸cardBack = Bitmap.createScaledBitmap(tempBitmap, ❹scaledCW, scaledCH, false);}protected void onDraw(Canvas canvas) {for (int i = 0; i < computerHand.size(); i++) {canvas.drawBitmap(cardBack, ❺i(scale5),paint.getTextSize()+(50scale),null);}}Listing 6-19

画出电脑的手

| -好的 | 我们不会使用卡图形的实际大小;我们希望按照屏幕密度的比例来绘制它们。变量 **scaledCW** 和 **scaledCH** (缩放后的卡片高度和宽度)将用于绘制缩放后的位图。这些被定义为成员变量,因为我们需要在 **onSizeChanged()** 方法之外访问它们。 | | ❷ | 我们希望缩放后的高度比缩放后的卡片宽度长 1.28 倍。 | | -你好 | 像我们以前加载位图一样加载位图。 | | (a) | 现在我们从已经加载的 tempBitmap 创建一个缩放位图。 | | (一) | 我们正在绘制计算机手中的所有卡片,一次一个图形,并且相隔 5 个像素(水平方向),以便它们重叠;我们还从屏幕顶部绘制了卡片的 50 个缩放因子,加上 Paint 对象的默认文本大小。 |

In bullet number ❺, we referred to a Paint object. This variable is defined as a member variable, so if you’re following, you need to add this variable right now, like this:private Paint paint;Then, somewhere in the constructor, add this statement:paint = new Paint();

那应该已经让我们赶上了。我们不仅使用 Paint 对象来确定默认文本的大小,而且还使用它(稍后)将一些文本写到屏幕上。

Next, we draw the human player’s hand. Listing 6-20 shows the annotated code.protected void onDraw(Canvas canvas) {// other statementsfor (int i = 0; i < playerHand.size(); i++) { ❶canvas.drawBitmap(playerHand.get(i).getBitmap(), ❷i(scaledCW +5),scrH - scaledCH - paint.getTextSize()-(50scale),null);}}Listing 6-20

画人类玩家的手

| -好的 | 我们遍历手中所有的牌。 | | ❷ | 然后,我们使用缩放后的卡片高度和宽度变量来绘制位图。这些卡片相隔 5 个像素绘制,其 **Y** 位置减去(1)卡片的高度,(2)文本高度(我们稍后将使用它来绘制分数),以及(3)屏幕底部的 50 个缩放像素。 |

Next, we show the draw pile; add the code in Listing 6-21 to the onDraw method so we can show the draw pile.protected void onDraw(Canvas canvas) {// other statementsfloat cbackLeft = (scrW/2) - cardBack.getWidth() - 10;float cbackTop = (scrH/2) - (cardBack.getHeight() / 2);canvas.drawBitmap(cardBack, cbackLeft, cbackTop, null);}Listing 6-21

抽屉堆

抽牌堆由卡片图形的单个背面表示。它大约画在屏幕的中央。

Next, we draw the discard pile. Remember that the discard pile is started as by getting the top card of what remains in the deck after the cards have been dealt with the players; so, before we draw them, we need to check if it’s empty or not. Listing 6-22 shows the code for showing the discard pile.if (!discardPile.isEmpty()) {canvas.drawBitmap(discardPile.get(0).getBitmap(),(scrW /2)+10,(scrH /2)-(cardBack.getHeight()/2),null);}Listing 6-22

废弃堆

处理转弯

Crazy Eights is a turn-based game. We need to route the program logic based on whose turn it is, whether it’s the computer or the human player. We can facilitate this by adding a boolean variable as a member of the CrazyEightView class, like this:private boolean myTurn;Throughout our code, we will enable or disable certain logic based on whose turn it is. In the onSizeChanged method, we add the following code:myTurn = new Random().nextBoolean();if (!myTurn) {computerPlay();}

应该随机选择谁先走。自然,每次玩家打出有效的牌时,都需要切换 myTurn 变量,我们还需要将 computerPlay() 方法添加到我们的类中;我们一会儿就去做。

玩牌

A valid play in Crazy Eights requires that a player matches the top card of the discard pile, which means we now need a way to get the rank and suit from a Card object. Let’s modify the Card class to do just that. Listing 6-23 shows the revised Card class.public class Card {private int id;private int suit;private int rank;private Bitmap bmp;private int scoreValue;public Card(int newId) {id = newId;suit = Math.round((id/100) * 100);****rank = id - suit;}public int getScoreValue() {return scoreValue;}public void setBitmap(Bitmap newBitmap) {bmp = newBitmap;}public Bitmap getBitmap() {return bmp;}public int getId() {return id;}public int getSuit() {return suit;}public int getRank() {return rank;****}}Listing 6-23

修正了卡牌等级和花色的计算

我们添加了套装等级变量来分别保存套装和等级的值。我们还添加了计算这两个值所需的逻辑。

套装变量是通过四舍五入到最接近的百来计算的;例如,如果 id 为 102(方块 2),则花色值为 100。等级变量通过从 id 中减去花色来计算;如果 id 是 102,我们用 102 减去 100;因此,我们得到 2 作为等级的值。

最后,我们添加一个 getSuit()getRank() 方法,分别为 Suit 和 Rank 值提供 getters。

Having a way to get the rank and the suit of the card, we can start writing the code for when it’s the computer’s turn to play. The code for computerPlay(), which must be added to the CrazyEightView class, is shown in Listing 6-24.private void computerPlay() {int tempPlay = 0;while (tempPlay == 0) {tempPlay = computerPlayer.playCard(computerHand, validSuit, validRank); ❶if (tempPlay == 0) {drawCard(computerHand); ❷}}}Listing 6-24

电脑游戏()

| -好的 | **computerPlay** 变量应为成员变量;我们还没有为 ComputerPlayer 创建类,但是我们很快会创建的。现在,假设 **playCard()** 方法应该返回一个有效的 play。playCard 方法应该检查计算机手中的所有牌,如果它有一个有效的玩法,将被返回到 **tempPlay** 变量。 | | ❷ | 如果计算机没有玩法,它需要从一副牌中抽一张牌。 |

Now, let’s build the ComputerPlayer class. Add another class to the project and name it ComputerPlayer.java, as shown in Figure 6-12.

![img/340874_4_En_6_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig12_HTML.jpg)
Figure 6-12

向项目添加另一个类

Code for ComputerPlayer.java is shown in Listing 6-25.import java.util.List;public class ComputerPlayer {public int playCard(List hand, int suit, int rank) {int play = 0;for (int i = 0; i < hand.size(); i++) { ❶int tempId = hand.get(i).getId(); ❷int tempRank = hand.get(i).getRank(); ❸int tempSuit = hand.get(i).getSuit(); ❹if (tempRank != 8) {if (rank == 8) { ❺if (suit == tempSuit) {play = tempId;}} else if (suit == tempSuit || rank == tempRank) {play = tempId;}}}if (play == 0) { ❻for (int i = 0; i < hand.size(); i++) { ❼int tempId = hand.get(i).getId();if (tempId == 108 || tempId == 208 || tempId == 308 || tempId == 408) { // <>play = tempId;}}}return play;}}Listing 6-25

计算机播放器. java

| -好的 | **playCard** 方法需要遍历计算机手中的所有牌,以查看我们是否有有效的玩法。 | | ❷ | 这将获取当前卡的 id。 | | -你好 | 我们来获取当前卡的等级。 | | (a) | 我们也去买套衣服吧。 | | (一) | 如果顶牌不是 8,让我们看看是否可以匹配顶牌的等级或花色。 | | ❻ | 翻遍了我们所有的牌,都比不上顶牌;这就是为什么 **play** 变量仍然等于零。 | | ❼ | 让我们把所有的牌循环一遍,看看是否有 8。 |

现在我们为对手准备了一些简单的逻辑。让我们回到人类玩家。

A play is made by dragging a valid card to the top card. We need to show some animation that the card is being dragged. We can do this on onTouchEvent. Listing 6-26 shows a snippet on how we can start doing exactly that.public boolean onTouchEvent(MotionEvent event) {int eventaction = event.getAction();int X = (int)event.getX();int Y = (int)event.getY();switch (eventaction ) {case MotionEvent.ACTION_DOWN:if (myTurn) { ❶for (int i = 0; i < 7; i++) { ❷if (X > i(scaledCW +5) && X < i(scaledCW +5) + scaledCW &&Y > scrH - scaledCH - paint.getTextSize()-(50*scale)) {movingIdx = i;movingX = X;movingY = Y;}}}break;case MotionEvent.ACTION_MOVE:movingX = X; ❸movingY = Y;break;case MotionEvent.ACTION_UP:movingIdx = -1; ❹break;}invalidate();return true;}Listing 6-26

移动卡片

| -好的 | 人类玩家只能在轮到他们的时候移动一张牌。电脑对手玩得很快,所以这不应该是一个问题。这个游戏实际上感觉总是轮到人类。 | | ❷ | 循环人类玩家手中的所有牌,看看他们是否接触到了屏幕上抽取任何牌的区域。如果有,我们将该卡的索引分配给 **movingIdx** 变量;这是玩家动过的牌。 | | -你好 | 当玩家拖动卡片通过屏幕时,我们监控 X 和 Y 坐标;我们将使用它来绘制卡片,因为它正被拖过屏幕。 | | (a) | 当玩家放松时,我们重置 **movingIdx** 的值。值为–1 表示没有移动任何卡。 |

The next thing we need to do is to reflect all these movements in the onDraw method. Listing 6-27 shows the annotated code for drawing the card as it’s dragged across the screen.@Overrideprotected void onDraw(Canvas canvas) {// some other statementsfor (int i = 0; i < playerHand.size(); i++) {if (i == movingIdx) { ❶canvas.drawBitmap(playerHand.get(i).getBitmap(),movingX,movingY,null);} else { ❷if (i < 7) {canvas.drawBitmap(playerHand.get(i).getBitmap(),i(scaledCW +5),scrH - scaledCH - paint.getTextSize()-(50scale),null);}}}invalidate();setToFullScreen();}Listing 6-27

出示移动的卡片

| -好的 | 我们来看看当前牌是否与 **movingIdx** 变量的值匹配(用户拖动的牌);如果它是正确的卡片,我们使用更新的 X 和 Y 坐标画它。 | | ❷ | 如果没有一张牌在移动,我们就像在之前那样抽取所有的牌。 |

When you test the code as it stands now, you might notice that the position where the card is drawn (as you drag a card across the screen) isn’t right. The card might be obscured by your finger. We can fix this by drawing the card with some offset values. Listing 6-28 shows the code.public boolean onTouchEvent(MotionEvent event) {int eventaction = event.getAction();int X = (int)event.getX();int Y = (int)event.getY();switch (eventaction ) {case MotionEvent.ACTION_DOWN:if (myTurn) {for (int i = 0; i < 7; i++) {if (X > i(scaledCW +5) && X < i(scaledCW +5) + scaledCW &&Y > scrH - scaledCH - paint.getTextSize()-(50scale)) {movingIdx = i;**movingX = X-(int)(30scale);movingY = Y-(int)(70scale);}}}break;case MotionEvent.ACTION_MOVE:movingX = X-(int)(30scale);*movingY = Y-(int)(70scale);**break;invalidate();return true;}Listing 6-28

向 X 和 Y 坐标添加一些偏移量

突出显示的行是我们需要的唯一更改;我们没有按照事件传递给我们的原始 X 和 Y 坐标,而是向右多画了 30 个像素,向上多画了 70 个像素。这样,当卡被拖动时,玩家可以看到它。

Now that we can drag the card across the screen, we need to ensure that what’s being dragged is a valid card for play. A valid card for play matches the top card either in rank or in suit; now, we need to keep track of the suit and rank of the top card. Listing 6-29 shows the onSizeChanged() method in the CrazyEightView class. The variables validSuit and validRank are added.@Overridepublic void onSizeChanged (int w, int h, int oldw, int oldh){super.onSizeChanged(w, h, oldw, oldh);scrW = w;scrH = h;Bitmap tempBitmap = BitmapFactory.decodeResource(ctx.getResources(),R.drawable.card_back);scaledCW = (int) (scrW /8);scaledCH = (int) (scaledCW 1.28);cardBack = Bitmap.createScaledBitmap(tempBitmap, scaledCW, scaledCH, false);initializeDeck();dealCards();drawCard(discardPile);**validSuit = discardPile.get(0).getSuit();***validRank = discardPile.get(0).getRank();**myTurn = new Random().nextBoolean();if (!myTurn) {computerPlay();}}Listing 6-29

跟踪用于游戏的有效卡

当我们从一副牌中抽出一张牌并将其加入弃牌堆时,弃牌堆的顶牌决定有效牌的花色和等级。

So, when the human player tries to drag a card into the discard pile, we can determine if that card is a valid play; if it is, we add it to the discard pile; if not, we return it to the player’s hand. With that, let’s check for valid plays. Listing 6-30 shows the updated and annotated ACTION_UP of the onTouchEvent.case MotionEvent.ACTION_UP:if (movingIdx > -1 && ❶X > (scrW /2)-(100scale) && ❷X < (scrW /2)+(100scale) &&Y > (scrH /2)-(100scale) &&Y < (scrH /2)+(100scale) &&(playerHand.get(movingIdx).getRank() == 8 ||playerHand.get(movingIdx).getRank() == validRank || ❸playerHand.get(movingIdx).getSuit() == validSuit)) { ❹validRank = playerHand.get(movingIdx).getRank(); ❺validSuit = playerHand.get(movingIdx).getSuit();discardPile.add(0, playerHand.get(movingIdx)); ❻playerHand.remove(movingIdx); ❼}break;Listing 6-30

检查有效间隙

| -好的 | 让我们检查一下卡是否被移动了。 | | ❷ | 这些线负责放置区域,我们基本上是在屏幕中间放置卡片。没有必要精确定位。 | | -你好 | 让我们检查它是否有一个有效的排名。 | | (a) | 让我们检查一下被拖的牌是否有有效的花色。 | | (一) | 如果该播放有效,我们更新**有效等级**和**有效套装**的值。玩家提供的牌现在是具有有效花色和等级的牌。 | | ❻ | 我们把新卡加入弃牌堆。 | | ❼ | 我们从玩家手中拿走卡片。 |

接下来要处理的是当人类玩家玩 8 的时候。记住 8 是百搭;它们总是可以玩的。当人类玩家打出一张 8 的牌时(让我们先处理它;电脑还会打八,记得吗?),我们需要一种让玩家为下一次有效玩法选择花色的方法。

To choose the next suit when an eight is played, we need a way to show some options to the user. A dialog box is usually used for such tasks. We can draw the dialog box just like we did the Play button, or we can use Android’s built-in dialogs. Figures 6-13 and 6-14 show the dialog in action.

![img/340874_4_En_6_Fig13_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig13_HTML.jpg)
Figure 6-13

选择套装对话框

![img/340874_4_En_6_Fig14_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig14_HTML.jpg)
Figure 6-14

选择套装对话框,下拉

要开始构建这个对话框,我们需要一个数组资源到项目中。我们可以通过向文件夹 app/res/values 添加一个 XML 文件来实现这一点。目前,该文件夹中已经有三个 XML 文件(颜色、字符串和样式);这些文件是在我们创建项目时为我们创建的。Android 使用这些文件作为应用标签和配色的资源。我们将向该文件夹添加另一个文件。

Right-click the app/res/values folder as shown in Figure 6-15, then choose NewXMLValues XML File.

![img/340874_4_En_6_Fig15_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig15_HTML.jpg)
Figure 6-15

添加值 XML 文件

The next dialog window will ask for the name of the new resource file. Type arrays, as shown in Figure 6-16.

![img/340874_4_En_6_Fig16_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig16_HTML.jpg)
Figure 6-16

将新值文件命名为数组

Click Finish. Android Studio will try to update the Gradle file and other parts of the project; it could take a while. When it’s done, Android Studio will open the XML file in the main editor. Modify arrays.xml to match the contents of Listing 6-31.DiamondsClubsHeartsSpadesListing 6-31

arrays.xml

We will use this array to load the option for our dialog. Next, let’s create a layout file for the actual dialog. The layout file is also an XML file; to create it, right-click app/res/layout from the Project tool window, then choose New ➤ XML ➤ Layout XML File, as shown in Figure 6-17.

![img/340874_4_En_6_Fig17_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig17_HTML.jpg)
Figure 6-17

创建新的布局 XML 文件

Next, provide the layout file name, then type choose_suit_dialog (shown in Figure 6-18).

![img/340874_4_En_6_Fig18_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig18_HTML.jpg)
Figure 6-18

创建选择套装对话框 XML 文件

![img/340874_4_En_6_Fig19_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig19_HTML.jpg)
Figure 6-19

在设计模式中选择 _ 套装 _ 对话框

You can build the dialog in WYSIWYG style using the Palette, or you can go directly to the code. When Android Studio launches the newly created layout file, it might open it in Design mode. Switch to Text or Code mode, and modify the contents of choose_suit_dialog.xml to match the contents of Listing 6-32.LinearLayoutandroid:id="@+id/chooseSuitLayout"android:layout_width="275dp"android:layout_height="wrap_content"android:orientation="vertical"android:layout_gravity="top"xmlns:android="http://schemas.android.com/apk/res/android"<TextViewandroid:id="@+id/chooseSuitText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Choose a suit."android:textSize="16sp"android:layout_marginLeft="5dp"android:textColor="#FFFFFF">Spinnerandroid:id="@+id/suitSpinner"android:layout_width="fill_parent"android:layout_height="wrap_content"android:drawSelectorOnTop="true"/Buttonandroid:id="@+id/okButton"android:layout_width="125dp"android:layout_height="wrap_content"android:text="OK"Listing 6-32

choose_suit.dialog.xml

图 图 6-19 显示了设计模式下的对话框布局文件。您可以单击对话框文件的每个组成视图对象,并在“属性”窗口中检查各个属性。

布局文件有三个视图对象作为 UI 元素——一个 TextView、一个微调器和一个按钮。LinearLayout 以线性方式(直线)排列这些元素。垂直方向从上到下排列元素。

以后可以选择不使用 Android 内置的视图对象,让 UI 在视觉上更有吸引力;但是正如你可能已经从这一章推测到的,绘制你自己的屏幕元素需要大量的工作。

TextView、Spinner 和 Button 都有 id。我们稍后将使用这些 id 来引用它们。

Now that we have the dialog sorted out, we can build the code to show the dialog. When the human player plays an eight for a card, we will show this dialog. Let’s add a method to the CrazyEightView class and call this method changeSuit(). The contents of the changeSuit method are shown in Listing 6-33.private void changeSuit() {final Dialog changeSuitDlg = new Dialog(ctx); ❶changeSuitDlg.requestWindowFeature(Window.FEATURE_NO_TITLE); ❷changeSuitDlg.setContentView(R.layout.choose_suit_dialog); ❸final Spinner spinner = (Spinner) changeSuitDlg.findViewById(R.id.suitSpinner); ❹ArrayAdapter adapter = ArrayAdapter.createFromResource( ❺ctx, R.array.suits, android.R.layout.simple_spinner_item);adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);spinner.setAdapter(adapter);Button okButton = (Button) changeSuitDlg.findViewById(R.id.okButton); ❻okButton.setOnClickListener(new View.OnClickListener(){ ❼public void onClick(View view){validSuit = (spinner.getSelectedItemPosition()+1)*100;String suitText = "";if (validSuit == 100) {suitText = "Diamonds";} else if (validSuit == 200) {suitText = "Clubs";} else if (validSuit == 300) {suitText = "Hearts";} else if (validSuit == 400) {suitText = "Spades";}changeSuitDlg.dismiss();Toast.makeText(ctx, "You chose " + suitText, Toast.LENGTH_SHORT).show(); ❽myTurn = false;computerPlay();}});changeSuitDlg.show();}Listing 6-33

更换套装方法

| -好的 | 这一行创建一个对话框对象;我们将当前上下文传递给它的构造函数。 | | ❷ | 删除对话框的标题。我们希望它尽可能简单。 | | -你好 | 然后,我们将对话框对象的 contentView 设置为我们之前创建的布局资源文件。 | | (a) | 这一行创建了 Spinner 对象。 | | (一) | **ArrayAdapter** 向视图提供数据并决定其格式。这将使用我们之前创建的 **arrays.xml** 创建 ArrayAdapter。 | | ❻ | 使用按钮对象的 id 获取对它的编程引用。 | | ❼ | 为按钮创建一个事件处理程序。我们在这里使用 onClickListener 对象来处理 click 事件。覆盖这个处理程序的 onClick 方法可以让我们编写单击按钮时所需的逻辑。 | | ❽ | 一条 **Toast** 是显示在屏幕上的一条小消息,就像工具提示一样。只能看几秒钟。我们在这里使用 Toast 作为反馈,向用户显示选择了什么样的套装。 |

The changeSuit() method must be called only when the human player plays an eight. We need to put this logic into the ACTION_UP branch of the onTouchEvent method. Listing 6-34 shows the annotated ACTION_UP branch.case MotionEvent.ACTION_UP:if (movingIdx > -1 &&X > (scrW /2)-(100scale) &&X < (scrW /2)+(100scale) &&Y > (scrH /2)-(100scale) &&Y < (scrH /2)+(100scale) &&(playerHand.get(movingIdx).getRank() == 8 ||playerHand.get(movingIdx).getRank() == validRank ||playerHand.get(movingIdx).getSuit() == validSuit)) {validRank = playerHand.get(movingIdx).getRank();validSuit = playerHand.get(movingIdx).getSuit();discardPile.add(0, playerHand.get(movingIdx));playerHand.remove(movingIdx);if (playerHand.isEmpty()) {endHand();} else {if (validRank == 8) { ❶changeSuit();} else {myTurn = false;computerPlay();}}}break;Listing 6-34

触发 changeSuit()方法

| -好的 | 当人类玩家玩 8 时,我们调用 **changeSuit** 方法,让玩家选择花色。此时,仍然轮到人类玩家了;据推测,他们会打出另一张牌。 |

当没有有效播放时

可能会用完张有效的牌来玩。当这种情况发生时,人类玩家必须从牌堆中抽出一张牌;他们必须继续这样做,直到有一张牌可以打。这意味着一个玩家可能有七张以上的牌。还记得在 onDraw 方法中,我们缩放玩家牌组上的牌,只显示七张牌吗?我们现在可能会超过这个数字。

为了解决这个问题,我们可以画一个箭头图标,向用户表示他们的牌组中有七张以上的牌。通过点击箭头图标,我们应该能够平移玩家的纸牌视图。为此,我们需要画出箭头。

Add the following Bitmap object to the member variables of the CrazyEightView class.private Bitmap nextCardBtn;We can load the Bitmap on the onSizeChanged method, just like the other Bitmaps we drew earlier.nextCardBtn = BitmapFactory.decodeResource(getResources(),R.drawable.arrow_next);We need to draw the arrow when the player’s cards exceed seven. We can do this in the onDraw method. Listing 6-35 shows that code.if (playerHand.size() > 7) { ❶canvas.drawBitmap(nextCardBtn, ❷scrW - nextCardBtn.getWidth()-(30scale),scrH - nextCardBtn.getHeight()- scaledCH -(90scale),null);}for (int i = 0; i < playerHand.size(); i++) {if (i == movingIdx) {canvas.drawBitmap(playerHand.get(i).getBitmap(),movingX,movingY,null);} else {if (i < 7) {canvas.drawBitmap(playerHand.get(i).getBitmap(),i(scaledCW +5),scrH - scaledCH - paint.getTextSize()-(50scale),null);}}}Listing 6-35

画下一个箭头

| -好的 | 确定玩家是否有七张以上的牌。 | | ❷ | 如果多于七个,画下一个箭头。 |

Drawing the arrow is simply groundwork for our next task. Of course, before we allow the player to draw a card from the pile, we need to determine if they truly need to draw a card. If the player has a valid card to play (if they have cards with matching suit and rank or they’ve got an eight), then we should not let them draw. We need to provide that logic; so, we add another method to the CrazyEightView class named isValidDraw() . This method goes through all the cards in the player’s deck and checks if there are cards with matching suit or rank (or if there’s an eight card). Listing 6-36 shows the code for isValidDraw().private boolean isValidDraw() {boolean canDraw = true;for (int i = 0; i < playerHand.size(); i++) {int tempId = playerHand.get(i).getId();int tempRank = playerHand.get(i).getRank();int tempSuit = playerHand.get(i).getSuit();if (validSuit == tempSuit || validRank == tempRank ||tempId == 108 || tempId == 208 || tempId == 308 || tempId == 408) {canDraw = false;}}return canDraw;}Listing 6-36

isValidDraw()

我们循环所有的卡片;检查我们是否可以匹配花色或级别,或者牌中是否有 8;如果有,我们返回 false(因为玩家有有效打法);否则,我们返回 true。

When the human player tries to draw a card from the deck despite having a valid play, let’s display a Toast message to remind them that they can’t draw a card because they’ve got a valid play. This can be done on the ACTION_UP branch of the onTouchEvent method (code shown in Listing 6-37).if (movingIdx == -1 && myTurn &&X > (scrW /2)-(100scale) &&X < (scrW /2)+(100scale) &&Y > (scrH /2)-(100scale) &&Y < (scrH /2)+(100scale)) {if (isValidDraw()) { ❶drawCard(playerHand); ❷} else {Toast.makeText(ctx, "You have a valid play.",Toast.LENGTH_SHORT).show(); ❸}}Listing 6-37

玩家有有效玩法时的祝酒词

| -好的 | 在我们允许他们从一副牌中抽一张牌之前,检查玩家是否有有效的玩法。如果有, **isValidDraw()** 将返回 false。 | | ❷ | 否则,让玩家抽一张牌。 | | -你好 | 如果玩家有有效的玩法,显示祝酒词。 |

当轮到电脑的时候

在本章的前面,我们创建了一个名为 computerPlay() 的方法;当人类玩家完成他们的回合时,这个方法被调用;我们只编写了那个方法的存根。现在,我们需要把额外的逻辑,这样我们就可以有一个真正可玩的对手。

Let’s modify the computerPlay() method in the CrazyEightView class to reflect the code in Listing 6-38.private void computerPlay() {int tempPlay = 0; ❶while (tempPlay == 0) { ❷tempPlay = computerPlayer.playCard(computerHand, validSuit, validRank);if (tempPlay == 0) {drawCard(computerHand);}}if (tempPlay == 108 ||tempPlay == 208 ||tempPlay == 308 ||tempPlay == 408) {validRank = 8;validSuit = computerPlayer.chooseSuit(computerHand); ❸String suitText = "";if (validSuit == 100) {suitText = "Diamonds";} else if (validSuit == 200) {suitText = "Clubs";} else if (validSuit == 300) {suitText = "Hearts";} else if (validSuit == 400) {suitText = "Spades";}Toast.makeText(ctx, "Computer chose " + suitText, Toast.LENGTH_SHORT).show();} else {validSuit = Math.round((tempPlay/100) * 100); ❹validRank = tempPlay - validSuit;}for (int i = 0; i < computerHand.size(); i++) { ❺Card tempCard = computerHand.get(i);if (tempPlay == tempCard.getId()) {discardPile.add(0, computerHand.get(i));computerHand.remove(i);}}if (computerHand.isEmpty()) {endHand();}myTurn = true; ❻}Listing 6-38

计算机播放()方法

| -好的 | **tempPlay** 变量保存已出牌的 id。 | | ❷ | 值为零表示电脑手牌没有有效玩法。当我们调用 **ComputerPlayer** 类的 **playCard()** 方法时,它将返回有效游戏的卡的 id。如果计算机的手没有有效的玩法,让计算机从牌堆中抽一张牌;继续抽牌,直到有有效的牌可供使用。 | | -你好 | 如果电脑选择打一个八,我们就需要换花色;我们已经为人类玩家这样做了,但是我们还没有为电脑玩家这样做。我们现在会的。 **chooseSuit()** 方法还不存在,我们将很快实现它。现在,假设 **chooseSuit()** 方法将返回一个整数值,让我们为下一次播放设置新的 **validSuit** 。 | | (a) | 如果电脑没有打出 8,我们只需将**有效等级**和**有效花色**重置为打出的牌的价值。 | | (一) | 我们循环通过计算机的手牌,将打出的牌添加到弃牌堆。 | | ❻ | 最后,人类将进入下一轮。 |

结束一手牌

When either the computer or the human player plays the last card, the hand ends. When this happens, we need to

  1. 1.

    A dialog box is displayed, indicating that the current hand has ended

    .

  2. 2.

    Display and update the scores of human and computer players

  3. 3.

    Start a new hand of cards

We’ll display the scores on the top and bottom parts of the screen, as shown in Figure 6-20.

![img/340874_4_En_6_Fig20_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig20_HTML.jpg)
Figure 6-20

显示分数

To display the scores, we first need to calculate it. When a hand ends, all the remaining cards (either the computer’s or the human player’s) must be totaled. To facilitate this, we need to update the Card class. Listing 6-39 shows the updated Card class.public class Card {private int id;private int suit;private int rank;private Bitmap bmp;private int scoreValue; ❶public Card(int newId) {id = newId;suit = Math.round((id/100) * 100);rank = id - suit;if (rank == 8) { ❷scoreValue = 50;} else if (rank == 14) {scoreValue = 1;} else if (rank > 9 && rank < 14) {scoreValue = 10;} else {scoreValue = rank;}}public int getScoreValue() {return scoreValue;}public void setBitmap(Bitmap newBitmap) {bmp = newBitmap;}public Bitmap getBitmap() {return bmp;}public int getId() {return id;}public int getSuit() {return suit;}public int getRank() {return rank;}}Listing 6-39

Card.java

| -好的 | 创建一个变量来保存卡片的分数。 | | ❷ | 检查卡片的等级并分配一个分值。如果玩家手里剩下一张 8,对手就有 50 分。面牌值 10 分,ace 值 1 分,其余牌值面值。 |

Next, we need a method to update the scores of both the computer and the human player. Let’s add a new method to CrazyEightView named updateScores(); the code for this method is shown in Listing 6-40.private void updateScores() {for (int i = 0; i < playerHand.size(); i++) {computerScore += playerHand.get(i).getScoreValue();currScore += playerHand.get(i).getScoreValue();}for (int i = 0; i < computerHand.size(); i++) {myScore += computerHand.get(i).getScoreValue();currScore += computerHand.get(i).getScoreValue();}}Listing 6-40

updateScores()方法

变量 currScorecomputerScoremyScore 需要在 CrazyEightView 中声明为成员变量。

如果计算机的手是空的,我们检查人类玩家手里的所有牌,将其相加,并将其记入计算机的分数。如果人类玩家的手是空的,我们检查计算机手里所有剩余的牌,将其相加,并将分数记入人类玩家。

现在分数已经计算出来了,我们可以显示它们了。

To display the scores, we will use the Paint object we defined earlier in the chapter. We need to set some attributes of the Paint object before we can draw some text with it. Listing 6-41 shows the constructor of CrazyEightView, which contains the code we need for the Paint object.import android.graphics.Color;public CrazyEightView(Context context) {super(context);ctx = context;scale = ctx.getResources().getDisplayMetrics().density;paint = new Paint();paint.setAntiAlias(true);paint.setColor(Color.BLACK);paint.setStyle(Paint.Style.FILL);paint.setTextAlign(Paint.Align.LEFT);paint.setTextSize(scale*15);}Listing 6-41

绘制对象

To draw the scores, modify the onDraw() method and add the two drawText() methods, as shown in Listing 6-42.protected void onDraw(Canvas canvas) {canvas.drawText("Opponent Score: " + Integer.toString(computerScore), 10,paint.getTextSize()+10, paint);canvas.drawText("My Score: " + Integer.toString(myScore), 10, scrH –paint.getTextSize()-10, paint);// ...}Listing 6-42

绘制分数

Next, we need to take care of the dialog for starting a new hand. This will be similar to the change suit dialog. This is a new dialog, so we need to create it. Right-click the res/layout folder in the Project tool window, as shown in Figure 6-21.

![img/340874_4_En_6_Fig21_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_6_Fig21_HTML.jpg)
Figure 6-21

新布局 XML 文件

In the next window, enter end_hand_dialog for the layout file name. When Android Studio opens the newly created layout file in the main editor window, modify it to reflect the code, as shown in Listing 6-43.LinearLayoutandroid:id="@+id/endHandLayout"android:layout_width="275dp"android:layout_height="wrap_content"android:orientation="vertical"android:layout_gravity="top"xmlns:android="http://schemas.android.com/apk/res/android"TextViewandroid:id="@+id/endHandText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text=""android:textSize="16sp"android:layout_marginLeft="5dp"android:textColor="#FFFFFF"<Buttonandroid:id="@+id/nextHandButton"android:layout_width="125dp"android:layout_height="wrap_content"android:text="Next Hand">Listing 6-43

end_hand_dialog.xml

这个布局文件比修改套装对话框简单得多。这个只有一个文本视图和一个按钮。

Next, we add another method to CrazyEightView to handle the logic when a given hand ends. Listing 6-44 shows the code for the endHand() method.private void endHand() {String endHandMsg = "";final Dialog endHandDlg = new Dialog(ctx); ❶endHandDlg.requestWindowFeature(Window.FEATURE_NO_TITLE);endHandDlg.setContentView(R.layout.end_hand_dialog);updateScores(); ❷TextView endHandText = (TextView) endHandDlg.findViewById(R.id.endHandText); ❸if (playerHand.isEmpty()) {if (myScore >= 300) {endHandMsg = String.format("You won. You have %d points. Play again?",myScore);} else {endHandMsg = String.format("You lost, you only got %d", currScore);}} else if (computerHand.isEmpty()) {if (computerScore >= 300) {endHandMsg = String.format("Opponent scored %d. You lost. Play again?",computerScore);} else {endHandMsg = String.format("Opponent has lost. He scored %d points.",currScore);}endHandText.setText(endHandMsg);}Button nextHandBtn = (Button) endHandDlg.findViewById(R.id.nextHandButton); ❹if (computerScore >= 300 || myScore >= 300) { ❺nextHandBtn.setText("New Game");}nextHandBtn.setOnClickListener(new View.OnClickListener(){ ❻public void onClick(View view){if (computerScore >= 300 || myScore >= 300) {myScore = 0;computerScore = 0;}initNewHand();endHandDlg.dismiss();}});endHandDlg.show();}Listing 6-44

endHand()方法

| -好的 | 与我们之前创建的对话框相同。创建一个对话框的实例,并确保它不显示任何标题。然后将内容视图设置为我们创建的布局文件。 | | ❷ | 当一手牌结束时,我们调用 **updateScore()** 方法来显示分数信息。 | | -你好 | 获取对 TextView 对象的编程引用,在随后的语句中,根据谁用完了牌,我们显示赢得了多少分。 | | (a) | 获取对该按钮的编程引用。 | | (一) | 让我们检查一下游戏是否已经结束。当其中一个玩家达到 300 分时,游戏结束。如果是,我们将按钮上的文本改为“新游戏”,而不是“新手牌” | | ❻ | 为按钮创建一个侦听器对象来处理 click 事件。在 Click 处理程序的 onClick 方法中,我们调用 **initNewHand()** 方法启动一个新的手;该方法的代码如清单 6-45 所示。 |

private void initNewHand() {currScore = 0; ❶if (playerHand.isEmpty()) { ❷myTurn = true;} else if (computerHand.isEmpty()) {myTurn = false;}deck.addAll(discardPile); ❸deck.addAll(playerHand);deck.addAll(computerHand);discardPile.clear();playerHand.clear();computerHand.clear();dealCards(); ❹drawCard(discardPile);validSuit = discardPile.get(0).getSuit();validRank = discardPile.get(0).getRank();if (!myTurn) {computerPlay();}}Listing 6-45

initNewHand()方法

| -好的 | 让我们重新设置这手牌的得分。 | | ❷ | 如果人类玩家赢了前一手牌,那么轮到他们先玩。 | | -你好 | 将弃牌堆和两位玩家的牌放回牌组,然后清除列表和弃牌堆。我们基本上是把所有的牌放回牌堆。 | | (a) | 像游戏开始时一样发牌。 |

Now that we have all the required logic and assets for ending a hand, it’s time to put the code for checking if the hand has ended. We can do this on the ACTION_UP case of the onTouchEvent method; Listing 6-46 shows this code. The pertinent code is in bold.case MotionEvent.ACTION_UP:if (movingIdx > -1 &&X > (scrW /2)-(100scale) &&X < (scrW /2)+(100scale) &&Y > (scrH /2)-(100scale) &&Y < (scrH /2)+(100scale) &&(playerHand.get(movingIdx).getRank() == 8 ||playerHand.get(movingIdx).getRank() == validRank ||playerHand.get(movingIdx).getSuit() == validSuit)) {validRank = playerHand.get(movingIdx).getRank();validSuit = playerHand.get(movingIdx).getSuit();discardPile.add(0, playerHand.get(movingIdx));playerHand.remove(movingIdx);if (playerHand.isEmpty()) {endHand();} else {if (validRank == 8) {changeSuit();} else {myTurn = false;computerPlay();}}}Listing 6-46

检查这手牌是否已经结束

We simply need to check if the player’s hand is empty; if it is, the hand has ended. The next thing we need to do is to check on the computer’s side if the hand has ended. Listing 6-47 shows that code.private void computerPlay() {int tempPlay = 0;while (tempPlay == 0) {tempPlay = computerPlayer.playCard(computerHand, validSuit, validRank);if (tempPlay == 0) {drawCard(computerHand);}}if (tempPlay == 108 || tempPlay == 208 || tempPlay == 308 || tempPlay == 408) {validRank = 8;validSuit = computerPlayer.chooseSuit(computerHand);String suitText = "";if (validSuit == 100) {suitText = "Diamonds";} else if (validSuit == 200) {suitText = "Clubs";} else if (validSuit == 300) {suitText = "Hearts";} else if (validSuit == 400) {suitText = "Spades";}Toast.makeText(ctx, "Computer chose " + suitText, Toast.LENGTH_SHORT).show();} else {validSuit = Math.round((tempPlay/100) * 100);validRank = tempPlay - validSuit;}for (int i = 0; i < computerHand.size(); i++) {Card tempCard = computerHand.get(i);if (tempPlay == tempCard.getId()) {discardPile.add(0, computerHand.get(i));computerHand.remove(i);}}if (computerHand.isEmpty()) { ❶endHand();}myTurn = true;}Listing 6-47

完成 computerPlay()方法的列表

| -好的 | 我们简单的检查一下电脑的手是不是空的;如果是,那手牌已经结束了。 |

这就是我们需要为疯狂的 8 游戏编写的所有逻辑。结束游戏的逻辑已经在清单 6-44 (第 5 项)中显示;当任何一个玩家达到 300 时,游戏结束。

七、构建气球爆炸游戏

  • 如何在游戏中使用 ImageView 作为图形对象

  • 使用值 Animator 来制作游戏对象运动的动画

  • 使用 AudioManager、MediaPlayer 和 SoundPool 类为游戏添加音效和音乐

  • 使用 Java 线程在后台运行

像上一章一样,我将展示构建游戏所必需的代码片段;有时,甚至会提供一些类的完整代码清单。理解和学习本章中的编程技术的最好方法是下载游戏的源代码,并在阅读本章的时候保持它在 Android Studio 中打开。如果您想继续学习并自己构建项目,最好将本章的源代码放在手边,这样您就可以根据需要复制和粘贴特定的代码片段。

游戏力学

我们将让气球从屏幕底部漂浮起来,上升到顶部。玩家的目标是在气球到达屏幕顶部之前弹出尽可能多的气球。如果一个气球到达顶部而没有被弹出,这将是对用户的一点。玩家将有五条命(在这种情况下是图钉);每当玩家错过一个气球,他们就会失去一个别针。当针用完时,游戏就结束了。

We’ll introduce the concept of levels. In each level, there will be several balloons. As the player progresses in levels, the time it takes for the balloon to float from the bottom to the top becomes less and less; the balloons float faster as the level increases. It’s that simple. Figure 7-1 shows a screenshot of Balloon Popper game.

![img/340874_4_En_7_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig1_HTML.jpg)
Figure 7-1

流行气球

气球将从屏幕底部随机出现。

我们将把屏幕的下半部分用于游戏统计。我们将用它来显示分数和等级。在左下角,我们将放置一个按钮视图,用户可以使用它来开始游戏和开始一个新的水平。

游戏将以全屏模式进行(就像我们之前的游戏一样),并且只在横向模式下进行。

创建项目

Create a new project with an empty Activity, as shown in Figure 7-2.

![img/340874_4_En_7_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig2_HTML.jpg)
Figure 7-2

活动为空的新项目

In the window that follows, fill out the project details, as shown in Figure 7-3.

![img/340874_4_En_7_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig3_HTML.jpg)
Figure 7-3

创建一个新的项目

点击完成创建项目。

绘制背景

游戏有一个背景图;你可以没有,但它增加了用户体验。当然,如果你发行一个商业游戏,你会使用一个更专业的形象。我从一个公共领域网站上抓取了这张图片;请随意使用您喜欢的任何图像。

当我得到背景图片时,我只下载了一个文件,并将其命名为“background.jpg”。我可以使用这个图片,并将其放在 app/res/drawable 文件夹中,然后就可以完成了。如果我这样做了,运行时将使用这个相同的图像文件作为不同显示密度的背景,并在游戏运行时尝试进行调整,这可能会导致游戏体验不稳定。因此,为不同的屏幕密度提供背景图像是非常重要的。如果你很熟悉 Photoshop 或 GIMP,你可以试着为不同的屏幕生成图像;或者,你可以只使用一张背景图像,然后使用一个名为Android Resizer(github.com/asystat/Final-Android-Resizer)的应用来为你生成图像。你可以从它的 GitHub repo 下载应用并立即使用。这是一个可执行的 Java 归档(JAR)文件。

Once downloaded, you can open the zipped file and double-click the file Final Android Resizer.jar in the Executable Jar folder (shown in Figure 7-4).

![img/340874_4_En_7_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig4_HTML.jpg)
Figure 7-4

Android resize app

In the window that follows (Figure 7-5), modify the settings of the “export” section; the various screen density targets are in the Export section. I ticked off ldpi because we don’t have to support the low-density screens. I also ticked off the tvdpi because our targets don’t include Android TVs.

![img/340874_4_En_7_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig5_HTML.jpg)
Figure 7-5

Android 【调整大小】

Click the browse button of the Android Resizer to set the target folder where you would like to generate the images, as shown in Figure 7-6; then click Choose.

![img/340874_4_En_7_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig6_HTML.jpg)
Figure 7-6

生成图像的目标文件夹

![img/340874_4_En_7_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig7_HTML.jpg)
Figure 7-7

Android Resizer,目标目录集

目标目录(资源目录)现在应该设置好了。记住这个目录,因为您将从这里获取图像,并将它们传输到 Android 项目。在随后的窗口中(图 7-7 ),您将设置目标目录。

Next, drag the image you’d like to resize in the center area of the Resizer app. As soon as you drop the image, the conversion begins. When the conversion finishes, you’ll see a message “Done! Gimme some more…”, as shown in Figure 7-8.

![img/340874_4_En_7_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig8_HTML.jpg)
Figure 7-8

Android Resizer,完成了转换

The generated images are neatly placed in their corresponding folders, as shown in Figure 7-9.

![img/340874_4_En_7_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig9_HTML.jpg)
Figure 7-9

生成的图像

The background image file isn’t the only thing we need to resize. We also need to do this for the balloon image. We will use a graphic image to represent the balloons in the game. The balloon file is just a grayscale image (shown in Figure 7-10); we’ll add the colors in the program later.

![img/340874_4_En_7_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig10_HTML.jpg)
Figure 7-10

气球的灰度图像

Drag and drop the balloon image in the Resizer app, as you did with the background file. When it’s done, the Android Resizer would have generated the files balloons.png and background.jpg in the appropriate folders (as shown in Figure 7-11).

![img/340874_4_En_7_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig11_HTML.jpg)
Figure 7-11

生成的文件

We can now use these images for the project. To move the images to the project, open the app/res folder; you can do this by using a context action; right-click app/res, then choose Reveal in Finder (if you’re on macOS); if you’re on Windows, it will be Show in Explorer (as shown in Figure 7-12).

![img/340874_4_En_7_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig12_HTML.jpg)
Figure 7-12

在取景器中显示

现在,您可以简单地将生成的图像文件夹(和文件)拖放到 app/res/ 目录中的正确文件夹中。

Figure 7-13 shows an updated app/res directory of the project. I switched the scope of the Project tool from Android scope to Project scope to see the physical layout of the files. I usually change scopes, depending on what I need.

![img/340874_4_En_7_Fig13_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig13_HTML.jpg)
Figure 7-13

app/res 文件夹中有合适的图像文件

Before we draw the background image, let’s take care of the screen orientation. It’s best to play this game in landscape mode; that’s why we’ll fix the orientation to landscape. We can do this in the AndroidManifest file. Edit the project’s AndroidManifest to match Listing 7-1; Figure 7-14 shows the location of the AndroidManifest file in the Project tool window.

![img/340874_4_En_7_Fig14_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig14_HTML.jpg)
Figure 7-14

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="net.workingdev.popballoons">applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"<activity android:name=".MainActivity"android:configChanges="orientation|keyboardHidden|screenSize"android:label="@string/app_name"android:screenOrientation="landscape"****android:theme="@style/FullscreenTheme">Listing 7-1

AndroidManifest.xml

在清单文件中的 <活动> 节点的属性上可以找到负责将方向固定为横向的条目。此时,项目会有一个错误,因为Android:theme = " style/full screen theme "属性还不存在。我们会尽快解决这个问题。

Edit the /app/res/styles.xml file and add another style, as shown in Listing 7-2.Listing 7-2

/app/res/styles.xml

That should fix it. Figure 7-15 shows the app in its current state.

![img/340874_4_En_7_Fig15_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig15_HTML.jpg)
Figure 7-15

PopBalloons

To load the background image from the app/res/mipmap folders, we will use the following code:getWindow().setBackgroundDrawableResource(R.mipmap.background);We need to call this statement in the onCreate() method of MainActivity, just before we call setContentView(). Listing 7-3 shows our (still) minimal MainActivity.public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);getWindow().setBackgroundDrawableResource(R.mipmap.background);setContentView(R.layout.activity_main);}}Listing 7-3

主活动

Now, build and run the app. You will notice that the app has a background image now (as shown in Figure 7-16).

![img/340874_4_En_7_Fig16_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig16_HTML.jpg)
Figure 7-16

用背景图像

游戏控件和大头针图标

我们将使用屏幕的底部来显示分数和级别。我们还将使用屏幕的这一部分来放置一个按钮,该按钮触发游戏和关卡的开始。

Let’s fix the activity_main layout file first. Currently, this layout file is set to ConstraintLayout (this is the default), but we don’t need this layout, so we’ll replace it with the RelativeLayout. We’ll set the layout_width and layout_height of this container to match_parent so that it expands to the available space. Listing 7-4 shows our refactored main layout.<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity">Listing 7-4

活动 _ 主要

Next, we will add the Button and the TextView objects, which we’ll use to start the game and to display game statistics. The idea is to nest the TextViews inside a LinearLayout container, which is oriented horizontally, and then put it side by side with a Button control; then, we’ll enclose the Button and the LinearLayout container within another RelativeLayout container. Listing 7-5 shows the complete activity_main layout, with the game controls added.<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity">RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:background="@color/lightGrey"< Buttonandroid:id="@+id/go_button"style="?android:borderlessButtonStyle"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentStart="true"android:layout_centerVertical="true"android:text="@string/play_game"android:layout_alignParentLeft="true"/>LinearLayoutandroid:id="@+id/status_display"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentEnd="true"android:layout_centerVertical="true"android:layout_marginEnd="8dp"android:orientation="horizontal"tools:ignore="RelativeOverlap"<TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/level_label"android:textSize="20sp"android:textStyle="bold"tools:ignore="RelativeOverlap" /><TextViewandroid:id="@+id/level_display"android:layout_width="40dp"android:layout_height="wrap_content"android:layout_marginEnd="32dp"android:gravity="end"android:text="@string/maxNumber"android:textSize="20sp"android:textStyle="bold" /><TextViewandroid:id="@+id/score_label"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/score_label"android:textSize="20sp"android:textStyle="bold"tools:ignore="RelativeOverlap" /><TextViewandroid:id="@+id/score_display"android:layout_width="40dp"android:layout_height="wrap_content"android:layout_marginEnd="16dp"android:gravity="end"android:text="@string/maxNumber"android:textSize="20sp"android:textStyle="bold" />Listing 7-5

activity_main.xml

我们在 activity_main.xml 中引用了几个字符串和颜色资源,我们需要将它们添加到 resources 文件夹中的 strings.xmlcolors.xml 中。

Open colors.xml and edit it to match Listing 7-6.#008577#00574B#D81B60#DDDDDD@color/black_overlay#66000000Listing 7-6

app/res/values/colors.xml

Open strings.xml and edit it to match Listing 7-7.PopBalloonsPlayStopScore:999Level:Wow, that was awesomeMore Levels than Ever!New Top Score!Top score: %sLevels completed: %sGame over!Missed that one!You finished level %s!Popping PinListing 7-7

app/RES/values/strings . XML

字符串文字存储在 strings.xml 中,以避免在我们的程序中硬编码字符串文字。这种将资源文件用于字符串文字的方法使得以后更改字符串变得更加容易——比如说,当您向非英语国家发布游戏时。

Figure 7-17 shows the app with game controls.

![img/340874_4_En_7_Fig17_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig17_HTML.jpg)
Figure 7-17

带游戏控制

接下来,让我们画大头针。你可以从谷歌的材料图标上找到图钉。这些是 SVG 图标,所以我们不必为不同的屏幕分辨率创建多个副本;它们伸缩自如。引脚的矢量定义将位于 drawable 文件夹中。我们将为引脚创建两个向量定义;一个图像代表完整的 pin(未使用的游戏寿命),另一个图像代表断裂的 pin(使用过的游戏寿命)。

We need to create these files inside the drawable folder; we can do this with the context menu actions. Right-click the app/res/drawable folder of the project, as shown in Figure 7-18.

![img/340874_4_En_7_Fig18_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig18_HTML.jpg)
Figure 7-18

新的可提取资源文件

In the window that follows, type the name of the file (as shown in Figure 7-19).

![img/340874_4_En_7_Fig19_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig19_HTML.jpg)
Figure 7-19

新的资源文件

检查目录名是否是“可绘制的”,然后点击 OK。简单地键入 pin 作为文件名;不需要添加 XML 扩展,它会由 Android Studio 自动添加。做同样的事情为pin _ breaked创建文件。

Edit the newly created resource files. Listings 7-8 and 7-9 show the code for pin.xml and pin_broken.xml, respectively.<vector xmlns:android="http://schemas.android.com/apk/res/android"android:height="24dp"android:width="24dp"android:viewportWidth="24"android:viewportHeight="24">Listing 7-8

app/res/drawable/pin.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"android:height="24dp"android:width="24dp"android:viewportWidth="24"android:viewportHeight="24">Listing 7-9

app/RES/drawable . pin _ broken . XML

Figure 7-20 shows a preview of the pin in Android Studio.

![img/340874_4_En_7_Fig20_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig20_HTML.jpg)
Figure 7-20

大头针图像的预览

Now that we have images for the pins, we can add them to the activity_main layout file. We’ll place five ImageView objects at the top part of the screen, and then we will point each ImageView to the pin images we recently created. Listing 7-10 shows a snippet of the pin definitions in XML.<ImageViewandroid:id="@+id/pushpin1"android:layout_width="40dp"android:layout_height="40dp"android:contentDescription="@string/popping_pin"android:src="@drawable/pin"android:tint="@color/pinColor" />Listing 7-10

XML 中的 Pin 定义

android:src 属性将 ImageView 指向我们在 drawable 文件夹中的矢量图。

Listing 7-11 shows the full activity_main.xml, which contains the game controls, the pin drawings, and the FrameLayout container, which will contain all our game action.<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><FrameLayoutandroid:id="@+id/content_view"android:layout_width="match_parent"android:layout_height="match_parent" />LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentEnd="true"android:layout_alignParentTop="true"android:layout_marginEnd="16dp"android:layout_marginTop="16dp"android:orientation="horizontal"<ImageViewandroid:id="@+id/pushpin1"android:layout_width="40dp"android:layout_height="40dp"android:contentDescription="@string/popping_pin"android:src="@drawable/pin"android:tint="@color/pinColor" /><ImageViewandroid:id="@+id/pushpin2"android:layout_width="40dp"android:layout_height="40dp"android:contentDescription="@string/popping_pin"android:src="@drawable/pin"android:tint="@color/pinColor" /><ImageViewandroid:id="@+id/pushpin3"android:layout_width="40dp"android:layout_height="40dp"android:contentDescription="@string/popping_pin"android:src="@drawable/pin"android:tint="@color/pinColor" /><ImageViewandroid:id="@+id/pushpin4"android:layout_width="40dp"android:layout_height="40dp"android:contentDescription="@string/popping_pin"android:src="@drawable/pin"android:tint="@color/pinColor" /><ImageViewandroid:id="@+id/pushpin5"android:layout_width="40dp"android:layout_height="40dp"android:contentDescription="@string/popping_pin"android:src="@drawable/pin"android:tint="@color/pinColor" />RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:background="@color/lightGrey"< Buttonandroid:id="n"style="?android:borderlessButtonStyle"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentStart="true"android:layout_centerVertical="true"android:text="@string/play_game" />LinearLayoutandroid:id="@+id/status_display"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentEnd="true"android:layout_centerVertical="true"android:layout_marginEnd="8dp"android:orientation="horizontal"tools:ignore="RelativeOverlap"<TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/level_label"android:textSize="20sp"android:textStyle="bold"tools:ignore="RelativeOverlap" /><TextViewandroid:id="@+id/level_display"android:layout_width="40dp"android:layout_height="wrap_content"android:layout_marginEnd="32dp"android:gravity="end"android:text="@string/maxNumber"android:textSize="20sp"android:textStyle="bold" /><TextViewandroid:id="@+id/score_label"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/score_label"android:textSize="20sp"android:textStyle="bold"tools:ignore="RelativeOverlap" /><TextViewandroid:id="@+id/score_display"android:layout_width="40dp"android:layout_height="wrap_content"android:layout_marginEnd="16dp"android:gravity="end"android:text="@string/maxNumber"android:textSize="20sp"android:textStyle="bold" />Listing 7-11

activity_main.xml 的完整代码

At this point, you should have something that looks like Figure 7-21.

![img/340874_4_En_7_Fig21_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig21_HTML.jpg)
Figure 7-21

带有游戏控件和 pin 码的应用

It’s starting to shape up, but we still need to fix that toolbar and the other widgets displayed on the top strip of the screen. We’ve already done this in the previous chapter so that this technique will be familiar. Listing 7-12 shows the code for the setToFullScreen() method.private void setToFullScreen() {contentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE| View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);}Listing 7-12

setToFullScreen()

启用全屏模式在 Android 开发者网站中有很好的记录;以下是更多信息的链接:

Listing 7-13 shows the annotated listing of MainActivity.import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;public class MainActivity extends AppCompatActivity {ViewGroup contentView; ❶@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);getWindow().setBackgroundDrawableResource(R.mipmap.background);setContentView(R.layout.activity_main);contentView = (ViewGroup) findViewById(R.id.content_view); ❷contentView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) { ❸if (event.getAction() == MotionEvent.ACTION_DOWN) {setToFullScreen();}return false;}});}@Overrideprotected void onResume() {super.onResume();setToFullScreen(); ❹}private void setToFullScreen() {contentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_OW_PROFILE| View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);}}Listing 7-13

带注释的主要活动

| -好的 | 将 **contentView** 变量声明为成员;我们将在几个方法中使用它,所以我们需要它在类范围内可用。 | | ❷ | 获取对我们之前在 **activity_main** 中定义的 FrameLayout 容器的引用。将返回值存储到**容器视图**变量中。 | | -你好 | 全屏设置是临时的。屏幕稍后可以恢复到显示工具栏(例如,当显示对话窗口时)。我们将 **setOnTouchListener()** 绑定到 FrameLayout,以允许用户只需点击屏幕上的任意位置一次即可恢复全屏。 | | (a) | 我们在这里调用 **onResume()** 生命周期方法中的 **setToFullScreen()** 。当所有视图对象对用户都可见时,我们希望将屏幕设置为全屏。 |

Figure 7-22 shows the app in fullscreen mode.

![img/340874_4_En_7_Fig22_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig22_HTML.jpg)
Figure 7-22

应用全屏屏幕

画气球

这个想法是创建许多气球,它们将从屏幕的底部升到顶部。我们需要以编程方式创建气球。我们可以通过创建一个表示气球的类来做到这一点。我们将编写一些逻辑来创建 Balloon 类的实例,并使它们出现在屏幕底部的随机位置,但首先,让我们创建 Balloon 类。

Right-click the project’s package, then choose NewJava Class, as shown in Figure 7-23.

![img/340874_4_En_7_Fig23_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig23_HTML.jpg)
Figure 7-23

新的 Java 类

In the window that follows, type the name of the class (Balloon) and type its superclass (AppCompatImageView), as shown in Figure 7-24.

![img/340874_4_En_7_Fig24_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig24_HTML.jpg)
Figure 7-24

创建一个新的类

Listing 7-14 shows the code for the Balloon class.import androidx.appcompat.widget.AppCompatImageView;import android.content.Context;import android.util.TypedValue;import android.view.ViewGroup;public class Balloon extends AppCompatImageView {public Balloon(Context context) { ❶super(context);}public Balloon(Context context, int color, int height, int level ) { ❷super(context);setImageResource(R.mipmap.balloon); ❸setColorFilter(color); ❹int width = height / 2; ❺int dpHeight = pixelsToDp(height, context); ❻int dpWidth = pixelsToDp(width, context);ViewGroup.LayoutParams params =new ViewGroup.LayoutParams(dpWidth, dpHeight);setLayoutParams(params);}public static int pixelsToDp(int px, Context context) {return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, px,context.getResources().getDisplayMetrics());}}Listing 7-14

气球类

| -好的 | 这是 AppCompatImageView 的默认构造函数。我们不去管这件事 | | ❷ | 我们需要一个新的构造函数,一个接受游戏所需参数的函数。重载构造函数并创建一个接受气球颜色、高度和游戏级别参数的函数 | | -你好 | 设置图像的来源。将它指向 mipmap 文件夹中的气球图像 | | (a) | 气球图像只是单色灰色。 **setColorFilter()** 用你喜欢的任何颜色给图像上色。这就是我们要参数化颜色的原因 | | (一) | 气球的图像文件被设置为两倍于其宽度。为了计算气球的宽度,我们用高度除以 2 | | ❻ | 我们想计算图像的设备无关像素;因此,我们在 Balloon 类中创建了一个静态方法来完成这个任务(参见 **pixelsToDp()** 的实现) |

If you want to see this in action, you can modify the onTouch() listener of the contentView container in MainActivity such that, every time you touch the screen, a red balloon pops up exactly where you touched the screen. The code for that is shown in Listing 7-15.contentView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {Balloon btemp = new Balloon(MainActivity.this, 0xFFFF0000, 100, 1); ❶btemp.setY(event.getY()); ❷btemp.setX(event.getX()); ❸contentView.addView(btemp); ❹if (event.getAction() == MotionEvent.ACTION_DOWN) {setToFullScreen();}return false;}});Listing 7-15

MainActivity's 本体搜索监听器

| -好的 | 创建 Balloon 类的实例;传递上下文、红色、任意高度和 1(对于关卡,这现在并不重要)。 | | ❷ | 设置我们希望气球对象显示的 Y 坐标。 | | -你好 | 设置 X 坐标。 | | (a) | 将新的气球对象作为子对象添加到视图对象中;这很重要,因为这让我们可以看到气球。 |

At this point, every time you click the screen, a red balloon shows up. We need to mix up the colors of the balloons to make it more interesting. Let’s use at least three colors: red, green, and blue. We can look up the hex values of these colors, or we can use the Color class in Android. To get the red color, we can write something like this:Color.argb(255, 255, 0, 0);For blue and green, it would be as follows:Color.argb(255, 0, 255, 0);Color.argb(255, 0, 0, 255);A simple solution to rotate the colors is to set up an array of three elements, where each element contains a color value. Listing 7-16 shows the partial code for this task.private int[] colors = new int[3];@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// ...colors[0] = Color.argb(255, 255, 0, 0);colors[1] = Color.argb(255, 0, 255, 0);colors[2] = Color.argb(255, 0, 0, 255);}Listing 7-16

颜色数组 (这进入主活动)

Next, we set up a method that returns a random number between 0 and 2. We’ll make this our random selector for color. Listing 7-17 shows this code.private static int nextColor() {int max = 2;int min = 0;int retval = 0;Random random = new Random();retval = random.nextInt((max - min) + 1) + min;return retval;}Listing 7-17

nextColor()方法

Next, we modify that part of our code in MainActivity when we create the Balloon (inside the onTouch() method) and assign it a color; now, we will assign it a random color. Listing 7-18 shows that code.int curColor = colors[nextColor()];Balloon btemp = new Balloon(MainActivity.this, curColor, 100, 1);btemp.setY(event.getY());btemp.setX(event.getX());contentView.addView(btemp);Listing 7-18

分配随机颜色

Figure 7-25 shows the app randomizing the colors of the balloons.

![img/340874_4_En_7_Fig25_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig25_HTML.jpg)
Figure 7-25

随机颜色

让气球漂浮起来

为了让气球从底部飘到顶部,我们将使用 Android SDK 中的内置类。当气球升到屏幕顶部时,我们不会对它的位置进行微操作。

ValueAnimator 类(Android . animation . value animator)本质上是一个运行动画的计时引擎。它计算动画值,然后将它们设置在目标对象上。

Since we want to animate each balloon, we’ll put the animation logic inside the Balloon class; let’s add a new method named release() where we will put the necessary code to make the balloon float. Listing 7-19 shows the code.private BalloonListener listener;// some other statements ...listener = new BalloonListener(this);// some other statements ...public void release(int scrHeight, int duration) { ❶animator = new ValueAnimator(); ❷animator.setDuration(duration); ❸animator.setFloatValues(scrHeight, 0f); ❹animator.setInterpolator(new LinearInterpolator()); ❺animator.setTarget(this); ❻animator.addListener(listener);animator.addUpdateListener(listener); ❼animator.start(); ❽}Listing 7-19

气球类中的 release()方法

| -好的 | **release()** 方法有两个参数;第一个是屏幕的高度(动画需要这个),第二个是*时长*;我们需要这个稍后的水平。随着高度的增加,气球上升的速度越快。 | | ❷ | 创建 Animator 对象。 | | -你好 | 这将设置动画的持续时间。该值越高,动画越长。 | | (a) | 这将设置浮点值,这些值将在。我们想从屏幕底部到顶部制作动画;因此,我们传递了的**和屏幕高度。** | | (一) | 我们设置时间插值器,用于计算该动画的已用部分。插值器确定动画是以线性运动还是非线性运动运行,如加速和减速。在我们的例子中,我们想要一个线性加速度,所以我们传递了 LinearInterpolator 的一个实例。 | | ❻ | 动画的目标是一个气球的具体实例,因此**这个**。 | | ❼ | 动画有一个生命周期。我们可以通过添加一些监听器对象来监听这些更新。我们稍后将实现这些侦听器。 | | ❽ | 开始播放动画。 |

Create a new class (on the same package) and name it BalloonListener.java; Listing 7-20 shows the code for the BalloonListener.import android.animation.Animator;import android.animation.ValueAnimator;public class BalloonListener implements ❶Animator.AnimatorListener,ValueAnimator.AnimatorUpdateListener{Balloon balloon;public BalloonListener(Balloon balloon) {this.balloon = balloon; ❷}@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {balloon.setY((float) valueAnimator.getAnimatedValue()); ❸}// some other lifecycle methods ...}Listing 7-20

BalloonListener.java

| -好的 | 我们对动画的生命周期方法感兴趣;因此,我们实现了 **Animator。动画师**和**值动画师。AnimatorUpdateListener** 。 | | ❷ | 我们需要对气球对象的引用;因此,当创建这个侦听器对象时,我们将它作为一个参数。 | | -你好 | 当 ValueAnimator 更新其值时,我们将气球实例的 Y 位置设置为该值。 |

In MainActivity (where we create an instance of the Balloon), we need to calculate the screen height. Listing 7-21 shows the annotated code that will accomplish that.ViewTreeObserver viewTreeObserver = contentView.getViewTreeObserver(); ❶if (viewTreeObserver.isAlive()) { ❷viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { ❸@Overridepublic void onGlobalLayout() {contentView.getViewTreeObserver().removeOnGlobalLayoutListener(this); ❹scrWidth = contentView.getWidth(); ❺scrHeight = contentView.getHeight();}});}Listing 7-21

计算屏幕的高度和宽度

| -好的 | 获取 ViewTreeObserver 的实例。 | | ❷ | 我们只能在这个观察者活着的时候和他一起工作。因此,我们将整个逻辑包装在一个 **if 语句**中。 | | -你好 | 当视图树中视图的全局布局状态或可见性发生变化时,我们希望得到通知。 | | (a) | 我们希望只收到一次通知;因此,一旦调用了 **onGlobalLayout()** 方法,我们就删除了监听器。 | | (一) | 现在,我们可以得到屏幕的高度和宽度。 |

Listing 7-22 shows MainActivity with the code to calculate the screen’s height and width.public class MainActivity extends AppCompatActivity {ViewGroup contentView;private static String TAG;private int[] colors = new int[3];private int scrWidth; ❶private int scrHeight;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);TAG = getClass().getName();// other statements ...contentView = (ViewGroup) findViewById(R.id.content_view);contentView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {Log.d(TAG, "onTouch");int curColor = colors[nextColor()];Balloon btemp = new Balloon(MainActivity.this, curColor, 100, 1);btemp.setY(scrHeight); ❷btemp.setX(event.getX());contentView.addView(btemp);btemp.release(scrHeight, 4000); ❸Log.d(TAG, "Balloon created");if (event.getAction() == MotionEvent.ACTION_DOWN) {setToFullScreen();}return false;}});}@Overrideprotected void onResume() {super.onResume();setToFullScreen(); ❹ViewTreeObserver viewTreeObserver = contentView.getViewTreeObserver(); ❺if (viewTreeObserver.isAlive()) {viewTreeObserver.addOnGlobalLayoutListener(newViewTreeObserver.OnGlobalLayoutListener() {@Overridepublic void onGlobalLayout() {contentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);scrWidth = contentView.getWidth();scrHeight = contentView.getHeight();}});}}}Listing 7-22

主要活动

| -好的 | 创建成员变量**scrh height**和**scrw width**。 | | ❷ | 更改气球实例的 Y 坐标值。让我们从屏幕底部的气球的 Y 位置开始,而不是显示发生点击的气球的 Y 位置。 | | -你好 | 调用气球的 **release()** 方法。我们打这个电话的时候应该已经计算出屏幕高度了。第二个参数现在是硬编码的(持续时间),这意味着气球需要大约 4 秒钟才能升到屏幕顶部。 | | (a) | 在我们计算屏幕高度和宽度之前,非常重要的是我们已经调用了**setToFullScreen()**;这样,我们就有了一组精确的尺寸。 | | (一) | 当所有视图对象对用户都可见时,将计算屏幕高度和宽度的代码放在回调函数中;那就是 **onResume()** 方法。 |

At this point, if you run the app, a Balloon object will rise from the bottom to the top of the screen whenever you click anywhere on the screen (Figure 7-26).

![img/340874_4_En_7_Fig26_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig26_HTML.jpg)
Figure 7-26

气球升到顶端

发射气球

现在我们可以让气球一次升到顶端,我们需要弄清楚如何发射几个类似游戏关卡的气球。现在,响应用户的点击,气球出现在屏幕上;这不是我们想要的游戏方式。我们需要做一些改变。

我们想要的是玩家点击一个按钮,然后开始游戏。当按钮第一次被点击时,用户自动进入第一级。游戏的关卡并不复杂;随着高度的上升,我们将简单地增加气球的速度。

To launch the balloons, we need to do the following:

  1. 1.

    Make the button in activity_main.xml respond to the click event.

  2. 2.

    Create a new method in MainActivity, which will contain all the codes needed to start a level.

  3. 3.

    Write a cycle that can launch several balloons.

  4. 4.

    Randomize the X position of the balloon when creating it.

To make the Button respond to click events, we need to bind it to an OnClickListener object, as shown in Listing 7-23.Button btn = (Button) findViewById(R.id.btn);btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {// start the level// when this is clicked}});Listing 7-23

将按钮绑定到 onClickListener

The code to start a level is shown in Listing 7-24.private void startLevel() {// we'll fill this codes later}Listing 7-24

MainActivity 中的 startLevel()

We need to refactor the code to launch a single balloon. Right now, we’re doing it inside the onTouchListener. We want to enclose this logic in a method. Listing 7-25 shows the launchBalloon() method in MainActivity.public void launchBalloon(int xPos) { ❶int curColor = colors[nextColor()];Balloon btemp = new Balloon(MainActivity.this, curColor, 100, 1);btemp.setY(scrHeight);btemp.setX(xPos); ❷contentView.addView(btemp);btemp.release(scrHeight, 3000);Log.d(TAG, "Balloon created");}Listing 7-25

发射气球()

| -好的 | 该方法采用一个 int 参数。这将是气球在屏幕上的 X 位置。 | | ❷ | 设置气球的水平位置。 |

We want to launch the balloons in the background; you don’t want to do these things in the main UI thread because that will affect the game’s responsiveness. We don’t want the game to feel janky. So, we’ll write the looping logic in a Thread. Listing 7-26 shows the code for this Thread class.class LevelLoop extends Thread { ❶int balloonsLaunched = 0;public void run() {while (balloonsLaunched <= 15) { ❷balloonsLaunched++;Random random = new Random(new Date().getTime());final int xPosition = random.nextInt(scrWidth - 200); ❸try {Thread.sleep(1000); ❹}catch(InterruptedException e) {Log.e(TAG, e.getMessage());}// need to wrap this on runOnUiThreadrunOnUiThread(new Thread() {public void run() {launchBalloon(xPosition); ❺}});}}}Listing 7-26

LevelLoop(在 MainActivity 中实现为内部类)

| -好的 | LevelLoop 是 MainActivity 中的内部类。将它实现为内部类可以让我们访问外部类(MainActivity)的成员变量和方法(这很方便)。 | | ❷ | 当我们发射 15 个气球后,循环就会停止。要发射的气球数量现在是硬编码的,但我们稍后会重构它。 | | -你好 | 获得一个随机数来选择一个 x 捐赠气球。 | | (a) | 我们来介绍一个延迟;如果不引入延迟,所有 15 个气球都可以同时出现并升到顶端。现在,延迟是硬编码的;我们稍后将对此进行重构。我们需要根据水平来改变这一点。对了, **Thread.sleep()** 抛出了**中断异常**;这就是为什么我们需要将它包装在一个 try-catch 块中。 | | (一) | 最后调用外层类的 **launchBalloon()** 方法。我们需要将这个调用包装在一个 **runOnUiThread()** 方法中,因为后台进程调用 UI 元素是非法的;UI 元素在主线程(也称为 UI 线程)上呈现。如果你在后台运行时需要调用 UI 线程上的对象,你需要像我们在这里所做的那样,在一个 **runOnUiThread()** 方法上包装这个调用。 |

此时,每当你点击“播放”按钮,游戏将发射一系列 15 个气球,这些气球将升至屏幕顶部;然而,这个游戏还没有等级的概念。无论你点击多少次“播放”,气球上升的速度保持不变。让我们在下一节中解决这个问题。

处理游戏关卡

To introduce levels, let’s create a member variable in MainActivity to hold the value of the levels, and every time we call the startLevel() method, we increment that variable by 1. Listing 7-27 shows the code for these changes.private int level; ❶// other statements ...@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// other statements ...levelDisplay = (TextView) findViewById(R.id.level_display); ❷scoreDisplay = (TextView) findViewById(R.id.score_display); ❸}private void startLevel() {level++; ❹new LevelLoop(level).start(); ❺levelDisplay.setText(String.format("%s", level)); ❻}Listing 7-27

准备关卡

| -好的 | 将**级别**声明为成员变量。 | | ❷ | 获取对显示当前级别的 TextView 对象的引用。 | | -你好 | 当我们这样做的时候,还要获取一个对显示当前分数的 TextView 对象的引用。 | | (a) | 每次调用 **startLevel()** 方法时,递增 **level** 变量。 | | (一) | 让我们将 **level** 变量传递给 **LevelLoop** 对象(我们需要重构 LevelLoop 类,这样它就知道游戏级别了)。 | | ❻ | 让我们显示当前级别。 |

Next, let’s refactor the LevelLoop class to make it sensitive to the current game level. Listing 7-28 shows these changes.class LevelLoop extends Thread {private int shortDelay = 500; ❶private int longDelay = 1_500;private int maxDelay;private int minDelay;private int delay;private int looplevel;int balloonsLaunched = 0;public LevelLoop(int argLevel) { ❷looplevel = argLevel;}public void run() {while (balloonsLaunched < 15) {balloonsLaunched++;Random random = new Random(new Date().getTime());final int xPosition = random.nextInt(scrWidth - 200);maxDelay = Math.max(shortDelay, (longDelay - ((looplevel -1)) * 500)); ❸minDelay = maxDelay / 2;delay = random.nextInt(minDelay) + minDelay;Log.i(TAG, String.format("Thread delay = %d", delay));try {Thread.sleep(delay); ❹}catch(InterruptedException e) {Log.e(TAG, e.getMessage());}// need to wrap this on runOnUiThreadrunOnUiThread(new Thread() {public void run() {launchBalloon(xPosition);}});}}}Listing 7-28

水平环路

| -好的 | 让我们引入变量 **longDelay** 和 **shortDelay** ,它们分别保存最长可能延迟(毫秒)和最短可能延迟的整数值。 | | ❷ | 重构构造函数以接受级别参数。将该参数分配给成员变量 **looplevel** 。 | | -你好 | 这一点数学计算延迟(现在受电平影响)。延迟不会低于**短延迟**也不会高于**长延迟**。 | | (a) | 在 Thread.sleep() 方法中使用计算出的**延迟**。 |

戳破气球

为了得分,玩家必须触摸气球,从而在它们到达屏幕顶部之前戳破它们。当一个气球到达屏幕顶部时,它也会弹出,但玩家不会得到一分;事实上,当这种情况发生时,玩家会失去一枚别针。

To pop a balloon, we need to set up a touch listener for the Balloon, then inform MainActivity that the player popped the balloon; we need to inform MainActivity because

  1. 1.

    In the MainActivity, we will update the score and the status of how many bottles are left.

  2. 2.

    Also in the MainActivity, we will remove the balloon from the view group, no matter how it pops up, whether the player pops it up or the balloon runs away.

To do this, we need to set up an interface between the Balloon class and MainActivity. Let’s create an interface and add it to the project. Creating an interface in Android Studio is very similar to how we create classes. Use the context menu; right-click the project’s package, then choose NewJava Class, as shown in Figure 7-27.

![img/340874_4_En_7_Fig27_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig27_HTML.jpg)
Figure 7-27

新的 Java 类

In the window that follows, type the name of the interface (PopListener) and choose Interface as the kind (shown in Figure 7-28).

![img/340874_4_En_7_Fig28_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig28_HTML.jpg)
Figure 7-28

新界面

The PopListener interface will only have one method (shown in Listing 7-29).public interface PopListener {void popBalloon(Balloon bal, boolean isTouched);}Listing 7-29

弹出式监听器接口

第一个参数( bal )指的是一个气球的具体实例。我们需要这个引用,因为这是我们将从视图组中移除的内容。从视图组中删除它会使它从屏幕上消失。第二个参数将告诉我们气球是否因为玩家得到它而弹出,在这种情况下,该参数将为,或者它是否因为它一直到顶部而弹出,在这种情况下,该参数将为

Now we make a quick change to MainActivity, as shown in Listing 7-30.public class MainActivity extends AppCompatActivityimplements PopListener { ❶@Overridepublic void popBalloon(Balloon bal, boolean isTouched) { ❷contentView.removeView(bal); ❸if(isTouched) {userScore++; ❹scoreDisplay.setText(String.format("%d", userScore)); ❺}}}Listing 7-30

主活动

| -好的 | 实现弹出式列表器接口。 | | ❷ | 实现实际的 **popBalloon()** 方法。 | | -你好 | 这段代码删除了视图组中一个气球的特定实例。 | | (a) | 现在我们可以增加玩家的分数。 | | (一) | 这将显示玩家的分数。 |

Then we make adjustments on the Balloon class; Listing 7-31 shows these changes.public class Balloon extends AppCompatImageViewimplements View.OnTouchListener { ❶private ValueAnimator animator;private BalloonListener listener;private boolean isPopped; ❷private PopListener mainactivity; ❸private final String TAG = getClass().getName();public Balloon(Context context) {super(context);}public Balloon(Context context, int color, int height, int level ) {super(context);mainactivity = (PopListener) context; ❹// other statements ...setOnTouchListener(this); ❺}// other methods ...@Overridepublic boolean onTouch(View view, MotionEvent motionEvent) {Log.d(TAG, "TOUCHED");if(!isPopped) {mainactivity.popBalloon(this, true);isPopped = true;animator.cancel();}return true;}public void pop(boolean isTouched) { ❻mainactivity.popBalloon(this, isTouched); ❼}public boolean isPopped() { ❽return isPopped;}}Listing 7-31

气球类

| -好的 | 实现**视图。气球类上的 OnTouchListener** 。我们将使这个类成为触摸事件的监听器。 | | ❷ | **isPopped** 变量保存任何特定气球的状态,无论是否弹出。 | | -你好 | 创建对 MainActivity 的引用(它实现了弹出式列表器接口)。 | | (a) | 在气球的构造函数中,将上下文对象转换为弹出式列表器,并将其分配给 **mainactivity** 变量。 | | (一) | 为此气球实例设置 onTouchListener。 | | ❻ | 创建一个名为 **pop()** 的实用函数。我们将它公开是因为我们稍后需要从 **BalloonListener** 类调用这个方法。 | | ❼ | 创建一个名为 **isPopped()** 的效用函数;我们还将从 **BalloonListener** 类中调用这个方法。 |

此时,您可以玩功能有限的游戏。当你点击“播放”时,一组气球会浮到顶部;单击气球可将其从视图组中移除。当气球到达顶部时,它也会从视图组中移除。

管理 pin

当一个气球离开玩家时,我们想要更新屏幕顶部的图钉图像。对于每个丢失的气球,我们希望显示一个损坏的图钉图像。我们需要更改的代码在 MainActivity 中;所以,让我们来实现这一改变。

We can start by declaring two member variables on MainActivity.

  • number of pins = 5;—我们布局中的引脚数量。

  • ;—每次气球飞走,我们增加这个变量。

Let’s also create an ArrayList to hold the pushpin images. We want to put them in an ArrayList so we can reference the pushpin images programmatically. Creating and populating the ArrayList with the pushpin images can be done with the code in Listing 7-32. This code can be written inside the onCreate() method of MainActivity.private** ArrayList pinImages = new ArrayList<>();pinImages.add((ImageView) findViewById(R.id.pushpin1));pinImages.add((ImageView) findViewById(R.id.pushpin2));pinImages.add((ImageView) findViewById(R.id.pushpin3));pinImages.add((ImageView) findViewById(R.id.pushpin4));pinImages.add((ImageView) findViewById(R.id.pushpin5));Listing 7-32

数组列表中的图钉图像

We’ve already got the logic to handle the missed balloons inside the popBalloon() method. We already know how to handle the case when the player pops the Balloon; all we need to do is add some more logic to the existing if-else condition. Listing 7-33 shows us that code.public void popBalloon(Balloon bal, boolean isTouched) {contentView.removeView(bal);if(isTouched) {userScore++;scoreDisplay.setText(String.format("%d", userScore));}else { ❶pinsUsed++; ❷if (pinsUsed <= pinImages.size() ) { ❸pinImages.get(pinsUsed -1).setImageResource(R.drawable.pin_broken); ❹Toast.makeText(this, "Ouch!",Toast.LENGTH_SHORT).show(); ❺}if(pinsUsed == numberOfPins) { ❻gameOver();}}}private void gameOver() {// TODO: implement GameOver methodToast.makeText(this, "Game Over", Toast.LENGTH_LONG).show();}Listing 7-33

popBalloon()

| -好的 | 如果**被触摸**为*假*,则意味着气球从玩家手中逃脱。 | | ❷ | 增加**引脚使用的**变量。对于每一个丢失的气球,我们增加这个变量。 | | -你好 | 让我们检查一下 **pinsUsed** 是否小于或等于包含图钉图像的数组列表的大小(它有五个元素);如果这个表达式为*真*,那就意味着游戏还没有结束,玩家还有多余的图钉,我们可以继续游戏。 | | (a) | 此代码替换图钉的图像;它将图像设置为断开的大头针的图像。 | | (一) | 我们向玩家显示一个简单的祝酒词。祝酒词是出现在屏幕底部的一个小弹出窗口,然后从视图中消失。 | | ❻ | 让我们检查一下玩家是否用完了所有的五个图钉。如果有,我们调用 **gameOver()** 方法,我们仍然需要实现它。 |**

**

当游戏结束时

When the game is over, we need to do some cleanup; at the very least, we have to reset the pushpin images—which is easy enough to do. Listing 7-34 should accomplish that job.for (ImageView pin: pinImages) {pin.setImageResource(R.drawable.pin);}Listing 7-34

重置图钉图像

We also need to reset a couple of counters. To do these cleanups, let’s reorganize MainActivity a little bit. Start with implementing the gameOver() method, as shown in Listing 7-35.private void gameOver() {isGameStopped = true;Toast.makeText(this, "Game Over", Toast.LENGTH_LONG).show();btn.setText("Play game");}Listing 7-35

gameOver()

我们只是向玩家敬酒,宣布游戏结束的消息。我们还重置了按钮的文本。你可能已经注意到了是一个被终止的变量;这是我们需要创建的另一个成员变量,以帮助我们管理一些基本的游戏状态。

Next, let’s add another method called finishLevel(), so we can group some actions we need to take when the player finishes a level; the code for that is in Listing 7-36.private void finishLevel() {Log.d(TAG, "FINISH LEVEL");String message = String.format("Level %d finished!", level);Toast.makeText(this, message, Toast.LENGTH_LONG).show(); // ❶level++; ❷updateGameStats(); ❸btn.setText(String.format("Start level %d", level)); ❹Log.d(TAG, String.format("balloonsLaunched = %d", balloonsLaunched));balloonsPopped = 0; ❺}Listing 7-36

finishLevel()

| -好的 | 告诉玩家这一关结束了。 | | ❷ | 增加级别变量。 | | -你好 | 我们还没有实现这个方法,但是你可以猜到它会做什么。它将简单地显示当前分数和当前级别。 | | (a) | 将按钮的文本更改为反映下一级别的文本。 | | (一) | 我们正在将**气球弹出的**变量重置为零。我们还需要创建这个成员变量。它会记录所有被戳破的气球。我们将用它来确定关卡是否已经完成。 |

Listing 7-37 shows the code for the updateGameStats() method.private void updateGameStats() {levelDisplay.setText(String.format("%s", level));scoreDisplay.setText(String.format("%s", userScore));}Listing 7-37

updateGameStats()

现在,我们需要知道关卡何时完成。我们以前从未为此烦恼过,因为我们只是让 LevelLoop 线程来完成发射气球的工作,但现在我们需要管理一些游戏状态。在 MainActivity 中有几个地方我们可以发出关卡结束的信号。我们可以在 LevelLoop 线程内部实现。只要 while 循环结束,就应该表示这个级别结束了;但是如果我们把它放在那里,游戏可能会感觉不同步。当一些气球仍在播放动画时,可能会出现祝酒词。我们将调用 popBalloon() 方法中的 finishLevel() 来代替。

If we simply count the number of Balloons that gets popped—which is everything, because every balloon gets popped one way or another—compare it with the number of balloons we launch per level; when the two variables are equal, that should signal the end of the level. Listing 7-38 shows that implementation.@Overridepublic void popBalloon(Balloon bal, boolean isTouched) {balloonsPopped++;contentView.removeView(bal);if(isTouched) {userScore++;scoreDisplay.setText(String.format("%d", userScore));}else {pinsUsed++;if (pinsUsed <= pinImages.size() ) {pinImages.get(pinsUsed -1).setImageResource(R.drawable.pin_broken);Toast.makeText(this, "Ouch!",Toast.LENGTH_SHORT).show();}if(pinsUsed == numberOfPins) {gameOver();}}if (balloonsPopped == balloonsPerLevel) {finishLevel();}}Listing 7-38

popBalloon()

Next, let’s move to the startLevel() method. The refactored code is shown in Listing 7-39.private void startLevel() {if (isGameStopped) { ❶isGameStopped = false; ❷startGame(); ❸}updateGameStats(); ❹new LevelLoop(level).start();}Listing 7-39

startLevel()

| -好的 | 让我们检查一些游戏状态。玩家第一次开始游戏时,这将是错误的。这在 **gameOver()** 方法中被重置。如果这个条件为真,就意味着我们要开始一个新游戏。 | | ❷ | 让我们将**is gamestadopted**的值设置为 false,因为我们已经开始了一个新游戏。 | | -你好 | 调用 **startGame()** 方法。我们将很快实现这一点。 | | (a) | 更新游戏统计数据。 |

Next, implement the startGame() method; Listing 7-40 shows us how.private void startGame() {// reset the scoresuserScore = 0;level = 1;updateGameStats();//reset the pushpin imagesfor (ImageView pin: pinImages) {pin.setImageResource(R.drawable.pin);}}Listing 7-40

startGame()方法

那应该可以处理一些基本的家务。

声音的

大多数游戏在背景中使用音乐来增强玩家的体验。这些游戏还使用声音效果来获得更身临其境的感觉。我们的小游戏会用到这两者。当游戏开始时,我们会播放背景音乐,当气球爆开时,我们也会播放音效。

我从 YouTube 音频库拿到了背景音乐和 popping 音效;请随意选择您喜欢的背景音乐。

Once you’ve procured the audio files, you need to add them to the project; firstly, you need to create a raw folder in the app/res directory. You can do that with the context menu. Right-click app/res, then choose NewFolderRaw Resources Folder, as shown in Figure 7-29.

![img/340874_4_En_7_Fig29_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig29_HTML.jpg)
Figure 7-29

新建资源文件夹

In the window that follows, click Finish, as shown in Figure 7-30.

![img/340874_4_En_7_Fig30_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_7_Fig30_HTML.jpg)
Figure 7-30

新的安卓组件

接下来,右键单击原始文件夹。根据您使用的操作系统,选择在 Finder 中显示在浏览器中显示

您现在可以将音频文件拖放到 raw 文件夹中。

To play the background music, we will need a MediaPlayer object. This object is built-in in Android SDK. We simply need to import it to our Java source file. The following are the key method calls for the MediaPlayer object. Listing 7-41 shows the important APIs we will use.import android.media.MediaPlayerMediaPlayer mplayer;mplayer = MediaPlayer.create(ctx.getApplicationContext(), R.raw.ngoni); ❶mplayer.setVolume(07.f, 0.7f); ❷mplayer.setLooping(true); ❸mplayer.start(); ❹mplayer.pause() ❺Listing 7-41

Key 方法调用 MediaPlayer 对象

| -好的 | 该语句创建 MediaPlayer 的一个实例。它需要两个参数:第一个参数是一个上下文对象,第二个参数是原始文件夹(ngoni.mp3)中的资源文件的名称。我们在这里指定一个资源文件,所以不需要添加 **.mp3** 扩展名。 | | ❷ | **setVolume()** 方法有两个参数。第一个是浮点值,用于指定左声道的音量,第二个是右声道的音量。这些值的范围是从 0.0 到 1.0。如您所见,我指定了 70%的音量。在实际的游戏中,您可能希望将这些值存储在一个首选项文件中,并让用户控制它。 | | -你好 | 我希望音乐继续播放。我把它设置成自动重复播放。 | | (a) | 这将开始播放音乐。 | | (一) | 这将暂停音乐。 |

为了播放气球的弹出声音,我们将使用 SoundPool 对象。爆音是一个非常短的音频文件,可以反复使用(每次我们爆气球时)。使用 SoundPool 对象可以最好地管理这些声音。

There’s a bit of setup required before you can use a SoundPool object; Listing 7-42 shows this setup.public Audio(Activity activity) { ❶AudioManager audioManager = (AudioManager)activity.getSystemService(Context.AUDIO_SERVICE);float actVolume = (float)audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); ❷float maxVolume = (float)audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);volume = actVolume / maxVolume;activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); ❸AudioAttributes audioAttrib = new AudioAttributes.Builder() ❹.setUsage(AudioAttributes.USAGE_GAME).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build();soundPool = new SoundPool.Builder().setAudioAttributes(audioAttrib).setMaxStreams(6).build();soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { ❺@Overridepublic void onLoadComplete(SoundPool soundPool, int sampleId, int status) {Log.d(TAG, "SoundPool is loaded");isLoaded = true;}});soundId = soundPool.load(activity, R.raw.pop, 1); ❻}public void playSound() {if (isLoaded) {soundPool.play(soundId, volume, volume, 1, 0, 1f); ❼}Log.d(TAG, "playSound");}Listing 7-42

声池〔??〕〔??〕

| -好的 | 设置 SoundPool 和 AudioManager 通常在构造函数上完成。我们需要传递一个 Activity 实例(将是 MainActivity),这样我们就可以获得对音频服务的引用。 | | ❷ | 我们将使用 **getStreamVolume()** 和 **getStreamMaxVolume()** 来确定我们想要的声音效果有多大。 | | -你好 | 这将音量控制绑定到 MainActivity。 | | (a) | 我们需要设置一些属性来构建声音池。这种构建音池的方法是针对 Android 及以上版本(Lollipop)的。 | | (一) | 声音是异步加载的。我们需要设置一个监听器,这样当它被加载时我们会得到通知。 | | ❻ | 现在我们可以从 raw 文件夹中加载声音文件。 | | ❼ | 这条线播放声音。这就是我们将在 **popBalloon()** 方法中调用的内容。 |

We’re going to put all of this code in a separate class; we’ll name it the Audio class. Create a new Java class named Audio. You can do that by right-clicking the project’s package, then choosing NewJava Class, as we did before. Listing 7-43 shows the full code for the Audio class.import android.app.Activity;import android.content.Context;import android.media.AudioAttributes;import android.media.AudioManager;import android.media.MediaPlayer;import android.media.SoundPool;import android.util.Log;public class Audio {private final int soundId;private MediaPlayer mplayer;private float volume;private SoundPool soundPool;private boolean isLoaded;private final String TAG = getClass().getName();public Audio(Activity activity) {AudioManager audioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);float actVolume = (float) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);float maxVolume = (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);volume = actVolume / maxVolume;activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);AudioAttributes audioAttrib = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_GAME).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build();soundPool = new SoundPool.Builder().setAudioAttributes(audioAttrib).setMaxStreams(6).build();soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {@Overridepublic void onLoadComplete(SoundPool soundPool, int sampleId, int status) {Log.d(TAG, "SoundPool is loaded");isLoaded = true;}});soundId = soundPool.load(activity, R.raw.pop, 1);}public void playSound() {if (isLoaded) {soundPool.play(soundId, volume, volume, 1, 0, 1f);}Log.d(TAG, "playSound");}public void prepareMediaPlayer(Context ctx) {mplayer = MediaPlayer.create(ctx.getApplicationContext(), R.raw.ngoni);mplayer.setVolume(05.f, 0.5f);mplayer.setLooping(true);}public void playMusic() {mplayer.start();}public void stopMusic() {mplayer.stop();}public void pauseMusic() {mplayer.pause();}}Listing 7-43

音频类

Now we can add some sounds to the app. In the MainActivity class, we need to create a member variable of type Audio, like this:Audio audio;Then, in the onCreate() method, we instantiate the Audio class and call the prepareMediaPlayer() method, as shown in the following:audio = new Audio(this);audio.prepareMediaPlayer(this);We want to play the music only when the game is in play; so, in MainActivity’s startGame() method, we add the following statement:audio.playMusic();When the game is not at play anymore, we want the music to stop; so, in the gameOver() method, we add this statement:audio.pauseMusic();Finally, in the popBalloon() method, add the following statement:audio.playSound();

最后润色

If you’ve been following the coding exercise (and running the game), you may have noticed that even after the game is over, you can still see some balloons flying around; you can thank the background thread for that. Even when all the five pins have been used up, the level is still active, and we still see some balloons being launched. To handle that, we can do the following:

  1. 1.

    Track all balloons released at each level. We can do this with an array list. Whenever we launch a balloon, we add it to the list.

  2. 2.

    Once the balloon is punctured, we will delete it from the list.

  3. 3.

    If the game is over, we will traverse all the remaining balloon objects in the array list and set their status to popped.

  4. 4.

    Finally, remove all remaining balloon objects from the view group.

First, let’s declare an ArrayList (as a member variable on MainActivity) to hold all the references to all Balloons that will be launched per level. The following code accomplishes that:private ArrayList balloons = new ArrayList<>();Next, in the launchBalloon() method, we insert a statement that adds a Balloon object to the ArrayList, like this:balloons.add(btemp);Next, in the gameOver() method, we add a logic that will loop through all the remaining Balloons in the ArrayList, set their popped status to true, and also remove the Balloon instance from the ViewGroup (the code is shown in Listing 7-44).private void gameOver() {isGameStopped = true;Toast.makeText(this, "Game Over", Toast.LENGTH_LONG).show();btn.setText("Play game");for (Balloon bal : balloons) {bal.setPopped(true);contentView.removeView(bal);}balloons.clear();audio.pauseMusic();}Listing 7-44

gameOver()方法

Finally, we need to add the setPopped() method to the Balloon class, as shown in Listing 7-45.public void setPopped(boolean b) {isPopped = true;}Listing 7-45

气球类中的 setPopped()方法

That should do it. The final code listing we will see in this chapter is the complete code for MainActivity. It may be difficult to keep things straight after all the changes we made to MainActivity; so, to provide as a reference, Listing 7-46 shows MainActivity’s complete code.import android.graphics.Color;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.view.ViewTreeObserver;import android.widget.Button;import android.widget.ImageView;import android.widget.TextView;import android.widget.Toast;import java.util.ArrayList;import java.util.Date;import java.util.Random;public class MainActivity extends AppCompatActivityimplements PopListener {ViewGroup contentView;private static String TAG;private int[] colors = new int[3];private int scrWidth;private int scrHeight;private int level = 1;private TextView levelDisplay;private TextView scoreDisplay;private int numberOfPins = 5;private int pinsUsed;private int balloonsLaunched;private int balloonsPerLevel = 8;private int balloonsPopped = 0;private boolean isGameStopped = true;private ArrayList pinImages = new ArrayList<>();private ArrayList balloons = new ArrayList<>();private int userScore;Button btn;Audio audio;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);TAG = getClass().getName();getWindow().setBackgroundDrawableResource(R.mipmap.background);setContentView(R.layout.activity_main);colors[0] = Color.argb(255, 255, 0, 0);colors[1] = Color.argb(255, 0, 255, 0);colors[2] = Color.argb(255, 0, 0, 255);contentView = (ViewGroup) findViewById(R.id.content_view);levelDisplay = (TextView) findViewById(R.id.level_display);scoreDisplay = (TextView) findViewById(R.id.score_display);pinImages.add((ImageView) findViewById(R.id.pushpin1));pinImages.add((ImageView) findViewById(R.id.pushpin2));pinImages.add((ImageView) findViewById(R.id.pushpin3));pinImages.add((ImageView) findViewById(R.id.pushpin4));pinImages.add((ImageView) findViewById(R.id.pushpin5));btn = (Button) findViewById(R.id.btn);btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {startLevel();}});contentView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {if (event.getAction() == MotionEvent.ACTION_DOWN) {setToFullScreen();}return false;}});audio = new Audio(this);audio.prepareMediaPlayer(this);}@Overrideprotected void onResume() {super.onResume();updateGameStats();setToFullScreen();ViewTreeObserver viewTreeObserver = contentView.getViewTreeObserver();if (viewTreeObserver.isAlive()) {viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {@Overridepublic void onGlobalLayout() {contentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);scrWidth = contentView.getWidth();scrHeight = contentView.getHeight();}});}}public void launchBalloon(int xPos) {balloonsLaunched++;int curColor = colors[nextColor()];Balloon btemp = new Balloon(MainActivity.this, curColor, 100, level);btemp.setY(scrHeight);btemp.setX(xPos);balloons.add(btemp);contentView.addView(btemp);btemp.release(scrHeight, 5000);Log.d(TAG, "Balloon created");}private void startLevel() {if (isGameStopped) {isGameStopped = false;startGame();}updateGameStats();new LevelLoop(level).start();}private void finishLevel() {Log.d(TAG, "FINISH LEVEL");String message = String.format("Level %d finished!", level);Toast.makeText(this, message, Toast.LENGTH_LONG).show();level++;updateGameStats();btn.setText(String.format("Start level %d", level));Log.d(TAG, String.format("balloonsLaunched = %d", balloonsLaunched));balloonsPopped = 0;}private void updateGameStats() {levelDisplay.setText(String.format("%s", level));scoreDisplay.setText(String.format("%s", userScore));}private void setToFullScreen() {contentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE| View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);}private static int nextColor() {int max = 2;int min = 0;int retval = 0;Random random = new Random();retval = random.nextInt((max - min) + 1) + min;Log.d(TAG, String.format("retval = %d", retval));return retval;}@Overridepublic void popBalloon(Balloon bal, boolean isTouched) {balloonsPopped++;balloons.remove(bal);contentView.removeView(bal);audio.playSound();if(isTouched) {userScore++;scoreDisplay.setText(String.format("%d", userScore));}else {pinsUsed++;if (pinsUsed <= pinImages.size() ) {pinImages.get(pinsUsed -1).setImageResource(R.drawable.pin_broken);Toast.makeText(this, "Ouch!",Toast.LENGTH_SHORT).show();}if(pinsUsed == numberOfPins) {gameOver();}}if (balloonsPopped == balloonsPerLevel) {finishLevel();}}private void startGame() {// reset the scoresuserScore = 0;level = 1;updateGameStats();//reset the pushpin imagesfor (ImageView pin: pinImages) {pin.setImageResource(R.drawable.pin);}audio.playMusic();}private void gameOver() {isGameStopped = true;Toast.makeText(this, "Game Over", Toast.LENGTH_LONG).show();btn.setText("Play game");for (Balloon bal : balloons) {bal.setPopped(true);contentView.removeView(bal);}balloons.clear();audio.pauseMusic();}class LevelLoop extends Thread {private int shortDelay = 500;private int longDelay = 1_500;private int maxDelay;private int minDelay;private int delay;private int looplevel;int balloonsLaunched = 0;public LevelLoop(int argLevel) {looplevel = argLevel;}public void run() {while (balloonsLaunched <= balloonsPerLevel) {balloonsLaunched++;Random random = new Random(new Date().getTime());final int xPosition = random.nextInt(scrWidth - 200);maxDelay = Math.max(shortDelay, (longDelay - ((looplevel -1)) * 500));minDelay = maxDelay / 2;delay = random.nextInt(minDelay) + minDelay;Log.i(TAG, String.format("Thread delay = %d", delay));try {Thread.sleep(delay);}catch(InterruptedException e) {Log.e(TAG, e.getMessage());}// need to wrap this on runOnUiThreadrunOnUiThread(new Thread() {public void run() {launchBalloon(xPosition);}});}}}}Listing 7-46

主要活动

**

八、测试和调试

  • 游戏测试的类型

  • 单元测试

  • 排除故障

  • Android Profiler

我们已经完成了项目的编程阶段;接下来,我们进行测试和调试。在这个阶段,我们必须找到代码中的所有错误和不一致之处。一个打磨过的游戏没有粗糙的边缘;我们需要测试它,调试它,并确保它不会占用计算资源。

游戏测试的类型

功能测试。一个游戏基本上就是一个 app。功能性测试 是测试一个应用的标准方式。它被称为 functional ,因为我们正在测试应用的功能(也称为功能),因为它们是在需求规格中指定的——需求规格是你(或游戏设计师)在游戏的规划阶段编写的。 这本来是要写在文档里的(通常称为功能需求说明书)。你可能在功能规范中发现的例子有“用户在进入游戏前必须登录到游戏服务器”和“用户可以选择或返回到已经完成的关卡;用户不能选择尚未完成的级别。测试人员,通常称为 QA 或 QC(分别是质量保证和质量控制的缩写),是执行这些测试的人。他们将创建测试资产,制定测试策略,执行它们,并最终报告执行的结果。失败的测试通常被分配给开发人员(您)来修复和重新提交。我在这里描述的是一个开发团队的典型实践,这个团队有一个单独的或者专门的测试团队;如果你是一个人的团队,QA 很可能也是你。测试是一种完全不同的技能;我强烈建议你寻求其他人的帮助,最好是那些有测试经验的人。

性能测试 。你可能从它的名字就能猜到这种测试是做什么的。它将游戏推向极限,并看到它在压力下的表现。这里你想看到的是游戏在高于正常水平的条件下是如何反应的。浸泡测试或 耐力测试 是一种性能测试;通常,你会让游戏在各种操作模式下运行很长一段时间,例如,在游戏暂停或出现标题屏幕时,让游戏运行很长一段时间。你在这里试图找到的是游戏如何响应这些条件,以及它如何利用系统资源,如内存、CPU、网络带宽等;您将使用类似于 Android Profiler 的工具来执行这些测量。

性能测试的另一种形式是 音量测试;如果您的游戏使用数据库,您可能想知道当数据加载到数据库时游戏将如何响应。你要检查的是系统在各种数据负载下的反应。

尖峰测试 或者可扩展性测试也是另一种性能测试。如果您的游戏依赖于中央服务器,该测试通常会增加连接到中央服务器的用户(设备端点)数量。你想要观察用户数量的激增如何影响玩家体验;游戏是否仍然响应迅速,是否对每秒帧数有影响,是否有滞后等等?

兼容性测试是检查游戏在不同设备和软硬件配置上的表现。这就是 AVDs (Android 虚拟设备)派上用场的地方;因为 avd 只是简单的软件仿真器,所以你不必购买不同的设备。尽可能使用 AVDs。有些游戏很难在模拟器上进行可靠的测试;当你处于这种情况下,你真的不得不为测试设备花钱。

**符合性或一致性测试 。这是你对照应用或游戏上的 Google Play 指南检查游戏的地方;请务必在 https://bit.ly/developerpolicycenter 阅读 Google Play 的开发者政策中心。确保你也熟悉 PEGI(泛欧游戏信息)和 ESRB(娱乐软件评级委员会)。如果游戏中有不符合特定分级的不良内容,需要识别并报告。违规可能是拒绝的原因,这可能导致昂贵的返工和重新提交。

本地化测试 非常重要,尤其是当游戏面向全球市场时。游戏标题、内容和文本需要用支持的语言翻译和测试。

恢复测试。这将边缘案例测试带到了另一个高度。在这里,应用被迫失败,您将观察应用在失败时的行为以及失败后如何恢复。它会让你了解你是否写了足够多的 try-catch-finally 块。应用应该优雅地失败,而不是突然失败。只要有可能,运行时错误应该由 try-catch 块来保护;并且当异常发生时,尽量写日志,保存游戏状态。

**渗透或安全测试 。这种测试试图发现游戏的弱点。它模拟了潜在攻击者为了规避游戏的所有安全功能而进行的活动;例如,如果游戏使用数据库来存储数据,尤其是用户数据,则在 Wireshark 运行时,一支笔测试人员(进行渗透测试的专业人员)可能会从头到尾玩一遍游戏—Wireshark 是一种检查数据包的工具;这是一个网络协议分析器。如果您以明文形式存储密码,它会在这些测试中显示出来。

声音检测 。检查加载文件时是否有任何错误;此外,如果有破裂声或其他声音,请听听声音文件。

开发者测试 。这是你(程序员)在给游戏添加一层又一层代码时所做的测试。这包括编写测试代码(也用 Java)来测试你的实际程序。这就是所谓的单元测试。Android 开发者通常会进行 JVM 测试和仪器测试;我们将在接下来的章节中对此进行更多的讨论。****

****

单元测试

单元测试实际上是开发人员执行的功能测试,而不是 QA 或 QC。单元测试很简单;这是一个方法可能会做或产生的特定的东西。一个应用通常有许多单元测试,因为每个测试都是一组定义非常狭窄的行为。所以,你需要大量的测试来覆盖整个功能。Android 开发人员通常使用 JUnit 来编写单元测试。

JUnit 是由 Kent Beck 和 Erich Gamma 编写的回归测试框架;你可能记得他们分别是极限编程的创始人和四人帮(g of,设计模式)的成员。

Java 开发人员长期以来一直使用 JUnit 进行单元测试。Android Studio 是 JUnit 自带的,并且很好地集成在其中。我们不需要做太多的设置工作。我们只需要编写我们的测试。

JVM 测试与仪器测试

如果你观察任何一个 Android 应用,你会发现它有两个部分:一个基于 Java 的行为和一个基于 Android 的行为。

The Java part is where we code business logic, calculations, and data transformations. The Android part is where we actually interact with the Android platform. This is where we get input from users or show results to them. It makes perfect sense if we can test the Java-based behavior separate from the Android part because it’s much quicker to execute. Fortunately, this is already the way it’s done in Android Studio. When you create a project, Android Studio creates two separate folders—one for the JVM tests and another for the instrumented tests. Figure 8-1 shows the two test folders in Android view, and Figure 8-2 shows the same two folders in Project view.

![img/340874_4_En_8_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig1_HTML.jpg)
Figure 8-1

Android 视图中的 JVM 测试和插装测试

![img/340874_4_En_8_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig2_HTML.jpg)
Figure 8-2

项目视图中的 JVM 测试和插装测试

从图 8-1 或 8-2 中可以看出,Android Studio 为 JVM 和插装测试生成了样本测试文件。示例文件只是作为快速参考;它向我们展示了单元测试可能是什么样子。

简单的演示

To dive into this, create a project with an empty Activity. Create a class, then name it Factorial.java, and fill it up with the code shown in Listing 8-1.public class Factorial {public static double factorial(int arg) {if (arg == 0) {return 1.0;}else {return arg + factorial(arg - 1);}}}Listing 8-1

Factorial.java

Make sure that Factorial.java is open in the main editor, as shown in Figure 8-3; then, from the main menu bar, go to NavigateTest. Similarly, you can also create a test using the keyboard shortcut (Shift+Command+T on macOS and Ctrl+Shift+T for Linux and Windows).

![img/340874_4_En_8_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig3_HTML.jpg)
Figure 8-3

为 Factorial.java 创建一个测试

Right after you click “Test,” a pop-up dialog (Figure 8-4) will prompt you to click another link—click “Create New Test” as shown in Figure 8-4.

![img/340874_4_En_8_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig4_HTML.jpg)
Figure 8-4

创建新的测试弹出窗口

Right after creating a new test, you’ll see another pop-up dialog, shown in Figure 8-5, which I’ve annotated. Please follow the annotations and instructions in Figure 8-5.

| -什么 | 您可以选择想要使用哪个测试库。您可以选择 JUnit 3、4 或 5。您甚至可以选择 Groovy JUnit、Spock 或 TestNG。我使用 JUnit4 是因为它是随 Android Studio 一起安装的。 | | ➋ | 命名测试类的约定是“要测试的类名”+“Test”。Android Studio 使用该约定填充该字段。 | | ➌ | 留空;我们不需要继承任何东西。 | | -你好 | 我们现在不需要 setUp() 和 tearDown() 例程,所以不要检查它们。 | | ➎ | 让我们检查一下 factorial() 方法,因为我们想为此生成一个测试。 |
![img/340874_4_En_8_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig5_HTML.jpg)
Figure 8-5

创建因子测试

When you click the OK button, Android Studio will ask where you want to save the test file. This is a JVM test, so we want to save it in the “test” folder (not in androidTest). See Figure 8-6. Click “OK.”

![img/340874_4_En_8_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig6_HTML.jpg)
Figure 8-6

选择目的地目录

Android Studio will now create the test file for us. If you open FactorialTest.java, you’ll see the generated skeleton code—shown in Figure 8-7.

| -什么 | 文件*Factorial.java*被创建在*测试*文件夹下。 | | ➋ | 创建了一个 factorial() 方法,并将其注释为 @Test 。这就是 JUnit 知道这个方法是单元测试的方式。您可以在方法名前面加上“test”,例如 testFactorial(),但这不是必需的,有了 @Test 注释就足够了。 | | ➌ | 这是我们放置断言的地方。 |
![img/340874_4_En_8_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig7_HTML.jpg)
Figure 8-7

FactorialTest.java 在项目视图和主编辑器中

看到这有多简单了吗?就设置和配置而言,在 Android Studio 中创建一个测试用例实际上并不涉及我们太多。我们现在需要做的就是编写我们的测试。

实施测试

JUnit supplies several static methods that we can use in our test to make assertions about our code’s behavior. We use assertions to show an expected result which is our control data. It’s usually calculated independently and is known to be true or correct—that’s why you use it as a control data. When the expected data is returned from the assertion, the test passes; otherwise, the test fails. Table 8-1 shows the common assert methods you might need for your code.Table 8-1

常见断言方法

|

方法

|

描述

|
| --- | --- |
| assertEquals() | 如果两个对象或基元具有相同的值,则返回 true |
| assertNotEquals() | assertEquals 的反义词() |
| assertSame() | 如果两个引用指向同一个对象,则返回 true |
| 断言紧急事件() | assertSame 的反向() |
| assertTrue() | 测试布尔表达式 |
| assertFalse() | assertTrue 的反函数() |
| 断言 Null() | 测试空对象 |
| 断言 NotNull() | assertNull 的反向() |

Now that we know a couple of assert methods, we’re ready to write some test. Listing 8-2 shows the code for FactorialTest.java.import org.junit.Test;import static org.junit.Assert.*;public class FactorialTest {@Testpublic void factorial() {assertEquals(1.0, Factorial.factorial(1),0.0);assertEquals(120.0, Factorial.factorial(5), 0.0);}}Listing 8-2

FactorialTest.java

我们的 FactorialTest 类只有一个方法,因为这只是为了举例说明。当然,真实世界的代码会有比这更多的方法。

注意,每个测试(方法)都由 @Test 注释。这就是 JUnit 如何知道 factorial() 是一个测试用例。还要注意的是, assertEquals() 是 Assert 类的一个方法,但是我们没有在这里写完全限定名,因为我们在 Assert 上有一个静态导入——这当然让事情变得更简单。

The assertEquals() method takes three parameters; they’re illustrated in Figure 8-8.

| -什么 | **期望值**是你的控制数据;这通常在测试中被硬编码。 | | ➋ | **实际值**是您的方法返回的值。如果预期值与实际值相同,则 assertEquals()通过——您的代码表现正常。 | | ➌ | **Delta** 意在反映*实际*和*预期*值能够有多接近并且仍然被认为是相等的。一些开发人员称这个参数为“模糊”因素。当期望值和实际值之间的差异大于“模糊因子”时,那么 assertEquals() 将会失败。我在这里使用 0.0 是因为我不想容忍任何形式的偏差。您可以使用其他值,如 0.001、0.002 等等;这取决于你的用例以及你的应用能够容忍多少模糊。 |
![img/340874_4_En_8_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig8_HTML.jpg)
Figure 8-8

assertEquals 方法

现在,我们的代码完成了。如果你愿意,你可以在代码中插入更多的断言,这样你就可以更好地理解事物。

有几件事我没有包括在这个示例代码中。我没有重写 setUp() 和 tearDown() 方法,因为我不需要它。如果需要建立数据库连接、网络连接等等,通常会使用 setUp() 方法。使用 tearDown()方法关闭您在设置()中打开的任何东西。

现在,我们准备运行测试。

运行单元测试

You can run just one test or all the tests in the class. The little green arrows in the gutter of the main editor are clickable. When you click the little arrow beside the name of the class, that will run all the tests in the class. When you click the one beside the name of the test method, that will run only that test case. See Figure 8-9.

![img/340874_4_En_8_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig9_HTML.jpg)
Figure 8-9

FactorialTest.java 在主编辑

同样,您也可以从主菜单栏运行测试;前往运行运行

Figure 8-10 shows the result of the text execution.

![img/340874_4_En_8_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig10_HTML.jpg)
Figure 8-10

运行 FactorialTest.java 的结果

Android Studio 为您提供了大量的提示,因此您可以判断您的测试是通过还是失败。我们的第一次运行告诉我们Factorial.java有问题; assertEquals() 失败。

Tip

当测试失败时,最好使用调试器来调查代码。FactorialTest.java 与我们项目中的其他职业没有什么不同;这只是另一个 Java 文件,我们肯定可以调试它。在测试代码的关键位置设置一些断点,然后运行“调试器”而不是“运行”它,这样你就可以遍历它了。

我们的测试失败了,因为 1 的阶乘不是 2,而是 1。如果你仔细观察Factorial.java,你会注意到阶乘值没有正确计算。

Edit the Factorial.java file, then change this line:return arg + factorial(arg - 1);to this linereturn arg * factorial(arg - 1);If we run the test again, we see successful results, as shown in Figure 8-11.

![img/340874_4_En_8_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig11_HTML.jpg)
Figure 8-11

成功测试

我们现在看到的不是黄色的感叹号,而是绿色的复选标记。我们现在看到的不是“测试失败”,而是“测试通过”现在我们知道我们的代码按预期工作。

排除故障

我们写代码已经有一段时间了;我敢肯定,到目前为止,您的代码已经遇到了一些问题,并且已经看到了 Android Studio 提醒您注意这些错误的各种方式。

句法误差

你会经常遇到的一个错误是语法错误。它们发生是因为你在代码中写了一些不应该出现的东西;或者你忘了写什么(比如分号)。这些错误可能是良性的,如忘记了右花括号,也可能是复杂的,如在使用泛型时将错误类型的参数传递给方法或参数化类。幸运的是,Android Studio 非常善于发现这类错误。这几乎就像 IDE 在不断地读取代码并编译它。

Syntax errors are simple enough to solve, and you’ve probably figured it out by now. Whenever you see red squiggly lines or red-colored text in the IDE (as shown in Figure 8-12), just hover the mouse on top of the red-colored text or red squiggly lines, and you should see Android Studio’s tips.

![img/340874_4_En_8_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig12_HTML.jpg)
Figure 8-12

语法错误显示在编辑器中

The tips typically tell you what’s wrong with the code. In Figure 8-12, the error is Cannot resolve symbol ‘Button’, which means you haven’t imported the Button class just yet. To resolve this, position the mouse cursor on the offending word (Button, in this case), then use the Quick Fix feature (Option+Enter in Mac, Alt+Enter in Windows). Quick Fix in action is shown in Figure 8-13.

![img/340874_4_En_8_Fig13_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig13_HTML.jpg)
Figure 8-13

权宜之计

运行时错误

Runtime errors happen when your code encounters a situation it doesn’t expect; and as its name implies, that errant condition is something that appears only when the program is running—it’s not something you or the compiler can see at the time of compilation. Your code will compile without problems, but it may stop running when something in the runtime environment doesn’t agree with what your code wants to do. There are many examples of these things; here are some of them:

  • 这个应用从互联网上获得一些东西,一张图片或一个文件等等,所以它假设互联网是可用的,并且有网络连接。一直都是。经验应该告诉你,情况并不总是这样。网络连接有时会中断,如果您不在代码中考虑这一点,它可能会崩溃。

  • 该应用需要从文件中读取。就像我们前面的第一个案例一样,您的代码假设文件将一直存在。有时,文件会损坏,可能变得不可读。这也应该在代码中考虑。

  • 该应用执行数学计算。它使用用户输入的值,有时也使用其他计算得出的值。如果您的代码碰巧执行了除法,并且在其中一个除法中,除数为零,这也将导致运行时问题。

在大多数情况下,在处理运行时错误时,Java 是你的后盾。异常处理在 Java 中不是可选的。只要确保你没有在你的 try-catch 块上吝啬;总是放异常处理代码,你应该没问题。

逻辑错误

逻辑错误最难发现。顾名思义,这是你逻辑上的错误。当你的代码没有做你认为它应该做的事情时,那就是逻辑错误。有许多方法可以解决这个问题,但最常用的方法是(1)使用日志语句和(2)使用断点和遍历/单步执行代码。

Printing log messages is a simple way of marking the footprints of the program; you can do it with the simple System.out.println() statement, but I’d encourage you to use the Log class instead. Listing 8-3 shows a basic usage of the Log class.public class MainActivity extends AppCompatActivity {final String TAG = getClass().getName();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// ...}void doSomething() {Log.d(TAG, "Log message, doSomething");}}Listing 8-3

Log 类的基本用法

You can define the TAG variable anywhere in the class, but in Listing 8-3, I defined it as a class member; Log.d() prints a debug message. You can use the other methods of the Log class to print warnings, info, or errors. The other methods are shown here:Log.v(TAG, message) // verboseLog.d(TAG, message) // debugLog.i(TAG, message) // infoLog.w(TAG, message) // warningLog.e(TAG, message) // error

在每种情况下,标签是一个字符串或变量。您可以使用标记来过滤 Logcat 窗口中的消息。消息也是一个字符串或变量,它包含了您真正想在日志中看到的内容。

When you run your app, you can see the Log messages in the Logcat tool window. You can launch it either by clicking its tab in the menu strip at the bottom of the window (as shown in Figure 8-14) or from the main menu bar, ViewTool WindowsLogcat.

![img/340874_4_En_8_Fig14_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig14_HTML.jpg)
Figure 8-14

Logcat 工具窗口

遍历代码

Android Studio 包括一个交互式调试器,它允许你在代码运行时一步一步地调试代码。使用交互式调试器,我们可以在代码中的特定位置和特定时间点检查应用的快照—变量值、运行的线程等。代码中的这些特定位置被称为断点;你可以选择这些断点。

To set a breakpoint, choose a line that has an executable statement, then click its line number in the gutter. When you set a breakpoint, there will be a pink circle icon in the gutter, and the whole line is lit in pink—as shown in Figure 8-15.

![img/340874_4_En_8_Fig15_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig15_HTML.jpg)
Figure 8-15

调试器窗口

设置断点后,您必须在调试模式下运行应用。如果应用当前正在运行,请将其停止,然后从主菜单栏中单击运行调试“应用”。

Note

在调试模式下运行应用并不是调试应用的唯一方式。您还可以在当前运行的应用中附加调试器进程。在有些情况下,第二种技术是有用的;例如,当您试图解决的错误发生在非常特定的条件下时,您可能希望运行应用一段时间,当您认为您接近错误点时,您可以附加调试器。

照常使用该应用。当执行到您设置断点的一行时,该行将从粉红色变为蓝色。这就是你如何知道代码在断点处执行。此时,调试器窗口打开,执行停止,Android Studio 进入交互式调试模式。当您在这里时,应用的状态显示在调试工具窗口中。在此期间,您可以检查变量值,甚至看到应用中运行的线程。

您甚至可以通过单击带有眼镜图标的加号,在“监视”窗口中添加变量或表达式。将有一个文本字段,您可以在其中输入任何有效的表达式。当你按下输入时,Android Studio 会对表达式进行求值,并向你显示结果。要删除监视表达式,请选择表达式,然后单击“监视”窗口上的减号图标。

要恢复程序执行,您可以单击调试器工具栏顶部的“恢复程序”按钮—它是指向右侧的绿色箭头。或者,您也可以从主菜单栏运行恢复程序中恢复程序。如果你想在程序自然结束前暂停它,你可以点击调试器工具栏上的“停止应用”按钮;这是红色方块图标。或者,您也可以从主菜单栏运行停止应用中执行此操作。

仿形铣床

分析器让我们了解我们的应用/游戏如何使用计算资源,如 CPU、内存、网络带宽和电池。

The Profiler is new in Android Studio 3. It replaces the Android monitor with its new unified and shared timeline view for the CPU, memory, network, and energy graphs. Figure 8-16 shows the Profiler.

![img/340874_4_En_8_Fig16_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig16_HTML.jpg)
Figure 8-16

仿形铣床

You can get to the Profiler by going to the main menu bar, then selecting ViewTool WindowsProfiler.

| -什么 | 它显示正在分析的进程和设备。 | | ➋ | 它会显示要查看的会话。您还可以通过单击+按钮添加新的会话。 | | ➌ | 使用缩放按钮来控制要查看多少时间线。 | | -你好 | 新的共享时间线视图允许您查看 CPU、内存、网络和能源使用情况的所有图表。在顶部,您还会看到重要的应用事件,如用户输入或活动状态转换。 |

当您启动一个应用时,无论是在连接的设备上还是在仿真器上,您都会在 Profiler 上看到它的图形。

Note

如果您尝试使用低于 API 级别 26 的版本来分析 APK,您将会看到一些警告,因为 Android Studio 需要完全检测您的代码。您需要启用“高级分析”;但是,如果你的 APK 是奥利奥或更高,你不会看到任何警告。

如果您单击任何图表,Profiler 窗口将带您进入其中一个详细视图。例如,如果您单击 CPU,您将看到 CPU 利用率的详细视图。

中央处理器

Figure 8-17 shows the detailed view for the CPU utilization on the sample app I was running.

![img/340874_4_En_8_Fig17_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig17_HTML.jpg)
Figure 8-17

CPU 视图

除了实时利用率图,CPU 详细视图还显示了应用中所有线程及其状态的列表,您可以看到线程是否正在等待 I/O 或它们何时处于活动状态。

You might have noticed the “Record” button in Figure 8-17; if you click that button, you can get a report on all the methods that were executed in a given period. Notice also the selected trace type in the drop-down (Sample Java Methods); this trace type has a smaller overhead but not as detailed nor as accurate as the instrumented type (Trace Java Methods), meaning the sampled type may miss the execution of a very short-lived method. You might think, “just always use the instrumented type then”—you have to remember, though, that while instrumented type can record every method call, on Android devices before version 8, there is a limit on how much data can be captured; so, if you use the instrumented trace, that limit will be reached quickly. You can change that limit by editing the configuration for the instrumented capture. On the trace type drop-down, choose “Edit Configurations” as shown in Figure 8-18.

![img/340874_4_En_8_Fig18_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig18_HTML.jpg)
Figure 8-18

编辑配置

![img/340874_4_En_8_Fig19_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig19_HTML.jpg)
Figure 8-19

CPU 记录配置

图 8-19 显示了“采样间隔”和“文件大小限制”设置,您可以使用它们来调整采样的频率以及您想要分配给录像的文件大小。只是重申一下,文件大小限制只存在于运行 Android 8.0 或更低版本(< API level 26)的 Android 设备上。如果你的设备有更高的 Android 版本,你就不会受到这些限制。

If you click record, Android Studio will begin capturing data. Click the “Stop” button when you’d like to stop recording, as shown in Figure 8-20.

![img/340874_4_En_8_Fig20_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig20_HTML.jpg)
Figure 8-20

录制会话

When you hit stop, you can take a look at the individual threads, as shown in Figure 8-21.

![img/340874_4_En_8_Fig21_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig21_HTML.jpg)
Figure 8-21

检查螺纹

记忆

The memory profiler shows, in real time, how much memory your app is consuming. Figure 8-22 shows a snapshot of the memory view as I captured the memory footprint of a test app. As you can see, not only does the graph show how much memory your app is gulping, it also shows the breakdown, for example, how much memory is used by the code, stack, graphics, Java, and so on.

![img/340874_4_En_8_Fig22_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig22_HTML.jpg)
Figure 8-22

内存视图

You can force garbage collection (GC) in the memory view. See that garbage can icon at the top? Yup, if you click that, it’ll force a GC. The button to its right is also useful—the icon with a down-pointing arrow inside a box is a memory dump . If you click that, the Java heap will be dumped, and then you can inspect it, as shown in Figure 8-23.

![img/340874_4_En_8_Fig23_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig23_HTML.jpg)
Figure 8-23

Java 堆

The heap is a preserved amount of storage memory that the Android runtime allocates for our app. When we dumped the heap, it gave us a chance to examine instance properties of objects, as shown in Figure 8-24.

![img/340874_4_En_8_Fig24_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig24_HTML.jpg)
Figure 8-24

实例视图,参考选项卡

Reference 选项卡在查找内存泄漏时非常有用,因为它显示了指向您正在检查的对象的所有引用。

Another useful tool in the memory view is the Allocation tracker, shown in Figure 8-25.

| -什么 | 单击内存图时间线中的任意位置,查看分配跟踪器。这将向您显示在该时间点分配和释放的所有对象的列表。 | | ➋ | 这显示了应用在某个时间点正在使用的所有类的列表。 | | ➌ | 这显示了在特定时间点分配和释放的所有对象的列表。 | | -你好 | 跟踪器甚至包括分配的调用堆栈。 |
![img/340874_4_En_8_Fig25_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig25_HTML.jpg)
Figure 8-25

分配跟踪器

网络

Like the other views in the Profiler, the network view also shows real-time data. It lets you see and inspect data that is sent and received by your app; it also shows the total number of connections. Figure 8-26 shows a snapshot of the network profiler .

![img/340874_4_En_8_Fig26_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig26_HTML.jpg)
Figure 8-26

网络分析器

每次你的应用向网络发出请求,它都使用 WiFi 无线电来发送和接收数据——无线电不是最节能的;它很耗电,如果你不注意你的应用如何发出网络请求,那肯定会比平时更快地耗尽设备电池。

当您使用网络分析器时,一个好的开始方式是寻找网络活动的短峰值。当您看到急剧上升和下降的尖峰信号,并且它们分散在整个时间线上时,似乎您可以通过批处理网络请求来进行一些优化,以减少 WiFi 无线电需要唤醒和发送或接收数据的次数。

活力

By now you’re probably seeing a pattern on how the Profiler works. It shows you real-time data. In the case of the Energy profiler, it shows data on how much energy your app is guzzling—though it doesn’t really show the direct measure of energy consumption, the Energy profiler shows an estimation of the energy consumption of the CPU, the radio, and the GPS sensor. Figure 8-27 shows a snapshot of the Energy profiler .

![img/340874_4_En_8_Fig27_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_8_Fig27_HTML.jpg)
Figure 8-27

能量分析器

You can also use the Energy profiler to find system events that affect energy consumption, for example, wake locks, jobs, and alarms.

  • 唤醒锁是一种在设备进入睡眠状态时保持屏幕 CPU 开启的机制,例如,当应用播放视频时,它可能会使用唤醒锁来保持屏幕开启,即使没有用户交互——使用唤醒锁没有问题,但忘记释放唤醒锁会有问题;它让 CPU 的运行时间超过了必要的时间,这无疑会更快地耗尽电池。

  • 警报可用于以特定的时间间隔运行应用上下文之外的后台任务。当警报响起时,它可以运行一些任务;如果它运行一段高能耗的代码,您肯定会在能量分析器中看到它。

  • 当满足某些条件时,例如,当网络变得可用时,一个作业可以执行动作。您通常会使用 JobBuilder 创建一个作业,并使用 JobScheduler 来调度执行;当一个任务开始时,您也可以在能源配置文件中看到它们。

这是 Android Studio Profiler 的一个简单介绍;务必在 查看官方文档 https://developer . Android . com/studio/profile/Android-profiler。使用分析器可以让你了解游戏代码的哪一部分占用了资源。优化使用资源可以节省电池;你的用户会感谢你的。

关键要点

  • 我们已经讨论了你可以为你的游戏做的各种各样的测试;你不必做所有的测试,但是要确保你做的测试适用于你的游戏。

  • 开发测试(单元测试)应该是核心开发任务;试着养成将测试用例与实际代码一起编写的习惯。

  • Android Studio Profile 可以从底层检查您的应用的行为。它可以让你了解应用是如何消耗资源的;当您进行性能测试时,请使用这个工具。

****

九、OpenGL ES 简介

  • 关于 OpenGL ES

  • OpenGL 专家系统理论

  • GLSurfaceView 和 GLSurfaceView。渲染器

  • 在 OpenGL ES 中使用 Blender 数据

从 API level 11 (Android 3)开始,2D 渲染管道已经支持硬件加速。当你在画布上画图的时候(这是我们上两个游戏搭建的时候用的),画图操作已经在 GPU 上完成了;但这也意味着应用会消耗更多的 RAM,因为实现硬件加速需要更多的资源。

如果你构建的游戏不是那么复杂,那么使用画布构建游戏是一个不错的技术选择;但是,当视觉复杂性水平上升时,画布可能会耗尽能量,无法满足您的游戏需求。你需要更实在的东西。这就是 OpenGL ES 的用武之地。

什么是 OpenGL ES

开放图形库(OpenGL)来自硅图形(SGI);他们是高端图形工作站和大型机的制造商。最初,SGI 有一个名为 IRIS GL 的专有图形框架(后来成为行业标准),但随着竞争的加剧,SGI 选择将 IRIS GL 转变为一个开放框架。IRIS GL 去掉了与图形无关的功能和硬件相关的特性,变成了 OpenGL。

OpenGL 是一种用于渲染 2D 和 3D 图形的跨语言、跨平台的应用编程接口(API)。这是一个渲染多边形的精益平均机器;它是用 C 编写的 API,用于与图形处理单元(GPU)进行交互,以实现硬件加速渲染。这是一个非常低级的硬件抽象。

随着小型手持设备变得越来越普遍,用于嵌入式系统的 OpenGL(OpenGL ES)被开发出来。OpenGL ES 是桌面版的精简版;它移除了许多更冗余的 API 调用,并简化了其他元素,使其能够在市场上功能较弱的 CPU 上高效运行;因此,OpenGL ES 在许多平台上被广泛采用,如 HP webOS、任天堂 3DS、iOS 和 Android。

OpenGL ES 现在是 3D 图形编程的行业标准。它由 Khronos Group 维护,Khronos Group 是一个行业联盟,其成员包括 ATI、NVIDIA 和 Intel 等;这些公司共同定义并扩展了标准。

Currently, there are six incremental versions of OpenGL ES: versions 1.0, 1.1, 2.0, 3.0, 3.1, and 3.2.

  • OpenGL ES 1.0 和 1.1—此 API 规范受 Android 1.0 及更高版本支持。

  • OpenGL ES 2.0—此 API 规范受 Android 2.2 (API level 8)及更高版本支持。

  • OpenGL ES 3.0—此 API 规范受 Android 4.3 (API level 18)及更高版本支持。

  • OpenGL ES 3.1—此 API 规范受 Android 5.0 (API 等级 21)及更高版本支持。

There are still developers, especially those who focus on games that run on multiple platforms, who write for OpenGL ES 1.0; this is because of its simplicity, flexibility, and standard implementation. All Android devices support OpenGL ES 1.0, some devices support 2.0, and any device after Jelly Bean supports OpenGL ES 3.0. At the time of writing, more than half of activated Android devices already support OpenGL ES 3.0. Table 9-1 shows the distribution and Figure 9-1 shows a nice pie chart to go with it; this data was taken from developer.android.com/about/dashboards#OpenGL.Table 9-1

OpenGL ES 版本发布

|

OpenGL 是版本

|

分配

|
| --- | --- |
| 仅限 GL 1.1 | 0.0% |
| GL 2.0 | 14.5% |
| GL 3.0 | 18.6% |
| GL 3.1 | 9.8% |
| GL 3.2 | 57.2% |

![img/340874_4_En_9_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig1_HTML.jpg)
Figure 9-1

OpenGL ES 版本发布

Note

对 OpenGL ES 的一个特定版本的支持也意味着对任何更低版本的支持(例如,对 2.0 版本的支持也意味着对 1.1 版本的支持)。

值得注意的是,OpenGL ES 2.0 打破了与 1.x 版本的兼容性。您可以使用 1.x 或 2.0,但不能同时使用两者。原因是 1.x 版本使用一种称为 固定功能管道 的编程模型,而 2.0 及更高版本允许您通过着色器以编程方式定义部分渲染管道。

OpenGL ES 是做什么的

The short answer is OpenGL ES just renders triangles on the screen, and it gives you some control on how those triangles are rendered. It’s probably best also to describe (as early as now) what OpenGL ES is not. It is not

  • 一个场景管理 API

  • 射线追踪仪

  • 物理引擎

  • 游戏引擎

  • 一个真实感渲染引擎

OpenGL ES 只是渲染三角形。没别的了。

Think of OpenGL ES as working like a camera. To take a picture, you have to go to the scene you want to photograph. Your scene is composed of objects that all have a position and orientation relative to your camera as well as different materials and textures. Glass is translucent and reflective; a table is probably made out of wood; a magazine has some photo of a face on it; and so on. Some of the objects might even move around (e.g., cars or people). Your camera also has properties, such as focal length, field of view, image resolution, size of the photo that will be taken, and a unique position and orientation within the world (relative to some origin). Even if both the objects and the camera are moving, when you press the shutter release, you catch a still image of the scene. For that small moment, everything stands still and is well defined, and the picture reflects exactly all those configurations of position, orientation, texture, materials, and lighting. Figure 9-2 shows an abstract scene with a camera, light, and three objects with different materials.

![img/340874_4_En_9_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig2_HTML.jpg)
Figure 9-2

抽象场景

每个对象都有相对于场景原点的位置和方向。由眼睛指示的摄像机也具有相对于场景原点的位置。图 9-2 中的金字塔被称为视体视见体 ,它显示了摄像机捕捉了多少场景以及摄像机是如何定向的。带有光线的小白球是场景中的光源,它也有一个相对于原点的位置。

我们可以将这个场景映射到 OpenGL ES,但是要这样做,我们需要定义(1)模型或对象,(2)灯光,(3)相机,和(4)视口。

模型或对象

OpenGL ES 是一个三角形渲染机器。OpenGL ES 对象是 3D 空间中的点的集合;它们的位置由三个值定义。这些值连接在一起形成面,面是看起来很像三角形的平面。三角形然后被连接在一起形成物体或物体的块(多边形)。

The resolution of your shapes can be improved by increasing the number of polygons in it. Figure 9-3 shows various shapes with varying number of polygons.

![img/340874_4_En_9_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig3_HTML.jpg)
Figure 9-3

从简单形状到复杂形状

图 9-3 最左边的是一个简单的球体;如果你仔细观察,你会发现它并不像一个球体。它旁边的形状(右)也是一个球体,但有更多的多边形。这些形状向右延伸,形成复杂的轮廓;这可以通过增加形状中多边形的数量来实现。

OpenGL ES 提供了几个不同的灯光类型和不同的属性。它们只是在 3D 空间中具有位置和/或方向的数学对象,加上诸如颜色之类的属性。

照相机

这也是一个在 3D 空间中具有位置和方向的数学对象。此外,它还有控制我们看到多少图像的参数,类似于真正的相机。所有这些共同定义了一个视见体或视见平截头体(在图 9-2 中用顶部被切掉的金字塔表示)。这个金字塔里面的任何东西都可以被摄像机看到;外面的任何东西都不会进入最终的画面。

视口

这定义了最终图像的尺寸和分辨率。可以把它想象成你放入模拟相机的胶片类型,或者你用数码相机拍摄的照片的图像分辨率。

预测

OpenGL ES 可以从相机的角度构建场景的 2D 位图。虽然一切都是在 3D 空间中定义的,但 OpenGL 通过所谓的 投影 将 3D 空间映射到 2D。单个三角形在 3D 空间中定义了三个点。为了渲染这样的三角形,OpenGL ES 需要知道这些 3D 点在基于像素的帧缓冲区坐标系中的坐标,这些点位于三角形内部。

矩阵

OpenGL ES expresses projections in the form of matrices. The internals are quite involved; for our introductory purposes, we don’t need to bother with the internals of matrices; we simply need to know what they do with the points we define in our scene.

  • 矩阵对要应用于点的变换进行编码。变换可以是投影、平移(其中点四处移动)、围绕另一个点和轴的旋转或缩放等。

  • 通过将这样的矩阵乘以一个点,我们将变换应用于该点。例如,将一个点与编码 x 轴上 10 个单位的平移的矩阵相乘,将使该点在 x 轴上移动 10 个单位,从而修改其坐标。

  • 我们可以通过矩阵相乘将存储在不同矩阵中的变换连接成一个矩阵。当我们用一个点乘以这个单个连接矩阵时,存储在该矩阵中的所有变换都将应用于该点。应用变换的顺序取决于矩阵相乘的顺序。

There are three different matrices in OpenGL ES that apply to the points in our models:

  • 模型-视图矩阵—该矩阵用于将模型放置在“世界”的某个地方例如,如果您有一个球体模型,并希望它位于东面 100 米处,您将使用模型矩阵来完成此操作。我们可以使用这个矩阵来移动、旋转或缩放三角形的点(这是模型-视图矩阵的模型部分)。这个矩阵也用于指定我们的摄像机的位置和方向(这是视图部分)。如果你想观察我们的球体,它在东边 100 米处,我们也必须将自己向东移动 100 米。另一种思考方式是,我们保持静止,世界的其他部分向西移动 100 米。

  • 投影矩阵—这是我们相机的视锥。由于我们的屏幕是平面的,我们需要做最后的转换,将我们的视图“投影”到我们的屏幕上,并获得漂亮的 3D 视角。这就是投影矩阵的用途。

  • 纹理矩阵—这个矩阵允许我们操作纹理坐标。

在 OpenGL ES 编程中,我们需要吸收更多的理论,但是让我们通过一个简单的编码练习来探索其中的一些理论。

渲染一个简单的球体

OpenGL ES APIs 内置在 Android 框架中,因此我们不需要导入任何其他库或者将任何其他依赖项包含到项目中。

OpenGL ES is widely supported among Android devices, but just to be prudent, if you want to exclude Google Play users whose device do not support OpenGL ES, you need to add a uses-feature in the Android Manifest file, like this:<uses-feature android:glEsVersion="0x00020000"android:required="true" />

清单条目基本上是说,应用希望设备支持 OpenGL ES 2,这实际上是编写时的所有设备。

Additionally (and optionally), if your application uses texture compression, you must also declare it in the manifest so that the app only installs on compatible devices; Listing 9-1 shows how to do this in the Android Manifest.Listing 9-1

AndroidManifest.xml,纹理压缩

Assuming you’ve already created a project with an empty Activity and a default activity_main layout file, the first thing to do is to add GLSurfaceView to the layout file. Modify activity_main.xml to match the contents of Listing 9-2.<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><android.opengl.GLSurfaceViewandroid:layout_width="400dp"android:layout_height="400dp"android:id="@+id/gl_view"/></androidx.constraintlayout.widget.ConstraintLayout>Listing 9-2

activity_main.xml

我移除了默认的 TextView 对象,并插入了一个 400dp 乘 400dp 大小的 GLSurfaceView 元素。现在让我们保持它均匀的正方形,这样我们的形状就不会倾斜。OpenGL 假设绘图区域总是正方形的。

Figure 9-4 shows the activity_main layout in design mode.

![img/340874_4_En_9_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig4_HTML.jpg)
Figure 9-4

在设计模式下的 activity_main.xml

GLSurfaceView 是 SurfaceView 类的一个实现,它使用一个专用图面来显示 OpenGL 渲染;这个对象管理一个 surface,它是一个特殊的内存块,可以合成到 Android view 系统中。GLSurfaceView 运行在一个专用线程上,将渲染性能与主 UI 线程分开。

Next, in MainActivity, let’s get a reference to the GLSurfaceView we just created. We can create a member variable on MainActivity that’s of type GLSurfaceView, then in the onCreate() method, we’ll get a reference to it using findViewByID. The code is shown in Listing 9-3.public class MainActivity extends AppCompatActivity {private GLSurfaceView glView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);glView = findViewById(R.id.gl_view);}}Listing 9-3

获取对 GLSurfaceView 的引用

Next, still on MainActivity, let’s determine if there’s support for OpenGL ES 2.0. This can be done by using an ActivityManager object which lets us interact with the global system state; we can use this to get the device configuration info, which in turn can tell us if the device supports OpenGL ES 2. The code to do this is shown in Listing 9-4.ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);ConfigurationInfo ci = am.getDeviceConfigurationInfo();boolean isES2Supported = ci.reqGlEsVersion > 0x20000;Listing 9-4

确定对 OpenGL ES 2.0 的支持

Once we know if the device supports OpenGL ES 2 (or not), we tell the surface that we’d like an OpenGL ES 2 compatible surface, and then we pass it in a custom renderer. The runtime will call this renderer whenever it’s time to adjust the surface or draw a new frame. Listing 9-5 shows the annotated code for MainActivity.import android.app.ActivityManager;import android.content.Context;import android.content.pm.ConfigurationInfo;import android.opengl.GLES20;import android.opengl.GLSurfaceView;import android.os.Bundle;import javax.microedition.khronos.egl.EGLConfig;import javax.microedition.khronos.opengles.GL10;import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity {private GLSurfaceView glView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);glView = findViewById(R.id.gl_view);ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);ConfigurationInfo ci = am.getDeviceConfigurationInfo();boolean isES2Supported = ci.reqGlEsVersion > 0x20000;if(isES2Supported) { ❶glView.setEGLContextClientVersion(2); ❷glView.setRenderer(new GLSurfaceView.Renderer() { ❸@Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); ❹// statements ❺}@Overridepublic void onSurfaceChanged(GL10 gl10, int width, int height) {GLES20.glViewport(0,0, width, height); ❻}@Overridepublic void onDrawFrame(GL10 gl10) {// statements ❼}});}else {}}}Listing 9-5

MainActivity,创建 OpenGL ES 2 环境

| -好的 | 一旦我们知道支持 OpenGL ES 2,我们就开始创建 OpenGL ES 2 环境。 | | ❷ | 我们告诉表面视图,我们想要一个 OpenGL ES 2 兼容的表面。 | | -你好 | 我们使用匿名类创建一个自定义渲染器,然后将该类的一个实例传递给表面视图的 **setRenderer()** 方法。 | | (a) | 我们将渲染模式设置为只有在图形数据发生变化时才进行绘制。 | | (一) | 这是创建用于绘图的对象的好地方;可以认为这相当于活动的 **onCreate()** 方法。如果我们丢失了表面上下文并在以后被重新创建,这个方法也可能被调用。 | | ❻ | 当图面已经创建,并且随后由于某种原因图面的大小发生变化时,运行库调用此方法一次。这是你设置视窗的地方,因为当这个被调用时,我们已经得到了表面的尺寸。可以认为这相当于视图类的 **onSizeChanged()** 。这也可以在设备切换方向时调用,例如,从纵向切换到横向。 | | ❼ | 这是我们画画的地方。当要画一个新的框架时,这个函数被调用。 |

渲染器的 onDrawFrame() 方法 就是我们告诉 OpenGL ES 在表面上画东西的地方。我们将通过传递表示位置、颜色等的数字数组来实现这一点。在我们的例子中,我们要画一个球体。我们可以手工编码数字数组——代表顶点的 X,Y,Z 坐标——我们需要将它们传递给 OpenGL ES,但这可能无法帮助我们想象我们要画的是什么。所以,相反,让我们使用一个 3D 创作套件像 Blender(【www.blender.org】)来绘制一个形状。

Blender is open source; you can use it freely. Once you’re done with the download and installation, you can launch Blender, then delete the default cube by pressing X; next, press Shift+A and select MeshIco Sphere, as shown in Figure 9-5.

![img/340874_4_En_9_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig5_HTML.jpg)
Figure 9-5

创建一个 Icosphere

现在我们有了一个有几个顶点的中等有趣的对象——手工编码这些顶点会很麻烦;所以我们走了搅拌机这条路。

要在我们的应用中使用球体,我们必须将其导出为波前对象。波前对象是一种几何定义文件格式。这是一种开放格式,被 3D 图形应用供应商所采用。这是一种简单的数据格式,表示 3D 几何图形,即每个顶点的位置;构成每个多边形的面被定义为一系列顶点。出于我们的目的,我们只对顶点和面的位置感兴趣。

In Blender, go to FileExport Wavefront (.obj) as shown in Figure 9-6. In the following screen, give it a name (sphere.obj) and save it in a location of your choice. Don’t forget to note the export settings of Blender; check only the following:

  • 作为 OBJ 对象导出

  • 三角测量人脸

  • 保持顶点顺序

![img/340874_4_En_9_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig6_HTML.jpg)
Figure 9-6

将球体导出到波前对象格式

这些是我发现很容易使用的设置,尤其是当您要解析导出的顶点和面数据时。

The resulting object file is actually a text file; Listing 9-6 shows a partial listing of that sphere.obj.# Blender v2.82 (sub 7) OBJ File: 'sphere.blend'# www.blender.orgo Icospherev 0.000000 -1.000000 0.000000v 0.723607 -0.447220 0.525725v -0.276388 -0.447220 0.850649v -0.894426 -0.447216 0.000000v -0.276388 -0.447220 -0.850649v 0.723607 -0.447220 -0.525725v 0.276388 0.447220 0.850649s offf 1 14 13f 2 14 16f 1 13 18f 1 18 20f 1 20 17f 2 16 23f 3 15 25f 4 19 27f 5 21 29Listing 9-6

Partial sphere.obj

注意每一行是如何以“v”或“f”开头的。以“v”开头的线代表单个顶点,以“f”开头的线代表面。顶点线具有顶点的 X、Y 和 Z 坐标,而面线具有三个顶点的索引(它们一起形成一个面)。

为了让事情有条理,让我们创建一个代表我们的球体对象的类——我们并不真的想现在就在 onDrawFrame() 方法中编写所有的绘图代码,不是吗?

Let’s create a new class and add it to the project. You can do this by using Android Studio’s context menu; right-click the package name (as shown in Figure 9-7), then choose NewJava Class.

![img/340874_4_En_9_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig7_HTML.jpg)
Figure 9-7

创建一个新的类

In the screen that follows, provide the name of the class (Sphere), as shown in Figure 9-8.

![img/340874_4_En_9_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig8_HTML.jpg)
Figure 9-8

为类提供一个名称

We’ll build the Sphere class a basic POJO that contains all the data that OpenGL ES requires to draw a shape. Listing 9-7 shows the starting code for Sphere.java.public class Sphere {private List vertList;private List facesList;private Context ctx;private final String TAG = getClass().getName();public Sphere(Context context) {ctx = context;vertList = new ArrayList<>();facesList = new ArrayList<>();}}Listing 9-7

Sphere.java

The Sphere class has two List objects which will hold the vertices and faces data (which we will load from the OBJ file). Apart from that, there’s a Context object and a String object:

  • Context 对象将被我们的一些方法所需要,所以我把它作为一个成员变量。

  • 字符串标签—我只需要一个识别字符串,用于我们做一些日志记录的时候。

The idea is to read the exported Wavefront OBJ file and load the vertices and faces data into their corresponding List objects. Before we can read the file, we need to add it to the project. We can do that by creating an assets folder. An assets folder gives us the ability to add external files to the project and make them accessible to our code. If your project doesn’t have an assets folder, you can create them. To do that, use the context menu; right-click the “app” in the Project tool window (as shown in Figure 9-9), then select NewFolderAssets Folder.

![img/340874_4_En_9_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig9_HTML.jpg)
Figure 9-9

创建一个资产文件夹

In the window that follows, click Finish, as shown in Figure 9-10.

![img/340874_4_En_9_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig10_HTML.jpg)
Figure 9-10

新的安卓组件

Gradle will perform a “sync” after you’ve added a folder to the project. Figure 9-11 shows the Project tool window with the newly created assets folder.

![img/340874_4_En_9_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig11_HTML.jpg)
Figure 9-11

资产文件夹已创建

Next, right-click the assets folder, then choose Reveal in Finder (as shown in Figure 9-12)—this is the prompt I got because I’m using macOS. If you’re on Windows, you will see “Show in Explorer instead.

![img/340874_4_En_9_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig12_HTML.jpg)
Figure 9-12

在 Finder 中显示或在资源管理器中显示(适用于 Windows 用户)

现在您可以将 sphere.obj 文件转移到项目的 assets 文件夹中。

Alternatively, you can copy the sphere.obj file to the assets folder using the Terminal of Android Studio (as shown in Figure 9-13).

![img/340874_4_En_9_Fig13_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig13_HTML.jpg)
Figure 9-13

使用终端复制文件

用哪种方式对你更方便。有些人喜欢 GUI 方式,有些人喜欢命令行方式。使用您更熟悉的工具。

Now we can read the contents of the OBJ file and load them onto the ArrayList objects. In the Sphere class, add a method named loadVertices() and modify it to match Listing 9-8.import java.util.Scanner;// class definition and other statementsprivate void loadVertices() {try {Scanner scanner = new Scanner(ctx.getAssets().open("sphere.obj")); ❶while(scanner.hasNextLine()) { ❷String line = scanner.nextLine(); ❸if(line.startsWith("v ")) {vertList.add(line); ❹} else if(line.startsWith("f ")) {facesList.add(line); ❺}}scanner.close();}catch(IOException ioe) {Log.e(TAG, ioe.getMessage()); ❻}}Listing 9-8

load vertices()

| -好的 | 创建一个新的扫描仪对象并打开 **sphere.obj** 文本文件。 | | ❷ | 虽然我们还没有到达文件的末尾, **hasNextLine()** 将总是返回 true。 | | -你好 | 读取当前行的内容并保存到*行*变量中。 | | (a) | 如果该行以“v”开头,则将它添加到 **vertList** ArrayList 中。 | | (一) | 如果该行以“f”开头,将其添加到 **facesList** ArrayList 中。 |

我们使用 Java 语言编写应用,但是你需要记住 OpenGL ES 实际上是一堆 C APIs。我们不能简单地将顶点和面的列表直接传递给 OpenGL ES。我们需要将我们的顶点和面数据转换成 OpenGL ES 能够理解的东西。

Java and the native system might not store their bytes in the same order, so we use a special set of buffer classes and create a ByteBuffer large enough to hold our data and tell it to store its data using the native byte order. This is an extra step we need to do before passing our data to OpenGL. To do that, let’s add another method to the Sphere class; Listing 9-9 shows the contents of the createBuffers() method .private FloatBuffer vertBuffer; ❶private ShortBuffer facesBuffer;// some other statementsprivate void createBuffers() {// BUFFER FOR VERTICESByteBuffer buffer1 = ByteBuffer.allocateDirect(vertList.size() * 3 * 4); ❷buffer1.order(ByteOrder.nativeOrder());vertBuffer = buffer1.asFloatBuffer();// BUFFER FOR FACESByteBuffer buffer2 = ByteBuffer.allocateDirect(facesList.size() * 3 * 2); ❸buffer2.order(ByteOrder.nativeOrder());facesBuffer = buffer2.asShortBuffer();for(String vertex: vertList) { ❹String coords[] = vertex.split(" "); ❺float x = Float.parseFloat(coords[1]);float y = Float.parseFloat(coords[2]);float z = Float.parseFloat(coords[3]);vertBuffer.put(x);vertBuffer.put(y);vertBuffer.put(z);}vertBuffer.position(0); ❻for(String face: facesList) {String vertexIndices[] = face.split(" "); ❼short vertex1 = Short.parseShort(vertexIndices[1]);short vertex2 = Short.parseShort(vertexIndices[2]);short vertex3 = Short.parseShort(vertexIndices[3]);facesBuffer.put((short)(vertex1 - 1)); ❽facesBuffer.put((short)(vertex2 - 1));facesBuffer.put((short)(vertex3 - 1));}}Listing 9-9

创建缓冲区()

| -好的 | 您必须向 Sphere 类添加 FloatBuffer 和 ShortBuffer 成员变量。我们将用它来保存顶点和面的数据。 | | ❷ | 使用**allocated direct()**方法初始化缓冲区。我们为每个坐标分配 4 个字节(因为它们是浮点数)。一旦创建了缓冲区,我们就通过调用 **asFloatBuffer()** 方法将其转换为 FloatBuffer。 | | -你好 | 类似地,我们为面初始化一个 ByteBuffer,但是这一次,我们只为每个顶点索引分配 2 个字节,因为索引是无符号的 short。接下来,我们调用 **asShortBuffer()** 方法将 ByteBuffer 转换为 ShortBuffer。 | | (a) | 为了解析顶点列表对象,我们使用 Java 的增强 for-loop 遍历它。 | | (一) | 顶点列表对象中的每个条目都是保存顶点的 X,Y,Z 位置的一条线,像**0.723607-0.447220 0.525725**;它被一个空格隔开。因此,我们使用字符串对象的 **split()** 方法,使用空格作为分隔符。这个调用将返回一个包含三个元素的字符串数组。我们将这些元素转换成浮点数并填充 FloatBuffer。 | | ❻ | 重置缓冲器的位置。 | | ❼ | 和我们在顶点列表中做的一样,我们把它们分成数组元素,但是这次把它们转换成 short。 | | ❽ | 索引从 1(非零)开始;因此,在将转换后的值添加到 ShortBuffer 之前,我们将它减去 1。 |

下一步是创建着色器。如果我们不创建着色器,就无法渲染我们的 3D 球体;我们需要一个顶点着色器和一个片段着色器。着色器是用类似 C 的语言编写的,称为 OpenGL 着色语言(简称 GLSL)。

顶点着色器负责 3D 对象的顶点,而片段着色器(也称为像素着色器)处理 3D 对象像素的着色。

To create the vertex shader, add a file to the project’s assets folder and name it vertex_shader.txt , as shown in Figure 9-14.

![img/340874_4_En_9_Fig14_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig14_HTML.jpg)
Figure 9-14

新的文件

In the window that follows (Figure 9-15), enter the name of the file.

![img/340874_4_En_9_Fig15_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig15_HTML.jpg)
Figure 9-15

输入新的文件名

Modify the newly created vertex_shader.txt to match the contents of Listing 9-10.attribute vec4 position; ❶uniform mat4 matrix; ❷void main() {gl_Position = matrix * position; ❸}Listing 9-10

vertex_shader.txt

| -好的 | **属性**全局变量从我们的 Java 程序接收顶点位置数据。 | | ❷ | 这是来自我们 Java 代码的**统一**全局变量视图-项目矩阵。 | | -你好 | 在 **main()** 函数中,我们将 **gl_position** (一个 GLSL 内置变量)的值设置为统一和属性全局变量的乘积。 |

Next, we create the fragment shader. Like what we did in vertex_shader, add a file to the project and name it fragment_shader.txt. Modify the contents of the fragment shader program to match Listing 9-11.precision mediump float;void main() {gl_FragColor = vec4(0.481,1.000,0.865,1.000);}Listing 9-11

fragment_shader.txt

这是一个极简的片段着色器代码;它基本上给所有的像素分配一个浅绿色。

The next step is to load these shaders into our Java program and compile them. We will add another method to the Sphere class named createShaders(); its contents are shown in Listing 9-12.// class definition and other statementsprivate int vertexShader; ❶private int fragmentShader;private void createShaders() {try {Scanner scannerFrag = new Scanner(ctx.getAssets().open("fragment_shader.txt")); ❷Scanner scannerVert = new Scanner(ctx.getAssets().open("vertex_shader.txt")); ❸StringBuilder sbFrag = new StringBuilder(); ❹StringBuilder sbVert = new StringBuilder();while (scannerFrag.hasNext()) {sbFrag.append(scannerFrag.nextLine()); ❺}while(scannerVert.hasNext()) {sbVert.append(scannerVert.nextLine());}String vertexShaderCode = new String(sbVert.toString()); ❻String fragmentShaderCode = new String(sbFrag.toString());Log.d(TAG, vertexShaderCode);vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER); ❼GLES20.glShaderSource(vertexShader, vertexShaderCode);fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);GLES20.glShaderSource(fragmentShader, fragmentShaderCode);GLES20.glCompileShader(vertexShader); ❽GLES20.glCompileShader(fragmentShader);}catch(IOException ioe) {Log.e(TAG, ioe.getMessage());}}Listing 9-12

createShaders()

| -好的 | 为**顶点着色器**和**片段着色器**添加成员变量声明。 | | ❷ | 打开 **fragment_shader.txt** 进行读取。 | | -你好 | 打开 **vertex_shader.txt** 进行读取。 | | (a) | 创建一个 StringBuffer 来保存我们将从 Scanner 对象中读取的部分字符串;对 fragment_shader.txt 和 vertex_shader.txt 都执行此操作。 | | (一) | 将当前行追加到 StringBuffer(对两个 StringBuffer 对象都这样做)。 | | ❻ | 当 Scanner 对象中的所有行都被读取并追加到 StringBuffer 后,我们创建一个新的 String 对象。对两个 StringBuffers 都这样做。 | | ❼ | 着色器的代码必须添加到 OpenGL ES 的着色器对象中。我们使用 **glCreateShader()** 方法创建一个新的着色器,然后我们使用新创建的着色器和着色器程序代码设置着色器源;对顶点着色器和片段着色器都执行此操作。 | | ❽ | 最后,编译着色器。 |

在我们可以使用着色器之前,我们需要将它们链接到一个程序。我们不能直接使用着色器。这是连接顶点着色器的输出和片段着色器的输入的部分。它也让我们传递来自程序的输入,并使用着色器来绘制我们的形状。

We’ll create a new program object, and if that turns out well, we’ll attach the shaders. Let’s add a new method to the Sphere class and name it runProgram(); the code for this method is shown in Listing 9-13.private int program; ❶// other statementsprivate void runProgram() {program = GLES20.glCreateProgram(); ❷GLES20.glAttachShader(program, vertexShader); ❸GLES20.glAttachShader(program, fragmentShader); ❹GLES20.glLinkProgram(program); ❺GLES20.glUseProgram(program);}Listing 9-13

运行时程序()??

| -好的 | 您需要创建**程序**作为 Sphere 类中的成员变量。 | | ❷ | 使用 **glCreateProgram()** 方法创建一个程序。 | | -你好 | 将顶点着色器附加到程序中。 | | (a) | 将片段着色器附加到程序。 | | (一) | 要开始使用这个程序,我们需要使用 **glLinkProgram()** 方法链接它,并通过 **glUseProgram()** 方法使用它。 |

Now that all the buffers and the shaders are ready, we can finally draw something to the screen. Let’s add another method to the Sphere class and name it draw(); the code for this method is shown in Listing 9-14.import android.opengl.Matrix; ❶// class definition and other statementspublic void draw() {int position = GLES20.glGetAttribLocation(program, "position"); ❷GLES20.glEnableVertexAttribArray(position);GLES20.glVertexAttribPointer(position, 3, GLES20.GL_FLOAT, false, 3 * 4, vertBuffer); ❸float[] projectionMatrix = new float[16]; ❹float[] viewMatrix = new float[16];float[] productMatrix = new float[16];Matrix.frustumM(projectionMatrix, 0, -1, 1, -1, 1, 2, 9); ❺Matrix.setLookAtM(viewMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0f); ❻Matrix.multiplyMM(productMatrix, 0, projectionMatrix, 0, viewMatrix, 0);int matrix = GLES20.glGetUniformLocation(program, "matrix"); ❼GLES20.glUniformMatrix4fv(matrix, 1, false, productMatrix, 0);GLES20.glDrawElements(GLES20.GL_TRIANGLES, facesList.size() * 3,GLES20.GL_UNSIGNED_SHORT, facesBuffer); ❽GLES20.glDisableVertexAttribArray(position);}Listing 9-14

draw()

| -好的 | 您需要导入矩阵类。 | | ❷ | 如果你还记得在 **vertex_shader.txt** 中,我们定义了一个 **position** 变量,它应该从我们的 Java 代码中接收顶点位置数据;我们将要把数据发送到这个**位置**变量。为此,我们必须首先在 vertex_shader 中获取一个对 **position** 变量的引用。我们使用**glgetattributelocation()**方法来实现这一点,然后使用**glEnableVertexAttribArray()**方法来启用它。 | | -你好 | 将**位置**手柄指向顶点缓冲区。**glvertexattributepointer()**方法也期望每个顶点的坐标数和每个顶点的字节偏移量。每个坐标是一个浮点数,所以字节偏移量是 **3 * 4** 。 | | (a) | 我们的顶点着色器需要一个视图投影矩阵,它是视图和投影矩阵的乘积。一个**视图矩阵**允许我们指定摄像机的位置和它正在看的点。一个**投影矩阵**让我们映射 Android 设备的方形坐标,并指定视见平截头体的远近平面。我们简单地为这些矩阵创建浮点数组。 | | (一) | 使用 matrix 类的**frustrum()**方法初始化投影矩阵。您需要向该方法传递一些参数;它需要左、右、下、上、近和远裁剪平面的位置。当我们在 activity_main 布局文件中定义 GLSurfaceView 时,它已经是一个正方形了,所以我们可以使用值 **-1 和 1** 来表示近裁剪平面和远裁剪平面。 | | ❻ | **setLookAtM()** 方法用于初始化视图矩阵。它期望摄像机的位置和它正在观察的点。然后使用**multiplym()**方法计算乘积矩阵。 | | ❼ | 让我们使用 **glGetUniformLocation()** 方法将乘积矩阵传递给着色器。当我们得到句柄(**矩阵**变量)时,使用 **glUniformMatrix4fv()** 方法将其指向乘积矩阵。 | | ❽ | glDrawElements() 方法让我们使用 faces 缓冲区来创建三角形;它的参数期望顶点索引的总数、每个索引的类型以及面缓冲区。 |

Now that we’ve got the methods to load the vertices from a blender file, create all the buffers, compile the shaders, and create an OpenGL program, we can now tie all these methods together in the constructor of the Sphere class, as shown in Listing 9-15.public Sphere(Context context) {ctx = context;vertList = new ArrayList<>();facesList = new ArrayList<>();loadVertices();createBuffers();createShaders();****runProgram();}Listing 9-15

球体类的构造函数

After adding all these methods, it may be difficult to keep the code straight. So, I’m showing all the contents of the Sphere class in Listing 9-16, for your reference.import android.content.Context;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.nio.FloatBuffer;import java.nio.ShortBuffer;import java.util.ArrayList;import java.util.List;import java.util.Scanner;import android.opengl.GLES20;import android.opengl.Matrix;import android.util.Log;public class Sphere {private FloatBuffer vertBuffer;****private ShortBuffer facesBuffer;****private List vertList;****private List facesList;****private Context ctx;****private final String TAG = getClass().getName();****private int vertexShader;private int fragmentShader;private int program;public Sphere(Context context) {ctx = context;vertList = new ArrayList<>();facesList = new ArrayList<>();loadVertices();createBuffers();createShaders();runProgram();}private void loadVertices() {try {Scanner scanner = new Scanner(ctx.getAssets().open("sphere.obj"));while(scanner.hasNextLine()) {String line = scanner.nextLine();if(line.startsWith("v ")) {vertList.add(line);} else if(line.startsWith("f ")) {facesList.add(line);}}scanner.close();}catch(IOException ioe) {Log.e(TAG, ioe.getMessage());}}private void createBuffers() {// BUFFER FOR VERTICESByteBuffer buffer1 = ByteBuffer.allocateDirect(vertList.size() * 3 * 4);buffer1.order(ByteOrder.nativeOrder());vertBuffer = buffer1.asFloatBuffer();// BUFFER FOR FACESByteBuffer buffer2 = ByteBuffer.allocateDirect(facesList.size() * 3 * 2);buffer2.order(ByteOrder.nativeOrder());facesBuffer = buffer2.asShortBuffer();for(String vertex: vertList) {String coords[] = vertex.split(" ");float x = Float.parseFloat(coords[1]);float y = Float.parseFloat(coords[2]);float z = Float.parseFloat(coords[3]);vertBuffer.put(x);vertBuffer.put(y);vertBuffer.put(z);}vertBuffer.position(0);for(String face: facesList) {String vertexIndices[] = face.split(" ");short vertex1 = Short.parseShort(vertexIndices[1]);short vertex2 = Short.parseShort(vertexIndices[2]);short vertex3 = Short.parseShort(vertexIndices[3]);facesBuffer.put((short)(vertex1 - 1));facesBuffer.put((short)(vertex2 - 1));facesBuffer.put((short)(vertex3 - 1));}facesBuffer.position(0);}private void createShaders() {try {Scanner scannerFrag = new Scanner(ctx.getAssets().open("fragment_shader.txt"));Scanner scannerVert = new Scanner(ctx.getAssets().open("vertex_shader.txt"));StringBuilder sbFrag = new StringBuilder();StringBuilder sbVert = new StringBuilder();while (scannerFrag.hasNext()) {sbFrag.append(scannerFrag.nextLine());}while(scannerVert.hasNext()) {sbVert.append(scannerVert.nextLine());}String vertexShaderCode = new String(sbVert.toString());String fragmentShaderCode = new String(sbFrag.toString());Log.d(TAG, vertexShaderCode);vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);GLES20.glShaderSource(vertexShader, vertexShaderCode);fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);GLES20.glShaderSource(fragmentShader, fragmentShaderCode);GLES20.glCompileShader(vertexShader);GLES20.glCompileShader(fragmentShader);}catch(IOException ioe) {Log.e(TAG, ioe.getMessage());}}private void runProgram() {program = GLES20.glCreateProgram();GLES20.glAttachShader(program, vertexShader);GLES20.glAttachShader(program, fragmentShader);GLES20.glLinkProgram(program);GLES20.glUseProgram(program);}public void draw() {int position = GLES20.glGetAttribLocation(program, "position");GLES20.glEnableVertexAttribArray(position);GLES20.glVertexAttribPointer(position, 3, GLES20.GL_FLOAT, false, 3 * 4, vertBuffer);float[] projectionMatrix = new float[16];float[] viewMatrix = new float[16];float[] productMatrix = new float[16];Matrix.frustumM(projectionMatrix, 0, -1, 1, -1, 1, 2, 9);Matrix.setLookAtM(viewMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0f);Matrix.multiplyMM(productMatrix, 0, projectionMatrix, 0, viewMatrix, 0);int matrix = GLES20.glGetUniformLocation(program, "matrix");GLES20.glUniformMatrix4fv(matrix, 1, false, productMatrix, 0);GLES20.glDrawElements(GLES20.GL_TRIANGLES, facesList.size() * 3, GLES20.GL_UNSIGNED_SHORT, facesBuffer);GLES20.glDisableVertexAttribArray(position);}}Listing 9-16

球体类的完整代码

Now that all of the code for the Sphere class is complete, we can go back to MainActivity. Remember in MainActivity that we created a Renderer object using an anonymous inner class. We created that renderer because a GLSurfaceView needs a renderer object so that it can, well, render 3D graphics. Listing 9-17 shows the complete code for MainActivity.public class MainActivity extends AppCompatActivity {private GLSurfaceView glView;private Sphere sphere; ❶@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);glView = findViewById(R.id.gl_view);ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);ConfigurationInfo ci = am.getDeviceConfigurationInfo();boolean isES2Supported = ci.reqGlEsVersion > 0x20000;if(isES2Supported) {glView.setEGLContextClientVersion(2);glView.setRenderer(new GLSurfaceView.Renderer() {@Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);sphere = new Sphere(getApplicationContext()); ❷}@Overridepublic void onSurfaceChanged(GL10 gl10, int width, int height) {GLES20.glViewport(0,0, width, height);}@Overridepublic void onDrawFrame(GL10 gl10) {sphere.draw(); ❸}});}else {}}}Listing 9-17

主要活动,完成

| -好的 | 创建一个成员变量作为我们将要创建的球体对象的引用。 | | ❷ | 创建球体对象;将当前上下文作为参数传递。 | | -你好 | 调用球体的 **draw()** 方法。 |

At this point, you’re ready to run the app. Figure 9-16 shows the app at runtime.

![img/340874_4_En_9_Fig16_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_9_Fig16_HTML.jpg)
Figure 9-16

OpenGL 中渲染的 icosphereES

在将近 300 行代码之后,我们得到的只是一个没有太多定义的绿色小 Icosphere。欢迎来到 OpenGL ES 编程。这应该让你知道一个 OpenGL ES 游戏有多复杂,需要做多少工作。

关键要点

  • 从 Android 3 (API level 11)开始,在画布上完成的绘图已经享受到了硬件加速,因此对于游戏编程来说,这是一个不错的技术选择。但是,如果游戏的视觉复杂性超过了画布的能力,您应该考虑使用 OpenGL ES 来绘制图形。

  • OpenGL ES 真的只擅长画三角形,别的不多。它给了你很多控制力来画这些三角形。有了它,你可以控制相机,光源和纹理等。

  • Android SDK 已经内置了对 OpenGL ES 的支持。GLSurfaceView 已经包含在 SDK 中,这是您通常用于绘制 OpenGL ES 对象的视图。

十、定价

  • 定价模型

  • Freemium

  • 广告

  • 可发现性

付费或免费

你必须决定游戏是付费的还是免费的。app 一旦免费发布,就不能再改成付费了。然而,付费应用可能会在以后转为免费。付费发布应用是一种直接获得回报的方式;建 app,发布 app,要钱。

你也可以免费发布你的游戏,但是如果你免费发布的话,你怎么能建立一个收入流呢?一些开发商已经采取了“免费广告”的路线,虽然许多人仍在这样做,但由于纯粹的竞争,你可能要考虑其他形式的收入流。普遍的想法是,有很多移动应用广告,但广告背后没有多少钱,所以钱分散了。基本的想法是你免费提供游戏,你展示广告来创收。这里需要关注的关键指标是点击率,它是点击率的缩写。CTR 是被点击的广告数量除以显示的广告(印象)数量,以百分比表示。如果你展示了 100 个广告,用户点击了两次,点击率是 2%;玩你游戏的人越多,你展示的广告就越多,获得更高点击率的机会也就越多;这是基本的想法。

广告并不都是平等的;有些人比其他人更有潜力。一个 banner 广告,比方说,点击率 0.02%;如果你的游戏显示 100,000 次展示,你得到 200 次点击;每次点击 0.05 美元,就是 10 美元。如果你的应用每月获得 100,000 次展示,你可能要在放弃日常工作之前先考虑一下。仅仅做一下数学计算,你就可以算出一款游戏需要多少印象才能获得 1000 美元的收入。

幸运的是,广告并不是免费游戏赚钱的唯一途径。大约十年前,免费增值定价模式进入主流意识。免费增值是“免费”和“高级”两个词的组合。这是一种定价策略,你可以免费发布游戏,并从其他地方获得收入,如应用内购买、虚拟货币等。

Freemium

If you look at Google’s top grossing apps (bit.ly/topgrossingapps), you’ll find that quite a few of them are free; to be more precise, they’re freemium. They’re free to use and download, but they also have in-app purchases that cost real money. These purchases allow the users to buy extra content, for example, levels, new characters, costumes, virtual currencies, or coins, which can be used for upgrades. There are many more that you can buy in an in-app purchase, but these are the popular ones. The freemium model is very successful, but it requires more development work. On the side of the users, it works to their advantage because there is no cost in trying out the game. If they like it and they’ve invested some playing hours already, they’re more likely to spend real money to buy more content. Going freemium is more work because of two things:

  1. 1.

    Extra content is not defined in the game itself, but defined somewhere in Google Play, which means that you need to spend time to manage Google Play. When the items are defined, your game can query Google Play to get the list of items available for purchase.

  2. 2.

    When the new game content has been purchased (and downloaded), the game needs to change its behavior. The change of game behavior depending on available content needs to be considered in the overall structure of the game; This increases the complexity of programming.

顺便说一下,走向免费增值模式与广告并不相互排斥,与付费应用也不相互排斥。有些可疑的开发者可能会发布带有广告的付费应用,然后提供应用内购买来移除广告。不难看出这会适得其反。当用户开始发表评论,告诉其他用户这个令人讨厌的策略时,游戏就结束了。

应用内购买

In-app purchases (IAP) or in-app products refer to the buying of goods and services from inside an application on a mobile device. The idea is that the player wants something that’s offered in your game, and he’s willing to pay a small amount of money to get it. There are two types of in-app product options given on the Google Play Store:

  • —只能购买一次的物品。它们附属于购买者,而不是设备。Google Play 跟踪这些购买,这允许用户在以后查询这些项目以进行恢复;此外,如果买家试图购买他们已经购买的物品,Google Play 将回应“该物品已经被购买。”管理项目的例子有等级、角色或能力。

*** 未被管理的物品—这些是被用户用完的物品,如硬币、虚拟货币(VC)或任何需要“补充”的东西未被管理的项目不会被 Google Play 追踪;用户不能在以后“恢复”这些购买。如果你想追踪未被管理的物品,你需要在你的游戏中为其编写代码。像被管理的项目一样,这些项目也附加到 Google 帐户,而不是设备。**

另一个与 IAPs 相关的货币化选项是“订阅”Google Play 允许您设置定期计费的订阅。应用会简单地将订阅视为“开启”或“关闭”当它“开启”时,用户可以付费连续访问内容或服务。玩家可以享受你的游戏所提供的一切,只要他们订阅了。

**

虚拟货币

虚拟货币是游戏内的货币。它们有很多名字。在一些游戏中,他们被称为黄金,硬币,红宝石,信用,等等。VC 是你的游戏为玩家存储的点数或数字;它允许玩家在游戏中做事情或买东西。有了 VCs,玩家可以购买提示、升级武器、更多生命值等等。

风投可以(通常)通过在玩游戏时获得,或者通过用真金白银购买(从 Google Play,作为非托管项目)获得。

**

**

广告

If you’re considering putting ads on the game, you need to get familiar with the ad providers; they deliver the ads from advertisers and pay you for the clicks. The money is split between you and the ad provider (it’s not split in the middle). A portion of the money goes to you (the game publisher) and the rest of the money goes to the ad provider, which is how the provider makes money. You’ll need to configure some keywords for the app, so the ads are more relevant; this is where you need a bit of SEO background and keyword wizardry. The ads can be in a variety of formats, but the common ones are banner and full-page ads. Here are some of the major ad providers and aggregators:

这些服务有自己的 API,通常很容易使用。请访问他们的网站获取技术文档。

实施广告时很容易激动,可能会做过头。只要记住展示广告的目的是为了赚钱。在显示广告和激怒用户之间有一个平衡点;收益递减法则显然适用于此。如果广告带来太多的干扰,用户可能会感到恼火;当这种情况发生时,用户群可能会缩小——你的收入也会随之减少。

发现你的游戏

已经有成千上万的游戏可供选择(大约。在撰写本文时,Google Play 中有 300,000 个用户),更多用户还在路上。这是红海的领土;这是一个非常拥挤的地方;但也不乏成功的故事。那么,你如何让你的游戏受到关注呢?你如何让人们意识到你的游戏已经在 Google Play 上发布了,而且很棒?嗯,你总是可以在广告上花很多钱,或者你可以试试本节概述的东西。

社交网络

脸书和推特是社交媒体的重量级人物。我假设您现在已经使用过这些平台了。有很多策略可以利用社交媒体让你的游戏获得一些关注。你总是可以做很多人已经在做的事情,比如建立一个脸书页面并“提升”页面(你必须为此付费)。与此同时,你可以告诉你的朋友喜欢这个网页。这可能会给你带来一些下载,但仅此而已——除非你有数百万的朋友或粉丝。我想你没有那么多,所以我们继续找吧。

从营销的角度来看,这两个社交网站的一些优点是,几乎每个人都使用它们,它们可以免费使用,并且它们对更有创意的解决方案很友好。这里有一些你如何利用这些网站来营销你的游戏的例子:给在脸书上“喜欢”你的游戏的用户 50 个免费的风险投资信用。给在推文中提到你的游戏的用户 50 个免费 VC 积分。

每月举行一次高分竞赛,奖品是一台新的安卓设备,只允许在脸书上喜欢你的人注册。在最后一个例子中,你必须真的购买一个设备作为奖品,当然,但是就激励“喜欢”而言,这样的策略真的很有效。很容易创造激励让人们互相分享你的游戏,这些网络是这种信息分享的完美平台。

脸书和 Twitter 都提供了 Android SDKs,你可以下载并使用它们来将网络与你的游戏整合在一起。API 集成文档通常很容易理解,所以一定要尝试一下。

发现服务

有 AppBrain(【https://appbrain.com】)这样的公司,他们的唯一目的就是帮助你让你的游戏被发现。其他公司,如 tap joy()和 Flurry(www.flurry.com),也有发现服务。这些服务中的大部分都提供了将你的游戏“放到网络中”的方法,这样它就会被其他游戏所推广。你可以支付安装费,并控制一场运动,让你的游戏进入许多人的手中。

不同的公司提供不同的发现方法,但是,简而言之,如果你想让你的游戏被发现,并且你有预算,你可能想看看这些服务中的一个或多个。当你把这样的服务和一个好的社交网络策略结合起来,你可能会让雪球越滚越大,为你的游戏制造轰动。

博客和网络媒体

让你的游戏被发现的另一个策略是为故事收集试点,为演示创建视频,并将所有这些发送到评论新的 Android 应用和游戏的博客。这些网站的编辑被审查应用和游戏的请求轰炸,所以要为他们做尽可能多的工作,提前给他们所有他们需要的信息。

游戏设计

I mentioned earlier that a game with in-app purchase capabilities is more complex to develop and administer. It’s better to anticipate the structural complexities warranted on the outset if you want to monetize the game rather than retrofitting an already finished game for monetization. A game that’s designed for monetization may have one or more of the following elements:

  • 影响游戏性的可选修改器

    • 促进

    • 升级

    • 欺骗

  • 不影响游戏性的可选内容

    • 外皮

    • 特性

  • Additional content

    • New level

    • New film art

    • New parts

    • Checkpoint

  • Virtual currency that

    • You can get

    • You can buy

    • You can buy the in-game upgrade

    • can be used to purchase additional content

Also, during the early planning stage, make the game discoverable by design; these kinds of games provide incentives for players to tell other people about the game. Much like a game that’s designed to be monetized, a game that’s designed to be discoverable incorporates most or all of the same elements (virtual currency, virtual goods, unlockables, additional content, etc.) as incentives for telling other people about the game. Here are some ideas on how to do this:

  • 制作一个只能通过输入从另一个玩家处收到的推荐代码来解锁的内容。

  • 提供额外的内容或风险资本,用于在脸书上发布关于该游戏的微博或分享或喜欢该游戏。

  • 奖励所有推荐给其他玩家的 VC 玩家。

  • 整合脸书或其他社交媒体来发布成就和新的高分。

  • 创建游戏的另一部分,作为一个脸书应用来玩,但以某种方式与移动游戏联系在一起。

关键要点

  • 有很多方法可以让你的游戏赚钱;你可以直接把它卖几美元一个。仅此而已。你可以免费发布它,并在游戏中提供应用内购买。你也可以免费发布游戏,并通过展示广告获得收入;或者你可以三者结合使用。

  • 在推广你的游戏时,创造性地使用社交网络;除了简单地在广告上砸钱,还有更划算的方法。

  • 货币化游戏更复杂,因此更难开发;但是要确保游戏赚钱不是事后的想法。在游戏开发和设计的规划阶段,你需要包括盈利策略。

**

十一、发布游戏

你可以相当自由地发布你的游戏,没有太多的限制;你可以让你的用户从你的网站、Google Drive、Dropbox 等等下载;如果你愿意,你甚至可以把游戏《APK》直接发给用户;但许多开发者选择在谷歌或亚马逊这样的市场上分发他们的应用或游戏,以最大限度地扩大影响。

In this chapter, we’ll discuss the things you need to do to get your game out in Google Play. Here’s what we’ll cover:

  • 准备发布

  • 签署应用

  • 谷歌游戏

  • 应用捆绑包

准备项目发布

There are three things you need to keep in mind when preparing for release; these are

  • 准备发布的材料和资产

  • 为发布配置项目

  • 构建一个发布就绪的应用

准备材料和资产用于发布

你的代码很棒,你甚至可能认为它很聪明,但是用户永远看不到它。他们将看到您的视图对象、图标和其他图形资产。你应该擦亮它们。

如果你认为应用的图标没什么大不了的,那可能是个错误。图标可以帮助用户识别您的应用,因为它位于主屏幕上。这个图标还出现在其他区域,如启动窗口和下载部分,更重要的是,它出现在 Google Play 上。图标在创造用户对你的游戏的第一印象中起着很大的作用。这是一个很好的主意,你可以在这里找到谷歌的图标指南:【http://bit.ly/androidreleaseiconguidelines】

如果你要在 Google market place 上发布,其他要考虑的是图形资产,比如屏幕截图和促销文本。请务必阅读谷歌的图形资产指南,可以在这里找到:【http://bit.ly/androidreleasegraphicassets】

配置要发布的应用

  1. 1.

    Check the package name —You may want to check the package name of the application. Make sure it is still not com.example.myapp .. Package name makes the application unique in Google marketplace; Once you decide the name of a bag, you can't change it again. So, think about it.

  2. 2.

    Processing debugging information -Make sure that you have deleted the Android: Debugable attribute in the < application > tab of the manifest file.

  3. 3.

    Delete the log statement —Different developers will do different things. Some people will bother to check the code and delete statements manually. Some people will write sed or awk programs to delete log statements. Some people will use ProGuard, while others will use third-party tools, such as Timber, to deal with logging activities. Which one you will use depends on yourself; But make sure your users don't accidentally see the log information.

  4. 4.

    Check the permissions of the application —At some point in the development process, you may have tested some functions of the application, and you may have set permissions on the list, such as using the network and writing to external storage. Check the < uses-permission > label on the list to ensure that the game is not granted unnecessary permissions.

  5. 5.

    Check the remote server and URL —If the game depends on web APIs or cloud services, make sure that the release version uses the production URL instead of the test path. During the development process, you may have obtained the sandbox and test URLs, and you need to upgrade them to the production version.

构建发布就绪的应用

During development, Android Studio did quite a few things for you; it

  • 创建了调试证书

  • 将项目的所有资产、配置文件和运行时二进制文件组装到一个 APK 中

  • 使用调试证书签署了 APK

  • 将 APK 部署到模拟器或连接的设备

这些事情都发生在背景;除了写代码,你不需要做任何其他事情。现在,你需要保管好那个证书。Google Play 和其他类似的市场不会发布带有调试证书的应用。它需要是一个适当的证书。不需要去 Thawte 或者 Verisign 这样的认证机构;自签名证书就足够了。此外,请确保保留该证书;当您对应用进行更新时,您需要使用相同的证书对其进行签名。

在接下来的步骤中,您将看到如何生成一个签名包或 APK;您已经知道什么是 APK——它是包含您的应用的包。而是你上传到 Google Play 的内容。另一方面,bundle 很像 APK,但它是一种更新的上传格式。像 APK 一样,它也包括所有应用的编译代码和资源,但它推迟了 APK 一代。这是 Google Play 的新应用服务模式,称为动态交付。它使用您的应用捆绑包为每个用户的设备配置生成和提供优化的 APK,因此他们只需下载运行您的应用所需的代码和资源。您不再需要构建、签署和管理多个 apk。

在 Android Studio 中,生成 APK 和 bundle 的步骤几乎相同。在下面的步骤中,我们将看到如何生成包和 APK。

Launch Android Studio, if you haven’t done so yet. Open the project, then from the main menu bar, go to BuildGenerate Signed Bundle/APK, as shown in Figure 11-1.

![img/340874_4_En_11_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig1_HTML.jpg)
Figure 11-1

生成签名的 APK

Choose either Bundle or APK, then click Next; in this example, I chose to create a bundle. When you click Next, you will see the “Keystore” dialog, as shown in Figure 11-2.

![img/340874_4_En_11_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig2_HTML.jpg)
Figure 11-2

密钥库对话框

The Key store path is asking where the Java Keystore (JKS) file is. At this point, you don’t have it yet. So, click Create New. You’ll see the dialog window for creating a new keystore, as shown in Figure 11-3.

![img/340874_4_En_11_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig3_HTML.jpg)
Figure 11-3

新密钥存储

Table 11-1 shows the description for the input items of the keystore.Table 11-1

密钥库项和描述

|

密钥库项目

|

描述

|
| --- | --- |
| 密钥库路径 | 要保存密钥库的位置。这完全取决于你。只要确保你记得这个位置 |
| 密码 | 这是密钥库的密码 |
| 别名 | 此别名标识密钥。这只是它的一个友好的名字 |
| (钥匙)密码 | 这是钥匙的密码。这与密钥库的密码不同(但是如果您愿意,也可以使用相同的密码) |
| 有效期,以年计 | 默认为 25 年;你可以接受默认值。如果在 Google Play 上发布,证书的有效期必须到 2033 年 10 月,所以 25 年应该没问题 |
| 其他信息 | 只有名字和姓氏字段是必需的 |

When you’re done filling up the New Key Store dialog, click OK. This will bring you back to the Generate Signed Bundle or APK window, as shown in Figure 11-4; but now, the JKS file is created and the Keystore dialog is populated with it.

![img/340874_4_En_11_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig4_HTML.jpg)
Figure 11-4

生成签名包或 APK,填充

Click Next. Now we choose the destination of the signed bundle as shown in Figure 11-5.

![img/340874_4_En_11_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig5_HTML.jpg)
Figure 11-5

签名 APK APK 目的地文件夹

你需要记住“目标文件夹”的位置,如图 11-5 所示。这是 Android Studio 存储签名包的地方。同样,确保构建变体被设置为“发布”

当您点击完成时,Android Studio 将为您的应用生成签名包。这是您将提交给 Google Play 的文件。

发布应用

Before you can submit an app to Google Play, you’ll need a developer account. If you don’t have one yet, you can sign up at developer.android.com. There’s a lot of assumptions I’m making about the next activities. I’m assuming that

  1. 1.

    You already have a Google account (Gmail).

  2. 2 .

    范思哲,范思哲,范思哲【https://developer . Android . com】

  3. 3.

    Your Google account is logged into Chrome.

If your Google account isn’t logged in to Chrome, you might see something like Figure 11-6. Chrome will ask you to go select an account (or create one).

![img/340874_4_En_11_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig6_HTML.jpg)
Figure 11-6

选择一个账户

When you get your Google account sorted out, you’ll be taken to the developer.android.com website, as shown in Figure 11-7.

![img/340874_4_En_11_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig7_HTML.jpg)
Figure 11-7

developer.android.com

点击 Google Play ,如图 11-7T5。

Click Launch Play Console, as shown in Figure 11-8.

![img/340874_4_En_11_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig8_HTML.jpg)
Figure 11-8

启动游戏控制台

You need to go through four steps to complete the registration, (shown in Figure 11-9):

  • 使用您的 Google 帐户登录。

  • 接受开发者协议。

  • 交报名费。

  • 填写您的帐户详细信息。

![img/340874_4_En_11_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig9_HTML.jpg)
Figure 11-9

Google Play 控制台,注册

Once you have completed the registration and payment, you will now have access to the Google Play Console, as shown in Figure 11-10.

![img/340874_4_En_11_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_11_Fig10_HTML.jpg)
Figure 11-10

播放控制台

您可以从这里开始向商店提交应用的流程。单击“创建应用”按钮开始。

关键要点

  • 在用户体验你的游戏之前,他们会先看到图标和其他图形资产——确保图形资产和你的代码一样完美。

  • 在构建一个版本之前,去掉代码中的所有调试信息和日志语句。

  • 对你自己的工作进行代码审查。如果你有伙伴或者其他人可以和你一起审查代码,那就更好了。如果您的应用使用服务器、RESTful URLs 等等,请确保它们是生产就绪的,而不是沙箱。

  • 在将你的应用上传到 Google Play 之前,你需要使用适当的证书对你的应用进行签名。

  • 如果你想在 Google Play 上销售你的应用,你需要一个 Google Play 帐户。我一次性支付了 25 美元的费用。

  • 别忘了在真实的设备上测试游戏,尽可能多的种类和大小。

十二、下一步是什么

在学习了 11 章 Android 编程的基础知识、Android Studio、一些游戏开发的理论以及两个从零开始构建的游戏之后,我们正准备做出结论。

我相信你从零开始制作这两款游戏后,已经获得了一些新的自信。当你看到你的作品在模拟器或设备上运行时,那是一种温暖的感觉;但是游戏编程的学习曲线很陡。如今,对游戏质量的要求已经很高了。

In this chapter, we’ll look at some areas of interest that you can add to your game programming arsenal. We’ll cover the following:

  • 安卓 NDK

  • Vulkan 简介和基本设置

  • 游戏引擎和游戏框架

安卓 NDK

你在游戏编程中会遇到的相当多的游戏资源、库、框架甚至引擎都是用 C 或 C++编写的。因此,您需要知道如何很好地使用这些库和语言本身。Android 有办法与 C/C++并肩工作。那是 NDK,是本地开发工具包的缩写。

NDK 是 Android SDK 的一个补充,它让你可以编写 C/C++和汇编代码,然后集成到你的 Android 应用中。NDK 包括一组特定于 Android 的 C 库,一个基于 GNU 编译器集合(GCC) 的交叉编译器工具链,它可以编译 Android 支持的所有不同的 CPU 架构(ARM、x86 和 MIPS),以及一个定制的系统(【https://developer.android.com/ndk/guides/ndk-build),与编写自己的 make 文件相比,它应该会使编译 C/C++代码更容易。

NDK 没有公开大多数 Android APIs,比如 UI 工具包。它主要是为了加速一些代码,这些代码可以通过用 C/C++编写并在 Java 中调用它们而受益。从 Android 2.3 开始,使用 NativeActivity 类代替 Java activities,几乎可以完全绕过 Java。NativeActivity 类是专门为全窗口控制的游戏设计的,但它根本不提供对 Java 的访问,所以它不能与其他基于 Java 的 Android 库一起使用。许多来自 iOS 的游戏开发人员选择这条路线,因为这让他们可以重用 Android 上的大部分 C/C++,而不必深入研究 Android Java APIs。然而,脸书认证或 ads 等服务的集成仍然需要用 Java 来完成,因此将游戏设计为在 Java 中启动并通过 JNI (Java 本地接口)调用 C++通常是最首选的方式。也就是说,如何使用 JNI 呢?

JNI 是让虚拟机与 C/C++代码通信的一种方式。这是双向的;可以从 Java 调用 C/C++代码,也可以从 C/C++调用 Java 方法。Android 的许多库使用这种机制来公开本机代码,如 OpenGL ES 或音频解码器。

Once you use JNI, your application consists of two parts: Java code and C/C++ code. On the Java side, you declare class methods to be implemented in native code by adding a special qualifier called native. The code could look like the one in Listing 12-1.class NativeSample {public native void doSomething(String a);}Listing 12-1

原生样本. java

如您所见,我们声明的方法没有方法体。当运行 Java 代码的 JVM 在方法上看到这个限定符时,它知道相应的实现是在共享库中找到的,而不是在 JAR 文件或 APK 文件中。

共享库非常类似于 Java JAR 文件。它包含编译的 C/C++代码,任何加载这个共享库的程序都可以调用这些代码。在 Windows 上,这些共享库通常带有后缀。dll 在 Unix 系统上,它们以. so 结尾。

On the C/C++ side, we have a lot of header and source files that define the signature of the native methods in C and contain the actual implementation. The header file for our class in the preceding code would look something like Listing 12-2./* DO NOT EDIT THIS FILE - it is machine generated /#include <jni.h>/ Header for class NativeSample /#ifndef _Included_NativeSample#define _Included_NativeSample#ifdef __cplusplusextern "C" {#endif/* Class: NativeSample* Method: doSomething* Signature: (Ljava/lang/String;)V*/JNIEXPORT void JNICALL Java_NativeSample_doSomething(JNIEnv *, jobject, jstring);#ifdef __cplusplus}#endif#endifListing 12-2

NativeSample.h .原始样本

Before Java 10, programmers used javah to generate header files like the preceding code, but javah became obsolete when Java 10 came about. To generate this header files for JNI, we now usejavac NativeSample.java -h .

该工具将一个 Java 类作为输入,并为它找到的任何本机方法生成一个 C 函数签名。这里发生了很多事情,因为 C 代码需要遵循特定的命名模式,并且需要能够将 Java 类型封送到它们对应的 C 类型(例如,Java 的 int 变成了 C 中的 jint)。我们还获得了 JNIEnv 和 jobject 类型的两个附加参数。第一个可以被认为是虚拟机的句柄。它包含与 VM 通信的方法,比如调用类实例的方法。第二个参数是调用该方法的类实例的句柄。我们可以将它与 JNIEnv 参数结合使用,从 C 代码中调用这个类实例的其他方法。

当然,您仍然需要编写实际实现该函数的 C 源文件,并在 Java 代码可以使用它之前编译它。

To install the NDK, you need to go to the SDK manager. If you have an open project in Android Studio, go to Preferences or Settings (Windows and Linux); then choose Android SDK, then check the boxes NDK (Side by side) and CMake, as shown in Figure 12-1, then click OK.

![img/340874_4_En_12_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig1_HTML.jpg)
Figure 12-1

安装 CMake 和 NDK (并排)

In the window that follows (Figure 12-2.), click OK to confirm the change and proceed.

![img/340874_4_En_12_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig2_HTML.jpg)
Figure 12-2

确认更改

In the window that follows (Figure 12-3), click Finish.

![img/340874_4_En_12_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig3_HTML.jpg)
Figure 12-3

组件安装程序

现在,您已经准备好在您的项目中使用 NDK 了。

瓦肯

Vulkan 是 Khronos 小组(也是给了我们 OpenGL 的小组)的新 API,它为现代图形卡提供了更好的抽象。这个新接口允许我们更好地描述应用的意图,与现有的 API(如 OpenGL 和 Direct3D)相比,这可以带来更好的性能和更少令人惊讶的驱动程序行为。Vulkan 背后的想法类似于 Direct3D 12(只能在 Windows 上使用)和 Metal (只能在苹果生态系统上使用的图形 API),但 Vulkan 的优势是完全跨平台,允许你同时为 Windows、Linux 和 Android 开发。

这些好处的代价是我们必须使用一个更加冗长的 API。与图形 API 相关的每个细节都需要由您的应用从头开始设置,包括初始帧缓冲区创建和缓冲区和纹理图像等对象的内存管理。图形驱动程序将会减少很多手持操作,这意味着我们需要在应用中做更多的工作来确保正确的行为。

Vulkan 可能不适合所有人。如果你对高性能显卡着迷,并愿意投入一些工作,这可能是你的拿手好戏。另一方面,如果你对游戏开发而不是计算机图形更感兴趣,你可以一直使用 OpenGL ES——它不会很快被弃用而支持 Vulkan。

Android 平台包括一个特定于 Android 的 Vulkan API 实现。

To get started with Vulkan on Android, you can download the LunarG Vulkan repository. You’ll need to download the project from GitHub. You can simply download the git file from github.com/LunarG/VulkanSamples. Click the “Clone or download” button as shown in Figure 12-4.

![img/340874_4_En_12_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig4_HTML.jpg)
Figure 12-4

VulkanSamples.git

Or use git on a command line, like this (this was done on a Mac; same commands will work on Linux):mkdir vulkancd vulkangit clone --recursive https://github.com/LunarG/VulkanSamples.gitcd VulkanSamples/API-Samplescmake -DANDROID=ON -DABI_NAME=abicd androidpython3 compile_shaders.pyNote

如果你还没有 Python 3,你需要在你的系统上安装它。可以从 Python 网站www.python.org/downloads/获取。

Next, open Android Studio, if you haven’t launched it yet. Choose FileOpen and select VulkanSamples/API-Samples/android/build.gradle. The project looks like the window shown in Figure 12-5.

![img/340874_4_En_12_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig5_HTML.jpg)
Figure 12-5

项目窗格显示导入后的个样本

We need to configure the SDK and NDK directories; to do that, go to FileProject Structure and then ensure that the SDK and NDK locations are set (as shown in Figure 12-6).

![img/340874_4_En_12_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig6_HTML.jpg)
Figure 12-6

项目结构、NDK 和 SDK

如果您的 NDK 尚未设置,请单击下拉箭头(省略号附近,右侧的三个点)。下拉菜单应该建议推荐的目录。如果 Android Studio 没有建议的目录,你需要检查你是否已经安装了 NDK。请参阅本章前面几节中关于 NDK 安装的讨论。

You can now compile the individual modules in the project. Select the project you want to compile in the Project tool window, as shown in Figure 12-7.

![img/340874_4_En_12_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig7_HTML.jpg)
Figure 12-7

制作模块

From the Build menu, choose Make Module . Resolve any dependency issues, then compile. Most of the samples have simple functionality. The drawcube example is one of the visually interesting examples (shown in Figure 12-8).

![img/340874_4_En_12_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig8_HTML.jpg)
Figure 12-8

drawcube 模块

这些关于如何在 Android 中设置 Vulkan 环境的说明来自 Android 开发网站()https://developer . Android . com/ndk/guides/graphics/getting-started);本书付印时,说明可能会改变;因此,在设置 Vulkan 环境时,请务必访问该页面。

游戏引擎和框架

在第六章和 第七章 中,你只看到了一个游戏开发者生活的一小部分,因为我们制作了两个小游戏,但我们是从零开始制作的。尽管游戏的规模不是很大,但就代码行和资产而言,我们必须做所有的事情。我们必须告诉程序从哪里获取图形文件,将它们加载到屏幕上的特定坐标,在游戏中的特定时间播放一些音频,等等。这就像用牙刷粉刷房子一样——是的,你对游戏的每个方面都有很大的控制权,但这也是很大的工作量。你可以打赌,你玩过的大多数 AAA 游戏都不是那样构建的。

大多数现代游戏要么使用游戏框架,要么使用游戏引擎。游戏引擎是一个完整的包。这是一套全面的工具,帮助您从头开始构建游戏。引擎通常包含一些场景或关卡编辑器,导入游戏资源(模型,纹理,声音,精灵等)的工具。)、动画系统以及对游戏逻辑进行编程的脚本语言或 API。您仍然需要编写代码来使用引擎,但是大部分代码将集中在游戏逻辑上。游戏引擎将为您提供系统级样板代码。

Android SDK 为游戏提供了一个不错的框架。还记得我们使用视图对象和 ImageView 对象吗?Android SDK 也提供了一些不错的支持,所以我们可以处理事件,让窗口达到最大尺寸,并在屏幕上绘制一些基本的图形。这些是框架要做的事情;但是除了 Android SDK 提供的框架之外,还有其他框架。

说实话,你并不真的需要游戏引擎,也不需要框架;但是在游戏编程过程中,它们确实让你的生活变得容易多了。在没有引擎或框架的情况下,构建一个不平凡的游戏可能是艰巨而危险的。如果你的最终目标是构建一个游戏,考虑使用第三方工具会更好。

外面有许多框架和引擎;我只编译了那些包含 Android 作为目标平台的应用;并不是所有的公司都会使用 Java 或 Android SDK 进行开发。你应该记住,这个列表并不全面,但它应该让你开始。

结构

HaxeFlixel 。【http://haxeflixel.com/】

这是一个 2D 游戏框架。您可以在 HTML5、Android、iOS 和桌面上部署它。如果你不介意学习 Haxe 语言,你可以试试这个。

勒夫【https://love 2d . org/

It’s also a 2D framework. You’ll have to use the Lua language, but you can deploy it on Android, iOS, Linux, macOS, and Windows. This framework has already been used on some commercial games; check out Figure 12-9.

![img/340874_4_En_12_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/begin-andr-game-dev/img/340874_4_En_12_Fig9_HTML.jpg)
Figure 12-9

用 love 完成的商业游戏

一夫一妻制。【www.monogame.net/】

这是另一个针对 iOS、Windows、Android、macOS、PS4、PSVita、Xbox One 和 Switch 的 2D 框架。使用的语言是 C#(它与 Java 有很多语言元素的相似之处)。

发动机

Cocos2D 。【http://cocos2d.org/】

这是一个针对 Android(开发中)、PC、macOS 和 iOS 的 2D 引擎。根据您的平台,您必须使用 C++、C#或 Objective-C。

铜立方体。【www . ambira . com/copper cube】

这是一个 3D 引擎,你可以用它来运行在 Windows、macOS、Android 和网络上的游戏。它支持 C++、JavaScript 和可视化脚本语言。

去折叠。【www.defold.com/】

如果你不介意使用 Lua 语言,你可以用这个 2D 引擎来瞄准 Windows、macOS、Linux、iOS、Android 和 HTML。

埃森瑟尔。【www.esenthel.com/】

这是一个面向 Windows、Xbox、Mac、Linux、Android、iOS 和 Web 的 2D/3D 引擎。你必须用 C++编写代码。

GameMaker Studio 2 。【www.yoyogames.com/】

这是一个针对 Windows,Mac,Android,iOS,Windows Phone 8,HTML5,Ubuntu,Tizen 和 Windows UWP 的商业 2D 引擎。它使用一种叫做 GML 的定制语言。有一个免费(但有限)的试用。

团结【http://unity 3d . com/

这是一个面向 Windows、macOS、Linux、HTML5、iOS、Android、PS4、XB1、N3DS、Wii U 和 Switch 的 2D/3D 引擎。C#是这里的首选语言。这是免费使用,直到第一个 100,000 美元的收入。查看他们的网站了解更多详情。

虚幻引擎 4 。【www.unrealengine.com/】

你可以针对 Windows,iOS,Mac,PS4,XB1,Switch,HTML5,HoloLens,鲁珉,Android 和 Linux。这是一个 2D/3D 引擎。你必须使用 C++或者 Blueprints 可视化脚本(JavaScript 语言可以和一些插件一起使用)。在该项目盈利超过 100 万美元之前,它是免费使用的。查看网站了解更多详情。

关键要点

在这最后一章,我们学习了一些关于 NDK、Vulkan 和游戏引擎和框架的知识。游戏编程是个大话题;我们只是触及了这本书的表面。我希望你继续你的旅程,构建有趣和引人入胜的游戏。愿原力与你同在!

posted @   绝不原创的飞龙  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验
点击右上角即可分享
微信分享提示