当前位置: 首页 > article >正文

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://img-blog.csdnimg.cn/img_convert/727861a1ff512c53fdb7d51eed2081a3.jpeg)
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://img-blog.csdnimg.cn/img_convert/7daa190554df5f0936015ab97af2450f.jpeg)
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://img-blog.csdnimg.cn/img_convert/dccfae38fde878f5e2d1153860d01391.jpeg)
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

  • 从【https://software.intel.com/en-us/android】下载。像安装其他 Windows 软件一样安装它,双击它,然后按照提示进行操作。

  • 或者,您可以通过 SDK 管理器获得 HAXM 这是推荐的方法。

对于 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://img-blog.csdnimg.cn/img_convert/2d93759ad2077d88cddbad2c75a43e4c.jpeg)
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://img-blog.csdnimg.cn/img_convert/f696c626923a4c90e8c1c7d9e9fcc737.jpeg)
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://img-blog.csdnimg.cn/img_convert/1a9b5e94a7a0095d6576d94f720f4754.jpeg)
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://img-blog.csdnimg.cn/img_convert/b4de72edc5fe1c14c090a552af3fbeb4.jpeg)
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://img-blog.csdnimg.cn/img_convert/8974abe003fdd16a68babcb4b7801db0.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_2_Fig6_HTML.jpg&pos_id=img-4j2GD60p-1723516328070)
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://img-blog.csdnimg.cn/img_convert/bf96a545f0a2a91156de6004007ad73d.jpeg)
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://img-blog.csdnimg.cn/img_convert/6d9cfa921571e329e8a041c117cfda76.jpeg)
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://img-blog.csdnimg.cn/img_convert/72beb0a36e7728e007d9e0b6ac27ce01.jpeg)
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://img-blog.csdnimg.cn/img_convert/a3526ddeb1dd83aab9b95766f6dd2154.jpeg)
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://img-blog.csdnimg.cn/img_convert/eee11147d2c861693e50f92f4d4f2110.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_2_Fig12_HTML.jpg&pos_id=img-ArzKowOf-1723516328071)
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://img-blog.csdnimg.cn/img_convert/41a9f7d98cf7f5a1de4145576f6b48bb.jpeg)
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://img-blog.csdnimg.cn/img_convert/58650b2bbf1d990bf7cea1d48a83acb5.jpeg)
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://img-blog.csdnimg.cn/img_convert/8a6df60e29ce683391f4ea23132778c9.jpeg)
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://img-blog.csdnimg.cn/img_convert/c7437df8125b940bc1b59563f37598b3.jpeg)
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://img-blog.csdnimg.cn/img_convert/9d36ee13b823b245a872abc0a121ee4e.jpeg)
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://img-blog.csdnimg.cn/img_convert/89ee960a5adc29841274b2289fbe556f.jpeg)
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://img-blog.csdnimg.cn/img_convert/9f26f57f32103a13cae1a83c82864689.jpeg)
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://img-blog.csdnimg.cn/img_convert/9c4c532a7b20290232c51bb32fd86d83.jpeg)
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.”

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_3_Fig7_HTML.jpg&pos_id=img-yVT9jh2l-1723516328073)
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://img-blog.csdnimg.cn/img_convert/1f7167d6bf5356da6251d3bb264607b5.jpeg)
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://img-blog.csdnimg.cn/img_convert/6ad6a0659007b46ac56415d42446978a.jpeg)
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://img-blog.csdnimg.cn/img_convert/754045c843fcde870a66244ea469fe42.jpeg)
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://img-blog.csdnimg.cn/img_convert/6c1c1c705afbf731defa515772f975e2.jpeg)
Figure 3-11

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

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_3_Fig12_HTML.jpg&pos_id=img-c0C3jzv7-1723516328074)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_4_Fig1_HTML.jpg&pos_id=img-1txpIOVt-1723516328074)
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://img-blog.csdnimg.cn/img_convert/c787006e854817d7fd97828e66fc4cbe.jpeg)
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://img-blog.csdnimg.cn/img_convert/21c250085136d94a2ef053eb6b520a8f.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_5_Fig2_HTML.jpg&pos_id=img-APq9SD99-1723516328075)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_5_Fig3_HTML.jpg&pos_id=img-BedB2QD3-1723516328075)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_5_Fig4_HTML.jpg&pos_id=img-3mNYhOwh-1723516328075)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_5_Fig5_HTML.jpg&pos_id=img-E5k9MKno-1723516328075)
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://img-blog.csdnimg.cn/img_convert/c4112fb35ef54dcfa3391c251da7fb38.jpeg)
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://img-blog.csdnimg.cn/img_convert/b44d90a4d19b25216c448df7034c3c39.jpeg)
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://img-blog.csdnimg.cn/img_convert/c2ddeaa88252712e24a6b056a2bfad09.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig3_HTML.jpg&pos_id=img-O35rgA3U-1723516328076)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig4_HTML.jpg&pos_id=img-pqGi2GL3-1723516328076)
Figure 6-4

雄激素类化合物

<?xml version="1.0" encoding="utf-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://img-blog.csdnimg.cn/img_convert/b50eb05294751db87d749967ca3ea346.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig6_HTML.jpg&pos_id=img-rNpiQ2jt-1723516328076)
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://img-blog.csdnimg.cn/img_convert/abebd49a1811b6e9af829d56a7025b5d.jpeg)
Figure 6-7

新空活动

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

![img/340874_4_En_6_Fig8_HTML.jpg](https://img-blog.csdnimg.cn/img_convert/67aeab02a4a5c2a4e8245cd68110411b.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig9_HTML.jpg&pos_id=img-POjRs6op-1723516328077)
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://img-blog.csdnimg.cn/img_convert/0a8235fed89fadb8e31cb282c09aca1a.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig11_HTML.jpg&pos_id=img-g6NtXhcN-1723516328077)
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()-(50*scale),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://img-blog.csdnimg.cn/img_convert/31182298d0dd91ad4bc24f61b4d47473.jpeg)
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()-(50*scale),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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig13_HTML.jpg&pos_id=img-yI5eaOGi-1723516328078)
Figure 6-13

选择套装对话框

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig14_HTML.jpg&pos_id=img-JXakMkWC-1723516328078)
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://img-blog.csdnimg.cn/img_convert/3f60201784c5be4d6115a1636d8610f8.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig16_HTML.jpg&pos_id=img-xi98wdgh-1723516328078)
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.<?*xml version="1.0" encoding="utf-8"*?>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://img-blog.csdnimg.cn/img_convert/cffb9547c22596f9d7f849e7dd92ba8f.jpeg)
Figure 6-17

创建新的布局 XML 文件

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

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig18_HTML.jpg&pos_id=img-gGp1MNpL-1723516328078)
Figure 6-18

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

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig19_HTML.jpg&pos_id=img-sTfqeFbU-1723516328079)
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.<?*xml version="1.0" encoding="utf-8"*?>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()-(50*scale),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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_6_Fig20_HTML.jpg&pos_id=img-1an28itE-1723516328079)
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(scale15);}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://img-blog.csdnimg.cn/img_convert/1a8d446b0e56ccf43e21cd223c5dd37e.jpeg)
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.<?*xml version="1.0" encoding="utf-8"*?>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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig1_HTML.jpg&pos_id=img-mX3sUBst-1723516328079)
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://img-blog.csdnimg.cn/img_convert/8f31b74e52e814f1abd4f53b22738ad0.jpeg)
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://img-blog.csdnimg.cn/img_convert/af883c42817f8f8628befae4002a16f7.jpeg)
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://img-blog.csdnimg.cn/img_convert/b93b384aec158ac7bbc5afef60a7be7c.jpeg)
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://img-blog.csdnimg.cn/img_convert/3940158fa2e1f411e9eb836395f37091.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig6_HTML.jpg&pos_id=img-1iwA7lEV-1723516328080)
Figure 7-6

生成图像的目标文件夹

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig7_HTML.jpg&pos_id=img-5m0k5CVy-1723516328080)
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://img-blog.csdnimg.cn/img_convert/116dcd32b12e3eb97a531bf7f2f8e94f.jpeg)
Figure 7-8

Android Resizer,完成了转换

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

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig9_HTML.jpg&pos_id=img-ReeZtT8D-1723516328080)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig10_HTML.jpg&pos_id=img-8SW2YIKj-1723516328081)
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://img-blog.csdnimg.cn/img_convert/155ad5df48407a8c09a15f518e5823e8.jpeg)
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://img-blog.csdnimg.cn/img_convert/08f2e52201432b8f101736fe08e2b018.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig13_HTML.jpg&pos_id=img-LMewTsYr-1723516328081)
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://img-blog.csdnimg.cn/img_convert/e63976884975226be424c4dc4313af22.jpeg)
Figure 7-14

AndroidManifest.xml

<?*xml version="1.0" encoding="utf-8"*?><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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig15_HTML.jpg&pos_id=img-KXO0Z6lg-1723516328081)
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).

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig16_HTML.jpg&pos_id=img-8C8jmqOf-1723516328082)
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.<?*xml version="1.0" encoding="utf-8"*?>#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://img-blog.csdnimg.cn/img_convert/c1dfc9bf32fad385519987443cb567b9.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig18_HTML.jpg&pos_id=img-ERUGIsqC-1723516328082)
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://img-blog.csdnimg.cn/img_convert/72f396df7d6ef67ebb83730bafe36093.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig20_HTML.jpg&pos_id=img-f2A7xHIn-1723516328082)
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://img-blog.csdnimg.cn/img_convert/ae2c5570f5198f360379d20e0a7f7327.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig22_HTML.jpg&pos_id=img-uNczF3JT-1723516328083)
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://img-blog.csdnimg.cn/img_convert/30507cd419320bea54eced42f27ada4d.jpeg)
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://img-blog.csdnimg.cn/img_convert/26ec5441525ccba01c33f6850c3231b9.jpeg)
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.

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig25_HTML.jpg&pos_id=img-lKHIWcbr-1723516328083)
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://img-blog.csdnimg.cn/img_convert/d741354e36d994aa08bc40a47214ef00.jpeg)
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://img-blog.csdnimg.cn/img_convert/29b32f5aa92a39d23a9ca1f0c3d8f3c7.jpeg)
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://img-blog.csdnimg.cn/img_convert/fc08d03f19a1eecbe417b5a3d8d7caf1.jpeg)
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://img-blog.csdnimg.cn/img_convert/865ebeb64a5436b9b68cff4ada6ebf6b.jpeg)
Figure 7-29

新建资源文件夹

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

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Fbegin-andr-game-dev%2Fimg%2F340874_4_En_7_Fig30_HTML.jpg&pos_id=img-VAC7T63p-1723516328084)
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

主要活动

**
http://www.lryc.cn/news/2417888.html

相关文章:

  • java Map遍历的5种方法和一些基本使用
  • Cocoa 框架概述
  • alternatives命令总结
  • PS(Photoshop)去水印的4个方法
  • MPEG音频文件格式(包括MP3文件格式)详解
  • 思科RIP路由协议介绍与实验操作步骤
  • 1.图文并茂详解Linux安装,客户端连接,xshell,虚拟机,虚拟网卡配置
  • 【统计类知识】大数定律与中心极限定理
  • 【笔记】位图(.bmp)和矢量图(Vector):位图是点阵图或光栅图,使用像素的一格一格来描述图像,放大以后每一个像素看就像是一个个的马赛克;矢量图是使用直线和曲线来描述图形,可以无限方法,不会失真
  • conan 详解
  • 什么是DI(依赖注入),依赖注入的原理
  • 数据库的索引
  • 用AVPlayer播放视频
  • 说走就走的「Windows」—— Windows To Go 制作详解
  • 算法篇-----粒子群算法
  • linux解压缩命令大全
  • html网页制作——HTML5响应式个人简历网站模板 web前端网页制作课作业
  • 项目介绍——面向对象与软件工程实验四
  • flex与bison入门,编译原理:flex编写词法分析器(使用windows环境)
  • NIVIDIA 硬解码学习2
  • 泛函分析的優勢在數值解析中:高效計算與準確度的平衡
  • 向量的点乘和叉乘
  • 打印机连接三种方式
  • Netbeans 适配C/C++、JAVA防坑秘笈
  • JS中几个getElementByXXX方法的区别
  • Varnish的基本应用详解
  • 使用autoit,可以节省您很多时间
  • 图文结合!非常详细Linux简介与安装!
  • CSS行高——line-height
  • Android面试:Invalidate、RequestLayout