通过构建安卓游戏学习-Java-全-

通过构建安卓游戏学习 Java(全)

原文:zh.annas-archive.org/md5/94381ED211CDAA9276E19DB483447D97

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

如果你完全不了解 Java、Android 或游戏编程,并且打算出版 Android 游戏以供娱乐或商业用途,但你不知道从哪里开始,那么这本书适合你。这本书也可以作为那些在其他平台上已经有经验的人的复习。

Android 是增长最快的操作系统,Android 设备可以赋予、娱乐和教育全球。Android 使用最流行的编程语言之一——Java,这是一种高性能、安全、面向对象的语言。

在每一章中,我们将在前一章学到的基础上构建,逐渐理解更高级的 Java 概念,并通过构建 Android 游戏来应用它们。

这本书涵盖了什么

第一章,“为什么选择 Java、Android 和游戏?”,告诉我们为什么我们可能会选择 Java、Android 和游戏而不是其他语言、平台和应用类型。然后我们通过安装 Java 开发工具包和 Android Studio 快速准备我们的 Android 和 Java 开发环境。

第二章,“开始使用 Android”,向我们展示了如何在真实的 Android 设备上设计、构建和运行游戏菜单 UI。这是我们数学游戏项目的第一部分。

第三章,“使用 Java——你的第一个游戏”,涵盖了 Java 基础知识及其在 Android 环境中的应用,制作一个简单的数学测验游戏。

第四章,“发现循环和方法”,包含了更多的 Java 基础知识,如决策和循环。然后我们使用它们来为我们的数学游戏添加功能。本章还涵盖了一些基本的 Android 游戏要素,如锁定和处理屏幕旋转,并介绍了设备传感器。

第五章,“游戏和 Java 基础知识”,带我们快速浏览了任何游戏都必不可少的一些重要基础知识。我们还通过动画和存储玩家得分来使 UI 更加有趣。通过一个 Simon 风格的记忆游戏进行演示,使用 Java 数组和 for 循环存储序列。

第六章,“面向对象编程——利用他人的辛勤工作”,是你迄今为止学到的一切都被漂亮地捆绑起来的一章。你应该会发现自己喃喃自语着“哦,我明白了”和“原来是这样”。我们在这一章探讨了面向对象编程(OOP)。

第七章,“复古乒乓游戏”,教会我们如何使用 Android Canvas 类逐像素绘制图形。然后我们可以将这些新技能与我们已经掌握的知识相结合,制作一个复古的乒乓风格的乒乓游戏。

第八章,“贪吃蛇游戏”,涵盖了我们最先进的游戏项目的开始——一个动画的“贪吃蛇”风格的街机游戏。我们还将能够练习在前几章中学到的重要的 Java 基础知识。

第九章,“让你的游戏成为下一个大事件”,通过教你如何发布你的游戏并添加在线排行榜和成就来为你的游戏锦上添花。这也让你了解 Java 库的概念。

附录,“自测问题和答案”,包含了测试你对主题理解的所有问题的答案。

你需要为这本书做好准备

你需要一个适度版本的 Windows(XP、Vista、7 或 8),以及 Mac、Linux PC 或笔记本电脑。32 位和 64 位系统都兼容。除此之外,所有需要的软件都是免费的,并且在书中有详细的说明。

这本书是为谁写的

如果你完全是 Java、Android 或游戏编程的新手,那么这本书就是为你准备的。由于本书不需要任何先前的知识,所以如果你对这三个领域(Java、Android 和游戏编程)都是新手,那么这本书也适合你。

如果你想为了娱乐或商业目的发布 Android 游戏,但不确定从哪里开始,那么这本书将从头开始逐步向你展示该怎么做。

如果你在其他平台(也许是 PC 或 Mac)有 Java 经验,那么这本书将是一个很好的 Java 复习,也会向你展示如何在 Android 环境中应用你现有的技能。

如果你之前在其他语言中编程,但想从头开始学习 Java,那么你可以从这本书中学习,并更快地通过实际示例。

本书假设你对自己选择的操作系统相当自信。逐步教程是在 Windows 中进行的,但如果你对 Mac 或 Linux 有相当了解,你也可以很容易地跟随它们。

约定

在本书中,你会发现一些区分不同信息类型的文本样式。以下是一些样式的例子和它们的含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:"在 Android Studio 项目资源管理器中,双击layout文件夹,以显示其中的activity_main.xml文件。"

代码块设置如下:

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

当我们希望引起你对代码块的特定部分的注意时,相关行或项目会以粗体显示:

int partA = 9;
int partB = 9;
correctAnswer = partA * partB;
int wrongAnswer1 = correctAnswer - 1;
int wrongAnswer2 = correctAnswer + 1;

任何命令行输入或输出都以以下方式书写:

info﹕ a = 10
info﹕ b = Alan Turing
info﹕ c = true

新术语重要单词以粗体显示。屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:"在提示导入另一个类时,点击确定。"

注意

警告或重要提示会以这种方式出现在一个框中。

提示

提示和技巧会以这种方式出现。

第一章:为什么学习 Java、Android 和游戏?

欢迎来到通过构建 Android 游戏学习 Java,我希望这只是你激动人心的设计和编写游戏之旅的开始。在本书结束时,我们将完成四款完整的游戏:一个难度动态增加的数学测验,一个类似经典 Simon 玩具的记忆游戏,一个类似乒乓球的游戏,以及一个经典的贪吃蛇游戏的克隆。

除了这些游戏,我们还将构建十多个可工作的应用程序,以练习和演示个别概念,帮助我们学习 Java、Android 和游戏。我们的游戏和应用程序将包括声音效果、图形和动画。我们将学习从使用标准 Android 用户界面(UI)设计师到通过绘制单个像素创建流畅动画的一切。

尽管我会鼓励你和我一起工作,并逐步实施本书中详细介绍的具体项目,但我完全期望一旦你掌握了不同的概念,你会想立即将它们用于自己独特的创作中。这正是我希望你会被激发去做的事情。

游戏项目本身并不是本书的目标,而是实现更崇高目标的手段。在本书结束时,你将能够设计和实现自己的 2D Android 游戏,在 Google Play 上销售或免费分享。

提示

首先需要做一些基础工作,但我保证不会花很长时间,也不会很复杂。任何人都可以学会编程。

然而,专家们有很多不同的观点,这在初学者中引起了关于学习编程的最佳方式的困惑。因此,看看为什么学习 Java、Android 和游戏是初学者的理想途径是一个好主意。这将是我们在本书中首先讨论的事情。

以下是我们将在本章学到的内容:

  • 这本书适合我吗?

  • 为什么要用游戏来学习编程?

  • 我为什么要学习 Java 和 Android?

  • 设置我们的开发环境

这本书适合我吗?

如果你已经决定要学习 Java、Android 或游戏,那么下一个问题可能是,“这本书适合我吗?”

有很多适合初学者的 Java 书籍,也有比我更有成就的作者和程序员写的书。我读过其中很多,并且钦佩这些作者。然而,当这些书开始涉及 Java 本地接口、Web 浏览器小程序或服务器端远程通信等话题时,我有时会质疑它们对我的直接相关性。

在这一点上,至少在潜意识中,我的承诺会减弱,学习过程会变得缓慢或停止。

如果你只想学习纯粹的 Java

如果你只想学习 Java 本身,这本书将是一个很好的开始。尽管 Android 的内容可能被视为对纯粹的 Java 学习的额外开销,但这远远少于任何其他 Java 书中可能引入的多余主题。这本书唯一的警告是必要的开销都在开始时。但一旦这个最小的开销被清除,我们就可以非常专注地学习 Java。

关于额外开销的问题:

  • 在本章中,我们将花大约六页来设置我们的编程环境

  • 需要第二章,开始使用 Android,来熟悉 Android 工具,创建你的第一个可工作的项目,并瞥见你的第一段真正的 Java 代码

  • 从那时起,几乎就是纯粹的 Java 和游戏开发了

你很快就会发现,这点额外的开销并不过分,而且是非常值得的。

如果你的重点是 Android

如果是 Android 本身吸引你看这本书,那么我很自豪地说,这是第一本会教你 Android 而不假设你有任何先前 Java 或编程知识的书。

这本书将带你去哪里

在本书结束时,你将能够轻松选择其中的一条道路,包括以下内容:

  • 为任何平台学习更高级别的 Java

  • 包括纯游戏框架的中级水平 Android 学习(将在第九章中更详细地介绍,使你的游戏成为下一个大事件

  • 更高级别的游戏开发

  • 更容易地处理任何现代面向对象的语言,比如 iOS、Windows 或 Web 开发

因此,如果你知道你想学习 Android 或 Java,希望我已经在某种程度上让你承诺这本书将如何帮助你。但为什么要学习游戏、Android 或 Java 呢?

为什么要制作游戏来学习编程?

当然,这很有趣!但也有其他原因。成功地运行我们编写的任何程序都是极其令人满意的,尤其是当它涉及使用我们以前不理解的一些代码时。

但是,正如你很快就会意识到的,制作我们自己的游戏会带来一种难以描述的愉悦感——这必须亲身体验才能感受到。然后,通过在手机或平板电脑上与朋友分享我们的创作,甚至在 Google Play 商店上公开分享它们,你可能会意识到一旦开始制作游戏,就停不下来了。

随着我们逐渐创建更复杂的游戏,你会意识到所有的技巧和代码片段都可以重新利用来创建其他游戏,然后你可以开始规划你自己独特的杰作。这至少是令人振奋的。

和许多其他学科一样,我们练习得越多,就会变得越好。所以游戏是学习编程 Java 的绝佳方式。然而,大多数针对 Android 游戏的初学者书籍都需要相当高水平的 Java 知识。但正如我们将看到的,完全可以将实际示例作为有趣的游戏项目,并从 Java 的基础知识开始。

这样做有一点儿取舍。我们不会总是按照“按部就班”的方式来处理工作中的游戏示例。这是为了避免在掌握前滚之前就做花式翻筋的问题。

学习成果的优先级始终是 Java 编程概念,其次是理解 Android 环境和游戏设计原则。话虽如此,我们将密切研究和实践大量的 Android 和游戏编程基础知识。

当然,从我们刚刚讨论的内容中,你可能可以推测出,如果我们没有制作游戏,那么在同样的页数中可能可以教授更多的 Java 知识。

这是真的,但我们失去了使用游戏作为学习主题所带来的所有好处。制作游戏确实可以带来愉悦,当我们的大脑敞开并渴望获取信息时,我们会学得更快。以这种方式学习的最小开销会被抵消一百倍。如果游戏对你一点兴趣都没有,那么有很多传统方法的 Java 初学者指南可以选择。只是不要期望发布你的第一个带有在线排行榜和成就的游戏时会有相同的刺激感。

为什么选择 Android 和 Java?

成功学习的一部分是学生的承诺,不仅是做工作,而且是相信他们正在以正确的方式做正确的事情。因此,许多技术课程和书籍并没有得到读者的承诺,至少在潜意识中没有。

问题在于学生们认为,他们可能在某种程度上,至少部分地,浪费时间在某些已经过时或即将过时的东西上,或者可能不太适合他们。这在很大程度上可能是真的,尤其是在编程方面。那么,为什么你要花费有限的时间学习 Java 和 Android 呢?

Android 是迄今为止发展最快、增长最快的操作系统

曾经,安卓更新几乎每两个月就会出现。即使现在,它们也大约每六个月出现一次。相比之下,Windows 版本之间需要数年的时间,即使 iOS 更新也只是每年一次,而且版本之间的变化通常相对较小。安卓显然正在以前所未有的速度发展和改进。

提示

查看安卓版本的历史,从第 1 版开始,www.cnet.com/news/history-of-android/

安卓的第一个版本于 2008 年发布,当时消费者已经对当时更加花哨的 iPhone 感到兴奋。新闻报道也在报道开发者通过在 iTunes 应用商店出售应用程序而变得富有。

但在这本书写成之前的整整一年里,仅三星一家就发货的安卓设备就比苹果销售的所有 iOS 设备加起来还要多。我不会加入关于哪种设备更好的战争。我喜欢安卓和苹果的各种方面,但纯粹从选择一个学习平台的角度来看,你可能选择了正确的时间和地点,选择了安卓。

安卓开发者前景广阔

现在你可能只是为了学习编程游戏的乐趣和满足感而拿起这本书。但如果你决定进一步发展你的学习,你会发现对安卓程序员的需求是巨大的,因此也非常有利可图。

提示

一些数据表明薪水超过 10 万美元。欲了解更多信息,请访问www.indeed.com/salary?q1=Android+Developer&l1=United+States

安卓是开源的

开源意味着尽管谷歌开发了最新设备上使用的所有安卓版本,但一旦代码发布,任何人都可以随心所欲地使用它。谷歌只在有限的时间内施加控制。

实际上,大多数安卓用户使用的是纯谷歌操作系统或三星、HTC 等大型制造商生产的修改版本,但没有任何东西可以阻止任何人拿起操作系统并将其改变、调整或转换成他们喜欢的任何东西。简而言之,安卓永远不会从编程社区中消失。

Java 会长存下去

好的,所以我们看到安卓不太可能消失,但是 Java 可能会变得多余吗?你的大量时间投资会白费吗?在安卓上,与大多数平台一样,你可以使用许多语言和工具。然而,安卓是从头开始设计的,以促进 Java 开发。所有其他语言和工具都不是无效的,但往往只是为了服务于一个相当特定的目的,而不是真正的 Java 替代品。事实上,就游戏而言,许多纯 Java 开发环境的替代品也是基于 Java 的,并且需要在 Java 上具有良好的技能水平。例如,流行的 LibGDX 游戏开发库,允许你同时为安卓、iOS、Windows、Linux、Mac 甚至 Web 制作游戏,仍然使用 Java!我们将在第九章中更多地讨论这个问题,《使你的游戏成为下一个大事件》。关键是 Java 和安卓是紧密联系在一起的,很可能会共同繁荣。

Java 不仅仅是为了安卓

事实上,Java 已经存在了很长时间,早在 1990 年代初就有了。尽管 Java 的用途在两个多十年里发生了演变和多样化,但语言本身最初实现的优势至今仍然保持不变。

Java 被设计为平台或计算机无关的。这是通过使用虚拟机VM)来实现的。这是一个用另一种语言编写的程序,它解码我们编写的 Java 程序并与其运行的计算机平台进行交互。因此,只要你想在计算机上运行你的 Java 程序,只要有一个 VM,你的 Java 程序就会运行,除了一些例外。因此,如果你学习 Java,你就是在学习一种语言,它在从智能冰箱到网络以及其他大多数地方都被使用。

然而,每个平台上的虚拟机通常会实现特定于其可能被用于的用途的功能。一个明显的例子是移动设备特定的功能,比如传感器、GPS 或许多 Android 设备上的内置摄像头。在 Android 上使用 Java,你可以拍照、检测气压,并准确地确定你在世界的哪个位置。大多数冰箱的虚拟机可能不会这样做。因此,你不能总是在设备 y 上运行为设备 x 设计的 Java 程序,但语言和语法是相同的。在 Android 上学习 Java 在很大程度上为任何情况下的 Java 做好了准备。所以请放心,Java 不会很快消失。

Java 快速且易于使用

关于哪种语言是最佳的或哪种语言是最适合学习编程的语言的辩论已经进行了几十年。Java 的批评者可能会说 Java 的速度问题。的确,Java 的内存管理以及虚拟机解释过程确实会有一些速度成本。然而,这些事情也有好处;它们显著提高了我们的生产力,而 Android 虚拟机与设备的交互方式在很大程度上抵消了轻微的速度损失。自 Android 4.4 以来,它完全通过Android Run TimeART)来实现,它将用 Java 编写的应用程序安装为完全本地的应用程序。现在 Java 程序员可以使用友好的解释语言构建游戏,并使它们运行得就像它们是用更具挑战性的本地编译语言编写的一样。

Java 和 Android 摘要

在一个快速变化的世界中,如果你担心在哪里投资你宝贵的学习时间,很难有更多的信心。在这里,我们有一种语言(Java),其基本原理几乎在近 25 年里保持不变,以及一个由硬件、软件和零售业的最大名字支持的平台(Android),尽管它受到了巨大的影响,但实际上并不属于任何人。

我不是任何技术的传道者,尽管我确实喜欢在 Android 上做一些事情。但你可以确信,如果你正在考虑开始学习编程的最佳途径,有一个非常有力的论点,那就是 Java 和 Android 是最佳选择。

如果你想学习 Java 以及它的众多用途,那么这是一个非常好的开始。如果你想为 Android 开发或进入任何类型的 Android 开发,那么 Java 是绝对基础的开始方式,而制作游戏已经讨论过的巨大好处。

到书的结尾,你将能够为几乎任何支持 Java 的平台编写 Java 代码。你将能够在 Android 环境之外使用你在本书中学到的几乎所有东西。

如果你计划通过制作 Android 游戏或任何 Android 应用来追求职业或业务,那么这本书可能是初学者开始的唯一选择。

如果你完全是新手,想要掌握 Java 的最简单途径——这是地球上增长最快的平台——那么通过构建 Android 游戏学习 Java可能会非常适合你。

因此,希望你确信,这本书学习 Java 的路径是如此简单、有趣和全面,学习 Java 就像是。让我们开始设置,这样我们就可以开始制作游戏。

设置我们的开发环境

我们需要做的第一件事是准备我们的 PC 使用 Java 开发 Android。幸运的是,这对我们来说相当简单。

提示

如果您正在 Mac 或 Linux 上学习,本书中的所有内容仍然适用。接下来的两个教程有 Windows 特定的说明和截图。但是,稍微调整步骤以适应 Mac 或 Linux 应该不会太困难。

我们需要做的只是:

  1. 安装一个名为“Java 开发工具包”(JDK)的软件包,它允许我们使用 Java 进行开发。

  2. 安装 Android Studio,这是一个旨在使 Android 开发快速简单的程序。Android Studio 使用 JDK 和一些其他特定于 Android 的工具,这些工具在安装 Android Studio 时会自动安装。

安装 JDK

我们需要做的第一件事是获取 JDK 的最新版本。要完成本指南,执行以下步骤:

  1. 您需要在 Java 网站上,所以访问www.oracle.com/technetwork/java/javase/downloads/index.html

  2. 找到下面截图中显示的三个按钮,并点击标有“JDK”的按钮(高亮显示)。它们位于网页的右侧。点击“JDK”选项下的“下载”按钮:安装 JDK

  3. 您将被带到一个页面,上面有多个选项可以下载 JDK。在“产品/文件描述”列中,您需要点击与您的操作系统匹配的选项。Windows、Mac、Linux 和其他一些不太常见的选项都列在其中。

  4. 这里一个常见的问题是,“我有 32 位还是 64 位的 Windows?”。要找出来,右键单击“我的电脑”(在 Windows 8 上是“此电脑”)图标,点击“属性”选项,在“系统类型”条目下查看“系统”标题下,如下截图所示:安装 JDK

  5. 点击略微隐藏的“接受”“许可协议”复选框:安装 JDK

  6. 现在点击下载选项,选择您的操作系统和系统类型,等待下载完成。

  7. 在您的“下载”文件夹中,双击刚刚下载的文件。在撰写本文时,64 位 Windows PC 的最新版本是jdk-8u5-windows-x64。如果您使用 Mac/Linux 或 32 位操作系统,您的文件名将相应地有所不同。

  8. 在几个安装对话框中的第一个,点击“下一步”按钮,您将看到下一个对话框:安装 JDK

  9. 通过点击“下一步”接受前面截图中显示的默认设置。在下一个对话框中,您可以通过点击“下一步”接受默认的安装位置。

  10. 接下来是 Java 安装程序的最后一个对话框。点击“关闭”。

JDK 现在已安装。接下来我们将确保 Android Studio 能够使用 JDK。

  1. 右键单击“我的电脑”(在 Windows 8 上是“此电脑”)图标,导航到“属性”|“高级系统设置”|“环境变量”|“新建”(在“系统变量”下,而不是在“用户变量”下)。现在您可以看到“新建系统变量”对话框,如下截图所示:安装 JDK

  2. 在“变量名”中键入JAVA_HOME,在“变量值”字段中输入C:\Program Files\Java\jdk1.8.0_05。如果您在其他地方安装了 JDK,那么您在“变量值”字段中输入的文件路径将需要指向您放置它的地方。您的确切文件路径可能会有不同的结尾,以匹配您下载时的 Java 最新版本。

  3. 点击“确定”保存新设置。现在再次点击“确定”清除“高级系统设置”对话框。

现在我们在我们的 PC 上安装了 JDK。我们离开始学习 Java 编程还差一半,但我们需要一种友好的方式与 JDK 进行交互,并帮助我们用 Java 制作 Android 游戏。

Android Studio

我们了解到 Android Studio 是一个简化 Android 开发的工具,它使用 JDK 允许我们编写和构建 Java 程序。除了 Android Studio,还有其他工具可以使用。它们各有利弊。例如,另一个非常流行的选择是 Eclipse。就像编程中的许多事情一样,可以提出强有力的论据,说明为什么应该使用 Eclipse 而不是 Android Studio。我两者都使用,但我希望你会喜欢 Android Studio 的以下元素:

  • 这是一个非常整洁的界面,尽管仍在开发中,但非常精致和干净。

  • 与 Eclipse 相比,Android Studio 更容易上手,因为一些 Android 工具已经包含在软件包中,而不需要单独安装。

  • Android Studio 由 Google 开发,基于另一个名为 IntelliJ IDEA 的产品。有可能它将成为不久的将来开发 Android 的标准方式。

提示

如果你想使用 Eclipse,那很好;本书中的所有代码都可以工作。但是,一些键盘快捷键和用户界面按钮显然会有所不同。如果你还没有安装 Eclipse 并且没有使用 Eclipse 的经验,那我更加强烈地建议你使用 Android Studio。

安装 Android Studio

所以,不要拖延,让我们安装 Android Studio,然后我们可以开始我们的第一个游戏项目。为此,让我们访问developer.android.com/sdk/installing/studio.html

  1. 点击标有下载 Android Studio的按钮开始下载 Android Studio。这将带你到另一个网页,上面有一个看起来非常相似的按钮。

  2. 通过勾选复选框接受许可证,点击标有为 Windows 下载 Android Studio的按钮开始下载,并等待下载完成。按钮上的确切文本可能会根据当前最新版本而有所不同。

  3. 在你刚刚下载 Android Studio 的文件夹中,右键单击android-studio-bundle-135.12465-windows.exe文件,然后点击以管理员身份运行。你的文件名结尾会根据 Android Studio 的版本和你的操作系统而有所不同。

  4. 当询问是否允许未知发布者的以下程序更改您的计算机时,点击。在下一个屏幕上,点击下一步

  5. 在下图所示的屏幕上,你可以选择你的 PC 上的用户谁可以使用 Android Studio。选择适合你的选项,然后点击下一步安装 Android Studio

  6. 在下一个对话框中,保持默认设置,然后点击下一步

  7. 然后在选择开始菜单文件夹对话框中,保持默认设置,然后点击安装

  8. 安装完成对话框中,点击完成以第一次运行 Android Studio。

  9. 下一个对话框是给已经使用过 Android Studio 的用户,所以假设你是第一次使用者,选择我没有以前的 Android Studio 版本,也不想导入我的设置复选框,然后点击确定安装 Android Studio

这是我们需要的最后一个软件。我们刚刚完成的简单的九步流程实际上已经设置了一整套 Android 工具,我们将在下一章开始使用。

摘要

我们讨论了为什么游戏、Java 和 Android 不仅极其令人兴奋,而且可以说是学习编程的最佳方式。这是因为游戏可以是一个极具动力的主题,而 Java 和 Android 在流行度和长期性方面具有巨大优势,并且对我们所有人都是免费开放的。

我们还设置了 Java 开发工具包并安装了 Android Studio,为接下来的章节做好准备,我们将实际创建一个工作游戏的一部分,并首次查看一些 Java 代码。

第二章:开始使用 Android

在本章中,我们将通过所有 Android 主题的过山车之旅,这些主题是您需要学习的,以便开始学习 Java。不过,这不仅仅是理论。我们将设计一个游戏菜单的用户界面UI),并且我们还将看到并编辑我们的第一行 Java 代码。

此外,我们将看到如何在 PC/Mac 上的 Android 模拟器或者如果有的话在真实的 Android 设备上运行我们的应用程序。

本章中我们将涵盖的一些内容只是冰山一角。也就是说,我们讨论的一些主题下面有更多的内容,这些内容不适合于学习 Java 书籍的第二章。有时,我们可能需要相信一些信息。

这将使我们能够在本章结束时实际设计和运行我们自己的 Android 应用程序。然后我们可以在下一章的开始学习真正的 Java。

如果本章看起来有点困难,那么不要担心;继续前进,因为每个后续章节都会逐渐揭开一些不太清晰的主题。

在本章和接下来的两章中,我们将构建一个数学游戏。我们将从简单开始,到第四章结束时,发现循环和方法,我们将扩展到使用重要的 Java 技能的游戏功能。

在本章中,我们将:

  • 开始我们的第一个游戏项目

  • 探索 Android Studio

  • 使用 Android Studio 可视化设计器制作我们的游戏 UI

  • 了解为 Android 构建代码的结构

  • 首次查看一些 Java 代码

  • 在模拟器和真实设备上构建和安装我们的游戏

我们的第一个游戏项目

现在我们将直接开始使用 Android Studio。通过双击桌面的开始菜单上的 Android Studio 图标,或者在安装它的文件夹中双击 Android Studio 图标来运行 Android Studio。

注意

如果您在对话框中收到任何提到权限提升的错误,请尝试以管理员权限运行 Android Studio。要做到这一点,通过单击 Windows 开始按钮并搜索Android Studio来找到 Android Studio 图标。现在右键单击该图标,然后单击以管理员身份运行。每次运行 Android Studio 时都要这样做。

准备 Android Studio

因此,安装了 Android Studio 和 Java 后,我们只需要添加我们将用于制作第一个游戏的最新版本的 Android API。以下是安装 API 的步骤:

  1. 从 Android Studio UI 顶部的菜单栏,导航到工具 | Android | SDK 管理器。在Android SDK 管理器窗口中向下滚动,并选择Android 4.4.2 (API 19)的复选框。

注意

请注意,由于 Android 发展如此迅速,当您阅读本章时,可能会有比 19 更高的 API,如 20、21 等。如果您遇到这种情况,请选择更新的(编号更高的)API。

准备 Android Studio

  1. 点击安装软件包

  2. 在下一个屏幕上,点击接受许可证复选框,然后点击安装按钮。Android Studio 将下载并安装适当的软件包。

刚才所做的是设置 Android Studio,以便提供最新的预写代码,称为 API,我们将在整本书中与之交互。

构建项目

  1. 点击新项目...,如下面的屏幕截图所示:构建项目

  2. 创建新项目配置窗口将出现。在应用程序名称字段中填写Math Game Chapter 2,在公司域中填写packtpub.com(或者您可以在此处使用您自己公司的网站名称),如下面的屏幕截图所示:构建项目

  3. 现在点击下一步按钮。在下一个屏幕上,检查手机和平板电脑复选框是否被选中。现在我们必须选择要为其构建应用程序的最早版本的 Android。随意在下拉选择器中尝试几个选项。您会发现我们选择的版本越早,我们的应用程序支持的设备百分比就越大。然而,这里的权衡是,我们选择的版本越早,我们的应用程序中可用的尖端 Android 功能就越少。一个很好的平衡是选择API 8:Android 2.2(Froyo)。现在就像下一个截图中所示那样去做吧:构建项目

  4. 点击下一步。现在如下截图所示选择空白活动,然后再次点击下一步构建项目

  5. 在下一个屏幕上,只需将Activity Name更改为MainActivity,然后点击完成

提示

默认情况下,Android Studio 在每次启动时都会显示一个“每日提示”对话框。在您还在学习 Java 时,一些提示可能没有意义,但其中许多确实非常有用,并揭示了很多快捷方式和其他节省时间的方法。当它们出现时,花几秒钟时间阅读它们是非常值得的。正如已经讨论过的,Android Studio 是从 IntelliJ IDEA 构建的,您可以在www.jetbrains.com/idea/webhelp/keyboard-shortcuts-you-cannot-miss.html找到完整的键盘快捷键列表。

  1. 通过点击关闭清除每日提示

如果您是完全新手,那么代码、选项和文件可能看起来有点令人生畏。不要担心;在学习 Java 时,我们不需要关注它们中的大部分。当与更细节的东西互动的时候,我们将一步一步地进行。

也许很难相信,但我们刚刚创建了我们的第一个可工作的应用程序。我们可以在 Android 设备上构建和运行它,很快我们就会。

在我们继续进行游戏之前,让我们深入了解一下 Android Studio。

探索 Android Studio

Android Studio 是一个非常深入的工具,但只需要逐步学习其中的一部分就可以开始。对我们可能有用的是给 UI 的一些部分命名,这样在阅读本书时可以更容易地参考它们。

看一下这个编号的图表,以及对 Android Studio 一些关键部分的快速解释。如果可以的话,尽量记住这些部分,以便将来更容易地讨论它们。

探索 Android Studio

这是一个方便的表格,您可以快速参考并记住我们正在提到的 Android Studio 的哪个部分。接下来是对每个区域的更详细的解释。

编号 名称
1 项目资源管理器
2 编辑器
3 菜单栏
4 工具栏
5 导航栏
6 重要的工具窗口
  • 项目资源管理器1):这在截图中显示为1,有点像 Windows 资源管理器。它显示了为我们的项目生成的所有文件和文件夹。随着本书的继续,我们将从这里做很多事情。实际上,如果您深入研究 Android Studio 创建的文件和文件夹,项目资源管理器并不是一个精确的映射。它稍微简化并突出显示,以便更轻松地管理和探索我们的项目。

  • 编辑器2):顾名思义,我们将在编辑器中编辑我们的 Java 代码文件。但是,正如我们很快将看到的,编辑器窗口会根据我们正在编辑的文件类型而发生变化。我们还将在这里查看和编辑 UI 设计。

  • 菜单栏3):像大多数程序一样,菜单栏为我们提供了访问 Android Studio 全部功能的途径。

  • 工具栏4):这包含了许多非常有用的一键选项,可以执行诸如部署和调试游戏等操作。将鼠标悬停在图标上,以获得弹出提示,并更深入地了解每个工具栏图标。

  • 导航栏5):就像文件路径一样,它显示了当前在编辑器中的文件在项目中的位置。

  • 重要的工具窗口6):这是一些选项卡,可以通过单击弹出并再次单击关闭。如果愿意,现在可以尝试一些选项卡,看看它们是如何工作的。

让我们更多地谈谈 Android Studio UI 的各个部分,以及编辑窗口如何转变为可视化 UI 设计师。之后,当我们足够熟悉时,我们将看看为我们的数学游戏构建一个简单的菜单屏幕。

使用 Android Studio 可视化设计师

Android Studio 编辑器窗口是一个非常动态的区域。它以最有用的方式呈现不同的文件类型。稍早一点,当我们创建项目时,它还为我们制作了一个基本的 UI。在 Android 中,UI 可以使用 Java 代码构建,或者,正如我们将看到的那样,在不需要一行 Java 代码的情况下使用可视化设计师。然而,正如我们在构建游戏菜单的 UI 之后将要调查的那样,要使 UI 做任何有用的事情,我们需要与之交互。这种交互总是通过 Java 代码完成的。可视化设计师还为我们生成 UI 代码。我们也会快速看一下那个。

随着书籍的进展,我们将主要避开 Android UI 开发,因为这是更多非游戏应用的基本功能。相反,我们将花更多时间直接绘制像素和图像来制作我们的游戏。尽管如此,常规的 Android UI 也有其用途,而 Android Studio 可视化设计师是最快的入门方式。

现在让我们来看看:

  1. 在 Android Studio 项目资源管理器中,双击layout文件夹,以显示其中的activity_main.xml文件。这应该很容易看到,除非您已经折叠了目录。如果看不到layout文件夹,请使用项目资源管理器导航到它。它可以在 Android Studio 项目资源管理器中找到,路径为Math Game Chapter2/src/main/res/layout,如下面的截图所示:使用 Android Studio 可视化设计师

  2. 现在双击activity_main.xml以在编辑器窗口中打开它。加载一小段时间后,您将看到与下一个截图非常相似的东西。下一个截图显示了以前只包含我们代码的整个内容。正如您所看到的,以前只是一个文本窗口现在有了多个部分。让我们更仔细地看一下这个截图:

在前面标有(1)的截图中,称为Palette,您可以从可用的 Android UI 元素中进行选择,然后简单地点击并将它们拖放到您的 UI 设计中。区域(2)是您正在构建的 UI 的可视视图,您将从 Palette 中点击并拖动元素。在可视 UI 视图的右侧,您将看到Component Tree区域(3)。组件树允许您检查复杂 UI 的结构,并更轻松地选择特定元素。在此树下方是Properties面板(4)。在这里,您可以调整当前选定的 UI 元素的属性。这些可以是简单的东西,如颜色和大小,也可以是更高级的属性。

注意

请注意标有(5)的标签。这些标签允许您在 Android Studio 为此类型的布局文件提供的两个主要视图之间切换。正如您所看到的,这些视图是DesignText。设计视图是默认视图,并且显示在前面的截图中。文本视图还显示您正在建设的 UI,但它显示为我们自动生成的代码,而不是Palette元素和组件树。

我们不需要担心这段代码,因为它都是为我们处理的。不过,偶尔查看一下这个选项卡可能会有好处,这样我们就可以开始理解设计工具为我们生成的内容。但是,这并不是学习 Java 所必需的。这段代码称为可扩展标记语言XML)。

  1. 快速查看Text选项卡,完成后点击Design选项卡,我们将继续。

现在我们已经看到了可视设计师的概述,甚至还瞥见了它为我们生成的自动生成代码。我们可以更仔细地查看一些我们将在项目中使用的实际 UI 元素。

Android UI 类型

现在我们将快速浏览一些非常有用的 Android UI 元素,一些关键属性,以及如何将它们组合在一起制作 UI。这些将为我们介绍一些可能性以及如何使用它们。然后我们将快速使用我们所知道的知识来制作我们的菜单。

TextView

在可视 UI 区域,点击Hello world!。我们刚刚选择的是一个称为 TextView 的小部件。TextView 可以是像这样的小文本,也可以是大标题类型的文本,在我们的游戏菜单中可能会有用。

让我们尝试将另一个 TextView 拖放到我们的可视 UI 上:

  1. 在我们的调色板中Widgets标题下方,您可以看到多种类型的 TextView。它们在调色板中呈现为普通 TextView大文本中等文本小文本。将大文本小部件拖放到我们的可视设计中。不要立即放开。当您将其拖动到手机图像周围时,请注意 Android Studio 以图形方式显示不同的定位选项。在下一个屏幕截图中,您可以看到当被拖动的小部件位于中心时设计师的外观:TextView

  2. 在您想要小部件放置的位置松开鼠标左键。如果您在上一个屏幕截图中显示的位置放开,那么文本将如预期般出现在中心。

  3. 现在我们可以玩一下属性。在Properties窗口中,点击textSize右侧。您可能需要滚动查找。将值输入为100sp并按Enter键。注意文本变得更大。我们可以通过增加和减少在此处输入的值来调整文本的大小。单位sp代表缩放像素,只是一种尝试在不同屏幕密度下将文本缩放到适当的等效实际大小的测量系统。TextView

  4. 如果你喜欢的话,可以玩一些更多的属性,完成后,点击我们在可视化设计中创建的 TextView 以突出显示它。然后点击删除键来摆脱它。现在删除我们开始时存在的 TextView,上面写着Hello world!

布局元素

现在您有一个看似空白的屏幕。但是,如果您在设计预览中的任何位置单击,您将看到Properties窗口中仍然有一些选项。这个元素称为 RelativeLayout。它是作为基础提供的几种布局元素类型之一,用于控制和对齐布局小部件,如按钮、文本等。如果您查看Palette窗口的顶部,您将看到主要的布局选项。我们将在稍后实际构建游戏菜单时使用此布局元素。

ImageView 小部件

ImageViews 毫不奇怪地用于显示图像。在标准的 Android UI 中,这是一种快速将我们设计师的艺术品添加到我们的游戏中的方法:

  1. 以与刚才定位 TextView 相同的方式将ImageView元素拖放到设计中。ImageView元素可以在Widgets标题下方找到。现在将其放置在中心,或者通过拖动它在设计中玩一下选项。我们将在一会儿删除它;在真正删除之前,我们只是进行了一些探索。

  2. 属性窗口中,以与之前选择textSize属性相同的方式选择src属性。

  3. 注意,在选择后,您可以点击...以获得更多选项。点击...并滚动到选项列表的底部。这些都是我们可以在这个 ImageView 中显示的所有图像文件。只是为了好玩,滚动到列表的底部,选择ic_launcher,然后点击确定。我们可以使任何我们喜欢的图像可用,这是构建有吸引力的游戏菜单屏幕的一种简单而强大的方法。

  4. layout:width属性更改为150dp,将layout:height属性更改为150dp。单位dp是一种在具有非常不同像素数量的屏幕设备上保持相对恒定的元素和小部件大小的方法。

  5. 以与之前删除其他视图相同的方式删除 ImageView。

ButtonView

ButtonView 的使用可能已经被它的名称泄露了。尝试在我们的布局上单击并拖动一些按钮。请注意,有几种类型的 ButtonView,例如小按钮按钮,以及,如果您在小部件列表中继续向下查看,图像按钮。我们将使用常规的 ButtonView,简称为按钮

现在我们将对每个这些 Android UI 元素进行操作,以制作我们的游戏菜单。

注意

您可以从本书配套网站的代码下载部分下载整个示例。

使用示例代码

本书中的所有代码都是以项目形式组织的。如果一个项目跨越多个章节,那么每个章节都会提供一个已完成状态的项目。这有助于您看到进展,而不仅仅是最终结果。要在 Android Studio 中打开项目,只需按照以下说明操作:

  1. 下载本书的代码。

  2. Android Studio中,从菜单栏导航到文件 | 关闭项目

  3. 现在创建一个新的空白项目,就像之前一样。浏览到您下载本书代码的位置。

  4. 导航到Chapter2/MathGameChapter2文件夹。在这里,您将找到本章中创建的所有文件的代码。

  5. 使用诸如免费的 Notepad++之类的纯文本编辑器打开代码文件。

  6. 复制并粘贴到您的 Android Studio 项目中,或者只是按照您看到的代码进行比较。

提示

尽管本书提供了所需的每一行代码,但您仍然需要通过 Android Studio 为自己创建每个项目。然后,您可以简单地将代码的全部内容复制并粘贴到具有匹配名称的文件中,或者只是将您可能遇到困难的代码部分复制并粘贴。请记住,如果您创建了一个具有不同包名称的项目,那么您必须从提供的代码文件中省略包名称的代码行。当我们在本章后面更多地谈论包时,这一点将更加清晰。

让我们亲自看看如何做到这一切。

制作我们的游戏菜单

现在我们只是让我们的游戏菜单功能正常。稍后在第五章中,游戏和 Java 基础,我们将看到如何通过添加一些酷炫的动画来使菜单更具视觉吸引力和乐趣。

这是本教程中我们的目标:

制作我们的游戏菜单

在开始编码之前,您应该先在纸上设计您的布局。但是,Android Studio 的设计师非常友好,特别是对于简单的布局,有很强的论点,可以在布局设计师中实际完善您的设计。执行以下步骤创建游戏菜单:

  1. 通过依次单击它们然后依次点击删除键,从设计师中删除所有小部件。注意不要删除RelativeLayout布局元素,因为我们将把它用作所有其他元素的基础。

  2. 从面板中拖动一个Large Text元素到设计区域的顶部中心,并赋予它以下属性。请记住,您可以通过单击要更改的属性右侧来在Properties面板中更改属性。将text属性更改为My Math Gamesize更改为30sp

  3. 从面板中拖动一个ImageView元素到设计的中心,稍微低于之前的 TextView。将layout:width属性更改为150dp,将layout:height属性更改为150dp

  4. 现在点击并拖动三个按钮,分别为PlayHigh ScoresQuit。将它们垂直居中,放在之前的 ImageView 下方,一个接一个地放置,就像我们之前展示的设计一样。

  5. 点击顶部的按钮,配置text属性,并输入值Play

  6. 点击中间的按钮,配置text属性,并输入值High Scores

  7. 点击最低的按钮,配置text属性,并输入值Quit

  8. 由于按钮现在包含的文本量相对于彼此不同,它们的大小也会略有不同。您可以通过点击并拖动较小按钮的边缘来使它们与较大的按钮匹配,以匹配预期的布局。这与您在 Windows 中调整应用程序窗口大小的方式基本相同。

  9. 使用Ctrl + S保存项目,或者导航到File | Save All保存项目。

提示

如果您要在比设计师中显示的 Nexus 4 屏幕大得多或小得多的屏幕上测试游戏,那么您可能希望调整本教程中使用的spdp单位的值。

在多个设备上讨论 Android UI 的全部内容超出了本书的范围,也不需要为本书中的任何游戏做任何讨论。如果你想立即开始为不同的屏幕设计,请查看developer.android.com/training/multiscreen/index.html

您可以通过从下拉菜单中选择设备来查看其他设备上的菜单外观,如下面的屏幕截图所示:

制作我们的游戏菜单

在我们的菜单在实际设备上启动之前,让我们先看一下 Android 应用程序的结构以及我们在编写 Java 代码时如何使用该结构。

为 Android 构建我们的代码

如果您曾经使用过 Android 设备,您可能已经注意到它的工作方式与许多其他操作系统有很大不同。例如,您正在使用一个应用程序-比如您正在查看 Facebook 上的人们在做什么。然后您收到一封电子邮件通知,您点击电子邮件图标阅读它。在阅读电子邮件的过程中,您可能会收到 Twitter 通知,因为您正在等待您关注的某人的重要消息,所以您中断了阅读电子邮件并触摸了 Twitter 应用。

阅读推特后,你想玩愤怒的小鸟,但在第一次大胆的投掷中途,你突然想起了 Facebook 的帖子。所以你退出了愤怒的小鸟,点击了 Facebook 图标。

然后你恢复了 Facebook,可能是在你离开的同一个点。你本可以继续阅读邮件,决定回复推特,或者开始一个全新的应用程序。所有这些来回都需要操作系统进行相当多的管理,显然独立于各个应用程序本身。

在我们刚刚讨论的情境中,Windows PC 和 Android 之间的区别在于,虽然用户决定使用哪个应用程序,但 Android 操作系统决定何时关闭(销毁)应用程序。我们在编写游戏时需要考虑这一点。

生命周期阶段-我们需要知道的内容

Android 系统有不同的阶段,任何给定的应用程序都可以处于这些阶段中。根据阶段,Android 系统决定应用程序如何被用户查看,或者是否被用户查看。Android 有这些阶段,以便它可以决定哪个应用程序正在使用,并分配正确数量的资源,如内存和处理能力。但也允许我们作为游戏开发人员与这些阶段进行交互。如果有人退出我们的游戏接听电话呢?他们会失去他们的进度吗?

Android 有一个相当复杂的系统,简化一下以便解释,确保 Android 设备上的每个应用程序都处于以下阶段之一:

  • 正在创建

  • 开始

  • 恢复

  • 运行

  • 暂停

  • 停止

  • 被销毁

希望阶段列表看起来相当合乎逻辑。例如,用户按下 Facebook 应用图标,应用程序创建。然后它启动。到目前为止,所有都相当简单,但接下来的是恢复!如果我们能暂时接受应用程序在启动后恢复,那么一切都会变得清晰。

恢复后,应用程序正在运行。这是当 Facebook 应用程序控制屏幕,可能也控制着更多的系统内存和处理能力。那么我们之前从 Facebook 应用切换到电子邮件应用的例子呢?

当我们点击去读我们的电子邮件时,Facebook 应用程序可能已经进入了暂停阶段,电子邮件应用程序将进入正在创建阶段,然后是恢复,然后是运行。如果我们决定重新访问 Facebook,就像之前的情景一样,Facebook 应用程序可能会直接进入恢复阶段,然后再次运行,很可能会准确地停留在我们离开时的帖子上。

请注意,随时,Android 可以决定停止销毁一个应用程序,在这种情况下,当我们再次运行应用程序时,它将需要重新创建。因此,如果 Facebook 应用程序长时间不活动,或者愤怒的小鸟需要太多系统资源,以至于 Android 将销毁 Facebook 应用程序,那么我们之前阅读的确切帖子的体验可能会有所不同。

现在,如果所有这些阶段的东西开始变得令人困惑,那么你会高兴地知道,提到的唯一原因如下:

  • 你知道它存在

  • 我们偶尔需要与它交互

  • 我们将一步一步地进行

生命周期阶段 - 我们需要做什么

当我们制作游戏时,我们如何可能与这种复杂性进行交互?好消息是,当我们创建第一个项目时自动生成的 Android 代码大部分为我们处理了交互。

作为游戏开发人员,我们所要做的就是确保 Android 知道在每个阶段发生时该如何处理我们的应用程序。更好的消息是,除非我们覆盖默认处理,否则所有这些阶段都将被默认处理。

这意味着我们可以继续学习 Java 和制作游戏,直到我们遇到少数需要在游戏中做一些事情的情况,特别是在其中一个阶段。

将我们的游戏分成活动

我们编写的 Java 代码将被分成称为活动的部分或部分。我们可以将活动视为游戏的不同屏幕。例如,在游戏中,我们经常会为主屏幕创建一个活动,为游戏屏幕创建一个活动,以及为高分屏幕创建一个活动。

每个活动都将有自己的生命周期,并将进一步分成部分,这些部分将对应于我们刚刚讨论的 Android 阶段之一。在 Java 中,这些部分被称为方法。方法是 Java 编程中的一个重要概念。

然而,在这个阶段,我们只需要知道方法用于将我们编写的 Java 代码分隔开,并且一些方法是由 Android 系统提供的,以便我们可以轻松处理否则复杂的 Android 生命周期。

接下来的列表是 Android 为了我们的方便提供的方法的快速解释,以管理生命周期的各个阶段。为了澄清我们对生命周期阶段的讨论,方法被列在我们一直在讨论的相应阶段旁边。然而,正如您将看到的,方法名称本身已经相当清楚地说明了它们在哪里适用。

在列表中,还有关于何时使用给定方法以及在特定阶段进行交互的简要解释或建议。随着我们在书中的进展,我们将遇到大多数这些方法。我们将在本章后面看到onCreate方法。以下是列表:

  • onCreate:当活动正在创建时,将执行此方法。在这里,我们准备好一切游戏所需的东西,包括图形、声音,也许还有高分。

  • onStart:当应用程序处于启动阶段时执行此方法。

  • onResume:此方法在onStart之后运行,但也可以在我们的活动在先前暂停后恢复时进入,这可能是最合乎逻辑的。当应用程序被中断时,我们可能会重新加载先前保存的游戏情况,例如电话呼叫或用户运行其他应用程序。

  • onPause:当我们的应用程序暂停时发生。在这里,我们可能希望保存当前的游戏。您可能已经掌握了这些方法。

  • onStop:这与停止阶段有关。这是我们可能会撤消在onCreate中所做的一切的地方。如果我们到达这里,我们的活动很可能很快就会被销毁。

  • onDestroy:这是我们的活动最终被销毁时——我们拆除游戏的最后机会。如果我们到达这里,我们肯定会再次经历生命周期的各个阶段。

所有方法的描述及其相关阶段应该是直接的。也许,唯一真正的问题是关于运行阶段。正如我们将看到的,当我们在其他方法/阶段中编写代码时,onCreateonStartonResume方法将准备游戏,这将形成运行阶段。onPauseonStoponDestroy方法将随后发生。现在我们实际上可以看一下其中一个方法以及其他一些方法。

我们对 Java 的第一次了解

那么早些时候在创建新项目时 Android Studio 生成的所有代码呢?这些代码将使我们的游戏菜单生动起来。让我们仔细看一下。编辑窗口中的第一行代码是这样的:

package com.packtpub.mathgamechapter2;

这行代码定义了我们在创建项目时命名的包。随着书籍的进展,我们将编写跨越多个文件的更复杂的代码。我们创建的所有代码文件都需要清楚地定义它们所属的包,就像前一行代码一样。代码实际上并没有在我们的游戏中任何事情。还要注意,这行代码以分号(;)结束。这是 Java 语法的一部分,它表示代码行的结束。删除分号,您将会得到一个错误,因为 Android Studio 试图理解两行代码。如果您愿意,可以尝试一下。

提示

请记住,如果您要从下载包中复制和粘贴代码,这是可能会有所不同的一行代码,具体取决于您如何设置项目。如果代码文件中的包名称与您创建的包名称不同,请始终使用创建项目时的包名称。

要查看接下来的四行代码,您可能需要单击小+图标以显示它们。Android Studio 试图通过简化我们对代码的视图来提供帮助。请注意,编辑窗口的侧边还有几个小-图标。您可以展开和折叠它们以适应自己的需求,而不会影响程序的功能。如下截图所示:

我们第一次接触 Java

扩展代码后,您将看到这四行:

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem; 

请注意,所有前面的行都以单词import开头。这是一个指示,包括其他包在我们的游戏中,而不仅仅是我们自己的包。这非常重要,因为它使我们能够使用其他程序员的辛勤工作,本例中是安卓开发团队的辛勤工作。正是这些导入使我们能够使用我们之前讨论过的方法,并允许我们与安卓生命周期阶段进行交互。再次注意,所有行都以分号(;)结尾。

下一行介绍了 Java 的一个基本构建块,称为class。类是我们将在整本书中不断扩展知识和理解的内容。现在,先看一下这行代码,然后我们将详细讨论它:

public class MainActivity extends ActionBarActivity {

逐字逐句,以下是正在发生的事情。前一行是说:创建一个名为MainActivity的新public class,并基于(extendsActionBarActivity

你可能还记得,在创建这个项目时我们选择的名称是MainActivityActionBarActivity是由安卓开发团队编写的代码(称为类),它使我们能够将我们的 Java 代码放入安卓中。

如果您有敏锐的眼光,您可能会注意到这行末尾没有分号。但是,有一个左花括号({)。这是因为MainActivity包含了其余的代码。实际上,一切都是我们的MainActivity类的一部分,它是基于ActionBarActivity类/代码构建的。如果您滚动到编辑窗口底部,您将看到一个右花括号(}),这表示我们称为MainActivity的类的结束。

  • 我们现在不需要知道类如何工作

  • 我们将使用类来访问其中包含的一些方法的代码,而不需要做任何其他操作,我们已经默认地利用了我们之前讨论过的安卓生命周期方法

  • 我们现在可以自由选择是否、何时以及在这些类中定义的方法中覆盖或保留默认值

所以,ActionBarActivity类包含了使我们能够与安卓生命周期交互的方法。实际上,有许多不同的类使我们能够做到这一点,一会儿,我们将从使用ActionBarActivity更改为一个更合适的类,该类也执行刚才提到的所有操作。

提示

此时重要的不是正确理解 Java 类;只需了解您可以导入一个包,一个包可以包含一个或多个类,然后您可以使用这些类的功能或基于自己的 Java 程序。

在接下来的几章中,我们将经常遇到类。把它们看作是做事情的编程黑匣子。在第六章中,OOP – Using Other People's Hard Work,我们将打开这个黑匣子,真正掌握它们,甚至开始制作我们自己的类。

继续进行代码,让我们看看我们的类中实际包含的代码是做什么的。

在我们刚刚讨论过的关键行之后,直接是代码块:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

希望现在这些代码中的一些内容开始变得有意义,并与我们已经讨论过的内容联系起来。尽管确切的语法仍然会感觉有点陌生,但只要我们意识到发生了什么,我们就可以继续学习 Java。

在前面的代码中,我们注意到的第一件事是@override关键字。还记得我们说过所有与 Android 生命周期交互的方法都是默认实现的,我们可以自行选择是否以及何时覆盖它们吗?这就是我们在这里用onCreate方法所做的。

@override关键字表示接下来的方法被覆盖。protected void onCreate(Bundle savedInstanceState) {行包含我们正在覆盖的方法。你可能能猜到,动作从问题行的{开始,三行后以}结束。

在方法名onCreate之前和方法名后的(Bundle savedInstanceState)看起来有点奇怪,但这些在这个时候并不重要,因为它们已经为我们处理了。这与数据在我们程序的各个部分之间传递有关。我们只需要知道这里发生的事情将在 Android 生命周期的创建阶段发生。其余内容将在第四章发现循环和方法中变得清晰。让我们继续到下一行:

super.onCreate(savedInstanceState);

在这里,super关键字引用了原始的onCreate方法中的代码,即使我们看不到它,它仍然存在。代码的意思是:尽管我正在覆盖你,但我希望你首先像往常一样设置好一切。然后,在onCreate完成了我们看不到也不需要看到的大量工作之后,方法继续进行,我们实际上可以用这行代码做一些事情:

setContentView(R.layout.activity_main);

在这里,我们告诉 Android 设置主内容视图(我们用户的屏幕),这是我们之前创建的酷炫游戏菜单。具体来说,我们声明它是layout文件夹中的R或资源,文件名为activity_main

清理我们的代码

接下来的两个代码块是由 Android Studio 创建的,假设我们想要覆盖另外两个方法。但我们不需要,因为这些方法更常用于非游戏应用程序:

  1. 删除以下代码中显示的整个内容。注意不要删除我们MainActivity类的结束大括号:
@Override
    public boolean onCreateOptionsMenu(Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
  1. 现在我们可以删除一些@import语句。这是因为我们刚刚删除了不再需要的类的覆盖方法(之前导入的)。请注意编辑器窗口中以下行是灰色的。请注意,如果你保留它们,程序仍然可以正常工作。现在删除它们,以使你的代码尽可能清晰:
import android.view.Menu;
import android.view.MenuItem;
  1. 在我们的代码完成之前进行一些最终修改:此时,你可能会认为我们已经删除和更改了我们的代码很多,以至于我们可能干脆从空白页面开始重新输入。这几乎是正确的。但是,让 Android Studio 为我们创建一个新项目,然后进行这些修改的过程更加彻底,也避免了很多步骤。以下是最后的代码更改。将import android.support.v7.app.ActionBarActivity;行更改为import android.support.app.Activity;

  2. 现在你会看到我们的代码下面有几条红线标出错误。这是因为我们试图使用一个尚未导入的类。只需将public class MainActivity extends ActionBarActivity {行更改为public class MainActivity extends Activity {

我们对最后两个更改所做的是使用Activity类的稍微更合适的版本。为此,我们还必须更改我们导入的内容。

完成后,你的编辑器窗口应该看起来像这样:

package com.packtpub.mathgamechapter2.mathgamechapter2;

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstancePhase) {
        super.onCreate(savedInstancePhase);
        setContentView(R.layout.activity_main);
    }

}

提示

下载示例代码

你可以从www.packtpub.com的账户中下载你购买的所有 Packt Publishing 图书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,将文件直接发送到你的邮箱。

现在我们知道发生了什么,我们的代码干净而简洁,我们实际上可以看一下我们游戏的开头部分在运行中的样子!

提示

如果我们刚才讨论的任何内容看起来复杂,不用担心。安卓强制我们在 Activity 生命周期内工作,所以之前的步骤是不可避免的。即使你没有完全理解关于类和方法等的所有解释,你仍然完全有能力从这里学习 Java。随着书的进展,所有的类和方法都会变得更加简单明了。

构建和安装我们的游戏

很快,我们将实际看到我们的菜单在运行。但在这之前,我们需要找出如何使用安卓模拟器以及如何构建我们的游戏。然后我们将把这些放在一起,把我们的游戏放入模拟器或真实设备中,以便像我们的玩家一样看到它。

模拟器和设备

现在我们已经准备好运行游戏的第一部分。我们需要测试它,检查是否有任何错误、崩溃或其他意外情况。还要确保它在您想要定位的设备类型/尺寸上看起来好并且运行正确。

注意

我们不会详细讨论处理不同设备类型的细节。我们所有的游戏都是全屏的,我们将稍后锁定方向并动态计算屏幕分辨率等方面。所以我们可以只为一个设备类型编写,并专注于学习 Java。

现在知道,你可以为任何屏幕尺寸分类或像素密度创建不同的布局文件。你所需要做的就是将布局文件放在适当的文件夹中,使用完全相同的文件名。安卓设备将知道最适合它使用的布局。有关详细讨论,请参阅 Google 开发者网站developer.android.com/guide/practices/screens_support.html

请注意,您不需要理解上述链接中的任何信息就可以学习 Java 并发布您的第一个游戏。

有几种方法可以做到这一点,我们将看两种。首先,我们将使用 Android Studio 和 Android 开发工具来制作一个设备模拟器,这样我们就可以在同一台 PC/Mac 上使用、测试和调试我们的游戏在各种设备模拟器上。所以我们不需要拥有一个设备。这将允许我们从我们的游戏中获得崩溃报告。

然后我们将直接将游戏安装到真实设备上,以便我们可以看到当他们下载我们的应用时,设备的所有者将看到什么。

还有更多的选择。例如,您可以通过 USB 连接真实设备,并在 Android Studio 中直接调试设备上的错误和语法反馈。这个过程可能因不同的设备而有所不同,由于我们只关注基本的调试,我们不会在本书中涵盖这个内容。

创建一个模拟器

让我们启动我们的模拟器:

  1. 在 Android Studio 快速启动栏的右侧,找到 AVD 管理器图标:创建模拟器

  2. 点击图标启动 Android 虚拟设备管理器。然后点击左下角的创建虚拟设备...按钮,打开虚拟设备配置窗口。

  3. 现在点击Nexus 4选项,然后点击下一步

  4. 现在我们需要选择我们将在其上构建和测试我们的游戏的安卓版本。最新版本(写作时)是Lollipop - 21 - x86。这是唯一一个我们不需要完成下载就可以继续的选项。所以选择它(或者在您阅读本文时的默认选项),然后点击下一步继续。

  5. 在下一个屏幕上,我们可以保留所有默认设置。所以点击完成

我们现在有一个可以直接运行的安卓模拟器。

运行模拟器

现在我们将启动(打开)我们的虚拟设备,然后通过以下步骤实际运行我们之前制作的游戏:

  1. 点击Name列下的Nexus 4 API 21。现在点击描述我们的模拟器右侧的三角形播放图标。

注意

安卓模拟器需要很长时间才能启动。即使在高规格的 PC 上也是如此。预计至少要等待几分钟,甚至 10 分钟。

  1. 一旦启动,通过点击和拖动模拟设备屏幕的任何位置来解锁设备。这类似于在真实的 Nexus 4 上滑动解锁。当我们的 Nexus 4 虚拟设备运行并解锁时,它看起来是这样的:Running the emulator

您可以几乎以与真实安卓设备相同的方式玩这个模拟器。但是,您无法从 Google Play 下载应用。您可能会注意到,与真实设备相比,甚至与旧设备相比,模拟器速度有点慢。不久,我们将看看如何在真实设备上运行我们的应用程序。

在模拟器上运行我们的游戏

一旦模拟器运行起来,通常最好让它保持运行,这样每次我们想要使用它时,就不必等待它启动。让我们使用模拟器:

  1. 如果模拟器尚未运行,请启动它,并确保设备已按先前描述的方式解锁。

  2. 点击工具栏中的运行图标(如下所示)来运行您的应用程序。您也可以通过从菜单栏导航到Run | Math Game Chapter 2来实现相同的功能:Running our game on the emulator

  3. 在 Android Studio 构建我们的应用程序时,弹出对话框将询问您要在哪个设备上运行该应用程序。选择描述中带有Nexus 4 API 21的设备。这是我们之前创建的已经运行的设备。现在按OK

  4. 注意此时在 Android Studio 的底部部分出现了有用的安卓窗口。如果您遇到任何问题,只需检查代码中的拼写错误。如果事情真的不顺利,只需返回到使用示例代码部分,与提供的代码进行比较或复制粘贴。

在另一段暂停后,我们的游戏菜单屏幕将出现在模拟器上。当然,它现在还没有做任何事情,但它正在运行,按钮可以被按下。

完成后,您可以按返回或主屏幕图标退出应用程序,就像在真实的安卓设备上一样。

现在我们已经看到了一种我们可以通过在安卓模拟器中运行来测试我们的应用程序的方法。让我们找出如何将我们的代码制作成一个可以在真实设备上分发和使用的应用程序。

构建我们的游戏

在真实的安卓设备上运行我们的游戏,我们需要创建一个以.apk结尾的文件,也就是一个以.apk为扩展名的文件。.apk文件是安卓系统用来运行和安装我们的应用程序的文件和文件夹的压缩存档。以下是使用安卓工作室制作我们游戏的.apk的步骤:

  1. 从菜单栏导航到Build | Generate Signed APK

  2. 一个略显冗长的窗口将弹出并显示:对于基于 Gradle 的项目,签名配置应在 Gradle 构建脚本中指定。您可以点击OK安全地关闭此窗口。

  3. 接下来是Generate Signed APK Wizard对话框。在这里,我们正在创建一个标识持有者被授权分发 APK 的密钥。在此过程结束时,您将获得一个.keys文件,您可以在每次构建.apk文件时使用。因此,这一步在将来可以省略。点击Create new按钮。

  4. Key Store Path字段中,键入或转到您的硬盘上希望存储密钥的位置。然后,您将被提示选择密钥库的文件名。这是任意的。键入MyKeystore并点击OK

  5. Password字段中输入密码,然后在Confirm字段中重新输入。这是用于保护您的密钥的存储的密码。

  6. 接下来,在别名字段中,输入一个易记的别名。您可以将其视为密钥的用户名。再次在密码字段中输入密码,然后在确认字段中重新输入。这是您的密钥密码。

  7. 有效年限下拉菜单保持默认的25

  8. 然后您可以填写您的姓名和组织详情(如果有),然后点击确定

  9. 现在我们的密钥和密钥库已经完成,我们可以在生成已签名 APK 向导对话框上点击确定

  10. 然后我们被提示选择运行 Proguard。在这个时候,加密和优化我们的.apk是不必要的。所以只需点击完成生成我们应用程序的.apk文件。

  11. 生成的.apk文件将放在您选择放置项目文件的同一目录中。例如,MathGameChapter2/app

我们现在已经构建了一个.apk文件,可以在我们首次创建项目时指定的任何 Android 设备上运行。

将设置安装到设备

所以我们有了.apk文件,也知道了在哪里找到它。以下是我们将在 Android 设备上运行它的方法。

我们可以使用多种方法将.apk文件传输到设备中。我发现其中一种最简单的方法是使用云存储服务,比如 Dropbox。然后您只需点击并拖动.apk文件到您的 Dropbox 文件夹,就完成了。或者,您的 Android 设备可能附带了 PC 同步软件,允许您将文件拖放到设备中。在您将.apk文件放入 Android 设备后,继续进行教程。

大多数 Android 手机默认设置为只能从 Google Play 商店以外的地方安装应用程序。所以我们需要改变这一点。您将要导航到的确切菜单在您的设备上可能略有不同,但以下选项在大多数设备上几乎是相同的,无论新旧:

  1. 找到并点击设置应用。大多数 Android 手机也有设置菜单选项。任何一个都可以。现在选择安全,并滚动到未知来源选项。点击未知来源复选框,允许从未知来源安装应用程序。

  2. 使用 Dropbox 应用或您设备的文件浏览器在您的 Android 设备上找到文件,具体取决于您选择的方法将 APK 放入设备。点击MathGameChapter2.apk文件。

  3. 现在您可以像安装其他应用程序一样安装该应用程序。在提示时,点击安装,然后点击打开。游戏现在将在您的设备上运行。

将您的设备保持竖直方向,因为这是 UI 设计的方式。恭喜您在自己的设备上运行自己的 Android 应用程序。在数学游戏的以后版本中,我们将锁定方向,使其更加用户友好。

未来的项目

在整本书中,我们将测试和运行我们的游戏项目。你可以自行选择我们讨论过的方法中的哪一种。如果出现崩溃或无法解释的错误,那么您需要使用模拟器。如果一切正常,那么最快、最令人愉悦的方式可能就是在您拥有的设备上运行它。

自测问题

Q1)如果所有这些关于生命周期、类和方法的讨论让您感到困惑,您应该怎么办?

Q2)什么是 Java 类?

Q3)方法和类之间有什么区别?

Q4)查看 Android 开发者网站,以及其更详细的生命周期阶段解释,网址为developer.android.com/reference/android/app/Activity.html。您能看到我们没有讨论过的阶段及其相关方法吗?在应用程序中它会在什么时候触发?从创建到销毁,活动的确切路径是什么?

总结

到目前为止,我们讨论了完全理解代码的工作原理并不重要。这是因为它将仅充当我们在本书其余部分中编写的代码的容器。然而,当我们在第四章中详细讨论诸如方法和第六章中的类时,我们将开始理解我们游戏中的所有代码。

我们详细讨论了有些复杂的 Android 生命周期。我们了解到,我们现阶段需要理解的是,我们必须在与生命周期不同阶段相关的正确方法中编写我们的代码。然后,学习 Java 就不会有任何困难。与类和方法一样,一切都将在实践中得到解释并变得更加清晰。

我们还学习了 Android Studio UI 的关键领域。我们使用 Android Studio 设计师构建了我们数学游戏的开始菜单。此外,我们创建了必要的 Java 代码,使游戏出现在玩家的设备上。这主要是通过修改为我们自动生成的代码来实现的。

这可能是本书中最困难的一章,因为我们需要介绍一些东西,比如 Java 类、Java 方法和 Android 生命周期。我们这样做是因为我们需要了解在学习 Java 时周围发生的事情。

然而,从现在开始,我们可以循序渐进地以非常合乎逻辑的方式进行。如果你已经达到了这一点,那么你将毫无问题地完成本书中最艰难的项目。

如果这一章让你的大脑有点疼,那么请放心,你已经走到了这一步,这非常好地表明你将来会成为 Java 高手。从基础开始,让我们现在学习一些 Java。

第三章:说 Java-你的第一个游戏

在这一章中,我们将开始编写我们自己的 Java 代码,同时开始理解 Java 语法。我们将学习如何存储、检索和操作存储在内存中的不同类型的值。我们还将研究如何根据这些数据的值做出决策和分支我们代码的流程。

按照这个顺序,我们将:

  • 学习一些 Java 语法,并看看编译器是如何将其转换为运行应用程序的

  • 存储数据并使用变量

  • 学习如何用 Java 表达自己

  • 通过提问继续数学游戏

  • 学习 Java 中的决策

  • 通过获取和检查答案继续数学游戏

获得前述的 Java 技能将使我们能够构建我们数学游戏的下两个阶段。这个游戏将能够向玩家提出一个乘法问题,检查答案,并根据给出的答案给出反馈,如下图所示:

说 Java-你的第一个游戏

Java 语法

在本书中,我们将使用简单的英语讨论一些相当技术性的事情。你永远不会被要求阅读一个之前没有以非技术方式解释过的 Java 或 Android 概念的技术解释。

偶尔,我可能会要求或暗示您接受一个简化的解释,以便在更合适的时候提供更完整的解释,比如 Java 类作为一个黑匣子;但是,你永远不需要匆忙去谷歌以理解一个大词或充满术语的句子。

话虽如此,Java 和 Android 社区充满了讲技术术语的人,要加入并从这些社区中学习,你需要理解他们使用的术语。因此,本书的方法是使用完全简单的语言学习概念或欣赏想法,同时将行话作为学习的一部分。

然后,许多行话将开始显示其有用性,通常作为澄清的一种方式,或者避免解释/讨论变得比必要的更长。

“Java 语法”这个术语本身可能被认为是技术性的或术语性的。那么它是什么?Java 语法是我们将 Java 语言元素组合在一起以便在 Java/Dalvik 虚拟机中运行的代码。语法也应尽可能清晰地呈现给人类读者,尤其是在未来重新访问我们的程序时。Java 语法是我们使用的词和将这些词组成类似句子的结构的组合。

这些 Java 元素或词汇数量众多,但分成小块学习几乎肯定比任何人类语言更容易。原因在于 Java 语言及其语法是专门设计为尽可能简单明了。我们还有 Android Studio 在我们这边,它通常会告诉我们如果我们犯了错误,有时甚至会提前思考并提示我们。

我相信如果你能阅读,你就能学会 Java;因为学习 Java 非常容易。那么,是什么让一个完成了初级 Java 课程的人与一个专业程序员分开呢?同样的东西也分开了语言学生和大师诗人。语言的掌握来自于实践和进一步的学习。

在最后一章中,我将向你展示正确的方向,如果你想自己掌握 Java。

编译器

编译器是将我们可读的 Java 代码转换为可以在虚拟机中运行的另一段代码的东西。这被称为编译。Dalvik 虚拟机将在我们的玩家点击我们应用程序图标时运行这段编译后的代码。除了编译 Java 代码,编译器还会检查错误。虽然我们的发布应用程序可能仍然存在错误,但许多错误是在编译我们的代码时发现的。

用注释清晰地编写代码

随着你在编写 Java 程序方面变得更加高级,你用来创建程序的解决方案将变得更长、更复杂。此外,正如我们将在后面的章节中看到的,Java 被设计为通过将代码分成单独的块来管理复杂性,往往跨越多个文件。

注释是 Java 程序的一部分,在程序本身中没有任何功能。编译器会忽略它们。它们用于帮助程序员记录、解释和澄清他们的代码,以便在以后更容易理解自己或其他需要使用或修改代码的程序员。

因此,一个好的代码片段将大量地添加这样的行:

//this is a comment explaining what is going on

前面的注释以两个斜杠字符//开头。注释在行末结束。这被称为单行注释。因此,该行上的任何内容只供人类阅读,而下一行上的任何内容(除非是另一个注释)都需要是语法正确的 Java 代码。

//I can write anything I like here
but this line will cause an error

我们可以使用多个单行注释:

//Below is an important note
//I am an important note
//We can have as many single line comments like this as we like

单行注释也很有用,如果我们想暂时禁用一行代码。我们可以在代码前面加上//,这样它就不会包含在程序中。回想一下这段代码,它告诉 Android 加载我们的菜单 UI:

//setContentView(R.layout.activity_main);

在前面的情况下,当运行时菜单不会被加载,应用程序将有一个空白屏幕,因为整行代码被编译器忽略。Java 中还有另一种类型的注释——多行注释。这对于较长的注释以及在代码文件顶部添加版权信息等内容非常有用。与单行注释一样,它可以用于暂时禁用代码,通常是多行代码。

在前导/*和结束*/之间的所有内容都会被编译器忽略。以下是一些例子:

/*
This program was written by a Java expert
You can tell I am good at this because my
code has so many helpful comments in it.
*/

多行注释中的行数没有限制。使用哪种类型的注释将取决于情况。在本书中,我将始终明确解释每一行代码,但你经常会在代码本身中发现大量的注释,这些注释会进一步解释、洞察或澄清。因此,阅读所有代码总是一个好主意:

/*
The winning lottery numbers for next Saturday are
9,7,12,34,29,22
But you still want to learn Java? Right?
*/

提示

所有优秀的 Java 程序员都会在他们的代码中大量使用注释。

存储数据并使用变量

我们可以将变量看作是带有标签的存储箱。它们也像是程序员对 Android 设备内存或者我们正在编程的任何设备的窗口。变量可以在内存中存储数据(存储箱),在需要时通过适当的标签进行调用或更改。

计算机内存具有高度复杂的寻址系统,幸运的是,在 Java 中我们不需要与之交互。Java 变量允许我们为程序中需要处理的所有数据编写方便的名称;JVM 将处理与操作系统交互的所有技术细节,而操作系统可能通过几层传递交互与硬件。

因此,我们可以将我们的 Android 设备内存想象成一个巨大的仓库。当我们为变量分配名称时,它们被存储在仓库中,等待我们需要它们时。当我们使用变量的名称时,设备会准确知道我们在引用什么。然后我们可以告诉它做一些事情,比如“拿出 A 箱并将其添加到 C 箱,删除 B 箱”,等等。

在游戏中,我们可能会有一个名为score的变量。正是这个score变量,我们用来管理与用户分数相关的任何事情,比如增加、减少或者只是向玩家展示分数。

可能出现以下一些情况:

  • 玩家回答正确一个问题,所以将 10 加到他们现有的score

  • 玩家查看他们的统计屏幕,所以在屏幕上打印score

  • 玩家获得了有史以来最高的分数,所以将hiScore设置为他们当前的score

这些都是相当任意的变量名称示例,只要你不使用 Java 限制的字符关键字,你实际上可以随意命名你的变量。然而,在实践中,最好采用一种命名约定,这样你的变量名称将是一致的。在本书中,我们将使用一个变量名称以小写字母开头的宽松约定。当变量的名称中有多个单词时,第二个单词将以大写字母开头。这被称为“驼峰命名法”。

以下是一些驼峰命名的示例:

  • score

  • hiScore

  • playersPersonalBest

在我们看一些带有变量的真正的 Java 代码之前,我们首先需要看一下我们可以创建和使用的变量的类型。

变量类型

可以想象,即使是一个简单的游戏可能也会有相当多的变量。在前一节中,我们介绍了hiScore变量作为一个例子。如果游戏有一个记住前 10 名玩家姓名的高分榜,那么我们可能需要为每个玩家创建变量。

那么当游戏需要知道可玩角色是死了还是活着,或者还有几条命/重试机会时呢?我们可能需要一些代码来测试生命,然后在可玩角色死亡时结束游戏并播放一个漂亮的血花动画。

计算机程序中的另一个常见要求,包括游戏,是正确或错误的计算:真或假。

为了涵盖您可能想要跟踪的这些和许多其他类型的信息,Java 有类型。有许多类型的变量,正如我们将在第六章中看到的,OOP – Using Other People's Hard Work,我们也可以发明自己的类型或使用其他人的类型。但现在,我们将看看内置的 Java 类型。公平地说,它们几乎涵盖了我们可能会遇到的每种情况。一些示例是解释这种类型的东西的最佳方式。

我们已经讨论了假设但非常可能的score变量。变量score很可能是一个数字,所以我们必须通过给分数一个适当的类型来向 Java 编译器传达这一点(分数是一个数字)。同样假设但同样可能的playerName当然将保存组成玩家姓名的字符。再往前跳几段,保存常规数字的类型称为int,保存类似姓名的数据的类型称为String。如果我们试图将一个玩家的名字,比如“Ada Lovelace”,存储在score中,而score是用于数字的,我们肯定会遇到麻烦。

编译器说不行!实际上,错误会说:

变量类型

正如我们所看到的,Java 被设计成不可能让这样的错误出现在运行的程序中。你是否也注意到在之前的截图中,我忘记了在行末加上分号?有了这个编译器来识别我们的错误,可能会出现什么问题呢?

以下是 Java 中的主要类型。稍后,我们将看到如何开始使用它们:

  • int:这种类型用于存储整数。它使用 32 位内存,因此可以存储略大于 20 亿的值,包括负值。

  • long:顾名思义,当需要更大的数字时可以使用这种数据类型。long数据类型使用 64 位内存,我们可以在这种类型中存储 2 的 63 次方。如果你想看看它是什么样子,试试这个:9,223,372,036,854,775,807。也许令人惊讶的是,long变量也有用处,但如果较小的变量可以胜任,我们应该使用它,这样我们的程序就会使用更少的内存。

注意

你可能会想知道何时会使用这么大的数字。明显的例子可能是进行复杂计算的数学或科学应用,但另一个用途可能是用于计时。当你计算某事花费的时间时,Java Date类使用自 1970 年 1 月 1 日以来的毫秒数。long数据类型可能有用,用于从开始时间减去结束时间以确定经过的时间。我们将在第五章游戏和 Java 基础中使用long

  • float:这是用于浮点数的,也就是说,小数点后有精度的数字。由于数字的小数部分占用的内存空间与整数部分一样,因此与非浮点数相比,浮点数的可能范围会减少。因此,除非我们的变量肯定会使用额外的精度,否则浮点数不会是我们的数据类型选择。

  • double:当float中的精度不够时,我们有double

  • short:当即使是int数据类型都过度时,超薄的 short 适合于最小的存储盒,但我们只能存储大约 64,000 个值,从-32,768 到 32,767。

  • byte:这是比 short 类型更小的存储盒。内存中有足够的空间,但一个字节只能存储-128 到 127 的值。

  • boolean:我们将在整本书中大量使用布尔值。布尔变量只能是真或假,没有其他选项。也许布尔值可以回答这样的问题:

  • 玩家还活着吗?

  • 是否达到了新的最高分?

  • 布尔变量的两个例子足够吗?

  • char:这存储一个单个的字母数字字符。它本身不会改变任何东西,但如果我们把它们放在一起,它可能会有用。

提示

我将这种数据类型的讨论保持在一个在本书的上下文中有用的实际水平上。如果你对数据类型的值是如何存储的以及为什么限制是什么感兴趣,请访问 Oracle Java 教程网站docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html。请注意,你不需要比我们已经讨论过的更多信息来继续阅读本书。

正如我们刚刚学到的,我们可能想要存储的每种数据类型都需要特定的内存量。因此,在我们开始使用变量之前,我们必须让 Java 编译器知道变量的类型。

前面的变量被称为原始类型。它们使用预定义的内存量,因此,使用我们的存储类比,适合于预定义大小的存储盒。

正如“原始”标签所示,它们不像引用类型那样复杂。

引用类型

你可能已经注意到,我们没有涵盖我们之前用来介绍变量概念的String变量类型。

字符串是一种特殊的变量类型,称为引用类型。它们简单地指向内存中变量存储开始的位置,但引用类型本身并不定义特定的内存量。这样做的原因非常简单:我们并不总是知道在程序实际运行之前需要存储多少数据。

我们可以将字符串和其他引用类型视为不断扩展和收缩的存储盒。那么这些String引用类型中的一个不会最终碰到另一个变量吗?如果你把设备的内存想象成一个满是标记存储盒的货架的巨大仓库,那么你可以把达尔维克虚拟机想象成一个超级高效的叉车司机,把不同类型的存储盒放在最合适的地方。

如果有必要的话,虚拟机会在一秒钟内迅速移动物品,以避免碰撞。在适当的时候,它甚至会焚毁不需要的存储盒。这与不断卸载各种类型的新存储盒并将它们放在最佳位置同时发生,对于该类型的变量。Dalvik 倾向于将引用变量保存在仓库的一个部分,这与原始变量的部分不同,我们将在第六章中了解更多细节,OOP – 使用其他人的辛勤工作

因此,字符串可以用来存储任何键盘字符,就像char数据类型,但长度几乎可以是任意的。从玩家的名字到整本书都可以存储在一个字符串中。我们将经常使用字符串,包括在本章中。

还有一些我们将要探索的引用类型。数组是一种存储大量相同类型变量的方法,可以快速高效地访问。我们将在第五章中研究数组,游戏和 Java 基础

将数组视为仓库中的一条通道,其中按照精确的顺序排列了某种类型的所有变量。数组是引用类型,因此 Dalvik 将它们保存在与字符串相同的仓库部分。

另一种引用类型是神秘的对象或类,我们将在第六章中进行讨论,OOP – 使用其他人的辛勤工作

因此,我们知道我们可能想要存储的每种数据类型都需要一定的内存。因此,在开始使用变量之前,我们必须让 Java 编译器知道变量的类型。

声明

这就够了。让我们看看我们实际上如何使用我们的变量和类型。请记住,每种原始类型都需要特定数量的真实设备内存。这是编译器需要知道变量类型的原因之一。因此,在尝试对其进行任何操作之前,我们必须首先声明变量及其类型。

要声明一个名为scoreint类型的变量,我们会这样输入:

int score;

就是这样!只需声明类型,比如int,然后留下一个空格,输入您要用于此变量的名称。还要注意分号,像往常一样在行尾,以告诉编译器我们已经完成了这一行,接下来的内容(如果有的话)不是声明的一部分。

对于几乎所有其他变量类型,声明都是以相同的方式进行的。以下是一些示例。变量名是任意的。这就像在仓库中预留一个带标签的存储盒:

long millisecondsElapsed;
float gravity;
double accurateGravity;
boolean isAlive;
char playerInitial;
String playerName;

初始化

在这里,对于每种类型,我们将一个值初始化到变量中。想象一下将一个值放入存储盒中,就像下面的代码所示:

score = 0;
millisecondsElapsed = 1406879842000;//1st Aug 2014 08:57:22
gravity = 1.256;
double accurateGravity =1.256098;
isAlive = true;
playerInitial = 'C';
playerName = "Charles Babbage";
and initialize the same variables as we did previously, but in one step each:
int score = 0;
long millisecondsElapsed = 1406879842000;//1st Aug 2014 08:57:22
float gravity = 1.256;
double accurateGravity =1.256098;
boolean isAlive = true;
char playerInitial = 'C';
String playerName = "Charles Babbage";

注意

我们是否分开声明和初始化可能取决于具体情况。重要的是我们必须两者都做:

int a;
//The line below attempts to output a to the console
Log.i("info", "int a = " + a);

上述代码将导致以下结果:

Compiler Error: Variable a might not have been initialized

这个规则有一个重要的例外。在某些情况下,变量可以有默认值。我们将在第六章中看到这一点,但是良好的做法是同时声明和初始化变量。

使用运算符更改变量

当然,在几乎任何程序中,我们都需要对这些值进行操作。以下是可能是最常见的 Java 运算符列表,它们允许我们操作变量。您不需要记住它们,因为我们第一次使用它们时会逐行查看代码:

  • 赋值运算符(=):这使得操作符左边的变量与右边的值相同。例如,hiScore = score;score = 100;

  • 加法运算符(+):这会将运算符两侧的值相加。通常与赋值运算符一起使用,例如score = aliensShot + wavesCleared;score = score + 100;。请注意,同时在运算符的两侧使用相同的变量是完全可以接受的。

  • 减法运算符(-):这会将运算符右侧的值从左侧的值中减去。通常与赋值运算符一起使用,例如lives = lives - 1;balance = income - outgoings;

  • 除法运算符(/):这会将左侧的数字除以右侧的数字。同样,通常与赋值运算符一起使用,如fairShare = numSweets / numChildren;recycledValueOfBlock = originalValue / .9;

  • 乘法运算符(*):这会将变量和数字相乘,例如answer = 10 * 10;biggerAnswer = 10 * 10 * 10;

  • 递增运算符(++):这是一个非常简洁的方法,可以将变量的值加 1。myVariable = myVariable + 1;语句与myVariable++;相同。

  • 递减运算符(--):你猜对了:这是一个非常简洁的方法,可以从某个值中减去 1。myVariable = myVariable -1;语句与myVariable--;相同。

注意

这些运算符的正式名称与此处用于解释的名称略有不同。例如,除法运算符实际上是乘法运算符之一。但是,前面的名称对于学习 Java 来说更有用,如果您在与 Java 社区的某人交谈时使用术语“除法运算符”,他们会完全明白您的意思。

实际上,Java 中有比这更多的运算符。当我们在本章后面学习 Java 中的决策时,我们将看到更多的运算符。

提示

如果您对运算符感到好奇,可以在 Java 网站上找到完整的运算符列表docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html。本书中完成项目所需的所有运算符都将在本书中得到充分解释。链接是为我们中的好奇者提供的。

用 Java 表达自己

让我们尝试使用一些声明、赋值和运算符。当我们将这些元素捆绑到一些有意义的语法中时,我们称之为表达式。因此,让我们编写一个快速应用程序来尝试一些内容。

在这里,我们将制作一个小的辅助项目,以便我们可以玩转到目前为止学到的所有内容。我们需要创建一个新项目,就像我们在上一章中所做的那样,但这次我们不需要 UI。

相反,我们将简单地编写一些 Java 代码,并通过将变量的值输出到名为logcat的 Android 控制台来检查其效果。我们将通过构建简单的项目并检查代码和控制台输出来看到这是如何工作的:

提示

以下是如何创建新项目的快速提醒。

  1. 通过导航到文件 | 关闭项目关闭当前打开的任何项目。

  2. 点击开始一个新的 Android Studio 项目

  3. 创建新项目配置窗口将出现。在应用程序名称字段和公司域中填写packtpub.com,或者您可以在此处使用您自己公司的网站名称。

  4. 现在点击下一步按钮。在下一个屏幕上,确保手机和平板电脑复选框中有一个勾选。现在我们必须选择我们想要为其构建应用程序的最早版本的 Android。随便尝试一下下拉选择器中的几个选项。您会发现我们选择的版本越早,我们的应用程序支持的设备百分比就越大。但是,这里的权衡是,我们选择的版本越早,我们的应用程序中可用的最新 Android 功能就越少。一个很好的平衡是选择API 8:Android 2.2(Froyo)

  5. 单击下一步。现在选择空白活动,然后再次单击下一步

  6. 在下一个屏幕上,只需将Activity Name更改为MainActivity,然后单击Finish

  7. 就像我们在第二章中所做的那样,开始使用 Android,为了保持我们的代码清晰简单,您可以删除两个不需要的方法(onCreateOptionsMenuonOptionsItemSelected)及其相关的@override@import语句。但是,这对于示例的工作并不是必要的。

提示

有关创建新项目的详细说明和图像,请参见第二章中的开始使用 Android

与本书中的所有示例和项目一样,您可以从下载包中复制或查看代码。您将在Chapter3/ExpressionsInJava/MainActivity.java文件中找到本教程的代码。只需按照以前描述的方式创建项目,并将下载包中的MainActivity.java文件中的代码粘贴到在 Android Studio 中创建项目时生成的MainActivity.java文件中。只需确保包名称与创建项目时选择的包名称相同。但是,我强烈建议跟着教程一起学习,这样我们就可以学会如何自己做一切。

注意

由于此应用程序使用 logcat 控制台显示其输出,因此您应该仅在模拟器上运行此应用程序,而不是在真实的 Android 设备上运行。该应用程序不会损害真实设备,但您将无法看到任何发生的事情。

  1. 创建一个名为Expressions In Java的新空白项目。

  2. 现在,在onCreate方法中,在我们使用setContentView方法的那一行之后,添加以下代码来声明和初始化一些变量:

//first we declare and initialize a few variables
int a = 10;
String b = "Alan Turing";
boolean c = true;
  1. 现在添加以下代码。这段代码简单地输出了我们的变量的值,以便我们可以在一分钟内仔细检查它们:
//Let's look at how Android 'sees' these variables
//by outputting them, one at a time to the console
Log.i("info", "a = " + a);
Log.i("info", "b = " + b);
Log.i("info", "c = " + c);
  1. 现在让我们使用加法运算符和另一个新的运算符来改变我们的变量。在查看输出和代码解释之前,看看你能否计算出变量abc的输出值:
//Now let's make some changes
a++;
a = a + 10;
b = b + " was smarter than the average bear Booboo";
b = b + a;
c = (1 + 1 == 3);//1 + 1 is definitely 2! So false.
  1. 让我们再次以与步骤 3 相同的方式输出值,但这次,输出应该是不同的:
//Now to output them all again
Log.i("info", "a = " + a);
Log.i("info", "b = " + b);
Log.i("info", "c = " + c);
  1. 以通常的方式在模拟器上运行程序。您可以通过单击Android选项卡来查看输出,该选项卡位于项目资源管理器下方的“有用选项卡”区域。

以下是输出,其中去掉了一些不必要的格式:

info﹕ a = 10
info﹕ b = Alan Turing
info﹕ c = true
info﹕ a = 21
info﹕ b = Alan Turing was smarter than the average bear Booboo21
info﹕ c = false

现在让我们讨论一下发生了什么。在第 2 步中,我们声明并初始化了三个变量:

  • a:这是一个 int,其值为 10

  • b:这是一个字符串,其中包含一位杰出的计算机科学家的名字。

  • c:这是一个布尔值,其值为 false

所以当我们在步骤 3 中输出数值时,我们得到以下结果应该不足为奇:

info﹕ a = 10
info﹕ b = Alan Turing
info﹕ c = true

在第 4 步中,所有有趣的事情发生了。我们使用增量运算符将 1 添加到我们的 int a的值,就像这样:a++;。请记住,a++a = a + 1是相同的。

然后我们将 10 添加到a。请注意,我们是在已经添加 1 之后将 10 添加到a。所以我们得到了这样的输出,进行了 10 + 1 + 10 的操作:

info﹕ a = 21

现在让我们检查一下我们的字符串b。我们似乎在我们杰出的科学家身上使用了加法运算符。正在发生的事情可能正是你猜到的。我们将两个字符串"Alan Turing""was smarter than the average bear Booboo."相加在一起时,它被称为连接+符号同时也是连接运算符。

最后,对于我们的字符串,我们似乎在其中添加了int a。这是允许的,a的值被连接到b的末尾。

info﹕ b = Alan Turing was smarter than the average bear Booboo21

注意

这种方法不适用于反向操作;你不能将一个字符串添加到一个int。这是有道理的,因为没有逻辑上的答案。

a = a + b

在 Java 中表达自己

最后,让我们看一下改变我们的布尔值c从 true 变为 false 的代码:c = (1+1=3);。在这里,我们将括号中的表达式的值赋给了c。这本来很简单,但为什么要用双等号(==)呢?我们有点超前了。双等号是另一个叫做比较运算符的操作符。

所以我们真的在问,“1+1”是否等于 3?显然答案是否定的。你可能会问,“为什么要使用==而不是=?”简单地说,这是为了让编译器清楚地知道我们是要赋值还是要比较。

提示

无意中使用=而不是==是一个非常常见的错误。

赋值运算符(=)将右边的值赋给左边的值,而比较运算符(==)比较两边的值。

当我们这样做时,编译器会用错误警告我们,但乍一看你可能会觉得编译器错了。我们将在本章和整本书中更多地了解这个比较运算符和其他运算符。

现在让我们利用我们所知道的一切,再加一点,来制作我们的数学游戏项目。

第四章:发现循环和方法

在本章中,我们将学习如何通过查看 Java 中不同类型的循环来以受控且精确的方式重复执行我们代码的部分。这些包括while循环、do-while循环和for循环。我们将学习在何种情况下使用不同类型的循环。

然后我们将简要介绍随机数的主题。我们还将看到如何使用 Java 的Random类。这显然对增强我们的数学游戏非常有帮助。

接下来,我们将看看方法。它们允许我们将代码分隔成更易管理的块。然后,我们将看到如何在方法之间共享数据,并将编程任务分解以简化问题。

然后,我们将在我们的数学游戏项目中使用我们所学到的关于循环、随机数和方法的所有知识。例如,我们将使游戏在每次尝试答案后更改问题。

我们还将添加问题难度级别和在适当难度级别内的随机问题。我们将展示并更新我们的分数。答对问题的难度级别越高,分数增加得越快。最终,即使我们中最优秀的数学家也应该被游戏打败。然而,我们中的大多数人希望能够比下面的截图所显示的更进一步。

如果玩家答错了问题,难度将回到最简单的级别,分数将变为零。这是我们完成后游戏的样子:

发现循环和方法

在本章中,我们将:

  • 了解多种类型的循环中的循环

  • 学习如何在 Java 中生成随机数

  • 学习有关 Java 方法的所有内容,包括如何编写和调用它们

  • 显著增强我们的数学游戏

使用循环进行循环

询问循环与编程有什么关系是完全合理的,但它们确实如其名称所示。它们是一种多次执行相同代码的方式,或者循环执行相同代码的一部分,但每次可能会有不同的结果。

这可能意味着做同样的事情,直到循环的代码提示循环结束。它可以在由循环代码本身指定的预定次数后提示循环。它也可以在满足预定情况或条件时提示循环结束。或者可能是这些提示循环结束的方式的组合。除了ifelseswitch,循环也是 Java控制流语句的一部分。

我们将研究 Java 提供的所有主要类型的循环,然后在研究了方法之后,我们将使用其中一些来实现对我们数学游戏的增强。让我们继续进行我们的第一种循环。

While 循环

while循环具有最简单的语法。回想一下第三章中的if语句,说 Java - 你的第一个游戏。我们可以在if语句的条件表达式中放置几乎任何组合的运算符和变量。如果表达式评估为true,则执行if块中的代码。同样,在while循环中,我们放置一个可以评估为truefalse的表达式,如下所示:

int x = 10;

while(x > 0){
  x--;
  //x decreases by one each pass through the loop
}

这里发生的是,在while循环之外,声明了一个整数x,并将其初始化为10。然后while循环开始。它的条件是x > 0,因此它将继续循环执行其主体中的代码,直到条件评估为false。因此,代码将执行 10 次。

在第一次循环中,x等于10,然后是9,然后是8,依此类推。但一旦x等于0,显然它不再大于0。因此,程序将退出while循环,并继续执行循环后的第一行代码。

就像if语句一样,while循环有可能甚至不执行一次。看一下这个永远不执行的while循环的例子:

int x = 10;

while(x > 10){
  //more code here.
  //but it will never run unless x is greater than 10.
}

此外,条件表达式的复杂程度或循环体中可以编写的代码量没有限制:

int playerLives = 3;
int alienShips = 10;

while(playerLives >0 && alienShips >0){
  //Entire game code here.
  //...
  //...
  //etc.
}
//continue here when either playerLives or alienShips = 0

前面的while循环将继续执行,直到playerLivealienShips变为等于或小于零。一旦其中一个条件发生,表达式就会评估为false,程序就会从while循环后的第一行代码继续执行。

值得注意的是,一旦进入循环体,即使表达式在中间某个地方评估为false,循环体也会始终完成,因为在代码尝试开始另一次通过之前不会再次检查条件:

int x = 1;

while(x > 0){
  x--;
  //x is now 0 so the condition is false
  //But this line still runs
  //and this one
  //and me!

}

前面的循环体将确切地执行一次。我们还可以设置一个永远运行的while循环(不出所料地称为无限循环),就像这样:

int x = 0;

while(true){
  x++; //I am going to get mighty big!
}

跳出循环

我们可以使用一个无限循环,就像前面的例子中的循环一样,这样我们就可以决定何时从循环体内退出循环。当我们准备离开循环体时,我们会使用break关键字,如下面的代码所示:

int x = 0;

while(true){
  x++; //I am going to get mighty big!
  break; //No you're not haha.
  //code doesn't reach here
}

你可能已经猜到,我们可以在while循环和我们即将看到的其他循环中结合任何决策工具,比如ifelseswitch

int x = 0;
int tooBig = 10;

while(true){
  x++; //I am going to get mighty big!
  if(x == tooBig){
    break;
  } //No you're not haha.

  //code reaches here only until x = 10
}

我们可以简单地继续演示while循环的多种用途,但是在某个时候,我们想要回到做一些真正的编程。因此,这是最后一个概念,结合了while循环。

continue 关键字

continue关键字的作用方式与break类似,但有一点不同。continue关键字将跳出循环体,但之后也会检查条件表达式,所以循环可能会再次运行。下面的例子将展示continue的用法:

int x = 0;
int tooBig = 10;
int tooBigToPrint = 5;

while(true){
  x++; //I am going to get mighty big!
  if(x == tooBig){
    break;
  } //No your not haha.

  //code reaches here only until x = 10

  if(x >= tooBigToPrint){
    //No more printing but keep looping
    continue;
  }
  //code reaches here only until x = 5

  //Print out x 

}

Do-while 循环

do-while循环与while循环非常相似,唯一的区别是它在循环体之后评估表达式。这意味着do-while循环将始终至少执行一次,如下面的代码所示:

int x= 0;
do{
  x++;
}while(x < 10);
//x now = 10 

注意

breakcontinue关键字也可以在do-while循环中使用。

For 循环

for循环的语法比whiledo-while循环稍微复杂一些,因为它需要三个部分来初始化。首先看一下下面的for循环。然后我们将它拆分开来:

for(int i = 0; i < 10; i++){
  //Something that needs to happen 10 times goes here
}

for循环的这种看似晦涩的形式在这样表述时更加清晰:

for(declaration and initialization; condition; change after each pass through loop)

为了进一步澄清,我们在for循环中有以下内容:

  • 声明和初始化:我们创建一个新的int变量i,并将其初始化为 0。

  • 条件:就像其他循环一样,这指的是必须评估为真以使循环继续的条件。

  • 每次通过循环后更改:在前面的例子中,i++表示每次通过循环时i增加 1。我们也可以使用i--来减少/递减i,如下面的代码所示:

for(int i = 10; i > 0; i--){
  //countdown
}
//blast off i = 0

注意

请注意,breakcontinue也可以在for循环中使用。

for循环本质上控制了初始化、条件评估和控制变量。在我们看完随机数和方法之后,我们将使用for循环来增强我们的数学游戏。

Java 中的随机数

在我们深入研究方法之前,我们首先来看一下如何创建随机数,因为这是我们将生成随机问题的方法。

Random类为我们完成了所有的工作。首先我们需要创建一个Random类型的对象:

Random randInt = new Random();

然后我们使用新对象的nextInt方法来生成一个在某个范围内的随机数:

int ourRandomNumber = randInt.nextInt(10);

我们输入的数字范围从零开始。因此,前面的代码将生成一个介于 0 和 9 之间的随机数。如果我们想要一个介于 1 和 10 之间的随机数,我们只需要这样做:

ourRandomNumber++;

提示

在这些早期章节中,我们经常需要接受一些魔法在 Random 等对象中发生。在第六章中,OOP-使用他人的辛勤工作,我们将打开黑匣子,甚至制作我们自己的黑匣子。我们将能够编写我们自己的类和这些类中的方法。

一个很好的开始是查看常规的基本方法,接下来我们将这样做。

数学游戏-提问

现在我们已经掌握了所有这些知识,我们可以利用它来改进我们的数学游戏。首先,我们将创建一个新的 Android 活动,作为实际的游戏屏幕,而不是开始菜单屏幕。然后,我们将使用 UI 设计师来布置一个简单的游戏屏幕,以便我们可以使用我们的 Java 技能来使用变量、类型、声明、初始化、运算符和表达式来使我们的数学游戏为玩家生成一个问题。然后,我们可以使用一个按钮将开始菜单和游戏屏幕连接在一起。

如果您想节省输入时间,只需查看完成的项目,您可以使用从 Packt Publishing 网站下载的代码。如果您在使任何代码工作时遇到任何问题,您可以查看、比较或复制并粘贴下载包中提供的已完成代码。

完成的代码在以下文件中,这些文件对应于本教程中将使用的文件名:

  • Chapter3/MathGameChapter3a/java/MainActivity.java

  • Chapter3/MathGameChapter3a/java/GameActivity.java

  • Chapter3/MathGameChapter3a/layout/activity_main.xml

  • Chapter3/MathGameChapter3a/layout/activity_game.xml

像往常一样,我建议跟着本教程来看看我们如何为自己创建所有的代码。

创建新的游戏活动

我们首先需要为游戏活动代码创建一个新的 Java 文件和一个相关的布局文件来保存游戏活动 UI。

  1. 运行 Android Studio 并选择我们在第二章中构建的Math Game Chapter 2项目,开始使用 Android。它可能已经默认打开了。现在我们将创建一个新的 Android 活动,该活动将包含实际的游戏屏幕,当玩家点击主菜单屏幕上的Play按钮时将运行。

  2. 要创建一个新的活动,我们现在需要另一个布局文件和另一个 Java 文件。幸运的是,Android Studio 会帮助我们做到这一点。要开始创建新活动所需的所有文件,请在项目资源管理器中右键单击src文件夹,然后转到New | Activity。现在点击Blank Activity,然后点击Next

  3. 现在,我们需要在上面的对话框中输入一些关于我们的新活动的信息。将Activity Name字段更改为GameActivity。注意Layout Name字段会自动更改为activity_gameTitle字段也会自动更改为GameActivity

  4. 点击Finish。Android Studio 已经为我们创建了两个文件,并且还在清单文件中注册了我们的新活动,所以我们不需要关心它。

  5. 如果您查看编辑窗口顶部的选项卡,您会看到GameActivity.java已经准备好供我们编辑,如下面的屏幕截图所示:Creating the new game activity

  6. 通过点击之前显示的GameActivity.java选项卡,确保GameActivity.java在编辑窗口中处于活动状态。

  7. 回到第二章开始使用 Android,我们谈到了 Android 默认覆盖了一些方法,其中大多数是不必要的。在这里,我们可以看到不必要的代码。如果我们删除它,那么它将使我们的工作环境更简单、更清洁。你可能还记得从第二章开始使用 Android,删除和修改代码部分的过程虽然不复杂,但是相当长。为了避免这种情况,我们将简单地使用MainActivity.java中的代码作为GameActivity.java的模板。然后我们可以进行一些小的修改。

  8. 点击编辑器窗口中的MainActivity.java选项卡。使用键盘上的Ctrl + A突出显示编辑器窗口中的所有代码。

  9. 现在使用键盘上的Ctrl + C复制编辑器窗口中的所有代码。

  10. 现在点击GameActivity.java选项卡。

  11. 使用键盘上的Ctrl + A突出显示编辑器窗口中的所有代码。

  12. 现在粘贴复制的代码,并使用键盘上的Ctrl + V覆盖当前高亮显示的代码。

  13. 注意,我们的代码中有一个错误,如下截图所示,用红色下划线表示。这是因为我们将引用MainActivity的代码粘贴到名为GameActivity的文件中。

只需将文本MainActivity更改为GameActivity,错误就会消失。在我告诉你之前,花点时间看看还需要做出什么其他小的改变。

  1. 记住,setContentView加载我们的 UI 设计。我们需要做的是将setContentView更改为加载新设计(我们将在下面构建的)而不是主屏幕设计。将setContentView(R.layout.activity_main);更改为setContentView(R.layout.activity_game);

  2. 保存你的工作,我们准备继续。

注意项目资源管理器,Android Studio 将我们创建的两个新文件放在其中。在下一个截图中,我已经突出显示了两个文件夹。将来,我将简单地称它们为我们的java代码文件夹或layout文件夹。

创建新游戏活动

注意

你可能会想为什么我们不直接复制和粘贴MainActivity.java文件,而是要经过创建新活动的过程?原因是 Android Studio 在幕后做了一些事情。首先,它为我们制作了布局模板。它还通过一个稍后会看到的名为AndroidManifest.xml的文件注册了新的活动供使用。这对于新的活动能够首先工作是必要的。综上所述,我们的做法可能是最快的。

此阶段的代码与主菜单屏幕的代码完全相同。我们声明包名并导入一些 Android 提供的有用类:

package com.packtpub.mathgamechapter3a.mathgamechapter3a;

import android.app.Activity;
import android.os.Bundle;

我们创建一个新的活动,这次叫做GameActivity

public class GameActivity extends Activity {

然后我们覆盖onCreate方法,并使用setContentView方法将我们的 UI 设计设置为玩家屏幕的内容。然而,目前,这个 UI 是空的:

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

现在我们可以考虑实际游戏屏幕的布局。

布置游戏屏幕 UI

我们知道,我们的数学游戏将提出问题,并为玩家提供一些多个选择来选择答案。我们可以添加许多额外的功能,例如难度级别、高分和更多。但现在,让我们只问一个简单的、预定义的问题,并提供三个预定义的可能答案选择。

保持 UI 设计尽可能简单暗示着布局。我们的目标 UI 将看起来有点像这样:

布置游戏屏幕 UI

布局希望是不言自明的,但让我们确保我们真的清楚;当我们在 Android Studio 中构建这个布局时,显示2 x 2的部分是问题,将由三个文本视图组成(两个数字和=号也是一个单独的视图)。最后,答案的三个选项由按钮布局元素组成。我们在上一章中使用了所有这些 UI 元素,但这一次,因为我们将使用我们的 Java 代码来控制它们,我们需要对它们做一些额外的事情。因此,让我们一步一步地进行:

  1. 在编辑器窗口中打开将保存我们游戏 UI 的文件。通过双击activity_game.xml来执行此操作。这个文件位于项目资源管理器中的 UI layout文件夹中。

  2. 删除Hello World TextView,因为它不是必需的。

  3. 在调色板中找到大文本元素。它可以在小部件部分下找到。将三个元素拖放到 UI 设计区域并排列在设计的顶部附近,如下一张截图所示。它不必完全相同;只需确保它们在一行上,不重叠,如下一张截图所示:布局游戏屏幕 UI

  4. 请注意,在组件树窗口中,每个三个 TextView 都已被 Android Studio 自动分配了一个名称。它们是textViewtextView2textView3布局游戏屏幕 UI

  5. Android Studio 将这些元素名称称为id。这是一个重要的概念,我们将会用到。因此,为了确认这一点,通过单击组件树中的任何一个 textView 的名称(id)或直接在 UI 设计器中单击它,查看属性窗口并找到id属性。您可能需要滚动一下来做到这一点:布局游戏屏幕 UI

请注意,id属性的值是textView。正是这个id,我们将用它来与我们的 Java 代码交互。因此,我们希望将所有 TextView 的 ID 更改为一些有用且易于记忆的内容。

  1. 如果您回顾我们的设计,您会发现具有textView id 的 UI 元素将保存我们数学问题的第一部分的数字。因此,将 id 更改为textPartA。注意text中的小写tPart中的大写P和大写A。您可以使用任何大小写组合,实际上可以将 ID 命名为任何您喜欢的名称。但是,就像 Java 变量的命名约定一样,在这里遵循约定将使得当我们的程序变得更加复杂时,减少错误的可能性。

  2. 现在选择textView2并将id更改为textOperator

  3. 选择当前具有 id textView3的元素并将其更改为textPartB。这个 TextView 将保存我们问题的后半部分。

  4. 现在从调色板中添加另一个大文本。将其放在我们刚刚编辑的三个 TextView 的行后面。

这个大文本将简单地保存我们的等于号,而且没有计划去改变它。因此,我们不需要在我们的 Java 代码中与它交互。我们甚至不需要关心更改 ID 或知道它是什么。如果情况改变,我们随时可以在以后的时间回来编辑它的 ID。

  1. 然而,这个新的 TextView 目前显示大文本,我们希望它显示一个等于号。因此,在属性窗口中,找到text属性,并输入值=。我们之前在第二章中更改了text属性,开始使用 Android,你可能也想更改textPartAtextPartBtextOperator的文本属性。这并非绝对必要,因为我们很快就会看到如何通过我们的 Java 代码来更改它;然而,如果我们将text属性更改为更合适的内容,那么我们的 UI 设计师看起来将更像在真实设备上运行游戏时的样子。

  2. 所以将textPartA的文本属性更改为2textPartB更改为2textOperator更改为x。现在你的 UI 设计和组件树应该是这样的:布局游戏屏幕 UI

  3. 为了包含我们的多项选择答案,将三个按钮依次拖到=符号下面。将它们整齐地排列,就像我们的目标设计一样。

  4. 现在,就像我们为 TextViews 所做的那样,找到每个按钮的id属性,并从左到右,将id属性更改为buttonChoice1buttonChoice2buttonChoice3

  5. 为什么不为每个按钮的text属性输入一些任意的数字,以便设计更准确地反映我们的游戏将是什么样子,就像我们为其他 TextView 所做的那样?同样,这并非绝对必要,因为我们的 Java 代码将控制按钮的外观。

  6. 我们现在实际上已经准备好继续了。但你可能会同意 UI 元素看起来有点迷失。如果按钮和文本更大会更好看。我们只需要调整每个 TextView 和每个 Button 的 textSize 属性。然后,我们只需要找到每个元素的 textSize 属性,并输入带有 sp 语法的数字。如果你希望你的设计看起来就像我们之前的目标设计一样,那么为每个 TextView 的 textSize 属性输入70sp,为每个按钮的 textSize 属性输入40sp。当你在真实设备上运行游戏时,你可能需要回来调整一下大小。但在我们实际尝试游戏之前,我们还有更多的工作要做。

  7. 保存项目,然后我们可以继续。

与以前一样,我们已经构建了我们的用户界面。然而,这一次,我们为 UI 的所有重要部分提供了一个独特的、有用的、易于识别的 ID。正如我们将看到的,我们现在能够通过我们的 Java 代码与我们的 UI 进行通信。

在 Java 中编写问题

凭借我们目前对 Java 的了解,我们还不能完成我们的数学游戏,但我们可以有一个重要的开始。我们将看看如何向玩家提问并提供一些多项选择答案(一个正确答案和两个错误答案)。

在这个阶段,我们已经了解了足够的 Java 知识,可以声明和初始化一些变量来存储我们问题的各个部分。例如,如果我们想要问乘法表问题2 x 2,我们可以有以下变量初始化来存储问题的每个部分的值:

int partA = 2;
int partB = 2;

上面的代码声明并初始化了两个int类型的变量,每个变量的值都为 2。我们使用int,因为我们不会处理任何小数。请记住,变量名是任意的,只是因为它们看起来合适才选择的。显然,任何值得下载的数学游戏都需要提出比2 x 2更多样化和高级的问题,但这是一个开始。

现在我们知道我们的数学游戏将提供多个选择作为答案。因此,我们需要一个变量来存储正确答案,以及两个变量来存储两个错误答案。看一下这些组合声明和初始化:

int correctAnswer = partA * partB;
int wrongAnswer1 = correctAnswer - 1;
int wrongAnswer2 = correctAnswer + 1; 

请注意,错误答案的变量初始化取决于正确答案的值,并且在初始化correctAnswer变量之后初始化错误答案的变量。

现在我们需要将我们变量中保存的这些值放入我们 UI 上的适当元素中。问题变量(partApartB)需要显示在我们的 UI 元素textPartAtextPartB中,答案变量(correctAnswerwrongAnswer1wrongAnswer2)需要显示在具有以下 ID 的 UI 元素中:buttonChoice1buttonChoice2buttonChoice3。我们将在下一个逐步教程中看到如何做到这一点。我们还将实现我们刚才讨论过的变量声明和初始化代码:

  1. 首先,在编辑窗口中打开GameActivity.java。请记住,您可以通过双击java文件夹中的GameActivity或单击编辑窗口上方的选项卡来执行此操作,如果GameActivity.java已经打开。

  2. 我们所有的代码都将放在onCreate方法中。它将放在setContentView(R.layout.activity_game);行之后,但在onCreate方法的结束大括号}之前。也许,为了清晰起见,最好留一行空白,并添加一个好的解释性注释,如下面的代码所示。我们可以看到整个onCreate方法在最新的修改之后的样子。粗体部分是你需要添加的部分。如果愿意,可以像我一样添加有用的注释:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //The next line loads our UI design to the screen
        setContentView(R.layout.activity_game);

 //Here we initialize all our variables
 int partA = 9;
 int partB = 9;
 int correctAnswer = partA * partB;
 int wrongAnswer1 = correctAnswer - 1;
 int wrongAnswer2 = correctAnswer + 1;

    }//onCreate ends here
  1. 现在我们需要将变量中包含的值添加到我们 UI 的TextViewButton中。但首先,我们需要访问我们创建的 UI 元素。我们通过创建适当类的变量并通过适当 UI 元素的 ID 属性进行链接来实现这一点。我们已经知道我们 UI 元素的类:TextViewButton。以下是为每个必要的 UI 元素创建我们特殊类变量的代码。仔细看看这段代码,但如果现在不理解所有内容也不要担心。一切正常后,我们将详细解析代码。在上一步输入的代码之后立即输入这段代码。如果愿意,可以留一行空白以便清晰。在继续之前,请注意,在键入此代码时,有两个地方会提示您导入另一个类。在这两种情况下都可以继续操作:
/*Here we get a working object based on either the button
or TextView class and base as well as link our new objects directly to the appropriate UI elements that we created previously*/

TextView textObjectPartA = (TextView)findViewById(R.id.textPartA);

TextView textObjectPartB = (TextView)findViewById(R.id.textPartB);

Button buttonObjectChoice1 = (Button)findViewById(R.id.buttonChoice1);

Button buttonObjectChoice2 = (Button)findViewById(R.id.buttonChoice2);

Button buttonObjectChoice3 = (Button)findViewById(R.id.buttonChoice3);

注意

在上述代码中,如果您阅读多行注释,您会看到我使用了术语对象。当我们基于类创建变量类型时,我们称之为对象。一旦我们有了类的对象,我们就可以做任何该类设计的事情。这是非常强大的,并且在第六章中得到了充分探讨,OOP – Using Other People's Hard Work

  1. 现在我们有五个新对象与我们需要操作的 UI 元素相关联。我们要用它们做什么?我们需要在 UI 元素的文本中显示变量的值。我们可以使用我们刚刚创建的对象,结合类提供的方法,并将我们的变量作为文本的值。像往常一样,我们将在本教程结束时进一步解析这段代码。以下是要直接输入到上一步代码之后的代码。在我们一起查看之前,试着弄清楚发生了什么:
//Now we use the setText method of the class on our objects
//to show our variable values on the UI elements.
//Just like when we output to the console in the exercise -
//Expressions in Java, only now we use setText method
//to put the values in our variables onto the actual UI.
textObjectPartA.setText("" + partA);
textObjectPartB.setText("" + partB);

//which button receives which answer, at this stage is arbitrary.

buttonObjectChoice1.setText("" + correctAnswer);
buttonObjectChoice2.setText("" + wrongAnswer1);
buttonObjectChoice3.setText("" + wrongAnswer2);
  1. 保存您的工作。

如果您玩转partApartB的赋值,您可以随意设置它们,游戏会相应地调整答案。显然,我们不应该每次想要一个新问题时都需要重新编写我们的游戏,我们很快就会解决这个问题。现在我们需要做的就是将我们刚刚制作的游戏部分与开始屏幕菜单连接起来。我们将在下一个教程中完成这一点。

现在让我们更详细地探讨我们代码中更棘手和更新的部分。

在第 2 步中,我们声明并初始化了到目前为止所需的变量:

//Here we initialize all our variables
int partA = 2;
int partB = 2;
int correctAnswer = partA * partB;
int wrongAnswer1 = correctAnswer - 1;
int wrongAnswer2 = correctAnswer + 1;

然后在第 3 步,我们通过 Java 代码获得了对我们 UI 设计的引用。对于 TextView,是这样完成的:

TextView textObjectPartA = (TextView)findViewById(R.id.textPartA);

对于每个按钮,获得对我们 UI 设计的引用是这样完成的:

Button buttonObjectChoice1 = Button)findViewById(R.id.buttonChoice1);

在第 4 步中,我们做了一些新的事情。我们使用setText方法在 UI 元素(TextViewButton)上向玩家显示我们的变量的值。让我们完全分解一行,看看它是如何工作的。这是显示correctAnswer变量显示在buttonObjectChoice1上的代码。

buttonObjectChoice1.setText("" + correctAnswer);

通过输入buttonObjectChoice1并添加一个句点,如下面的代码行所示,我们可以访问 Android 提供的该对象类类型的所有预编程方法:

buttonObjectChoice1.

提示

按钮和 Android API 的力量

实际上,我们可以在 Button 类型的对象上执行许多方法。如果您感到勇敢,尝试一下,看看 Android 有多少功能。

输入以下代码:

buttonObjectChoice1.

确保在末尾输入句点。Android Studio 将弹出一个可能要在此对象上使用的方法列表。浏览列表,了解选项的数量和多样性:

在 Java 中编写问题

如果一个简单的按钮可以做到这一切,想象一下一旦我们掌握了 Android 中包含的所有类,我们的游戏的可能性。一个供他人使用的类集合被称为应用程序员接口API)。欢迎来到 Android API!

在这种情况下,我们只想设置按钮的文本。因此,我们使用setText并将存储在我们的correctAnswer变量中的值连接到空字符串的末尾,就像这样:

setText("" + correctAnswer);

我们为每个需要显示我们的变量的 UI 元素执行此操作。

提示

使用自动完成玩耍

如果您尝试了前面的提示,按钮和 Android API 的力量,并探索了 Button 类型对象可用的方法,您可能已经对自动完成有所了解。请注意,当您输入时,Android Studio 会不断地为您提供建议。如果您留意这一点,您可以节省很多时间。只需选择建议的正确代码完成语句并按Enter。您甚至可以通过选择帮助 | 生产力指南来查看您节省了多少时间。在这里,您将看到有关代码完成和更多内容的统计数据。以下是我的一些条目:

在 Java 中编写问题

正如您所看到的,如果您早期习惯使用快捷方式,您可以从长远来看节省很多时间。

从主菜单链接我们的游戏

目前,如果我们运行应用程序,玩家实际上没有办法到达我们的新游戏活动。我们希望当玩家点击主MainActivity UI 上的Play按钮时,游戏活动会运行。这是我们需要做的:

  1. 打开文件activity_main.xml,可以通过在项目资源管理器中双击它或者在编辑窗口中点击它的选项卡来打开。

  2. 现在,就像我们构建游戏 UI 时所做的那样,为Play按钮分配一个 ID。作为提醒,在 UI 设计或组件树中点击Play按钮。在属性窗口中找到id属性。将buttonPlay值分配给它。现在我们可以通过在我们的 Java 代码中引用它来使这个按钮做一些事情。

  3. 打开文件MainActivity.java,可以通过在项目资源管理器中双击它或者在编辑窗口中点击它的选项卡来打开。

  4. 在我们的onCreate方法中,在我们setContentView的行后面,添加以下突出显示的代码行:

setContentView(R.layout.activity_main);
Button buttonPlay = (Button)findViewById(R.id.buttonPlay);

  1. 一旦我们让它工作起来,我们将详细解剖这段代码。基本上,我们正在通过创建对Button对象的引用变量来连接到Play按钮。注意到两个词都是红色高亮显示的,表示有错误。就像之前一样,我们需要导入 Button 类来使这段代码工作。使用Alt + Enter键盘组合。现在点击弹出的选项列表中的Import class。这将自动在我们的MainActivity.java文件顶部添加所需的导入指令。

  2. 现在是新的内容。我们将使按钮能够监听用户点击它。在我们输入的最后一行代码之后立即输入以下内容:

buttonPlay.setOnClickListener(this);
  1. 注意到this关键字是红色高亮显示的,表示有一个错误。这引入了另一个 Java 特性,将在第六章中更加深入地探讨,OOP – Using Other People's Hard Work。暂且不谈这个,我们现在需要对我们的代码进行修改,以允许使用一个特殊的代码元素,即接口,它允许我们添加功能,比如监听按钮点击。编辑以下行。在提示导入另一个类时,点击OK
public class MainActivity extends Activity {

to

public class MainActivity extends Activity implements View.OnClickListener{

现在我们已经用红色下划线标出了整行。这表示了一个错误,但这是我们目前应该在的位置。我们提到通过添加implements View.OnClickListener,我们已经实现了一个接口。我们可以把它想象成一个我们可以使用的类,但有额外的规则。OnClickListener接口的规则规定我们必须实现/使用它的一个方法。注意到到目前为止,我们已经根据需要选择性地重写/使用方法。如果我们希望使用这个接口提供的功能,即监听按钮按下,那么我们必须添加/实现onClick方法。

  1. 这就是我们的做法。注意到开放的大括号{和闭合的大括号}。这表示方法的开始和结束。注意到这个方法是空的,它什么也不做,但一个空的方法足以符合OnClickListener接口的规则,红线表示我们的代码有错误已经消失。我们一直在使用的这些方法的语法,将在下一章中承诺时解释。确保你在onCreate方法的闭合大括号(})之外但在MainActivity类的闭合大括号之内输入以下代码:
@Override
    public void onClick(View view) {

    }
  1. 注意到onClick方法的{}之间有一行空白。我们现在可以在这里添加代码,使按钮实际上做一些事情。在onClick{}之间输入以下高亮显示的代码:
@Override
    public void onClick(View view) {
        Intent i;
 i = new Intent(this, GameActivity.class);
 startActivity(i);
    }
  1. 好的,这段代码一次理解起来有点费劲。看看你能否猜到发生了什么。提示在名为startActivity的方法和熟悉的术语GameActivity中。注意到我们正在给i赋值。我们将快速让我们的应用程序工作,然后全面诊断代码。当我们探索类在第六章中的工作方式时,我们的理解将是完整的,OOP – Using Other People's Hard Work

  2. 注意到我们有一个错误:所有Intent一词的实例都是红色的。我们可以通过导入所需的类来解决这个问题,使Intent工作。和之前一样,按下Alt + Enter

  3. 在模拟器或设备上运行游戏。

我们的应用现在将工作。这是在菜单屏幕上按下Play后新游戏屏幕的样子:

从主菜单链接到我们的游戏

我们代码的几乎每个部分都有一些变化,我们也添加了很多内容。让我们逐行查看MainActivity.java的内容。为了上下文,这是完整的代码:

package com.packtpub.mathgamechapter3a.mathgamechapter3a;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends Activity implements View.OnClickListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final Button buttonPlay = (Button)findViewById(R.id.buttonPlay);
        buttonPlay.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        Intent i;
        i = new Intent(this, GameActivity.class);
        startActivity(i);
    }

}

我们之前已经看到了大部分这段代码,但在继续之前,让我们一块一块地复习一下,以确保它绝对清晰。代码的工作方式如下:

package com.packtpub.mathgamechapter3a.mathgamechapter3a;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

你可能还记得,这个代码块定义了我们的包叫什么,并且使得所有 Android API 的东西对 Button、TextView 和 Activity 都是可用的。

从我们的MainActivity.java文件中,我们有这样的内容:

public class MainActivity extends Activity implements View.OnClickListener{

我们的MainActivity声明与我们的新代码一起实现了View.OnClickListener,这使我们能够检测按钮点击。

我们代码中的下一步是这样的:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); 

自第二章以来,这段代码就没有改变过,开始使用 Android。它位于我们的onCreate方法的开头,我们首先要求onCreate的隐藏代码使用super.onCreate(savedInstanceState);来执行其操作。然后我们使用setContentView(R.layout.activity_main);将我们的 UI 设置到屏幕上。

接下来,我们获取了一个 ID 为buttonPlay的按钮的引用:

Button buttonPlay = (Button)findViewById(R.id.buttonPlay);
buttonPlay.setOnClickListener(this);

最后,我们的onClick方法使用Intent类将玩家发送到我们的GameActivity类和相关的 UI,当用户点击Play按钮时:

    @Override
    public void onClick(View view) {
        Intent i;
        i = new Intent(this, GameActivity.class);
        startActivity(i);
    }

如果你运行这个应用,你会注意到我们现在可以点击Play按钮,我们的数学游戏会问我们一个问题。当然,我们现在还不能回答。虽然我们已经非常简要地看过如何处理按钮按下,但我们需要学习更多的 Java 知识才能对其做出智能反应。我们还将揭示如何编写代码来处理来自多个按钮的按下。这对于从我们的多选中心的game_activity UI 接收输入是必要的。

在 Java 中做决定

我们现在可以召唤足够的 Java 技能来提问,但一个真正的数学游戏显然需要做的远不止这些。我们需要捕获玩家的答案,我们几乎已经做到了——我们可以检测按钮按下。然后,我们需要能够决定他们的答案是对还是错。然后,根据这个决定,我们必须选择适当的行动方针。

暂时把数学游戏放在一边,看看 Java 如何帮助我们学习一些更基本的 Java 语言的基础知识和语法。

更多的运算符

让我们再看看一些运算符:我们已经可以使用运算符进行加法(+)、减法(-)、乘法(*)、除法(/)、赋值(=)、递增(++)、比较(==)和递减(--)。让我们介绍一些更加超级有用的运算符,然后我们将直接理解如何在 Java 中使用它们。

提示

不要担心记住这里给出的每个运算符。浏览它们和它们的解释,然后快速转到下一节。在那里,我们将使用一些运算符,当我们看到它们允许我们做些什么的几个例子时,它们将变得更加清晰。它们在这里以列表的形式呈现,只是为了从一开始就清楚地展示运算符的种类和范围。当不与随后的实现讨论混在一起时,这个列表也更方便作为参考。

  • ==:这是一个我们之前非常简要看到的比较运算符。它用于测试相等,结果要么是真要么是假。例如,表达式(10 == 9);是假的。

  • !:逻辑非运算符。表达式! (2+2==5).)是真的,因为 2+2 不等于 5。

  • !=:这是另一个比较运算符,用于测试某物是否不相等。例如,表达式(10 != 9);)是真的,也就是说,10 不等于 9。

  • >:这是另一个比较运算符,用于测试某物是否大于另一物。表达式(10 > 9);)是真的。还有一些其他的比较运算符。

  • <:你猜对了。这个测试左边的值是否小于右边的值。表达式(10 < 9);是假的。

  • >=:这个运算符测试一个值是否大于或等于另一个值,如果其中一个条件成立,结果就是真。例如,表达式(10 >= 9);是真的。表达式(10 >= 10);也是真的。

  • <=:与前一个运算符类似,这个运算符测试两个条件,这次是小于或等于。表达式(10 <= 9);为假。表达式(10 <= 10);为真。

  • &&:这个运算符称为逻辑与。它测试表达式的两个或多个独立部分,所有部分必须为真才能使结果为真。逻辑与通常与其他运算符一起使用,以构建更复杂的测试。表达式((10 > 9) && (10 < 11));是真的,因为两部分都为真。表达式((10 > 9) && (10 < 9));是假的,因为表达式的一部分为真,另一部分为假。

  • ||:这个运算符称为逻辑或。它与逻辑与类似,只是表达式中的两个或多个部分中只有一个为真,表达式才为真。让我们看看我们之前使用的最后一个例子,但是用||代替&&。表达式((10 > 9) || (10 < 9));现在为真,因为表达式的一部分为真。

所有这些运算符如果没有正确使用它们来影响真实变量和代码的决策,就几乎没有用。让我们看看如何在 Java 中做出决策。

决策 1 - 如果他们过桥,就射击他们

正如我们所看到的,运算符单独使用几乎没有任何意义,但是可能有用的是看到我们可以使用的广泛和多样的范围的一部分。现在,当我们开始使用最常见的运算符==时,我们可以开始看到运算符提供给我们的强大而精细的控制。

让我们使用 Java 的if关键字和一些条件运算符以及一个有趣的故事和一些代码来使之前的例子不那么抽象。

船长垂危,知道剩下的部下经验不足,他决定在自己死后写一个 Java 程序来传达他的最后命令。部队必须在等待增援的同时守住桥的一侧。

船长想要确保他的部队理解的第一个命令是:如果他们过桥,就射击他们。

那么我们如何在 Java 中模拟这种情况呢?我们需要一个布尔变量isComingOverBridge。下一段代码假设isComingOverBridge变量已经被声明和初始化。

我们可以这样使用它:

if(isComingOverBridge){
  //Shoot them
}

如果isComingOverBridge布尔值为真,则大括号内的代码将运行。如果不是,则程序在if块之后继续运行而不运行它。

决策 2 - 否则,执行这个操作

船长还想告诉他的部队,如果敌人没有过桥,他们应该待在原地。

现在我们介绍另一个 Java 关键字,else。当我们想要明确执行某些操作,而if块的条件不成立时,我们可以使用else

例如,如果敌人没有过桥,我们使用else命令部队留在原地:

if(isComingOverBridge){
  //Shoot them
}else{
  //Hold position
}

然后船长意识到问题并不像他最初想的那么简单。如果敌人过桥并且有更多的部队怎么办?他的小队将被压垮。因此,他想出了这段代码(这次我们也将使用一些变量):

boolean isComingOverTheBridge;
int enemyTroops;
int friendlyTroops;
//Code that initializes the above variables one way or another

//Now the if
if(isComingOverTheBridge && friendlyTroops > enemyTroops){
  //shoot them
}else if(isComingOverTheBridge && friendlyTroops < enemyTroops) {
  //blow the bridge
}else{
  //Hold position
}

最后,船长最后的担心是,如果敌人拿着白旗过桥投降,然后被立即屠杀,那么他的部队将成为战争罪犯。所需的 Java 代码是显而易见的。使用wavingWhiteFlag布尔变量,他写下了这个测试:

if (wavingWhiteFlag){
  //Take prisoners
}

但是在哪里放置这段代码并不太清楚。最后,船长选择了以下嵌套解决方案,并将wavingWhiteFlag的测试更改为逻辑非,就像这样:

if (!wavingWhiteFlag){//not surrendering so check everything else
  if(isComingOverTheBridge && friendlyTroops > enemyTroops){
    //shoot them
  }else if(isComingOverTheBridge && friendlyTroops < enemyTroops) {
    //blow the bridge
  }
}else{//this is the else for our first if
  //Take prisoners
{
//Holding position

这表明我们可以嵌套ifelse语句以创建更深层次的决策。

我们可以继续做出更多更复杂的决定,但我们已经看到的已经足够作为介绍。如果有任何不清楚的地方,请花时间重新阅读。谁知道,也许在本章末尾的自测中甚至会有一个棘手的逻辑问题。还要指出的是,很多时候,有两种或更多种方法可以得出解决方案。正确的方法通常是以最清晰和最简单的方式解决问题的方法。

切换以做出决策

我们已经看到了将 Java 运算符与ifelse语句相结合的广阔而几乎无限的可能性。但有时,在 Java 中做出决定可能有其他更好的方法。

当我们必须根据一个清晰的可能性列表做出决定,而不涉及复杂的组合时,switch通常是最好的选择。

我们开始一个switch决定就像这样:

switch(argument){

}

在前面的例子中,参数可以是表达式或变量。然后在大括号内,我们可以根据参数使用 case 和 break 元素做出决定:

case x:
  //code to for x
  break;

case y:
  //code for y
  break;

您可以看到在前面的例子中,每个 case 都陈述了可能的结果,每个 break 都表示该 case 的结束以及不再评估进一步 case 语句的点。遇到的第一个 break 将我们带出 switch 块,继续执行下一行代码。

我们还可以使用没有值的default来运行一些代码,如果没有一个 case 语句计算为 true,就像这样:

default://Look no value
  //Do something here if no other case statements are true
break;

假设我们正在编写一个老式的文本冒险游戏-玩家输入命令,比如“向东走”,“向西走”,“拿剑”等。在这种情况下,switch 可以像这个例子代码一样处理这种情况,我们可以使用default来处理玩家输入的未经特别处理的命令的情况:

//get input from user in a String variable called command
switch(command){

  case "Go East":":
  //code to go east
  break;

  case "Go West":
  //code to go west
  break;
  case "Take sword":
  //code to take the sword
  break;

  //more possible cases

  default:
  //Sorry I don't understand your command
  break;

}

在接下来的部分中,我们将使用switch,这样我们的onClick方法就可以处理数学游戏中不同的多选按钮。

提示

Java 甚至有比我们在这里介绍的更多的运算符。我们已经看过了本书中需要的所有运算符,也可能是一般情况下使用最多的运算符。如果您想了解有关运算符的完整信息,请查看官方 Java 文档docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html

数学游戏-获取和检查答案

在这里,我们将检测正确或错误的答案,并向玩家提供弹出消息。我们的 Java 现在已经相当不错了,所以让我们深入研究并添加这些功能。我会在我们进行的过程中解释事情,然后像往常一样,在最后彻底解剖代码。

已经完成的代码在下载包中,对应于我们将在 Android Studio 中创建/自动生成的文件名:

  • Chapter3/MathGameChapter3b/java/MainActivity.java

  • Chapter3/MathGameChapter3b/java/GameActivity.java

  • Chapter3/MathGameChapter3b/layout/activity_main.xml

  • Chapter3/MathGameChapter3b/layout/activity_game.xml

像往常一样,我建议按步骤跟着本教程,看看我们如何为自己创建所有的代码。

  1. 打开编辑器窗口中可见的GameActivity.java文件。

  2. 现在我们需要将点击检测功能添加到我们的GameActivity中,就像我们为MainActivity做的那样。但是,这次我们会比上次更进一步。所以让我们一步一步地做,就像它是全新的一样。再一次,我们将让按钮有能力监听用户点击它们。在我们输入onCreate方法中的最后一行代码之后,但在}之前立即输入这个。当然,这次我们需要添加一些代码来监听三个按钮:

buttonObjectChoice1.setOnClickListener(this);
buttonObjectChoice2.setOnClickListener(this);
buttonObjectChoice3.setOnClickListener(this);
  1. 注意this关键字被标记为红色,表示错误。同样,我们需要对代码进行修改,以便允许使用接口,这是一种特殊的代码元素,允许我们添加诸如监听按钮点击之类的功能。编辑以下行。在提示要导入另一个类时,点击OK。考虑以下代码行:
public class GameActivity extends Activity {

将其更改为以下行:

public class GameActivity extends Activity implements View.OnClickListener{
  1. 现在我们在整个前一行下划线标记为红色。这表示错误,但这是我们目前应该在的位置。我们提到通过添加implements View.OnClickListener,我们已经实现了一个接口。我们可以将其视为一个我们可以使用的类,但带有额外的规则。OnClickListener接口的规则之一是我们必须实现其方法之一,您可能还记得。现在我们将添加onClick方法。

  2. 输入以下代码。注意开头的大括号{和结尾的大括号}。这些表示方法的开始和结束。注意方法是空的;它什么也不做,但是一个空方法足以符合OnClickListener接口的规则,指示错误的红线已经消失。确保您在onCreate方法的结束大括号(})之外输入以下代码,但在我们的MainActivity类的结束大括号内部:

@Override
    public void onClick(View view) {

    }
  1. 注意我们的onClick方法的{}括号之间有一行空行。现在我们可以在这里放一些代码,使按钮实际上做一些事情。在onClick{}之间输入以下内容。这是与我们在MainActivity中的代码不同的地方。我们需要区分可能被按下的三个按钮。我们将使用我们之前讨论过的switch语句来做到这一点。看一下case条件;它们应该看起来很熟悉。以下是使用switch语句的代码:
switch (view.getId()) {

            case R.id.buttonChoice1:
            //button 1 stuff goes here
                break;

            case R.id.buttonChoice2:
            //button 2 stuff goes here
                break;

            case R.id.buttonChoice3:
           //button 3 stuff goes here
                break;

        }
  1. 每个case元素处理不同的按钮。对于每个按钮情况,我们需要获取刚刚按下的按钮中存储的值,并查看它是否与我们的correctAnswer变量匹配。如果匹配,我们必须告诉玩家他们答对了,如果不匹配,我们必须告诉他们答错了。然而,我们仍然有一个问题需要解决。onClick方法是单独的,与onCreate方法和 Button 对象分开。事实上,所有变量都是在onCreate方法中声明的。如果现在尝试输入第 9 步的代码,将会出现很多错误。我们需要使onClick中需要的所有变量在onClick中可用。为此,我们将它们的声明从onCreate方法上方移动到GameActivity的开头{下方。这意味着这些变量成为GameActivity类的变量,并且可以在GameActivity中的任何地方看到。像这样声明以下变量:
int correctAnswer;
Button buttonObjectChoice1;
Button buttonObjectChoice2;
Button buttonObjectChoice3;
  1. 现在更改onCreate中这些变量的初始化如下。需要更改的实际代码部分已经突出显示。其余部分仅用于上下文显示:
//Here we initialize all our variables
int partA = 9;
int partB = 9;
correctAnswer = partA * partB;
int wrongAnswer1 = correctAnswer - 1;
int wrongAnswer2 = correctAnswer + 1;

TextView textObjectPartA = (TextView)findViewById(R.id.textPartA);

TextView textObjectPartB = (TextView)findViewById(R.id.textPartB);

buttonObjectChoice1 = (Button)findViewById(R.id.buttonChoice1);

buttonObjectChoice2 = (Button)findViewById(R.id.buttonChoice2);

buttonObjectChoice3 = (Button)findViewById(R.id.buttonChoice3);

  1. 这是我们的onClick方法的顶部以及我们的onClick方法的第一个case语句:
@Override
    public void onClick(View view) {
        //declare a new int to be used in all the cases
        int answerGiven=0;
        switch (view.getId()) {

            case R.id.buttonChoice1:
            //initialize a new int with the value contained in buttonObjectChoice1
            //Remember we put it there ourselves previously
                answerGiven = Integer.parseInt("" + buttonObjectChoice1.getText());

                //is it the right answer?
                if(answerGiven==correctAnswer) {//yay it's the right answer
                    Toast.makeText(getApplicationContext(), "Well done!", Toast.LENGTH_LONG).show();
                }else{//uh oh!
                    Toast.makeText(getApplicationContext(),"Sorry that's wrong", Toast.LENGTH_LONG).show();
                }
                break;
  1. 以下是执行与上一步中的代码相同步骤的其余case语句,除了处理最后两个按钮。在上一步输入的代码之后输入以下代码:
            case R.id.buttonChoice2:
                //same as previous case but using the next button
                answerGiven = Integer.parseInt("" + buttonObjectChoice2.getText());
                if(answerGiven==correctAnswer) {
                    Toast.makeText(getApplicationContext(), "Well done!", Toast.LENGTH_LONG).show();
                }else{
                    Toast.makeText(getApplicationContext(),"Sorry that's wrong", Toast.LENGTH_LONG).show();
                }
                break;

            case R.id.buttonChoice3:
                //same as previous case but using the next button
                answerGiven = Integer.parseInt("" + buttonObjectChoice3.getText());
                if(answerGiven==correctAnswer) {
                    Toast.makeText(getApplicationContext(), "Well done!", Toast.LENGTH_LONG).show();
                }else{
                    Toast.makeText(getApplicationContext(),"Sorry that's wrong", Toast.LENGTH_LONG).show();
                }
                break;

        }
  1. 运行程序,然后我们将仔细查看代码,特别是那个看起来奇怪的Toast东西。当我们点击最左边的按钮时会发生什么:数学游戏-获取和检查答案

这就是我们做的:在步骤 1 到 6 中,我们设置了处理多选按钮的方法,包括使用onClick方法和switch块来处理根据按下的按钮做出的决定的能力。

在第 7 和第 8 步中,我们不得不修改我们的代码,以使我们的变量在onClick方法中可用。我们通过将它们作为GameActivity类的成员变量来实现这一点。

提示

当我们将变量作为类的成员时,我们称之为字段。我们将在第六章中讨论变量何时应该是字段,何时不应该是字段。

在第 9 和第 10 步中,我们实现了在onClick中执行实际工作的代码。让我们逐行查看当button1被按下时运行的代码。

case R.id.buttonChoice1:

首先,case语句在按下 id 为buttonChoice1的按钮时为真。然后执行的下一行代码是这样的:

answerGiven = Integer.parseInt(""+ buttonObjectChoice1.getText());

前一行使用了两个方法来获取按钮上的值。首先,getText以字符串形式获取数字,然后Integer.parseInt将其转换为整数。该值存储在我们的answerGiven变量中。接下来执行以下代码:

if(answerGiven==correctAnswer) {//yay it's the right answer
  Toast.makeText(getApplicationContext(), "Well done!", Toast.LENGTH_LONG).show();
}else{//uh oh!
    Toast.makeText(getApplicationContext(),"Sorry that's wrong", Toast.LENGTH_LONG).show();
                }

if语句测试answerGiven变量是否与correctAnswer相同,使用==运算符。如果是,Toast对象的makeText方法用于显示祝贺消息。如果两个变量的值不同,显示的消息会更消极一些。

注意

Toast代码行可能是我们迄今为止见过的最邪恶的东西。它看起来异常复杂,需要比我们目前所掌握的 Java 知识更多才能理解。我们现在只需要知道我们可以直接使用代码并只改变消息,这是一个向玩家宣布消息的好工具。到第六章结束时,Toast的代码将会清晰明了。如果你现在真的想要解释,你可以这样想:当我们创建按钮对象时,我们可以使用所有按钮方法。但是对于 Toast,我们直接使用类来访问其makeText方法,而不需要先创建对象。当类及其方法被设计允许这样做时,我们可以进行这个过程。

最后,我们通过以下方式跳出整个switch语句:

break;

现在我们已经根据本章学到的知识改进了项目,为什么不测试一下你迄今为止所学到的一切呢?

自测问题

Q1) 这段代码做什么?

// setContentView(R.layout.activity_main);

Q2) 以下哪行会导致错误?

String a = "Hello";
String b = " Vinton Cerf";
int c = 55;
a = a + b
c = c + c + 10;
a = a + c;
c = c + a;

Q3) 我们谈了很多关于运算符以及不同运算符如何一起构建复杂表达式。表达式乍一看有时会让代码看起来复杂。然而,仔细观察时,它们并不像看起来那么难。通常,只是将表达式分成较小的部分来弄清楚发生了什么。这是一个比本书中你将看到的任何其他东西都更复杂的表达式。作为挑战,你能计算出:x将是什么吗?

int x = 10;
int y = 9;
boolean isTrueOrFalse = false;
isTrueOrFalse = (((x <=y)||(x == 10))&&((!isTrueOrFalse) || (isTrueOrFalse)));

总结

本章我们涵盖了很多内容。我们从不了解 Java 语法到学习注释、变量、运算符和决策过程。

与任何语言一样,掌握 Java 可以通过简单的练习、学习和扩展词汇量来实现。此时,诱惑可能是要等到掌握当前 Java 语法后再继续,但最好的方法是同时学习新的语法,同时重新学习我们已经开始学习的内容。

在下一章中,我们将通过添加多种难度的随机问题以及为多选按钮使用更合适和随机的错误答案,最终完成我们的数学游戏。

为了使我们能够做到这一点,我们将首先学习更多的 Java。

方法

那么 Java 方法到底是什么?方法是一组变量、表达式和控制流语句。我们已经在使用很多方法;我们只是还没有深入研究过。

学习 Java 方法将是本章最后一个主题,在此之前我们将实际应用我们所学到的知识来增强我们的数学游戏。

方法的结构

我们编写的方法的第一部分称为签名。以下是一个虚构的签名示例:

public boolean shootLazers(int number, string type)

添加一对大括号并包含一些方法执行的代码,我们就有了一个完整的方法,或者定义。以下是一个虚构但语法正确的方法:

private void setCoordinates(int x, int y){
  //code to set coordinates goes here
}

然后我们可以在我们代码的另一个部分中使用我们的新方法,就像这样:

//I like it here

setCoordinates(4,6);//now I am going off to setCoordinates method

//Phew, I'm back again - code continues here

在我们调用setCoordinates的地方,我们程序的执行将分支到该方法中包含的代码,直到它达到结束或被告知返回为止。然后代码将从方法调用后的第一行继续运行。

以下是另一个方法的示例,包括使方法返回到调用它的代码的代码:

int addAToB(int a, int b){
  int answer = a + b;
  return answer;
}

使用前面的方法的调用可能如下所示:

int myAnswer = addAToB(2,4); 

显然,我们不需要编写方法来将两个int变量相加,但前面的示例帮助我们更多地了解了方法的工作原理。首先,我们传递值24。在方法的签名中,值2被赋给int a,值4被赋给int b

在方法体内,ab变量被相加并用于初始化一个新变量,即int答案。return answer行就是这样做的。它将存储在answer中的值返回给调用代码,导致myAnswer被初始化为6的值。

请注意,前面示例中的每个方法签名都有所不同。其原因是 Java 方法签名非常灵活,允许我们精确构建我们需要的方法。

方法签名如何定义方法必须如何调用以及方法必须返回值(如果必须返回值)的方式,值得进一步讨论。让我们给签名的每个部分命名,以便我们可以将其分成块并分别学习。

提示

以下是一个带有标签并准备讨论的方法签名。您还可以查看以下表格,进一步确定签名的哪个部分是哪个。这将使我们对方法的讨论变得简单明了。

修饰符 | 返回类型 | 方法名称参数

以下是我们迄今为止使用的一些示例,以便您可以清楚地确定正在讨论的签名的部分:

签名的一部分 示例
修饰符 publicprivate
返回类型 intbooleanfloat等,或任何 Java 类型,表达式或对象
方法名称 shootLazerssetCoordinatesaddAToB
参数 int numberstring type),(int xint y),(int aint b)等

修饰符

在我们之前的示例中,我们只使用了修饰符两次,部分原因是方法不必使用修饰符。修饰符是指定哪些代码可以使用您的方法的一种方式。一些修饰符的类型是publicprivate。实际上,常规变量也可以有修饰符,例如:

//Most code can see me
public int a;

//Code in other classes can't see me
private string secret = "Shhh, I am private";

修饰符(用于方法和变量)是一个重要的 Java 主题,但最好在我们讨论其他重要的 Java 主题时处理,这些主题我们迄今为止已经绕了几次——对象和类。

注意

正如之前承诺的那样,这些神秘的对象将在第六章中揭示,OOP – 使用他人的辛勤工作。然而,正如我们从我们的示例方法和迄今为止我们编写的所有示例都可以正常工作的事实中看到的,修饰符并不是必要的,以便促进我们迄今为止的学习。

返回类型

接下来是return类型。像修饰符一样,return类型也是可选的,尽管它对我们来说更有用。所以让我们仔细看一下。我们已经知道我们的方法可以完成任何事情。但是如果我们需要从它们所做的事情中得到结果呢?到目前为止,我们看到的最简单的返回类型示例是这样的:

int addAToB(int a, int b){
  int answer = a + b;
  return answer;
}

在这段代码中,签名中的return类型被突出显示。所以return类型是intaddAToB方法将一个值返回给调用它的代码,这个值将适合一个int变量。

return类型可以是我们到目前为止见过的任何 Java 类型。然而,方法不一定要返回一个值。在这种情况下,签名必须使用void关键字作为return类型。当使用void关键字时,方法体不得尝试返回一个值,否则会导致编译器错误。但是,它可以使用没有值的return关键字。以下是一些有效的返回类型组合和return关键字的用法:

void doSomething(){
  //our code

  //I'm done going back to calling code here
  //no return is necessary
}

returnvoid的另一种组合如下:

void doSomethingElse(){
  //our code

  //I can do this as long as I don't try and add a value
  return;
}

以下代码是returnvoid的另一种组合:

void doYetAnotherThing(){
  //some code
  if(someCondition){
    //if someCondition is true returning to calling code 
    //before the end of the method body
    return;
  }
  //More code that might or might not get executed

  return;
  //As I'm at the bottom of the method body 
  //and the return type is void, I'm 
  //really not necessary but I suppose I make it 
  //clear that the method is over.
}

String joinTogether(String firstName, String lastName){
  return firstName + lastName;
}

我们可以依次调用前面的每个方法,就像这样:

//OK time to call some methods
doSomething();
doSomethingElse();
doYetAnotherThing();
String fullName = joinTogether("Jeff ","Minter")
//fullName now = Jeff Minter
//continue with code from here

注意

前面的代码将依次执行每个方法中的所有代码语句。如果方法签名有参数,调用方法的代码会略有不同。

方法的名称

当我们设计自己的方法时,方法名是任意的,但有一个约定是使用能清楚解释方法将要做什么的动词。另一个约定是名称中第一个单词的第一个字母小写,后续单词的第一个字母大写。这被称为驼峰命名法,因为名称的形状中有一个驼峰:

XGHHY78802c(){
  //code here
}

这个名称是完全合法的,并且可以工作。然而,让我们看一个更清晰的示例,使用约定:

doSomeVerySpecificTask(){
  //code here
}

getMySpaceShipHealth(){
  //code here
}

startNewGame(){
  //code here
}

这些方法名更清晰。

现在让我们来看一下参数。

参数

我们知道方法可以将结果返回给调用代码。如果我们需要将一些数据值从调用代码与方法共享呢?参数允许我们与方法共享值。在查看返回类型时,我们已经看到了一个带有参数的示例。我们将仔细看一下相同的示例:

int addAToB(int a, int b){
  int answer = a + b;
  return answer;
}

这段代码中的参数被突出显示。请注意,在方法体的第一行中,我们使用a + b,好像它们已经声明并初始化了。那是因为它们确实是。方法签名的参数就是它们的声明,调用方法的代码初始化它们:

int returnedAnswer = addAToB(10,5);

此外,正如我们在之前的示例中部分看到的那样,我们不必在参数中使用int。我们可以使用任何 Java 类型,包括我们自己设计的类型。我们也可以混合和匹配类型。我们还可以使用尽可能多的参数来解决我们的问题。混合 Java 类型的示例可能会有所帮助:

void addToAddressBook(char firstInitial, String lastName, String city, int age){
  //all the parameters are now living breathing,
  //declared and initialized variables

  //code to add details to address book goes here
}

现在是时候认真对待我们的方法体了。

在方法体中完成任务

到目前为止,我们一直在避免的部分是方法体,有这样的注释:

//code here
    //some code

但实际上,我们已经完全知道在这里该做什么。到目前为止,我们学到的任何 Java 语法都可以在方法体中使用。事实上,如果我们回顾一下,到目前为止我们写的所有代码都是在一个方法中,尽管是别人的方法。例如,我们在onCreateonClick方法中编写了代码。

我们接下来最好做的事情是在方法体中写一些真正有用的方法。

使用方法

我们不必在我们的数学游戏项目中瞎搞。我们将快速为接下来的两个探索方法创建一个新的空白项目。

我们也不需要花时间制作 UI。我们将使用 Android 控制台查看结果,并讨论我们的方法示例的影响。由于我们正在使用 Android 控制台查看我们使用方法的工作结果,我们需要在 Android 模拟器上运行所有这些示例,而不是在真实设备上。

注意

可以设置真实设备输出到控制台,但我们在本书中没有涵盖这一点。如果您想了解更多关于使用实际设备进行调试的信息,请查看developer.android.com/tools/device.html上的文章。

通常情况下,您可以以通常的方式打开已输入的代码文件。关于方法的下两个示例可以在Chapter4文件夹和AWorkingMethodExploringMethodOverloading子文件夹中的 Packt Publishing 代码下载中找到。

提示

以下是如何创建一个新的空白项目的快速提醒。

  1. 通过导航到File | Close Project关闭当前打开的任何项目。

  2. 点击New Project...

  3. 将出现Create New Project配置窗口。在Application name字段和Company Domain中填写packtpub.com,或者您可以在此处使用您自己公司的网站名称。

  4. 现在点击Next按钮。在下一个屏幕上,确保Phone and tablet复选框中有一个勾。现在我们必须选择我们想要为其构建应用程序的最早版本的 Android。随意在下拉选择器中尝试几个选项。您会发现,我们选择的版本越早,我们的应用程序支持的设备百分比就越大。然而,这里的权衡是,我们选择的版本越早,我们的应用程序中可以拥有的最新 Android 功能就越少。一个很好的平衡是选择API 8: Android 2.2 (Froyo)。按照下一个截图中所示进行操作。

  5. 点击Next。现在选择Blank Activity,然后再次点击Next

  6. 在下一个屏幕上,只需将Activity Name更改为MainActivity,然后点击Finish

  7. 与我们在第二章中所做的一样,为了保持我们的代码清晰简单,您可以删除两个不需要的方法(onCreateOptionsMenuonOptionsItemSelected)及其相关的@override@import语句,但这对示例的工作并不是必需的。

有关创建新项目的详细说明和图像,请参见第二章,开始使用 Android

一个工作方法

首先,让我们创建一个简单的工作方法,包括返回类型和完全功能的主体。

这个方法将接受三个数字作为参数,并根据这三个数字中是否有一个是在方法中随机生成的,返回一个truefalse值给调用代码:

  1. 创建一个名为A Working Method的新空白项目。

  2. 在这个方法中,我们将使用我们之前看到的Random类及其randInt方法作为演示的一部分。将此方法的代码复制到onCreate的结束括号之后,但在MainActivity的结束括号之前。当提示导入任何类时,只需点击OK

  boolean guessANumber(int try1, int try2, int try3){
  //all the Log.i lines print to the Android console
  Log.i("info", "Hi there, I am in the method body");
  //prove our parameters have arrived in the method
  //By printing them in the console
  Log.i("info", "try1 = " + try1);
  Log.i("info", "try2 = " + try2);
  Log.i("info", "try3 = " + try3);
  1. 现在我们声明一个名为found的布尔变量,并将其初始化为false。如果我们猜对了随机数,我们将把found更改为true。接下来,我们声明我们的随机数,并将一些有用的值打印到控制台:
  //we use the found variable to store our true or false
  //setting it to false to begin with
  boolean found = false;

  //Create an object of the Random class so we can use it
  Random randInt = new Random();
  //Generate a random number between 0 and 5
  int randNum = randInt.nextInt(6);
  //show our random number in the console
  Log.i("info", "Our random number = " + randNum);
  1. 我们方法中的最后一部分代码测试是否有任何匹配我们传入参数的内容,打印一些输出,然后使用found变量将truefalse返回给onCreate方法中的调用代码:
  //Check if any of our guesses are the same as randNum
  if(try1 == randNum || try2 == randNum || try3 == randNum){
    found = true;
    Log.i("info", "aha!");
   }else{
     Log.i("info", "hmmm");
   }

  return found;
 }
  1. 现在在onCreate方法的结束括号之前编写以下代码,以调用代码并将一些值打印到 Android 控制台:
//all the Log.i lines print to the Android console
Log.i("info", "I am in the onCreate method");

//Call guessANumber with three values
//and if true is returned output - Found it!
if(guessANumber( 1,2,3 )) {
  Log.i("info", "Found It!");
}else{//guessANumber returned false -didn't find it
  Log.i ("info", "Can't find it");
}

//continuing with the rest of the program now
Log.i("info", "Back in onCreate");
  1. 启动模拟器。

  2. 在模拟器上运行应用程序。

  3. 我们所有的控制台消息都有一个名为info的标签。控制台窗口已经出现在编辑窗口下方。我们可以通过在搜索框中输入info来过滤其内容,只显示我们的消息,如下面的截图所示:A working method

在上面的截图中,您可以看到搜索过滤器和控制台输出。我们现在将运行代码并解释输出。

为了清晰起见,这是精确的控制台输出,没有在每行开头添加多余的日期、时间和包名。请记住,我们正在处理一个随机数,因此您的输出可能会有所不同:

info: I am in the onCreate method
info﹕Hi there, I am in the method body
info﹕try1 = 1
info﹕try2 = 2
info﹕try3 = 3
info﹕Our random number = 0
info﹕hmmm
info﹕Can't find it
info﹕Back in onCreate

这里发生了什么。在第 2 步中,我们开始编写我们的第一个方法。我们称它为guessANumber。它有三个int参数,并将返回一个布尔值。请记住,这三个int参数将成为完全初始化的变量。然而,首先在我们的方法中,我们只是输出传入参数的新变量的值,以及一个确认我们的方法中的代码当前正在执行的消息:

boolean guessANumber(int try1, int try2, int try3){
  //all the Log.i lines print to the Android console
  Log.i("info", "Hi there, I am in the method body");
  //prove our parameters have arrived in the method
  //By printing them in the console
  Log.i("info", "try1 = " + try1);
  Log.i("info", "try2 = " + try2);
  Log.i("info", "try3 = " + try3);

在第 3 步中,我们向我们的方法添加了更多代码。我们声明并初始化了一个名为found的布尔变量,我们将使用它来返回一个值给调用代码,并让调用代码知道传入的参数中是否有一个与随机数相同:

//we use the found variable to store our true or false
//setting it to false to begin with
boolean found = false;

接下来(仍然是第 3 步),我们以与本章前面相同的方式生成了一个随机数。我们还使用Log输出随机数,以便我们可以检查发生了什么:

//Create an object of the Random class so we can use it
Random randInt = new Random();
//Generate a random number between 0 and 5
int randNum = randInt.nextInt(6);
//show our random number in the console
Log.i("info", "Our random number = " + randNum);

在第 4 步中,我们使用了一个带有逻辑或运算符的if语句来检测传入的参数中是否有任何一个与我们刚生成的随机数匹配,如下面的代码所示:

//Check if any of our guesses are the same as randNum
if(try1 == randNum || try2 == randNum || try3 == randNum){

如果条件成立,也就是说,如果try1try2try3中的任何一个等于randNum,则运行以下代码。我们的found布尔值设置为true,并打印一条消息:

found = true;
Log.i("info", "aha!");

如果条件不成立,将执行else语句,打印不同的消息,并且found变量保持不变,仍为false

}else{
  Log.i("info", "hmmm");
}

最后,在我们的方法中,我们返回found变量,它将是truefalse,返回给调用代码:

  return found;
 }

现在我们来看第 5 步,即onCreate方法中调用我们的guessANumber方法的代码。我们首先简单地打印一条消息,说明我们目前正在onCreate中:

//all the Log.i lines print to the Android console
Log.i("info", "I am in the onCreate method");

然后我们调用guessANumber并传入三个参数。在这种情况下,我们使用了 1、2 和 3,但任何int值都可以工作。但是,我们将调用包装在一个if语句中。这意味着方法的return值将用于评估if语句。简而言之,如果返回true,则将执行if语句,并打印“Found It”:

//Call guessANumber with three values
//and if true is returned output - Found it!
if(guessANumber(1,2,3)){
  Log.i("info", "Found It!");
  }

相反,如果返回false,则执行else语句,并打印“Can't find it”:

else{//guessANumber returned false -didn't find it
  Log.i ("info", "Can't find it");
}

//continuing with the rest of the program now
Log.i("info", "Back in onCreate");

请记住,我们正在处理随机数,因此您可能需要运行几次才能看到这个输出:

A working method

当然,您应该注意,作为参数发送到函数的猜测是任意的。只要所有数字都在 0 到 5 之间,且不重复,它们一起有 50%的几率找到随机数。

最后,如果您只想读这本书中的一个提示,那就是这个。

提示

将变量值打印到控制台是检查游戏内部发生了什么以及查找错误的好方法。

让我们看一个方法的另一个例子。

探索方法重载

正如我们所学到的,方法作为一个主题确实是多样且深刻的,但希望一步一步地,我们会发现它们并不令人畏惧。当我们增强我们的数学游戏时,我们将使用我们对方法的了解。在第六章OOP – 使用他人的辛勤工作中,我们将更深入地探索方法。然而,现在,我们将看一看关于方法的另一个主题。让我们创建一个新项目来探索方法重载

正如我们将要看到的,我们可以创建多个具有相同名称的方法,只要参数不同。这个项目中的代码比上一个项目简单得多。直到我们稍后分析它的工作原理,这段代码可能看起来有点奇怪:

  1. 创建一个名为“探索方法重载”的新空项目。

  2. 在第一个方法中,我们将简单地调用它printStuff并通过参数传递一个int变量进行打印。将此方法的代码复制到onCreate的结束括号之后,但在MainActivity的结束括号之前。当提示导入任何类时,只需点击确定

void printStuff(int myInt){
  Log.i("info", "This is the int only version");
  Log.i("info", "myInt = "+ myInt);
}
  1. 我们还将调用第二个方法printStuff,但传递一个要打印的string变量。将此方法的代码复制到onCreate的结束括号之后,但在MainActivity的结束括号之前。同样,当提示导入任何类时,只需点击确定
void printStuff(String myString){
  Log.i("info", "This is the String only version");
  Log.i("info", "myString = "+ myString);
}
  1. 再一次,我们将调用这第三个方法printStuff,但传递一个要打印的string变量和一个int变量。与之前一样,将此方法的代码复制到onCreate的结束括号之后,但在MainActivity的结束括号之前:
void printStuff(int myInt, String myString){
  Log.i("info", "This is the combined int and String version");
  Log.i("info", "myInt = "+ myInt);
  Log.i("info", "myString = "+ myString);
}
  1. 现在,在onCreate方法的结束括号之前编写这段代码,以调用方法并将一些值打印到 Android 控制台:
//declare and initialize a String and an int
int anInt = 10;
String aString = "I am a string";

//Now call the different versions of printStuff
//The name stays the same, only the parameters vary
printStuff(anInt);
printStuff(aString);
printStuff(anInt, aString);
  1. 启动模拟器。

  2. 在模拟器上运行应用程序。

以下是控制台输出:

info﹕ This is the int only version
info﹕ myInt = 10
info﹕ This is the String only version
info﹕ myString = I am a string
info﹕ This is the combined int and String version
info﹕ myInt = 10
info﹕ myString = I am a string

正如你所看到的,Java 已经将具有相同名称的三个方法视为完全不同的方法。正如我们刚刚演示的那样,这可能非常有用。这被称为方法重载

提示

方法重载和覆盖的混淆

重载和覆盖的定义如下:

  • 重载发生在我们有多个具有相同名称但不同参数的方法时

  • 覆盖发生在我们实质上用相同名称和相同参数列表替换方法时

我们对重载和覆盖的了解已经足够完成本书,但如果你勇敢而且心神游荡,你可以覆盖重载的方法。然而,这是另一个时间的事情。

这就是前面的代码是如何工作的。在三个步骤(2、3 和 4)中,我们分别创建了一个名为printStuff的方法,但每个printStuff方法都有不同的参数,因此每个方法都是可以单独调用的不同方法:

void printStuff(int myInt){
...
}

void printStuff(String myString){
...
}

void printStuff(int myInt, String myString){
...
}

每个方法的主体都很简单。它只是打印传入的参数,并确认当前调用的方法版本。

我们代码的下一个重要部分是当我们明确指出要调用哪个方法时,使用适当的参数。在第 5 步中,我们依次调用它们,使用适当的参数,以便 Java 知道所需的确切方法:

printStuff(anInt);
printStuff(aString);
printStuff(anInt, aString);

现在我们对方法、循环和随机数的了解已经足够多,可以对我们的数学游戏进行一些改进了。

增强我们的数学游戏

我们将使用我们刚刚学到的关于方法和循环的知识,为我们的数学游戏添加一些功能。

通常情况下,代码可以在代码下载的Chapter4文件夹中找到。该项目位于MathGameChapter4子文件夹中,包括本章中涵盖的所有改进阶段,包括增强 UI、修改游戏活动、setQuestionupdateScoreAndLevelisCorrect和调用我们的新方法。

我们将使游戏在每次尝试答案后更改问题。

我们还将为问题添加难度级别和随机问题,但在适当难度级别范围内。

我们将显示和更新我们的分数。正确回答问题的难度级别越高,分数上升得越快。

如果玩家答错问题,难度将回到最简单的级别,分数将变为零。

增强 UI

让我们继续修改我们的数学游戏 UI,以整合我们的新游戏功能。我们将添加一个 TextView 来显示分数,另一个 TextView 来显示级别。

  1. 在编辑窗口中打开activity_game.xml文件。我们将在 UI 的底部添加一个新的 TextView 来显示我们的分数。

  2. Palette中拖动一个Large Text元素,并将其放置在左侧,放置在我们的三个答案按钮下方。

  3. 现在我们需要更改id属性,以便我们可以从 Java 代码中访问我们的新 TextView。确保通过单击它来选择新的 TextView。现在,在Properties窗口中,将id属性更改为textScore

  4. 为了清晰起见(尽管这一步在编程中没有用处),将text属性更改为Score:999

  5. 现在将另一个Large Text元素放置在我们刚刚配置的元素的右侧,并将id属性更改为textLevel。我们的 UI 的下半部分现在应该是这样的:Enhancing the UI

  6. 再次为了清晰起见(尽管这一步在编程中没有用处),将text属性更改为Level:4

  7. 保存项目。

我们刚刚添加了两个新的 TextView 元素,并为它们分配了一个我们可以在 Java 代码中引用的 ID。

提示

你可能已经意识到,就让游戏运行而言,我们的 UI 元素的精确布局和大小并不重要。这为我们在设计不同屏幕尺寸的布局时提供了很大的灵活性。只要每个屏幕尺寸的布局包含相同的元素类型和相同的 ID,相同的 Java 代码就可以用于不同的布局。如果你想了解更多关于为多个屏幕尺寸设计的信息,请查看developer.android.com/training/multiscreen/screensizes.html

现在我们有了增强的 UI 和对 Java 的Random类如何工作的理解,我们可以添加 Java 代码来实现我们的新功能。

新的 Java 代码

如前所述,项目代码可在可下载代码的Chapter4文件夹中找到。该项目称为MathGameChapter4,包含了本章涵盖的所有改进。

在这个阶段,我们将添加大量新代码,移动一些现有代码,并修改一些现有代码。由于变化如此之大,我们将从头开始处理代码。新代码将被完全解释,移动的代码将被指出原因,保持不变且位置不变的代码将有最少的解释。

我们将首先对我们现有的代码进行一些修改和删除。然后,我们将着手设计和实现每个新方法,以改进我们的代码并添加新功能。

修改 GameActivity

首先,让我们对我们当前的代码进行必要的修改和删除:

  1. 在编辑窗口中打开GameActivity.java文件。

  2. 现在我们需要考虑代表我们 UI 元素的对象的范围。textObjectPartAtextObjectPartB都需要从我们即将创建的方法中访问。因此,让我们像在上一章中对多选按钮所做的那样,将它们的声明移到onCreate方法之外,以便它们在GameActivity类的任何地方都可以访问。以下代码显示了到目前为止所有我们的声明。它们出现在GameActivity类开始之后。最近添加(或移动)的声明已经被突出显示。请注意,我们还为我们的两个新文本视图和分数和等级显示添加了声明。此外,还有两个新的int变量,我们可以用来操作我们的分数和跟踪我们的等级。它们是currentScorecurrentLevel

public class GameActivity extends Activity implements View.OnClickListener{

    int correctAnswer;
    Button buttonObjectChoice1;
    Button buttonObjectChoice2;
    Button buttonObjectChoice3;
    TextView textObjectPartA;
 TextView textObjectPartB;
 TextView textObjectScore;
 TextView textObjectLevel;

 int currentScore = 0;
 int currentLevel = 1;

  1. 为我们的按钮或文本视图对象分配文本的所有代码,以及初始化问题部分并为错误答案分配值的代码,现在都要更改和移动,所以我们需要全部删除。以下代码中显示的所有内容都将被删除:
//Here we initialize all our variables
int partA = 9;
int partB = 9;
correctAnswer = partA * partB;
int wrongAnswer1 = correctAnswer - 1;
int wrongAnswer2 = correctAnswer + 1;
  1. 以下代码片段也需要被删除:
//Now we use the setText method of the class on our objects
//to show our variable values on the UI elements.
textObjectPartA.setText("" + partA);
textObjectPartB.setText("" + partA);

//which button receives which answer, at this stage is arbitrary.
buttonObjectChoice1.setText("" + correctAnswer);
buttonObjectChoice2.setText("" + wrongAnswer1);
buttonObjectChoice3.setText("" + wrongAnswer2);
  1. 为了清晰和上下文,以下是整个onCreate方法的当前状态。这里没有新内容,但你可以看到我们在第 2 步中声明的按钮和文本视图对象的代码。再次强调,这段代码包括我们的两个新文本视图,但其他所有内容,即第 3 步和第 4 步中描述的内容,都已被删除。与以前一样,有一段代码使我们的游戏监听按钮点击:
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //The next line loads our UI design to the screen
        setContentView(R.layout.activity_game);

        /*Here we get a working object based on either the button
          or TextView class and base as well as link our new objects
          directly to the appropriate UI elements that we created previously*/
        textObjectPartA = (TextView)findViewById(R.id.textPartA);

        textObjectPartB = (TextView)findViewById(R.id.textPartB);

 textObjectScore = (TextView)findViewById(R.id.textScore);

 textObjectLevel = (TextView)findViewById(R.id.textLevel);

        buttonObjectChoice1 = (Button)findViewById(R.id.buttonChoice1);

        buttonObjectChoice2 = (Button)findViewById(R.id.buttonChoice2);

        buttonObjectChoice3 = (Button)findViewById(R.id.buttonChoice3);

        buttonObjectChoice1.setOnClickListener(this);
        buttonObjectChoice2.setOnClickListener(this);
        buttonObjectChoice3.setOnClickListener(this);

}//onCreate ends here
  1. 现在我们将删除一些不需要的代码,因为我们将通过将其分隔到我们的新方法中并同时添加新功能来使其更有效。因此,在我们的onClick方法中,在switch语句的每种情况下,我们要删除ifelse语句。我们将完全重写这些内容,但我们将保留初始化answerGiven变量的代码。我们的onClick方法现在将如下所示:
@Override
    public void onClick(View view) {
        //declare a new int to be used in all the cases
        int answerGiven=0;
        switch (view.getId()) {

            case R.id.buttonChoice1:
                //initialize a new int with the value contained in buttonObjectChoice1
                //Remember we put it there ourselves previously
                answerGiven = Integer.parseInt("" + buttonObjectChoice1.getText());

                break;

            case R.id.buttonChoice2:
                //same as previous case but using the next button
                answerGiven = Integer.parseInt("" + buttonObjectChoice2.getText());

                break;

            case R.id.buttonChoice3:
                //same as previous case but using the next button
                answerGiven = Integer.parseInt("" + buttonObjectChoice3.getText());

                break;

        }

    }
  1. 保存你的项目。

哇!那是很多代码,但正如我们一路看到的那样,没有新概念。在第 2 步中,我们只是将按钮和文本视图对象的初始化移到了一个现在可以从我们的类中的任何地方看到的地方。

在第 3 步和第 4 步中,我们进行了相当多的删除,因为我们将不再在onCreate中提出问题或填充多选按钮,因为这样做不够灵活。我们很快就会看到我们如何改进这一点。

在第 6 步中,我们删除了测试答案是否正确或不正确的代码。然而,正如我们所看到的,我们仍然以相同的方式初始化了answerGiven变量——在onClick方法的switch语句的适当情况下。

太棒了!现在我们准备考虑并设计一些新方法,将我们的代码分隔开,避免重复,并添加我们的额外功能。考虑一下我们很快将实现的以下方法。

方法

现在我们将逐步编写一些方法。正如我们将看到的那样,这些方法将把我们的代码分隔开,并防止我们的新功能的实现导致代码变得过长和杂乱:

  • 我们将编写一个setQuestion方法来准备一个适当难度的问题。

  • 我们将编写一个updateScoreAndLevel方法来实现这一点。我们还将编写一个isCorrect方法,我们的其他方法将使用它来评估答案的正确性。

  • 然后我们将策略性地放置调用我们新方法的代码。

我们将逐个执行这些任务,并在此过程中解释代码,因为将解释留到最后会使参考各个步骤变得麻烦。

我们将使用本章和上一章学到的许多 Java 特性。这些包括以下内容:

  • 方法

  • 一个for循环

  • 开关控制结构

所以让我们开始我们的第一个方法。

setQuestion 方法

我们确定我们需要一个方法来为我们准备一个问题;setQuestion似乎是这样一个方法的不错名称。每当我们的玩家通过点击三个多选按钮之一给出答案时,都需要准备一个新的问题。

这个方法将需要为我们的partApartB变量生成值,并在由textObjectPartAtextObjectPartB对象引用的 TextViews 中显示它们。此外,该方法将需要将新的正确答案分配给我们的correctAnswer变量,然后用它来计算一些合适的错误答案。最后,该方法将在我们的多选按钮上显示正确和错误答案。

此外,我们的setQuestion方法将需要考虑currentLevel中保存的级别,以确定它将提出的问题的范围或难度。让我们来看看代码。如果你想一边跟着我们打这段代码,那么请确保你把它放在onClick的闭合括号之后,但在我们的GameActivity类的闭合括号之前:

  1. 首先,我们有方法签名和方法体之前的左花括号:
void setQuestion(){
  1. 这告诉我们返回类型是void,所以setQuestion不会向调用它的代码返回值。此外,这里没有参数,所以它不需要任何值来工作。让我们看看它做了什么。现在我们输入代码来生成问题的两个部分:
//generate the parts of the question
int numberRange = currentLevel * 3;
Random randInt = new Random();

int partA = randInt.nextInt(numberRange);
partA++;//don't want a zero value

int partB = randInt.nextInt(numberRange);
partB++;//don't want a zero value
  1. 在上一步中,我们声明了一个新的int变量numberRange,并通过将玩家的currentLevel值乘以3来初始化它。然后我们得到了一个名为randInt的新的Random对象,并用它来生成基于numberRange的新值。我们对partApartB变量做了这个。随着currentLevel的值的增加,问题的难度也可能增加。现在,就像我们过去写的那样,我们写了这个:
correctAnswer = partA * partB;
int wrongAnswer1 = correctAnswer-2;
int wrongAnswer2 = correctAnswer+2;

textObjectPartA.setText(""+partA);
textObjectPartB.setText(""+partB);
  1. 我们将我们新的乘法问题的答案赋给correctAnswer。然后我们声明并赋值了两个错误答案给新的int变量wrongAnswer1wrongAnswer2。我们还使用了我们的 TextView 对象的setText方法来向玩家显示问题。注意我们还没有显示正确和错误答案。这就是它。试着弄清楚这里发生了什么:
//set the multi choice buttons
//A number between 0 and 2
int buttonLayout = randInt.nextInt(3);
switch (buttonLayout){

case 0:
buttonObjectChoice1.setText(""+correctAnswer);
buttonObjectChoice2.setText(""+wrongAnswer1);
buttonObjectChoice3.setText(""+wrongAnswer2);
   break;

case 1:

buttonObjectChoice2.setText(""+correctAnswer);
buttonObjectChoice3.setText(""+wrongAnswer1);
buttonObjectChoice1.setText(""+wrongAnswer2);
   break;

case 2:
buttonObjectChoice3.setText(""+correctAnswer);
buttonObjectChoice1.setText(""+wrongAnswer1);
buttonObjectChoice2.setText(""+wrongAnswer2);
   break;
 }

}
  1. 在前面的代码中,我们使用了我们的Random对象randInt来生成 0 到 2 之间的数字,并将该值赋给一个名为buttonLayout的新的int变量。然后我们使用buttonLayout来在它的所有可能值之间进行切换:0、1 或 2。每个case语句将正确和错误答案设置为多选按钮上的略有不同的顺序,这样玩家就不能一直点击同一个按钮来获得大量分数。注意在switch结束括号之后有一个额外的闭合括号。这是我们setQuestion方法的结束。

我们在通过代码时相当彻底地解释了代码,但再仔细看一些部分可能是值得的。

在步骤 1 中,我们看到了我们的方法签名,返回类型为void,没有参数。在步骤 2 中,我们生成了一些将在一定范围内的随机数。这个范围并不像乍看起来那么明显。首先,我们这样赋值、声明和初始化了numberRange

int numberRange = currentLevel * 3;

所以如果玩家在第一个问题上,那么currentLevel将保存值1numberRange将被初始化为3。然后我们创建了一个新的Random对象,就像之前讨论的那样,并输入了这行代码:

int partA = randInt.nextInt(numberRange);

这里发生的是Random对象randIntnextInt方法将返回 0、1 或 2 的值,因为我们给它一个种子 3。我们不希望游戏中有任何零,因为它们会导致非常简单的乘法,所以我们输入了这个:

partA++;//don't want a zero value

这个操作符,你可能还记得来自第三章,“说 Java-你的第一个游戏”,当我们讨论操作符时,将 1 添加到partA。然后我们对partB变量做完全相同的操作,这意味着假设玩家仍然处于 1 级,他们将有一个以下问题之一:

1 x 1, 1 x 2, 1 x 3, 2 x 1, 2 x 2, 2 x 3, 3 x 1, 3 x 2, 或 3 x 3

随着级别的提高,问题的潜在范围显着增加。因此,在第 2 级,问题的选项可能是 1 到 6;对于第 3 级,从 1 到 9;等等。在更高的级别仍然可能得到一个简单的问题,但随着级别的提高,这种可能性变得越来越小。最后,在这一步中,我们使用setText方法向玩家显示问题。

在第 3 步中,我们之前已经见过,但这次我们稍微改变了它。我们计算并为correctAnswer分配一个值,并声明并为wrongAnswer1wrongAnswer2分配值,它们将保存按钮的错误答案选择。

第三部分与上一章的onCreate稍有不同,因为我们分别从wrongAnswer1wrongAnswer2中减去和加上 2。这使得猜测乘法问题的答案变得更难,因为你不能根据答案是奇数还是偶数来排除答案。

第 4 步只是随机确定正确和错误答案将放在哪些按钮上。我们不需要跟踪这一点,因为当比较按钮上的值与正确答案时,我们可以简单地使用我们的 Java 代码来发现它,就像我们在第三章,“说 Java-你的第一个游戏”中所做的那样。

更新updateScoreAndLevel方法

这个方法的名称说明了它自己。因为得分的保持并不简单,而且我们希望更高的级别能够产生更高的分数,我们将把代码分隔开来保持我们的程序可读性。如果我们想对记分系统进行修改,它们都可以在那里进行。

让我们写代码。

  1. 这段代码可以放在GameActivity {}的大括号的任何位置,但最好的做法是按照它们将被使用的大致顺序放置。那么为什么不在setQuestion的结束大括号之后开始添加你的代码,但显然在GameActivity的结束大括号之前?这是带有开始大括号的方法签名:
void updateScoreAndLevel(int answerGiven){
  1. 这告诉我们,我们的方法不返回值,但它接收一个int,它将需要执行其操作。参数的名称是我们将要传递的一个重要线索。我们将在一分钟内在主体中看到它的作用,但如果将玩家的答案传递给这个方法而不是isCorrect方法有点困惑,我们将在下一段代码中看到事情变得更清晰。这是要添加的代码的下一部分:
if(isCorrect(answerGiven)){
  for(int i = 1; i <= currentLevel; i++){
    currentScore = currentScore + i;
   }

   currentLevel++;
}
  1. 这里发生了很多事情,所以一旦我们完成了这个方法,我们将更详细地解剖它。基本上,它调用isCorrect方法(我们很快将写出来),如果响应是true,则在for循环中增加玩家的分数。之后,该方法将currentLevel增加 1。这是代码的else部分,以防isCorrect的响应是false
else{
  currentScore = 0;
  currentLevel = 1;
}
  1. 如果响应是false,也就是说,如果玩家答错了,currentScore变量将设置为0,级别回到1。最后,对于这个方法,我们输入以下内容:
  //Actually update the two TextViews
  textObjectScore.setText("Score: " + currentScore);
  textObjectLevel.setText("Level: " + currentLevel);
}
  1. 在上一步中,我们更新了玩家看到的实际 TextViews,显示了新确定的分数和级别。然后方法结束,程序的控制返回到最初调用updateScoreAndLevel的代码。保存你的项目。

我们在进行代码编写时已经解释了大部分代码,但快速回顾并深入研究某些部分可能会有好处,特别是在那个看起来很奇怪的if语句中调用isCorrect

在步骤 1 中,我们从方法签名开始。然后在步骤 2 中,我们从上述奇怪的if开始:

if(isCorrect(answerGiven)){

我们在本章的方法部分的一个有效的方法示例中已经见过这种类型的语句。这里发生的是isCorrect的调用正在替换要评估的语句,或者更确切地说,要评估的语句。因此,isCorrectanswerGiven变量调用。您可能还记得,answerGiven变量被传递给updateScoreAndLevel。这次,它被传递给isCorrect方法,它将对其进行一些处理,也许还有其他一些事情。然后它将返回truefalse的值给if语句。如果问题回答正确,则该值为 true,否则为 false。

假设if语句评估为 true,则程序运行此代码段(也来自步骤 2):

for(int i = 1; i <= currentLevel; i++){
  currentScore = currentScore + i;
}

currentLevel++;

代码进入一个for循环,其中起始变量i被初始化为 1,如此:int i = 1;。此外,循环被指示只要i小于或等于我们的currentLevel变量就继续。然后在for循环内,我们将i添加到当前分数。例如,假设玩家刚刚回答了一个问题正确,我们以currentLevel为 1 进入for循环。玩家的分数仍然为 0,因为这是他们的第一个正确答案。

在第 1 次通过时,我们得到以下结果:

  • i = 1,所以它等于currentLevel,也是 1。所以我们进入for循环

  • i = 1,所以currentScore等于 0

  • 我们将i,即1,加到currentScore

  • 我们的currentScore变量现在等于1

在第 2 次通过时,发生以下步骤:

  • i增加到 2,所以现在大于currentLevel,即 1

  • for循环条件评估为false,我们继续for循环后的代码

  • currentLevel增加 1 到 2

现在让我们再次看看for循环,假设玩家下一个问题也回答正确,我们回到updateScoreAndLevel。这次,isCorrect评估为 true,我们进入for循环,但情况与上次略有不同。

在第 1 次通过时,发生以下步骤:

  • i = 1,所以i小于currentLevel是 2,我们进入for循环

  • i = 1currentScore = 1

  • 我们将i,即 1,加到currentScore

  • 我们的currentScore变量现在等于 2

在第 2 次通过时,发生以下步骤:

  • i增加到 2,现在等于currentLevel,也是 2

  • i = 2currentScore = 2

  • 我们将i,现在等于 2,加到currentScore

  • 我们的currentScore变量现在等于 4

在第 3 次通过时,发生以下步骤:

  • i增加到 3,现在大于currentLevel,即 2。

  • for循环条件评估为 false,我们继续for循环后的代码。

  • currentLevel的值增加 1 到 3。所以下次,我们将有额外的通过我们的for循环。

随着每个级别,玩家将获得通过for循环的另一个奖励,并且每次通过for循环都会为他们的分数增加更大的值。总结一下for循环中发生的情况,这里是一个简短的值表,显示了玩家的分数如何基于currentLevel变量增加:

currentLevel 添加到 currentScore for 循环后的 currentScore
1 1 1
2 3 (1 + 2) 4
3 6 (1 + 2 + 3) 10
4 10 (1 + 2 + 3 + 4) 20
5 15 (1 + 2 + 3 + 4 + 5) 35

注意

当然,我们本可以保持非常简单,不使用for循环。也许我们只需使用currentScore = currentScore + level,但这并不像我们当前的解决方案那样提供不断增加的奖励,并且我们也无法练习for循环。

如果if(isCorrect(answerGiven))评估为false,则在第 3 步中它只是将分数重置为 0,将级别重置为 1。然后在第 4 步中,使用我们刚讨论过的变量更新我们的分数和级别的 TextView。

现在我们只需要编写一个方法。当然,这就是我们刚刚调用的isCorrect方法。

isCorrect 方法

这个方法很简单,因为我们之前已经看到了所有相关的代码。我们只需要仔细查看方法签名和返回值:

  1. updateScoreAndLevel方法的右括号之后,但在GameActivity类的右括号之前输入代码。像这样输入方法签名:
boolean isCorrect(int answerGiven){
  1. 在这里,我们可以看到该方法必须返回一个布尔值,truefalse。如果没有,程序就无法编译。这保证了当我们将这个方法用作updateScoreAndLevel方法中的评估表达式时,我们一定会得到一个结果。它可以是 true 或 false。方法签名还向我们展示了传入的answerGiven变量,准备供我们使用。输入这段代码,它将确定结果:
boolean correctTrueOrFalse;
if(answerGiven == correctAnswer){//YAY!
  Toast.makeText(getApplicationContext(), "Well done!", Toast.LENGTH_LONG).show();
   correctTrueOrFalse=true;
}else{//Uh-oh!
    Toast.makeText(getApplicationContext(), "Sorry", Toast.LENGTH_LONG).show();
    correctTrueOrFalse=false;
}
  1. 我们几乎已经看到了前面的所有代码。唯一的例外是,我们声明了一个布尔变量correctTrueOrFalse,如果玩家回答正确则赋值为true,如果不正确则赋值为false。我们知道玩家是否正确,因为我们在if语句中将answerGivencorrectAnswer进行比较。请注意,我们还触发了适当的 Android 弹出式提示消息,就像以前一样。最后,我们这样做:
  return correctTrueOrFalse;
}

我们只返回correctTrueOrFalse中包含的任何值。因此,在我们详细讨论过的updateScoreAndLevel中的关键if语句将知道接下来该做什么。

为了确保我们理解isCorrect中发生了什么,让我们通过代码中的事件顺序进行一遍。在第 1 步中,我们有方法签名。我们看到我们将返回一个truefalse值,并接收int

在第 2 步中,我们声明一个名为correctTrueOrFalse的布尔变量来保存我们即将返回的值。然后我们用if(answerGiven == correctAnswer)来测试答案是否正确。如果两个比较的值匹配,将弹出祝贺消息,并将true赋给我们的布尔变量。当然,如果if语句为false,我们会向玩家表示慰问,并将false赋给我们重要的布尔变量。

最后,在第 3 步中,我们发送truefalse,以便updateScoreAndLevel方法可以继续工作。

我们现在已经实现了所有的方法。是时候让它们发挥作用了。

调用我们的新方法

当然,我们闪亮的新方法在我们调用它们之前不会做任何事情。因此,这是调用这些方法的计划:

  1. 游戏开始时,我们希望为玩家设置一个新问题。因此,在我们的onCreate方法中的最后一行代码中,我们可以这样调用我们的setQuestion方法:
  setQuestion();

}//onCreate ends here
  1. 然后我们转向onClick方法,它已经检测到哪个按钮被按下,并将玩家的答案加载到我们的answerGiven变量中。因此,在onClick方法的结尾,在switch语句的右括号之后,我们只需调用这个函数:
updateScoreAndLevel(answerGiven);
  1. 这将我们玩家尝试的答案发送到updateScoreAndLevel,它使用isCorrect来评估答案,添加分数,并在答案正确时增加分数,如果答案不正确,则重置分数和级别。现在我们只需要另一个问题。添加这行。它会问另一个问题:
setQuestion();

现在玩家通过在其 Android 设备上点击其图标来开始我们的数学游戏。我们的GameActivity类声明了一些我们需要访问的变量:

int correctAnswer;
Button buttonObjectChoice1;
Button buttonObjectChoice2;
Button buttonObjectChoice3;
TextView textObjectPartA;
TextView textObjectPartB;
TextView textObjectScore;
TextView textObjectLevel;

int currentScore = 0;
int currentLevel = 1;

然后onCreate初始化一些变量,并准备好接收玩家点击的按钮,然后通过调用setQuestion来询问第一个问题。游戏然后等待玩家尝试回答。当玩家尝试回答时,它将由onClickupdateScoreAndLevelisCorrect处理。然后程序控制再次回到onClick,再次调用setQuestion,然后我们再次等待玩家的答案。

最后的修饰

我们的数学游戏进展顺利。不幸的是,我们很快就要继续了。该项目已经达到了它展示一些 Java 编程基础以及一些关键 Android 功能的目的。现在我们需要开始介绍一些更多与游戏相关的主题。

在继续之前,有两件非常容易的事情可以使我们的游戏变得更酷更完整。如果你想知道高分按钮,我们将在第五章中看到如何实现它,游戏和 Java 基础。然后你将有足够的信息来轻松地回来并自己实现高分。

另一个真正完善我们的游戏并使其更具可玩性的功能是整体或每个问题的时间限制。也许根据正确答案给出的速度来增加分数会有所帮助。在我们谈论线程时,我们需要一些新的 Java 技巧,但我们将在第五章中看到我们如何测量和响应时间,游戏和 Java 基础

现在我们将快速学习两个改进:

  • 锁定屏幕方向

  • 更改主屏幕图像

全屏和锁定方向

您可能已经注意到,如果在应用程序运行时旋转设备,不仅游戏 UI 会变形,而且游戏进度也会丢失。出现问题的原因是当设备旋转时,将调用onPauseonStop方法。然后应用程序将重新启动。我们可以通过重写onPause方法并保存我们的数据来处理这个问题。我们稍后会这样做。现在无论如何我们都不希望屏幕旋转,所以如果我们停止它,我们就解决了两个问题。

在向此文件添加代码时,Android Studio 可能会尝试通过添加额外的格式来“帮助”。如果出现红色错误指示器,您可以将您的AndroidManifest.xml文件与Chapter4/MathGameChapter4文件夹中的代码下载中的文件进行比较。或者,您可以简单地用下载文件中的文件内容替换您的文件内容。这个指南详细说明了逐步的更改,只是为了突出发生了什么变化:

  1. 这是将应用程序锁定为纵向的第一步。打开AndroidManifest.xml文件。它位于项目资源资源管理器中的res文件夹直接下方。在代码中找到第一个开放的<activity

  2. 输入以下新行:

android:screenOrientation="portrait"
  1. 在第二个<activity实例之后重复步骤 2。我们现在已经将菜单和游戏屏幕都锁定在纵向模式下。

  2. 为了使游戏全屏,在同一文件中,找到以下文本,并在其后添加粗体行,但在关闭>符号之前:

<activity
android:name="com.packtpub.mathgamechapter4.app.MainActivity"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">

</activity>

  1. GameActivity活动进行相同的更改。同样,这里是上下文中的代码,以避免出现这些>符号的错误:
<activity
android:name="com.packtpub.mathgamechapter4.app.GameActivity"
android:label="@string/title_activity_game"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
</activity>

  1. 保存项目。

现在,在游戏过程中旋转设备时,纵向方向将被固定。

添加自定义图像(而不是 Android 图标)

我们可能不希望在我们完成的游戏主屏幕上有 Android 图像,所以这是更改它的过程。这个快速指南依赖于您是否有想要使用的图像:

  1. 首先,我们需要将所需的图像添加到布局文件夹中。通过在Windows 资源管理器中单击图像文件,然后使用Ctrl + C进行复制。

  2. 现在在 Android Studio 项目资源管理器中找到drawable-mdpi文件夹。点击该文件夹。

  3. 使用Ctrl + V将图像粘贴到文件夹中。

  4. 现在图像已经成为我们项目的一部分。我们只需要像之前选择 Android 机器人的图像一样选择它。在编辑窗口中打开activity_main.xml,然后点击ImageView(当前是一个 Android 机器人)。

  5. 属性窗口中,找到src属性。点击它,然后点击...

  6. 搜索你的图像并选择它。

  7. 保存你的项目。

  8. 现在你在主屏幕上有了你选择的图像。

自测问题

Q1)猜猜这个方法有什么问题:

void doSomething(){
  return 4;
}

Q2)在这段代码结束时,x将等于多少?

int x=19;
do{
  x=11;
  x++;
}while(x<20)

总结

在这一章中,我们走了很长的路。你对 Java 循环有了深入的了解,并且第一次深入地了解了 Java 方法以及如何使用它们。你学会了如何生成随机数,并且利用你所学到的知识显著增强了你的数学游戏。

随着章节的进行,游戏会变得越来越像真正的游戏。在下一章中,我们将制作一个测试玩家记忆的游戏。它将有声音、动画,并且实际上还会保存玩家的最高分。

恭喜你迄今为止的进步,但让我们继续前进。

第五章:游戏和 Java 基础知识

在本章中,我们将涵盖各种有趣的主题。我们将学习 Java 数组,它允许我们以有组织和高效的方式操纵潜在的大量数据。

然后,我们将研究线程在游戏中的作用,以便看起来可以同时做更多事情。

如果您认为我们的数学游戏有点安静,那么我们将研究如何为我们的游戏添加声音效果,以及介绍一个很酷的开源应用程序来生成真实的声音效果。

我们将学习的最后一件新事情将是持久性。这是当玩家退出我们的游戏甚至关闭他们的 Android 设备时会发生的情况。那时分数会怎么样?下次他们玩时我们将如何加载正确的级别?

一旦我们完成了所有这些,我们将使用所有新的技术和知识以及我们已经知道的内容来创建一个整洁的记忆游戏。

在本章中,我们将涵盖以下主题:

  • Java 数组-一组变量的数组

  • 线程定时

  • 创建和使用蜂鸣声和嗡嗡声-Android 声音

  • 毁灭后的生活展望-坚持

  • 构建记忆游戏

Java 数组-一组变量的数组

您可能想知道当我们有很多要跟踪的变量的游戏时会发生什么。如何处理一个包含前 100 名得分的高分榜?我们可以声明并初始化 100 个单独的变量,如下所示:

int topScore1;
int topScore2;
int topScore3;
//96 more lines like the above
int topScore100;

这可能立即显得笨拙,那么当有人获得新的最高分时,我们需要将每个变量中的分数向下移动一个位置,会发生什么?噩梦开始:

topScore100 = topScore99;
topScore99 = topScore98;
topScore98 = topScore97;
//96 more lines like the above
topScore1 = score;

更新分数必须有更好的方法。当我们有一大堆变量时,我们需要的是一个 Java 数组。数组是一个引用变量,最多可以容纳固定数量的元素。每个元素都是具有一致类型的变量。

下面的代码行声明了一个可以容纳int类型变量的数组,甚至可能是一个高分表:

int [] intArray;

我们还可以声明其他类型的数组,如下所示:

String [] classNames;
boolean [] bankOfSwitches;
float [] closingBalancesInMarch;

这些数组中的每一个都需要在使用之前分配固定的最大存储空间,如下所示:

intArray = new int [100];

前面的代码行分配了最多 100 个整数大小的存储空间。想象一下我们的变量仓库中有 100 个连续存储空间的长过道。空间可能标有intArray[0]intArray[1]intArray[2]等,每个空间都包含一个单个的int值。也许这里稍微令人惊讶的是,存储空间从 0 开始,而不是 1。因此,在大小为 100 的数组中,存储空间将从 0 到 99。

我们实际上可以像这样初始化一些存储空间:

intArray[0] = 5;
intArray[1] = 6;
intArray[2] = 7;

请注意,我们只能将声明的类型放入数组中,数组保存的类型永远不会改变:

intArray[3]= "John Carmack";//Won't compile

所以当我们有一个int类型的数组时,每个int变量被称为什么?数组表示法语法替换了名称。我们可以对数组中的变量做任何我们可以用名称对常规变量做的事情:

intArray[3] = 123;

这是数组变量被用作普通变量的另一个例子:

intArray[10] = intArray[9] - intArray[4];

我们还可以将数组中的值分配给相同类型的常规变量,如下所示:

int myNamedInt = intArray [3];

但是,请注意,myNamedInt是一个单独且独立的基本变量,因此对它所做的任何更改都不会影响存储在intArray引用中的值。它在仓库中有自己的空间,并且与数组没有关联。

数组是对象

我们说数组是引用变量。将数组变量视为给定类型的一组变量的地址。也许,使用仓库类比,someArray是一个过道编号。因此,someArray[0]someArray[1]等都是过道编号,后跟过道中的位置编号。

数组也是对象。这意味着它们有我们可以使用的方法和属性:

int lengthOfSomeArray = someArray.length;

在前一行代码中,我们将someArray的长度分配给了名为lengthOfSomeArrayint变量。

我们甚至可以声明一个数组的数组。这是一个数组,每个元素中存储另一个数组,就像这样:

String[][] countriesAndCities;

在前面的数组中,我们可以保存每个国家内的城市列表。现在先不要太疯狂地使用数组。只需记住,数组最多可以保存预定数量的任何类型的变量,并且可以使用以下语法访问它们的值:

someArray[someLocation];

让我们实际使用一些数组来尝试并了解如何在实际代码中使用它们以及我们可能用它们做什么。

一个数组的简单示例

让我们通过以下步骤编写一个真正简单的数组工作示例。您可以在可下载的代码包中找到此示例的完整代码。它在Chapter5/SimpleArrayExample/MainActivity.java中:

  1. 创建一个带有空白活动的项目,就像我们在第二章中所做的那样,开始使用 Android。同时,通过删除不必要的部分来清理代码,但这并非必要。

  2. 首先,我们声明我们的数组,分配五个空间,并为每个元素初始化一些值:

//Declaring an array
int[] ourArray;

//Allocate memory for a maximum size of 5 elements
ourArray = new int[5];

//Initialize ourArray with values
//The values are arbitrary as long as they are int
//The indexes are not arbitrary 0 through 4 or crash!

ourArray[0] = 25;
ourArray[1] = 50;
ourArray[2] = 125;
ourArray[3] = 68;
ourArray[4] = 47;
  1. 我们将每个值输出到logcat控制台。请注意,当我们将数组元素相加时,我们是在多行上这样做的。这没问题,因为我们在最后一个操作之前省略了分号,所以 Java 编译器将这些行视为一个语句:
//Output all the stored values
Log.i("info", "Here is ourArray:");
Log.i("info", "[0] = "+ourArray[0]);
Log.i("info", "[1] = "+ourArray[1]);
Log.i("info", "[2] = "+ourArray[2]);
Log.i("info", "[3] = "+ourArray[3]);
Log.i("info", "[4] = "+ourArray[4]);

//We can do any calculation with an array element
//As long as it is appropriate to the contained type
//Like this:
int answer = ourArray[0] +
    ourArray[1] +
    ourArray[2] +
    ourArray[3] +
    ourArray[4];

Log.i("info", "Answer = "+ answer);
  1. 在模拟器上运行示例。

请记住,在模拟器显示上不会发生任何事情,因为整个输出将被发送到我们在 Android Studio 中的logcat控制台窗口。以下是前面代码的输出:

info﹕ Here is ourArray:
info﹕ [0] = 25
info﹕
 [1] = 50
info﹕ [2] = 125
info﹕ [3] = 68
info﹕ [4] = 47
info﹕ Answer = 315 

在第 2 步中,我们声明了一个名为ourArray的数组,以保存int变量,并为该类型的最多五个变量分配了空间。

接下来,我们为数组中的五个空间中的每一个分配了一个值。请记住,第一个空间是ourArray[0],最后一个空间是ourArray[4]

在第 3 步,我们简单地将每个数组位置的值打印到控制台。从输出中,我们可以看到它们保存了我们在上一步中初始化的值。然后我们将ourArray中的每个元素相加,并将它们的值初始化为answer变量。然后我们将answer打印到控制台,并看到所有的值都被加在一起,就像它们是存储在稍微不同方式的普通旧int类型中一样,这正是它们的本质。

与数组一起变得动态起来

正如我们在所有这些数组内容的开头讨论的那样,如果我们需要单独声明和初始化数组的每个元素,那么数组与常规变量相比并没有太大的好处。让我们看一个动态声明和初始化数组的例子。

动态数组示例

通过以下步骤创建一个真正简单的动态数组。您可以在下载包中找到此示例的工作项目。它在Chapter5/DynamicArrayExample/MainActivity.java中:

  1. 创建一个带有空白活动的项目,就像我们在第二章中所做的那样,开始使用 Android。同时,通过删除不必要的部分来清理代码,但这并非必要。

  2. onCreate的大括号之间键入以下内容。在我们讨论并分析代码之前,看看您能否弄清楚输出将是什么:

//Declaring and allocating in one step
int[] ourArray = new int[1000];

//Let's initialize ourArray using a for loop
//Because more than a few variables is allot of typing!
for(int i = 0; i < 1000; i++){
   //Put the value of ourValue into our array
   //At the position determined by i.
   ourArray[i] = i*5;

            //Output what is going on
            Log.i("info", "i = " + i);
            Log.i("info", "ourArray[i] = " + ourArray[i]);
}
  1. 在模拟器上运行示例。请记住,在模拟器显示上不会发生任何事情,因为整个输出将被发送到我们在 Android Studio 中的logcat控制台窗口。以下是前面代码的输出:
info﹕ i = 0
info﹕ ourArray[i] = 0
info﹕ i = 1
info﹕ ourArray[i] = 5
info﹕ i = 2
info﹕
 ourArray[i] = 10

我已经删除了循环的 994 次迭代以简洁起见:

info﹕ ourArray[i] = 4985
info﹕ i = 998
info﹕ ourArray[i] = 4990
info﹕ i = 999
info﹕ ourArray[i] = 4995

所有的操作都发生在第 2 步。我们声明并分配了一个名为ourArray的数组,以容纳最多 1,000 个int值。然而,这一次,我们在一行代码中完成了这两个步骤:

int[] ourArray = new int[1000];

然后,我们使用了一个for循环,设置为循环 1,000 次:

(int i = 0; i < 1000; i++){

我们用i乘以5的值初始化了数组中从 0 到 999 的空间,如下所示:

ourArray[i] = i*5;

为了演示i的值以及数组中每个位置上保存的值的价值,我们按如下方式输出i的值,然后是数组中相应位置上保存的值:

Log.i("info", "i = " + i);
Log.i("info", "ourArray[i] = " + ourArray[i]);

所有这些都发生了 1,000 次,产生了我们看到的输出。

进入数组的第 n 维

我们非常简要地提到数组甚至可以在每个位置上容纳其他数组。现在,如果一个数组包含许多包含其他类型的数组,我们如何访问包含的数组中的值?为什么我们需要这个?看看多维数组何时有用的下一个示例。

多维数组的一个示例

让我们通过以下步骤创建一个非常简单的多维数组。您可以在下载包中找到此示例的工作项目。它位于Chapter5/MultidimensionalArrayExample/MainActivity.java

  1. 创建一个带有空白活动的项目,就像我们在第二章中所做的那样,开始 Android。同时,通过删除不必要的方法来清理代码,但这并非必需。

  2. 在调用setContentView之后,声明并初始化一个二维数组,如下所示:

//A Random object for generating question numbers later
Random randInt = new Random();
//And a variable to hold the random value generated
int questionNumber;

//We declare and allocate in separate stages for clarity
//but we don't have to
String[][] countriesAndCities;
//Here we have a 2 dimensional array

//Specifically 5 arrays with 2 elements each
//Perfect for 5 "What's the capital city" questions
countriesAndCities = new String[5][2];

//Now we load the questions and answers into our arrays
//You could do this with less questions to save typing
//But don't do more or you will get an exception
countriesAndCities [0][0] = "United Kingdom";
countriesAndCities [0][1] = "London";

countriesAndCities [1][0] = "USA";
countriesAndCities [1][1] = "Washington";

countriesAndCities [2][0] = "India";
countriesAndCities [2][1] = "New Delhi";

countriesAndCities [3][0] = "Brazil";
countriesAndCities [3][1] = "Brasilia";

countriesAndCities [4][0] = "Kenya";
countriesAndCities [4][1] = "Nairobi";
  1. 现在我们使用for循环和Random类对象输出数组的内容。请注意,尽管问题是随机的,但我们始终可以选择正确的答案:
//Now we know that the country is stored at element 0
//The matching capital at element 1
//Here are two variables that reflect this
int country = 0;
int capital = 1;

//A quick for loop to ask 3 questions
for(int i = 0; i < 3; i++){
   //get a random question number between 0 and 4
   questionNumber = randInt.nextInt(5);

   //and ask the question and in this case just
   //give the answer for the sake of brevity
  Log.i("info", "The capital of " +countriesAndCities[questionNumber][country]);

  Log.i("info", "is " +countriesAndCities[questionNumber][capital]);

}//end of for loop

在模拟器上运行示例。再次强调,模拟器显示屏上不会发生任何事情,因为输出将发送到我们在 Android Studio 中的logcat控制台窗口。这是先前代码的输出:

info﹕ The capital of USA
info﹕ is Washington
info﹕ The capital of India
info﹕ is New Delhi
info﹕ The capital of United Kingdom
info﹕ is London

刚刚发生了什么?让我们一块一块地过一遍,这样我们就知道到底发生了什么。

我们创建一个Random类型的新对象,称为randInt,准备在程序后面生成随机数:

Random randInt = new Random();

我们声明一个简单的int变量来保存问题编号:

int questionNumber;

然后我们声明countriesAndCities,我们的数组数组。外部数组保存数组:

String[][] countriesAndCities;

现在我们在数组中分配空间。第一个外部数组将能够容纳五个数组,每个内部数组将能够容纳两个字符串:

countriesAndCities = new String[5][2];

接下来,我们初始化数组以保存国家及其对应的首都。请注意,每对初始化中,外部数组编号保持不变,表示每个国家/首都对在一个内部数组(字符串数组)中。当然,这些内部数组中的每一个都保存在外部数组的一个元素中(保存数组):

countriesAndCities [0][0] = "United Kingdom";
countriesAndCities [0][1] = "London";

countriesAndCities [1][0] = "USA";
countriesAndCities [1][1] = "Washington";

countriesAndCities [2][0] = "India";
countriesAndCities [2][1] = "New Delhi";

countriesAndCities [3][0] = "Brazil";
countriesAndCities [3][1] = "Brasilia";

countriesAndCities [4][0] = "Kenya";
countriesAndCities [4][1] = "Nairobi";

为了使即将到来的for循环更清晰,我们声明并初始化int变量来表示数组中的国家和首都。如果您回顾一下数组初始化,所有国家都保存在内部数组的位置0,所有对应的首都都保存在位置1

int country = 0;
int capital = 1;

现在我们创建一个for循环,将运行三次。请注意,这个数字并不意味着我们访问数组的前三个元素。这只是循环的次数。我们可以让它循环一次或一千次,但示例仍然有效:

for(int i = 0; i < 3; i++){

接下来,我们实际确定要问什么问题,或者更具体地说,我们外部数组的哪个元素。请记住,randInt.nextInt(5)返回 0 到 4 之间的数字。这正是我们需要的,因为我们有一个包含五个元素的外部数组,从 0 到 4:

questionNumber = randInt.nextInt(5);

现在我们可以通过输出内部数组中保存的字符串来提问,而内部数组又由前一行中随机生成的数字选择的外部数组保存:

  Log.i("info", "The capital of " +countriesAndCities[questionNumber][country]);

  Log.i("info", "is " +countriesAndCities[questionNumber][capital]);

}//end of for loop

值得一提的是,我们在本书的其余部分将不再使用任何多维数组。因此,如果对这些数组内部的数组还有一点模糊,那也没关系。您知道它们存在以及它们能做什么,所以如果有必要,您可以重新访问它们。

数组越界异常

当我们尝试访问一个不存在的数组元素时,就会发生数组越界异常。每当我们尝试这样做,就会出现错误。有时,编译器会捕捉到它,以防止错误进入工作中的游戏,就像这样:

int[] ourArray = new int[1000];
int someValue = 1;//Arbitrary value
ourArray[1000] = someValue;//Won't compile as compiler knows this won't work.
//Only locations 0 through 999 are valid

猜猜如果我们写出这样的东西会发生什么:

int[] ourArray = new int[1000];
int someValue = 1;//Arbitrary value
int x = 999;
if(userDoesSomething){x++;//x now equals 1000
}
ourArray[x] = someValue;
//Array out of bounds exception if userDoesSomething evaluates to true! This is because we end up referencing position 1000 when the array only has positions 0 through 999
//Compiler can't spot it and game will crash on player - yuck!

我们避免这个问题的唯一方法是了解规则。规则是数组从零开始,一直到从分配的数字中减去一得到的数字。我们还可以使用清晰、可读的代码,在这种代码中很容易评估我们所做的事情并发现问题。

线程的时间控制

那么什么是线程呢?你可以把 Java 编程中的线程想象成故事中的线程。在故事的一个线程中,我们有主要角色在前线与敌人作战,而在另一个线程中,士兵的家人们日复一日地生活。当然,一个故事不一定只有两个线程。我们可以引入第三个线程。也许故事还讲述了政客和军事指挥官做出决策。这些决策会微妙地或者不那么微妙地影响其他线程中发生的事情。

编程中的线程就是这样。我们在程序中创建部分/线程,它们为我们控制不同的方面。我们引入线程来代表这些不同的方面,是因为以下原因:

  • 从组织的角度来看,它们是有意义的

  • 它们是一种经过验证的程序结构方式

  • 我们正在工作的系统的性质迫使我们使用它们

在 Android 中,我们同时出于以上所有原因使用线程。这是有道理的,它有效,而且我们必须使用它,因为系统的设计需要。

在游戏中,想象一下一个线程接收玩家的“左”、“右”和“射击”按钮点击,一个线程代表外星人思考下一步要移动到哪里,还有另一个线程在屏幕上绘制所有的图形。

多线程程序可能会出现问题。就像故事的线程一样,如果适当的同步没有发生,事情就会出错。如果我们的士兵在战斗甚至战争存在之前就进入了战斗,会怎么样?奇怪!

如果我们有一个变量int x,代表着我们程序中三个线程使用的关键数据,会发生什么呢?如果一个线程稍微领先一些,使得数据对其他两个线程来说变得“错误”了,会发生什么?这个问题就是正确性的问题,由多个线程竞争完成而引起,它们互相不知道对方的存在,因为它们毕竟只是愚蠢的代码。

正确性的问题可以通过密切监督线程和锁定来解决。锁定意味着暂时阻止一个线程的执行,以确保事情以同步的方式工作。这就像冻结士兵不让他登上战舰,直到战舰实际靠岸并放下了栈板,避免尴尬的溅水。

多线程程序的另一个问题是死锁的问题,其中一个或多个线程被锁住,等待合适的时机来访问x,但那个时机从未到来,整个程序最终停滞不前。

你可能已经注意到,第一个问题(正确性)的解决方案是导致第二个问题(死锁)的原因。现在考虑我们刚刚讨论的所有内容,并将其与 Android Activity 生命周期混合在一起。你可能开始感到这种复杂性有点令人恶心。

幸运的是,这个问题已经为我们解决了。就像我们使用Activity类并重写其方法与 Android 生命周期进行交互一样,我们也可以使用其他类来创建和管理我们的线程。就像使用Activity一样,我们只需要知道如何使用它们,而不需要知道它们是如何工作的。

所以为什么要告诉我所有这些关于线程的东西,当我不需要知道的时候,你可能会问。这只是因为我们将编写看起来不同并且结构不熟悉的代码。如果我们能做到以下几点,我们将毫不费力地编写我们的 Java 代码来创建和在我们的线程中工作:

  • 接受我们将介绍的新概念是我们需要处理的,以便为处理与线程相关的问题创建一个特定于 Android 的解决方案

  • 了解线程的一般概念,这与几乎同时发生的故事线程大致相同

  • 学习使用一些 Android 线程类的几个规则

请注意,我在第三个项目中说了类,复数。不同的线程类在不同的情况下效果最好。你可以写一整本关于 Android 线程的书。在本书中,我们将使用两个线程类。在本章中,我们将使用Handler。在第七章,复古乒乓球游戏,和第八章,贪吃蛇游戏中,我们将使用Runnable类。我们需要记住的是,我们将编写几乎同时运行的程序部分。

提示

我所说的“几乎”是什么意思?实际上发生的是 CPU 轮流在线程之间切换。然而,这一切发生得如此之快,以至于我们除了同时性之外无法感知到任何东西。

使用 Handler 类创建一个简单的线程计时器示例

在这个例子之后,当我们意识到线程并不像最初害怕的那么复杂时,我们可以松一口气。在真正的游戏中使用线程时,我们将不得不在这个简单的例子中的代码旁边添加一些额外的代码,但这并不多,我们会在到达时讨论它。

像往常一样,你可以直接使用下载包中的完整代码。这个项目位于Chapter5/SimpleThreadTimer/MainActivity.java

正如名称所示,我们将创建一个计时器 - 在许多游戏中非常有用的功能:

  1. 创建一个带有空白活动的项目,就像我们在第二章中所做的那样,开始使用 Android。同时,通过删除不必要的部分来清理代码,但这并不是必要的。

  2. 在类声明之后,输入三行高亮显示的代码:

public class MainActivity extends Activity {

    private Handler myHandler;
 boolean gameOn;
 long startTime;

  1. 将此代码输入onCreate方法中。它将创建一个线程,同时在if(gameOn)块中进行其他操作:
//How many milliseconds is it since the UNIX epoch
        startTime = System.currentTimeMillis();

        myHandler = new Handler() {
            public void handleMessage(Message msg) {
                super.handleMessage(msg);

                if (gameOn) {
                    long seconds = ((System.currentTimeMillis() - startTime)) / 1000;
                    Log.i("info", "seconds = " + seconds);
                }

                myHandler.sendEmptyMessageDelayed(0, 1000);
            }

        };

        gameOn = true;
        myHandler.sendEmptyMessage(0);
    }
  1. 运行应用程序。在模拟器上使用主页或返回按钮退出。请注意,它仍然在控制台上打印。当我们实现我们的记忆游戏时,我们将处理这个异常。

当你在模拟器上运行示例时,请记住模拟器显示屏上不会发生任何事情,因为所有的输出都将发送到我们在 Android Studio 中的logcat控制台窗口。这是先前代码的输出:

info﹕ seconds = 1
info﹕ seconds = 2
info﹕ seconds = 3
info﹕ seconds = 4
info﹕ seconds = 5
info﹕ seconds = 6

那刚刚发生了什么?每隔 1 秒,经过的秒数被打印到控制台上。让我们学习一下这是如何发生的。

首先,我们声明一个名为myHandlerHandler类型的新对象。然后我们声明一个名为gameOn的布尔变量。我们将使用这个变量来跟踪我们的游戏何时运行。最后,这段代码块的最后一行声明了一个long类型的变量。你可能还记得long类型来自第三章,说 Java - 你的第一个游戏。我们可以使用long变量来存储非常大的整数,这就是我们在这里使用startTime的方式:

private Handler myHandler;
boolean gameOn;
long startTime;

接下来,我们使用System类的currentTimeMillis方法初始化了startTime。这个方法保存了自 1970 年 1 月 1 日以来的毫秒数。我们将看到我们如何在下一行代码中使用这个值。

startTime = System.currentTimeMillis();

接下来是重要的代码。直到if(gameOn)之前的所有内容都是用来定义我们的线程的代码。当然,这段代码有点复杂,但实际上并没有看起来那么糟糕。还要记住,我们只需要使用线程;我们不需要理解它们的所有工作原理。

让我们分解前面的代码,以便更好地理解。myHandler = new Handler()这一行只是初始化了我们的myHandler对象。与之前看到的不同的是,我们立即对对象进行了自定义。我们重写了handleMessage方法(这是我们在线程中运行代码的地方),然后调用了super.handleMessage,它在运行我们的自定义代码之前调用了handleMessage的默认版本。这很像我们每次调用super.onCreate时对onCreate方法所做的事情。

然后我们有if(gameOn)块。该if块中的所有内容都是我们想要在线程中运行的代码。if(gameOn)块只是给了我们一个控制是否要运行代码的方式。例如,我们可能希望线程一直运行,但只有在某些时候运行我们的代码。if语句让我们可以轻松选择。现在看一下代码。稍后我们将分析if块中发生了什么:

myHandler = new Handler() {
     public void handleMessage(Message msg) {
       super.handleMessage(msg);

       if (gameOn) {
         long seconds = ((System.currentTimeMillis() - startTime)) / 1000;
             Log.i("info", "seconds = " + seconds);
         }

       myHandler.sendEmptyMessageDelayed(0, 1000);
      }

 };

if块内,我们声明并初始化了另一个名为secondslong变量,并对其进行了一些简单的数学运算:

long seconds = ((System.currentTimeMillis() - startTime)) / 1000;

首先,我们获取自 1970 年 1 月 1 日以来的当前毫秒数,然后从中减去startTime。这给了我们自初始化startTime以来的毫秒数。然后我们将答案除以 1000,得到一个秒数。我们用以下行将这个值打印到控制台:

Log.i("info", "seconds = " + seconds);

接下来,在我们的if块之后,我们有这样一行:

myHandler.sendEmptyMessageDelayed(0, 1000);

前一行告诉 Android 系统,我们希望每 1000 毫秒(一秒一次)运行一次handleMessage方法中的代码。

onCreate中,在handleMessage方法和Handler类的结束大括号之后,我们最终将gameOn设置为true,以便可以运行if块中的代码:

gameOn = true;

然后,代码的最后一行开始了我们的线程与 Android 系统之间的消息流:

myHandler.sendEmptyMessage(0);

值得指出的是,if块中的代码可以是我们需要的最少或最多。当我们实现我们的记忆游戏时,我们将在if块中看到更多的代码。

我们真正需要知道的是,我们刚刚看到的有些复杂的设置允许我们在一个新的线程中运行if块中的内容。就是这样!也许除了稍微快速地涉及System类之外。

注意

System类有很多用途。在这种情况下,我们使用它来获取自 1970 年 1 月 1 日以来的毫秒数。这是计算机中用于测量时间的常见系统。它被称为 Unix 时间,1970 年 1 月 1 日的第一毫秒被称为 Unix 纪元。在本书中我们将再次遇到这个概念。

关于线程就说这么多,让我们发出一些声音吧!

蜂鸣声和嗡嗡声 - Android 音效

这一部分将分为两部分——创建和使用音效。那么让我们开始吧。

创建音效

多年前,每当我制作游戏时,我会花费很多时间在提供免版税音效的网站上搜寻。虽然有很多好的音效,但真正优秀的音效总是很昂贵,无论你付多少钱,它们永远不会完全符合你的要求。然后一个朋友指出了一个名为 Bfxr 的简单开源应用,自那以后我再也没有浪费时间寻找音效。我们可以自己制作。

以下是使用 Bfxr 制作自己的音效的快速指南。从www.bfxr.net免费下载 Bfxr。

按照网站上的简单说明进行设置。尝试一些示例来制作酷炫的音效:

提示

这是一个严重简化的教程。您可以使用 Bfxr 做更多事情。要了解更多,请阅读上一个 URL 网站上的提示。

  1. 运行bfxr.exe创建声音效果

  2. 尝试所有预设类型,这些类型会生成该类型的随机声音。当您有一个接近您想要的声音时,转到下一步:创建声音效果

  3. 使用滑块微调您的新声音的音调、持续时间和其他方面:创建声音效果

  4. 通过单击导出 Wav按钮保存您的声音。尽管这个按钮的名称是这样的,但正如我们将看到的那样,我们可以保存为.wav以外的格式。创建声音效果

  5. Android 喜欢使用 OGG 格式的声音,因此在要求命名文件时,请在您决定的文件名后面使用.ogg扩展名。

  6. 根据需要重复 2 到 5 步。

提示

本书中需要声音样本的每个项目都提供了声音样本,但正如我们所见,制作自己的样本更有趣。您只需要将它们保存为与提供的样本相同的文件名。

在 Android 中播放声音

为了完成这个简短的示例,您需要三个以.ogg格式保存的声音效果。因此,如果您手头没有它们,请返回到创建声音效果部分进行制作。或者,您可以使用代码包的Chapter5/PlayingSounds/assets文件夹中提供的声音。像往常一样,您可以查看或使用Chapter5/PlayingSounds/java/MainActivity.javaChapter5/PlayingSounds/layout/activity_main.xml中已经完成的代码。现在执行以下步骤:

  1. 创建一个空白活动的项目,就像我们在第二章中所做的那样,开始使用 Android。此外,通过删除不必要的部分来清理代码,尽管这并非必要。

  2. 创建三个声音文件并将它们保存为sample1.oggsample2.oggsample3.ogg

  3. 在项目资源管理器窗口的main文件夹中,我们需要添加一个名为assets的文件夹。因此,在项目资源管理器窗口中,右键单击main文件夹,然后导航到新建 | 目录。在新建目录对话框中输入assets

  4. 现在将三个声音文件复制并粘贴到新创建的assets文件夹中。或者,选择这三个文件,右键单击它们,然后单击复制。然后单击 Android Studio 项目资源管理器中的assets文件夹。现在右键单击assets文件夹,然后单击粘贴

  5. 在编辑器窗口中打开activity_main.xml,并将三个按钮小部件拖放到您的 UI 上。它们的位置或对齐方式并不重要。当您查看我们三个新按钮中的任何一个的属性窗口中的id属性时,您会注意到它们已经自动分配了id属性。它们是buttonbutton2button3。正如我们将看到的那样,这正是我们需要的。

  6. 让我们通过实现onClickListener使我们的活动能够监听按钮的点击,就像我们在所有其他示例中使用按钮一样。在编辑器窗口中打开MainActivity.java。将public class MainActivity extends Activity {行替换为以下代码行:

public class MainActivity extends Activity implements View.
    OnClickListener {
  1. 与以前一样,我们的新代码行上出现了难看的红色下划线。上次发生这种情况时,我们在必须实现的onClick方法的空体中键入,一切都很好。这一次,因为我们已经知道这里发生了什么,我们将学习一个快捷方式。将鼠标悬停在错误上,右键单击它。现在单击生成...,然后选择实现方法...。在选择要实现的方法对话框中,onClick(View):void将被自动选择:在 Android 中播放声音

  2. 通过单击确定来选择此选项。现在滚动到代码底部,看到 Android Studio 已经很好地为您实现了onClick方法,错误也已经消失。

  3. MainActivity声明之后键入此代码,以声明一些用于我们的音效的变量:

private SoundPool soundPool;
int sample1 = -1;
int sample2 = -1;
int sample3 = -1;
  1. onCreate方法中键入此代码,将我们的声音加载到内存中:
soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC,0);
  try{
    //Create objects of the 2 required classes
          AssetManager assetManager = getAssets();
          AssetFileDescriptor descriptor;

          //create our three fx in memory ready for use
          descriptor = assetManager.openFd("sample1.ogg");
          sample1 = soundPool.load(descriptor, 0);

          descriptor = assetManager.openFd("sample2.ogg");
          sample2 = soundPool.load(descriptor, 0);

          descriptor = assetManager.openFd("sample3.ogg");
          sample3 = soundPool.load(descriptor, 0);

        }catch(IOException e){
            //catch exceptions here
        }
  1. 现在添加代码来获取对 UI 中按钮的引用并监听点击事件:
  //Make a button from each of the buttons in our layout
     Button button1 =(Button) findViewById(R.id.button);
     Button button2 =(Button) findViewById(R.id.button2);
     Button button3 =(Button) findViewById(R.id.button3);

     //Make each of them listen for clicks
     button1.setOnClickListener(this);
     button2.setOnClickListener(this);
     button3.setOnClickListener(this);
  1. 最后,在我们自动生成的onClick方法中键入此代码:
switch (view.getId()) {

  case R.id.button://when the first button is pressed
    //Play sample 1
          soundPool.play(sample1, 1, 1, 0, 0, 1);
          break;

          //Now the other buttons
          case R.id.button2:
          soundPool.play(sample2, 1, 1, 0, 0, 1);
          break;

          case R.id.button3:
          soundPool.play(sample3, 1, 1, 0, 0, 1);
          break;
        }

在模拟器或真实的 Android 设备上运行示例。注意,通过单击按钮,您可以随意播放三个声音样本中的任何一个。当然,几乎可以在任何时候播放声音,而不仅仅是在按下按钮时。也许它们也可以从一个线程中播放。在本章后面实现记忆游戏时,我们将看到更多的声音样本。

这就是代码的工作原理。我们首先按照通常的方式设置了一个新项目。然而,在步骤 2 到 5 中,我们使用 Bfxr 创建了一些声音,创建了一个assets文件夹,并将文件放在其中。这是 Android 期望找到声音文件的文件夹。因此,当我们在接下来的步骤中编写引用声音文件的代码时,Android 系统将能够找到它们。

在步骤 6 到 8 中,我们使我们的活动能够监听按钮点击,就像我们以前做过好几次一样。只是这一次,我们让 Android Studio 自动生成了onClick方法。

然后我们看到了这段代码:

private SoundPool soundPool;

首先,我们创建了一个名为soundPoolSoundPool类型的对象。这个对象将是我们的 Android 设备发出声音的关键。接下来,我们有这段代码:

int sample1 = -1;
int sample2 = -1;
int sample3 = -1;

上述代码非常简单;我们声明了三个int变量。然而,它们的作用略微超出了普通的int变量。正如我们将在下一段代码中看到的,它们将用于保存加载到内存中的声音文件的引用。换句话说,Android 系统将为每个变量分配一个数字,该数字将指向内存中我们的声音文件所在的位置。

我们可以把这看作是我们变量仓库中的一个位置。所以我们知道int变量的名称,它包含的是 Android 需要找到我们的声音的内容。这是我们如何将声音加载到内存中并使用我们刚刚讨论过的引用的方法。

让我们将步骤 10 中的代码分解成几个部分。仔细观察一下,然后我们将分析其中的内容:

soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC,0);

在这里,我们初始化了我们的soundPool对象,并请求最多 10 个同时播放的声音流。我们应该能够真正地按下应用按钮,并在每次按下时听到声音。AudioManager.STREAM_MUSIC描述了流的类型。这对于这种类型的应用程序是典型的。最后,0参数表示我们希望获得默认质量的声音。

现在我们看到了一些新的东西。注意到下一段代码被包裹在两个块trycatch中。这意味着如果try块中的代码失败,我们希望运行catch块中的代码。正如你所看到的,catch块中除了一个注释之外什么也没有。

我们必须这样做是因为SoundPool类的设计方式。如果你尝试在没有trycatch块的情况下编写代码,它是行不通的。这是 Java 类在读取文件时的典型情况。这是一个安全失败的过程,用来检查文件是否可读,甚至是否存在。你可以放置一行代码输出到控制台,表示发生了错误。

提示

如果你想尝试try/catch,那么在catch块中放置一行代码输出一条消息,并从assets文件夹中删除一个声音文件。当你运行应用程序时,加载将失败,并且catch块中的代码将被触发。

我们将放手一搏,因为我们非常确定文件会在那里并且会起作用。让我们仔细看看try块中的内容。仔细看一下下面的代码,然后我们将对其进行分析:

  try{
    //Create objects of the 2 required classes
          AssetManager assetManager = getAssets();
          AssetFileDescriptor descriptor;

          //create our three fx in memory ready for use
          descriptor = assetManager.openFd("sample1.ogg");
          sample1 = soundPool.load(descriptor, 0);

          descriptor = assetManager.openFd("sample2.ogg");
          sample2 = soundPool.load(descriptor, 0);

          descriptor = assetManager.openFd("sample3.ogg");
          sample3 = soundPool.load(descriptor, 0);

        }catch(IOException e){
            //catch exceptions here
        }

首先,我们创建一个名为assetManagerAssetManager类型对象和一个名为descriptorAssetFileDescriptor对象。然后,我们使用这两个对象组合来加载我们的第一个声音样本,就像这样:

          descriptor = assetManager.openFd("sample1.ogg");
          sample1 = soundPool.load(descriptor, 0);

现在我们已经在内存中加载了一个声音样本,并且将其位置保存在我们的名为sample1int变量中。第一个声音文件sample1.ogg现在已经准备好使用了。我们对sample2sample3执行相同的过程,然后我们就可以开始制造一些噪音了!

在第 11 步,我们设置了按钮,这是我们之前见过好几次的。在第 12 步,我们准备好了一个开关块,根据按下的按钮执行不同的操作。你可能会看到每个按钮执行的单个操作都是播放声音。例如,Button1就是这样的:

soundPool.play(sample1, 1, 1, 0, 0, 1);

这行代码播放了内存中加载的声音,位置由int sample1引用。

注意

方法的参数从左到右依次定义如下:要播放的样本、左声音、右声音、优先级、循环与否、播放速率。如果愿意的话,你可以玩一下这些参数。尝试将循环参数设置为3,将速率参数设置为1.5

我们以相同的方式处理每个按钮。现在让我们学点严肃的东西。

毁灭后的生活-持久性

好吧,这并不像听起来那么沉重,但在制作游戏时这是一个重要的话题。你可能已经注意到,最微小的事情都可能重置我们的数学游戏,比如来电、电池耗尽,甚至将设备倾斜到不同的方向。

当这些事件发生时,我们可能希望我们的游戏记住它的确切状态,这样当玩家回来时,它就会和离开时一样。如果你正在使用一个文字处理应用程序,你肯定会期望这种类型的行为。

我们不打算在游戏中做到这一点,但至少,作为最低限度,我们难道不应该记住最高分吗?这给玩家一个目标,最重要的是,给他们一个回到我们游戏的理由。

持久性的一个例子

Android 和 Java 有许多不同的方法来实现数据的持久性,从读写文件到设置和使用整个数据库。然而,在本书的示例中,最整洁、简单和合适的方法是使用SharedPreferences类。

在这个例子中,我们将使用SharedPreferences类来保存数据。实际上,我们将读写文件,但这个类会为我们隐藏所有的复杂性,让我们专注于游戏。

我们将看到一个相当抽象的持久性示例,以便在使用类似内容保存内存游戏的最高分之前,我们熟悉代码。这个示例的完整代码可以在代码包的Chapter5/Persistence/java/MainActivity.javaChapter5/Persistence/layout/activity_main.xml中找到:

  1. 创建一个空白活动的项目,就像我们在第二章中所做的那样,开始 Android。同时,通过删除不必要的部分来清理代码,但这并非必需。

  2. 在编辑窗口中打开activity_main.xml,从工具栏中拖动一个按钮到设计中。按钮分配的默认 ID 非常适合我们的用途,因此 UI 上不需要进一步的工作。

  3. 在编辑窗口中打开MainActivity.java。实现View.onClickListener,并自动生成所需的onClick方法,就像我们之前在在 Android 中播放声音示例的第 6 步和第 7 步中所做的那样。

  4. MainActivity声明之后输入以下代码。这声明了我们的两个对象,它们将在幕后执行所有复杂的工作:一堆有用的字符串和一个按钮:

SharedPreferences prefs;
SharedPreferences.Editor editor;
String dataName = "MyData";
String stringName = "MyString";
String defaultString = ":-(";
String currentString = "";//empty
Button button1;
  1. setContentView调用之后,将下一块代码添加到onCreate方法中。我们初始化我们的对象并设置我们的按钮。一旦示例完成,我们将仔细查看这段代码:
//initialize our two SharedPreferences objects
prefs = getSharedPreferences(dataName,MODE_PRIVATE);
editor = prefs.edit();

//Either load our string or
//if not available our default string
currentString = prefs.getString(stringName, defaultString);

 //Make a button from the button in our layout
 button1 =(Button) findViewById(R.id.button);

 //Make each it listen for clicks
 button1.setOnClickListener(this);

 //load currentString to the button
 button1.setText(currentString);
  1. 现在,动作发生在我们的onClick方法中。添加这段代码,它生成一个随机数并将其添加到currentString的末尾。然后它保存字符串并将字符串的值设置为按钮的值:
//we don't need to switch here!
//There is only one button
//so only the code that actually does stuff

//Get a random number between 0 and 9
Random randInt = new Random();
int ourRandom = randInt.nextInt(10);

//Add the random number to the end of currentString
currentString = currentString + ourRandom;

//Save currentString to a file in case the user 
//suddenly quits or gets a phone call
editor.putString(stringName, currentString);
editor.commit();

 //update the button text
 button1.setText(currentString);

在模拟器或设备上运行示例。请注意,每次按按钮时,都会将随机数附加到按钮的文本上。现在退出应用程序,甚至关闭设备。当您重新启动应用程序时,我们很酷的SharedPreferences类将简单地加载上次保存的字符串。

这是代码的工作原理。直到第 4 步,我们之前已经见过几次了:

SharedPreferences prefs;
SharedPreferences.Editor editor;

在这里,我们声明了两种类型的名为prefseditorSharedPreferences对象。我们将在一分钟内看到我们如何使用它们。

接下来,我们声明dataNamestringName字符串。我们这样做是因为要使用SharedPreferences的功能,我们需要使用一致的名称引用我们的数据集合以及其中的任何个别数据。通过初始化dataNamestringName,我们可以将它们用作我们数据存储的名称以及数据存储中的特定项目的名称。defaultString中的悲伤表情在SharedPreferences对象需要默认值时使用,因为要么之前没有保存任何内容,要么加载过程由于某种原因失败。currentString变量将保存我们将保存和加载的字符串的值,并显示给我们应用程序的用户。我们的按钮是button1

String dataName = "MyData";
String stringName = "MyString";
String defaultString = ":-(";
String currentString = "";//empty
Button button1;

在第 5 步中,真正的动作从这段代码开始:

prefs = getSharedPreferences(dataName,MODE_PRIVATE);
editor = prefs.edit();

currentString = prefs.getString(stringName, defaultString);

前面的代码执行的操作,如果没有SharedPreferences类,将需要更多的代码。前两行初始化对象,第三行从我们的数据存储项中加载值,该项的名称包含在stringName中,到我们的currentString变量。第一次发生这种情况时,它使用defaultString值,因为那里还没有存储任何内容,但一旦有值存储,这一行代码将加载我们保存的字符串。

在第 5 步结束时,我们设置了按钮,就像以前做过很多次一样。在onClick方法中继续进行第 6 步,没有switch块,因为只有一个按钮。因此,如果检测到点击,那么一定是我们的按钮。以下是onClick中的前三行:

Random randInt = new Random();
int ourRandom = randInt.nextInt(10);
currentString = currentString + ourRandom;

我们生成一个随机数并将其附加到currentString变量。接下来,在onClick中,我们这样做:

editor.putString(stringName, currentString);
editor.commit();

这就像加载我们的字符串的代码的相反。前两行中的第一行标识要将值写入的数据存储中的位置(stringName),以及要在那里写入的值(currentString)。接下来的一行,editor.commit();,简单地表示“继续并执行”。

以下一行将currentString显示为按钮上的文本,以便我们可以看到发生了什么:

button1.setText(currentString);

提示

有关持久性的更多信息,请查看本章末尾的自测问题部分的第二个问题。

记忆游戏

在记忆游戏中的代码不应该让我们感到太困难,因为我们已经对线程、数组、声音和持久性进行了背景研究。会有一些看起来新的代码,我们将在出现时详细检查它。

这是我们完成的游戏的屏幕截图:

记忆游戏

这是主屏幕。它显示高分,这些高分在游戏会话之间和设备关闭时保持不变。它还显示一个Play按钮,该按钮将带玩家到主游戏屏幕。看一下以下的屏幕截图:

记忆游戏

游戏屏幕本身将播放一系列声音和数字。相应的按钮将与相应的声音一起摇摆。然后玩家将能够与按钮交互,并尝试复制序列。对于玩家正确复制的每个部分,他们将获得积分。

如果整个序列被复制,那么将播放一个新的更长的序列,然后玩家将再次尝试重复序列。这将持续到玩家出现错误的部分。

随着分数的增加,它会显示在相关的 TextView 中,当一个序列被正确复制时,级别会增加并显示在分数下方。

玩家可以通过按下重播按钮开始新游戏。如果获得了最高分,它将被保存到文件并显示在主屏幕上。

游戏的实现分为五个阶段。每个阶段的结束都是休息的好地方。以下是游戏的不同阶段:

  • 阶段 1:实现 UI 和一些基础知识。

  • 阶段 2:这将准备我们的变量并向玩家呈现(要复制的)模式。

  • 阶段 3:在这个阶段,我们将处理玩家尝试复制模式时的响应。

  • 阶段 4:在这里,我们将使用刚刚学到的关于持久性的知识来在玩家退出游戏或关闭设备时保持玩家的最高分。

  • 阶段 5:在第 4 阶段结束时,我们将拥有一个完全可用的记忆游戏。然而,为了增加我们的 Android 技能,我们将在本章末讨论 Android UI 动画后完成这个阶段,这将增强我们的记忆游戏。

Chapter5/MemoryGame文件夹的下载包中可以找到所有五个阶段后包含完整代码和声音文件的文件。然而,在这个项目中,通过每个阶段都有很多东西可以学习。

阶段 1-UI 和基础知识

在这里,我们将布置主菜单屏幕 UI 和游戏本身的 UI。我们还将为一些 UI 元素配置一些 ID,以便稍后在我们的 Java 代码中控制它们:

  1. 创建一个名为Memory Game的新应用程序,并在需要时清理代码。

  2. 现在我们创建一个新的活动并将其命名为GameActivity。因此,在 Project Explorer 中右键单击java文件夹,导航到新建 | 活动,然后单击下一步,将活动命名为GameActivity,然后单击完成。为了清晰起见,以与我们清理所有其他活动相同的方式清理此活动。

  3. 使游戏全屏,并锁定方向,就像我们在第四章的全屏和锁定方向教程中所做的那样,发现循环和方法

  4. 打开res/layout文件夹中的activity_main.xml文件。

让我们通过以下步骤快速创建我们的主屏幕 UI:

  1. 在编辑器中打开activity_main.xml并删除Hello World TextView。

  2. 点击并拖动以下内容:大文本到顶部中心(创建我们的标题文本),图像就在其下方,另一个大文本在其下方(用于我们的最高分),以及一个按钮(供玩家点击以开始游戏)。您的 UI 应该看起来有点像下面截图中显示的样子:阶段 1-UI 和基础知识

  3. 调整两个 TextView 和 Button 元素的文本属性,以便清楚表明它们各自将用于什么。通常情况下,您可以用任何您选择的图像替换ImageView中的 Android 图标(就像我们在第四章的添加自定义图像教程中所做的那样)。

  4. 以通常的方式调整元素的大小,以适应您将在其上运行游戏的模拟器或设备。

  5. 让我们使Hi Score TextView 的 ID 更相关于其用途。左键单击选择Hi Score TextView,在属性窗口中找到其id属性,并将其更改为textHiScore。图像和标题的 ID 不是必需的,播放按钮的现有 ID 是button,似乎已经很合适了。所以这里没有其他需要更改的地方。

让我们将播放按钮连接起来,以在主屏幕和游戏屏幕之间创建链接,如下所示:

  1. 在编辑器中打开MainActivity.java

  2. MainActivity声明的末尾添加implements View.onClickListener,使其看起来像这样:

  public class MainActivity extends Activity implements View.OnClickListener {
  1. 现在将鼠标悬停在刚刚键入的行上,并右键单击它。现在单击生成,然后单击实现方法...,然后单击确定,以便 Android Studio 自动生成我们必须实现的onClick方法。

  2. 在我们的onCreate方法的结束处,在闭合大括号之前,输入以下代码以引用我们的播放按钮并监听点击:

  //Make a button from the button in our layout
   Button button =(Button) findViewById(R.id.button);

   //Make each it listen for clicks
   button.setOnClickListener(this);
  1. 滚动到我们的onClick方法,并在其主体中输入以下代码,使播放按钮将玩家带到我们即将设计的GameActivity
  Intent i;
   i = new Intent(this, GameActivity.class);
   startActivity(i);

此时,应用程序将运行,玩家可以单击播放按钮将他们带到我们的游戏屏幕。所以让我们快速创建我们的游戏屏幕 UI:

  1. 在编辑器中打开activity_game.xml,并删除Hello World TextView。

  2. 将三个大文本元素拖动到彼此下方,并水平居中。在它们下方,添加四个按钮,一个在另一个上方堆叠,最后,在下方添加另一个按钮,但将其偏移到右侧,使其看起来像下一个屏幕截图中显示的样子。我还调整了 UI 元素的文本属性,以清楚地说明每个元素将用于什么,但这是可选的,因为我们的 Java 代码将为我们完成所有工作。您还可以按照通常的方式调整元素的大小,以适应您将在其上运行游戏的模拟器或设备。第 1 阶段 - UI 和基础知识

  3. 现在让我们为我们的 UI 元素分配一些有用的 ID,以便我们可以在下一个教程中对它们进行一些 Java 魔术。以下是一个表,它将上一个屏幕截图中显示的 UI 元素与您需要分配的id属性值进行匹配。将以下id属性值分配给相应的 UI 元素:

目的 默认 id 属性 要分配的新 id
分数指示器 textView textScore
难度指示器 textView2 textDifficulty
观看/前进指示器 textView3 textWatchGo
按钮 1 button 保持默认
按钮 2 button2 保持默认
按钮 3 button3 保持默认
按钮 4 button4 保持默认
重播按钮 button5 buttonReplay

现在我们的游戏菜单和实际游戏 UI 已经准备就绪,我们可以开始让它运行。

第 2 阶段 - 准备我们的变量并呈现模式

在这里,我们将设置大量变量和对象供我们在这个阶段和以后的阶段使用。我们还将实现呈现给玩家模式的代码部分。我们将在以后的阶段添加使玩家能够做出反应的代码:

  1. 在编辑器窗口中打开GameActivity.java

  2. 我通过找到一个令人愉悦的声音,然后逐渐增加每个后续样本的频率滑块来制作声音。您可以使用MemoryGame项目中assets文件夹中的我的声音,也可以使用 Bfxr 创建自己的声音。

  3. 在项目资源管理器窗口中的main文件夹中,我们需要添加一个名为assets的文件夹。因此,在项目资源管理器窗口中,右键单击main文件夹,然后导航到新建 | 目录。在新目录对话框中键入assets

  4. 现在将四个声音文件复制并粘贴到新创建的assets文件夹中。您可以这样做:选择文件,右键单击它们,然后单击复制。然后单击 Android Studio 项目资源管理器中的assets文件夹。现在右键单击assets文件夹,然后单击粘贴

让我们准备GameActivity来监听按钮点击,就像我们为MainActivity所做的那样:

  1. GameActivity声明的末尾添加implementsView.onClickListener,使其看起来像这样:
  public class GameActivity extends Activity implements View.OnClickListener {
  1. 现在将鼠标悬停在您刚刚输入的行上,并右键单击它。现在单击生成,然后单击实现方法...,然后单击确定,以便让 Android Studio 自动生成我们即将使用的onClick方法。

  2. 让我们声明一些我们需要引用我们的 UI 和我们将很快加载的声音效果的int引用的对象。在GameActivity的声明之后写下这些代码。通过将它们放在这里,它们将对GameActivity.java中的所有部分都可用。以下是上下文中的代码:

public class GameActivity extends Activity implements View.OnClickListener {

//Prepare objects and sound references

    //initialize sound variables
    private SoundPool soundPool;
    int sample1 = -1;
    int sample2 = -1;
    int sample3 = -1;
    int sample4 = -1;

    //for our UI
    TextView textScore;
    TextView textDifficulty;
    TextView textWatchGo;

    Button button1;
    Button button2;
    Button button3;
    Button button4;
    Button buttonReplay;
  1. 现在,在上一步的代码的最后一行之后,输入以下代码片段,它将声明和初始化一些变量,供我们的线程使用。请注意,在最后,我们还声明了myHandler,它将是我们的线程,以及gameOn来控制我们线程中的代码是否被执行:
//Some variables for our thread
int difficultyLevel = 3;
//An array to hold the randomly generated sequence
int[] sequenceToCopy = new int[100];

private Handler myHandler;
//Are we playing a sequence at the moment?
boolean playSequence = false;
//And which element of the sequence are we on
int elementToPlay = 0;

//For checking the players answer
int playerResponses;
int playerScore;
boolean isResponding;
  1. onCreate方法中调用setContentView后,我们准备好播放我们的音效:
soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC,0);
try{
  //Create objects of the 2 required classes
  AssetManager assetManager = getAssets();
  AssetFileDescriptor descriptor;

  //create our three fx in memory ready for use
  descriptor = assetManager.openFd("sample1.ogg");
  sample1 = soundPool.load(descriptor, 0);

        descriptor = assetManager.openFd("sample2.ogg");
        sample2 = soundPool.load(descriptor, 0);

        descriptor = assetManager.openFd("sample3.ogg");
        sample3 = soundPool.load(descriptor, 0);

        descriptor = assetManager.openFd("sample4.ogg");
        sample4 = soundPool.load(descriptor, 0);

        }catch(IOException e){
            //catch exceptions here
        }
  1. 在上一步的代码之后,仍然在onCreate方法中,我们初始化我们的对象并为按钮设置点击监听器:
//Reference all the elements of our UI 
//First the TextViews
textScore = (TextView)findViewById(R.id.textScore);
textScore.setText("Score: " + playerScore);
textDifficulty = (TextView)findViewById(R.id.textDifficulty);

textDifficulty.setText("Level: " + difficultyLevel);
textWatchGo = (TextView)findViewById(R.id.textWatchGo);

//Now the buttons
button1 = (Button)findViewById(R.id.button);
button2 = (Button)findViewById(R.id.button2);
button3 = (Button)findViewById(R.id.button3);
button4 = (Button)findViewById(R.id.button4);
buttonReplay = (Button)findViewById(R.id.buttonReplay);

//Now set all the buttons to listen for clicks
button1.setOnClickListener(this);
button2.setOnClickListener(this);
button3.setOnClickListener(this);
button4.setOnClickListener(this);
buttonReplay.setOnClickListener(this);
  1. 现在,在上一步的代码的最后一行之后,输入将创建我们的线程的代码。我们将在if(playSequence)块中的下一步中添加细节。请注意,线程每九分之一秒(900 毫秒)运行一次。请注意,我们启动了线程,但没有将playSequence设置为true。所以它现在什么也不会做:
//This is the code which will define our thread
myHandler = new Handler() {
  public void handleMessage(Message msg) {
    super.handleMessage(msg);

          if (playSequence) {
          //All the thread action will go here

          }

          myHandler.sendEmptyMessageDelayed(0, 900);
  }
};//end of thread

myHandler.sendEmptyMessage(0);
  1. 在查看将在我们的线程中运行的代码之前,我们需要一种方法来生成适合难度级别的随机序列。这种情况听起来像是一个方法的候选。在GameActivity类的右花括号之前输入此方法:
public void createSequence(){
  //For choosing a random button
   Random randInt = new Random();
   int ourRandom;
   for(int i = 0; i < difficultyLevel; i++){
   //get a random number between 1 and 4
         ourRandom = randInt.nextInt(4);
         ourRandom ++;//make sure it is not zero
         //Save that number to our array
         sequenceToCopy[i] = ourRandom;
   }

}
  1. 我们还需要一个方法来准备和启动我们的线程。在createSequence的右花括号后输入以下方法:

提示

实际上,方法的实现顺序并不重要。然而,按顺序进行将意味着我们的代码看起来是一样的。即使您在引用下载的代码,顺序也是一样的。

public void playASequence(){
    createSequence();
    isResponding = false;
    elementToPlay = 0;
    playerResponses = 0;
    textWatchGo.setText("WATCH!");
    playSequence = true;
}
  1. 在查看线程代码的细节之前,我们需要一个方法来在播放序列后整理我们的变量。在playASequence的右花括号之后输入此方法:
public void sequenceFinished(){
        playSequence = false;
        //make sure all the buttons are made visible
        button1.setVisibility(View.VISIBLE);
        button2.setVisibility(View.VISIBLE);
        button3.setVisibility(View.VISIBLE);
        button4.setVisibility(View.VISIBLE);
        textWatchGo.setText("GO!");
        isResponding = true;
    }
  1. 最后,我们将实现我们的线程。这部分有一些新的代码,我们将在完成项目的这个阶段后详细介绍。在if(playSequence){ }块的左花括号和右花括号之间输入此代码:
if (playSequence) {
  //All the thread action will go here
  //make sure all the buttons are made visible
  button1.setVisibility(View.VISIBLE);
  button2.setVisibility(View.VISIBLE);
  button3.setVisibility(View.VISIBLE);
  button4.setVisibility(View.VISIBLE);

  switch (sequenceToCopy[elementToPlay]){
    case 1:
      //hide a button 
button1.setVisibility(View.INVISIBLE);
       //play a sound
       soundPool.play(sample1, 1, 1, 0, 0, 1);
       break;

    case 2:
      //hide a button 
button2.setVisibility(View.INVISIBLE)
      //play a sound
      soundPool.play(sample2, 1, 1, 0, 0, 1);
      break;

    case 3:
      //hide a button button3.setVisibility(View.INVISIBLE);
      //play a sound
      soundPool.play(sample3, 1, 1, 0, 0, 1);
      break;

  case 4:
      //hide a button 
button4.setVisibility(View.INVISIBLE);
      //play a sound
      soundPool.play(sample4, 1, 1, 0, 0, 1);
         break;
   }

   elementToPlay++;
   if(elementToPlay == difficultyLevel){
   sequenceFinished();
   }
}

    myHandler.sendEmptyMessageDelayed(0, 900);
}

};

提示

onCreate的右花括号之前,我们可以通过调用我们的playASequence方法来启动一个序列,就像这样:

playASequence();

然后我们可以运行我们的应用程序,在主屏幕上点击播放,观看四个随机按钮及其匹配的声音序列开始播放。在下一阶段,我们将连接重播按钮,以便玩家可以在准备好时开始序列。

哎呀!这是一个很长的过程。实际上,那里并没有太多新东西,但我们确实将我们学到的关于 Java 和 Android 的一切都塞进了一个地方,并且还以新的方式使用了它。因此,我们将逐步查看它,并对可能看起来棘手的部分进行额外关注。

让我们依次查看每个新的代码片段。

从步骤 1 到 7,我们初始化了变量,设置了按钮,并像以前一样加载了声音。我们还放入了我们线程代码的大纲。

在第 8 步中,我们实现了createSequence方法。我们使用Random对象生成 1 到 4 之间的随机数序列。我们在for循环中执行此操作,该循环会一直循环,直到创建了长度为difficultyLevel的序列。该序列存储在名为sequenceToCopy的数组中,我们稍后可以用它来与玩家的响应进行比较:

public void createSequence(){
        //For choosing a random button
        Random randInt = new Random();
        int ourRandom;
        for(int i = 0; i < difficultyLevel; i++){
            //get a random number between 1 and 4
            ourRandom = randInt.nextInt(4);
            ourRandom ++;//make sure it is not zero
            //Save that number to our array
            sequenceToCopy[i] = ourRandom;
        }

    }

在第 9 步中,我们实现了playASequence。首先,我们调用createSequence来加载我们的sequenceToCopy数组。然后,我们将isResponding设置为false,因为我们不希望玩家在序列仍在播放时乱按按钮。我们将elementToPlay设置为0,因为这是我们数组的第一个元素。我们还将playerResponses设置为0,准备计算玩家的响应。接下来,我们在 UI 上设置一些文本为"WATCH!",以便向玩家清楚地表明序列正在播放。最后,我们将playSequence设置为true,这允许我们的线程代码每 900 毫秒运行一次。以下是我们刚刚分析的代码:

public void playASequence(){
        createSequence();
        isResponding = false;
        elementToPlay = 0;
        playerResponses = 0;
        textWatchGo.setText("WATCH!");
        playSequence = true;

    }

在第 10 步中,我们处理了sequenceFinished。我们将playSequence设置为false,这样可以阻止我们线程中的代码运行。我们将所有按钮都设置为可见,因为正如我们将在线程代码中看到的那样,我们将它们设置为不可见以强调序列中接下来的按钮。我们将我们的 UI 文本设置为GO!,以明确表示。现在是玩家尝试复制序列并得分的时候了。为了使checkElement方法中的代码运行,我们将isResponding设置为true。我们将在下一个阶段查看checkElement方法中的代码:

public void sequenceFinished(){
        playSequence = false;
        //make sure all the buttons are made visible
        button1.setVisibility(View.VISIBLE);
        button2.setVisibility(View.VISIBLE);
        button3.setVisibility(View.VISIBLE);
        button4.setVisibility(View.VISIBLE);
        textWatchGo.setText("GO!");
        isResponding = true;
    }

在第 11 步中,我们实现了我们的线程。它相当长,但不太复杂。首先,我们将所有按钮都设置为可见,因为这比检查它们中的哪一个当前不可见并仅设置那一个要快得多:

if (playSequence) {
  //All the thread action will go here
  //make sure all the buttons are made visible
  button1.setVisibility(View.VISIBLE);
  button2.setVisibility(View.VISIBLE);
  button3.setVisibility(View.VISIBLE);
  button4.setVisibility(View.VISIBLE);

然后我们根据序列中的下一个数字进行切换,隐藏适当的按钮,并播放适当的声音。以下是switch块中的第一个情况供参考。其他情况元素执行相同的功能,但在不同的按钮上以及使用不同的声音:

switch (sequenceToCopy[elementToPlay]){
  case 1:
    //hide a buttonbutton1.setVisibility(View.INVISIBLE);
         //play a sound
         soundPool.play(sample1, 1, 1, 0, 0, 1);
         break;

    //case 2, 3 and go here
   }

现在我们增加elementToPlay,准备在线程大约 900 毫秒后再次运行时播放序列的下一部分:

   elementToPlay++;

接下来,我们检查是否已经播放了序列的最后一部分。如果是,我们调用我们的sequenceFinished方法来为玩家尝试他们的答案做好准备:

   if(elementToPlay == difficultyLevel){
   sequenceFinished();
   }
}

最后,我们告诉线程我们何时再次运行我们的代码:

    myHandler.sendEmptyMessageDelayed(0, 900);
}

};

当您运行序列(参见上一个提示)时,是否注意到我们游戏操作中的不完美/错误?这与序列的最后一个元素的动画方式有关。这是因为我们的sequenceFinished方法使所有按钮很快就可见,所以在按钮刚刚变得不可见后,看起来就像按钮根本没有变得不可见。当我们在第 5 阶段学习 UI 动画时,我们将解决按钮不保持足够长时间不可见的问题。

现在让我们处理玩家的响应。

第三阶段-玩家的响应

现在我们有一个应用程序,可以播放随机的按钮闪烁和匹配的声音序列。它还将该序列存储在一个数组中。所以现在我们要做的是让玩家尝试复制序列,并在成功时得分。

我们可以分两个阶段完成所有这些。首先,我们需要处理按钮的按压,这可以将所有的工作都传递给一个方法,该方法将完成其他所有工作。

让我们编写代码,并在进行时查看它。之后,我们将仔细检查不太明显的部分:

  1. 这是我们处理按钮按下的方法。我们有switch语句的空主体,还有一个额外的if语句,检查当前是否正在播放序列。如果有一个序列,那么不接受任何输入。我们将在下一步开始填写空主体中的代码:
if(!playSequence) {//only accept input if sequence not playing
            switch (view.getId()) {
                //case statements here...
            }
}
  1. 现在,这是处理button1的代码。请注意,它只播放与button1相关的声音,然后调用checkElement方法,传递一个值为 1。对于按钮 1 到 4,我们只需要做这些:播放声音,然后告诉我们的新方法(checkElement)按下了哪个编号的按钮,checkElement将完成其余工作:
case R.id.button:
  //play a sound
   soundPool.play(sample1, 1, 1, 0, 0, 1);
   checkElement(1);
   break;
  1. 以下是按钮 2 到 4 的几乎相同的代码。请注意,传递给checkElement和播放的声音样本是与上一步的唯一区别。在上一步的代码之后直接输入此代码:
case R.id.button2:
  //play a sound
   soundPool.play(sample2, 1, 1, 0, 0, 1);
   checkElement(2);
   break;

case R.id.button3:
   //play a sound
   soundPool.play(sample3, 1, 1, 0, 0, 1);
   checkElement(3);
   break;

case R.id.button4:
   //play a sound
   soundPool.play(sample4, 1, 1, 0, 0, 1);
   checkElement(4);
   break;
  1. 这是我们onClick方法中的最后一部分代码。这处理重新开始按钮。该代码只是重置分数和难度级别,然后调用我们的playASequence方法,该方法完成了重新开始游戏的其余工作。在上一步的代码之后直接输入此代码:
case R.id.buttonReplay:
   difficultyLevel = 3;
   playerScore = 0;
   textScore.setText("Score: " + playerScore);
   playASequence();
   break;
  1. 最后,这是我们的全能方法。与我们以前的大部分方法相比,这是一个相当长的方法,但看到它的整个结构会有所帮助。我们将在一分钟内逐行分解这个方法。在输入以下代码之后,您实际上将能够玩游戏并获得分数:
public void checkElement(int thisElement){

if(isResponding) {
  playerResponses++;
   if (sequenceToCopy[playerResponses-1] == thisElement) { //Correct
   playerScore = playerScore + ((thisElement + 1) * 2);
   textScore.setText("Score: " + playerScore);
   if (playerResponses == difficultyLevel) {//got the whole sequence
   //don't checkElement anymore
   isResponding = false;
   //now raise the difficulty
   difficultyLevel++;
   //and play another sequence
   playASequence();
    }

} else {//wrong answer
  textWatchGo.setText("FAILED!");
    //don't checkElement anymore
    isResponding = false;
}
}

我们在教程中对方法进行了相当全面的介绍。然而,房间里的大象是checkElement方法中代码的明显蔓延。所以让我们逐行通过第 6 步中的所有代码。

首先,我们有方法签名。请注意,它不返回值,但接收一个int值。请记住,调用这个方法的是onClick方法,它传递一个1234,取决于点击了哪个按钮:

public void checkElement(int thisElement){

接下来,我们将剩下的代码包装到一个if语句中。这是if语句。当isResponding布尔值为true时,我们进入这个块,而isRespondingsequenceFinnished方法完成时被设置为true,这正是我们需要的,这样玩家就不能在时机未到时乱按按钮,我们的游戏已经准备好听取指令:

if(isResponding) {

这是if块内发生的事情。我们增加了playerResponses变量中接收到的玩家响应的数量:

playerResponses++;

现在我们检查传递给checkElement方法并存储在thisElement中的数字是否与玩家试图复制的序列的适当部分匹配。如果匹配,我们将根据迄今为止正确匹配的序列部分数量增加playerScore。然后我们在屏幕上设置分数。请注意,如果响应不匹配,就会有一个else块与这个if块配对,我们很快会解释:

   if (sequenceToCopy[playerResponses-1] == thisElement) {  //Correct
      playerScore = playerScore + ((thisElement + 1) * 2);
      textScore.setText("Score: " + playerScore);

接下来,我们有另一个if块。请注意,这个if块嵌套在我们刚刚描述的if块内部。因此,只有在玩家的响应正确时才会被测试和可能运行。这个if语句检查是否是序列的最后一部分,就像这样:

      if (playerResponses == difficultyLevel) {

如果这是序列的最后一部分,它执行以下行:

//got the whole sequence
         //don't checkElement anymore
         isResponding = false;
         //now raise the difficulty
         difficultyLevel++;
         //and play another sequence
         playASequence();
   }

在检查嵌套的if语句内部发生的事情,检查整个序列是否已经正确复制,是这样的:它将isResponding设置为false,所以玩家不会从按钮得到任何响应。然后它将难度级别提高 1,以便下次序列会更加困难。最后,它调用playSequence方法来播放另一个序列,整个过程重新开始。

这是else块,如果玩家复制了序列的一部分错误,就会运行这个块:

} else {
  //wrong answer
  textWatchGo.setText("FAILED!");
  //don't checkElement anymore
  isResponding = false;
  }
}

在这里,我们在屏幕上设置一些文本,并将isResponding设置为false

现在让我们利用SharedPreferences类所学到的知识来保存高分。

第 4 阶段 - 保留最高分

这个阶段很简短。我们将使用本章前面学到的知识来保存玩家的分数,如果是新的最高分,然后在我们的MainActivity中的hi-score TextView 中显示最佳分数:

  1. 在编辑窗口中打开MainActivity.java

  2. 然后我们在类声明之后声明用于从文件中读取的对象,就像这样:

public class MainActivity extends Activity implements View.OnClickListener{

    //for our hiscore (phase 4)
 SharedPreferences prefs;
 String dataName = "MyData";
 String intName = "MyString";
 int defaultInt = 0;
 //both activities can see this
 public static int hiScore;

  1. 现在,在onCreate方法中的setContentView调用之后,我们初始化我们的对象,从文件中读取,并将结果设置为我们的hiScore变量。然后将其显示给玩家:
//for our high score (phase 4)
//initialize our two SharedPreferences objects
prefs = getSharedPreferences(dataName,MODE_PRIVATE);

//Either load our High score or
//if not available our default of 0
hiScore = prefs.getInt(intName, defaultInt);

//Make a reference to the Hiscore textview in our layout
TextView textHiScore =(TextView) findViewById(R.id.textHiScore);
//Display the hi score
textHiScore.setText("Hi: "+ hiScore);
  1. 接下来,我们需要回到GameActivity.java文件。

  2. 这次我们声明用于编辑文件的对象,就像这样:

//for our hiscore (phase 4)
SharedPreferences prefs;
SharedPreferences.Editor editor;
String dataName = "MyData";
String intName = "MyInt";
int defaultInt = 0;
int hiScore;
  1. onCreate方法中的setContentView调用之后,我们实例化我们的对象并为hiScore赋值:
//phase 4
//initialize our two SharedPreferences objects
prefs = getSharedPreferences(dataName,MODE_PRIVATE);
editor = prefs.edit();
hiScore = prefs.getInt(intName, defaultInt);
  1. 唯一与我们已经学到的不同的是,我们需要考虑在哪里放置代码来测试最高分,并在适当时写入我们的文件。考虑到:最终,每个玩家都必须失败。此外,当他们的分数最高但在再次尝试时重置之前,他们失败的时刻就是他们的分数最高的时刻。将以下代码放在else块中,处理玩家的错误答案。突出显示的代码是新代码;其余代码是为了帮助您理解上下文。
} else {//wrong answer

  textWatchGo.setText("FAILED!");
    //don't checkElement anymore
    isResponding = false;

 //for our high score (phase 4)
 if(playerScore > hiScore) {
 hiScore = playerScore;
 editor.putInt(intName, hiScore);
 editor.commit();
 Toast.makeText(getApplicationContext(), "New Hi-score", Toast.LENGTH_LONG).show();
 }

}

玩游戏并获得高分。现在退出应用程序,甚至重新启动手机。当您回到应用程序时,您的最高分仍然存在。

我们在这个阶段添加的代码几乎与我们在之前的持久性示例中编写的代码相同,唯一的区别是当达到新的最高分时,我们将数据存储到数据存储中,而不是当按下按钮时。此外,我们使用了editor.putInt方法,因为我们保存的是整数,而不是在保存字符串时使用editor.putString

动画我们的游戏

在继续之前,让我们思考一下动画。它到底是什么?这个词可能让人联想到移动的卡通角色和视频游戏中的游戏角色。

我们需要为我们的按钮添加动画效果(使它们移动),以便清楚地显示它们是序列的一部分。我们发现简单地使一个按钮消失然后重新出现是不够的。

控制 UI 元素的移动可能让我们想象复杂的for循环和逐像素计算。

幸运的是,Android 为我们提供了Animation类,允许我们在没有任何像素尴尬的情况下对 UI 对象进行动画处理。它是如何工作的。

注意

当然,要完全控制游戏对象的形状和大小,我们最终必须学会操作单个像素和线条。从第七章开始,当我们制作一个复古乒乓球风格的壁球游戏时,我们将这样做。

Android 中的 UI 动画

Android UI 中的动画可以分为三个阶段:

  • 使用我们将很快看到的特殊语法在文件中描述动画

  • 通过在我们的 Java 代码中创建一个对象来引用该动画

  • 在需要运行动画时将动画应用于 UI 元素

让我们看一些描述动画的代码。我们很快将在我们的记忆游戏中重用这段代码。展示它的目的并不是让我们理解其中的每一行。毕竟,学习 Java 应该足够成就感,而不必掌握这个。此外,目的是演示无论您能描述什么动画,都可以在我们的游戏中使用相同的 Java。

我们可以快速搜索网络以找到执行以下操作的代码:

  • 淡入淡出

  • 滑动

  • 旋转

  • 扩展或收缩

  • 变色

这是一些导致摇晃效果的代码。我们将在一个按钮上使用它,但您也可以在任何 UI 元素甚至整个屏幕上使用它:

<?xml version="1.0" encoding="utf-8"?>
<rotate 
    android:duration="100"
    android:fromDegrees="-5"
    android:pivotX="50%"
    android:pivotY="50%"
    android:repeatCount="8"
    android:repeatMode="reverse"
    android:toDegrees="5" />

第一行只是说明这是一个以 XML 格式编写的文件。接下来说明我们将执行一个旋转。然后我们说明持续时间将是 100 毫秒,旋转将从-5 度开始,中心将在xy轴上分别为 50%,重复八次,并反向到正 5 度。

这是一个很长的代码,但重点是很容易抓住一个有效的模板,然后自定义以适应我们的情况。我们可以将上述代码保存为类似wobble.xml的文件名。

然后我们可以简单地引用它如下:

Animation wobble = AnimationUtils.loadAnimation(this, R.anim.wobble);

现在我们可以这样播放动画,如下所示,对我们选择的 UI 对象进行操作,本例中是我们的button1对象:

button1.startAnimation(wobble);

第 5 阶段-UI 动画

让我们添加一个动画,当播放按钮声音时使按钮摇摆。同时,我们可以删除使按钮不可见的代码以及使其重新出现的代码。这不是最好的方法,但在开发游戏时起到了作用:

  1. 我们需要向我们的项目添加一个名为anim的新文件夹。因此,在 Project Explorer 窗口中右键单击res文件夹。导航到新建 | Android 资源目录,然后单击确定以创建新的anim文件夹。

  2. 现在右键单击anim文件夹,然后导航到新建 | 动画资源文件。在文件名字段中输入wobble,然后单击确定。现在我们在编辑器窗口中打开了一个名为wobble.xml的新文件。

  3. 用这段代码替换wobble.xml除第一行以外的所有内容:

<?xml version="1.0" encoding="utf-8"?>
<rotate 
 android:duration="100"
 android:fromDegrees="-5"
 android:pivotX="50%"
 android:pivotY="50%"
 android:repeatCount="8"
 android:repeatMode="reverse"
 android:toDegrees="5" />

  1. 现在切换到GameActivity.java

  2. 在我们的GameActivity类的声明之后添加以下代码:

//phase 5 - our animation object
Animation wobble;
  1. 在我们的onCreate方法中的setContentView调用后,添加以下代码:
//phase5 - animation
wobble = AnimationUtils.loadAnimation(this, R.anim.wobble);
  1. 现在,在我们的线程代码的开头附近,找到使我们的按钮重新出现的调用。像这样注释掉它们:
//code not needed as using animations
//make sure all the buttons are made visible
//button1.setVisibility(View.VISIBLE);
//button2.setVisibility(View.VISIBLE);
//button3.setVisibility(View.VISIBLE);
//button4.setVisibility(View.VISIBLE);
  1. 接下来,在上一步中的代码之后,在每个四个case语句中,我们需要注释掉调用setVisibility的行,并用我们的摇摆动画替换它们。以下代码略有缩写,但清楚地显示了在哪里进行注释以及在哪里添加新行:
switch (sequenceToCopy[elementToPlay]){
  case 1:
    //hide a button - not any more
    //button1.setVisibility(View.INVISIBLE);
 button1.startAnimation(wobble);
   ...
   ...
  case 2:
    //hide a button - not any more
    //button2.setVisibility(View.INVISIBLE);
 button2.startAnimation(wobble);
   ...
   ...
  case 3:
    //hide a button - not any more
    //button3.setVisibility(View.INVISIBLE);
 button3.startAnimation(wobble);
   ...
   ...
  case 4:
    //hide a button - not any more
    //button4.setVisibility(View.INVISIBLE);
 button4.startAnimation(wobble);

  1. 最后,在我们的sequenceFinished方法中,我们可以注释掉所有的setVisibility调用,就像我们在线程中所做的那样:
//button1.setVisibility(View.VISIBLE);
//button2.setVisibility(View.VISIBLE);
//button3.setVisibility(View.VISIBLE);
//button4.setVisibility(View.VISIBLE);

这并不太难。我们将摇晃动画添加到anim文件夹中,声明了一个动画对象,并对其进行了初始化。然后我们在适当的按钮上使用它。

显然,我们可以对这个游戏进行大量改进,特别是在外观方面。我相信你可以想到更多。当然,如果这是您的应用程序,并且您试图在 Play 商店上大展拳脚,那么您应该这样做。

不断改进所有方面,并努力成为您所在类型中的佼佼者。如果您有冲动,为什么不加以改进呢?

以下是一些自测问题,看看我们可以如何在本章的一些示例中做更多。

自测问题

Q1)假设我们想要进行一项测验,其中问题可以是命名总统以及首都。我们如何使用多维数组来实现这一点?

Q2)在我们的持久性示例部分,我们将一个不断更新的字符串保存到文件中,以便在应用程序关闭并重新启动后保留。这就像要求用户单击“保存”按钮一样。在您对第二章的所有知识中,您能想到一种在用户退出应用程序时保存字符串而不是在按钮单击时保存字符串的方法吗?

Q3)除了增加难度级别,我们如何增加我们的记忆游戏对玩家的挑战?

Q4)使用灰暗的按钮的普通 Android UI 并不令人兴奋。查看可视化设计师中的 UI 元素,并尝试找出我们如何可以快速改进 UI 外观的方法。

总结

这是一个相当庞大的章节,但我们学到了许多新的技术,比如存储和操作数组,创建和使用音效,以及保存重要数据,比如游戏中的高分。我们还简要地介绍了强大但简单易用的Animation类。

在下一章中,我们将采用更加理论化的方法,但也会有大量的工作样本。我们最终将打开 Java 类的黑匣子,以便我们能够理解当我们声明和使用类的对象时发生了什么。

第六章:面向对象编程 - 使用他人的辛勤工作

面向对象编程代表面向对象的编程。在本章中,你甚至不需要试图记住一切。为什么我这么说?当然,这就是学习。更重要的是掌握这些概念,并开始理解面向对象编程的为什么,而不是记住规则、语法和行话。

更重要的是实际开始使用一些概念,即使你可能不得不不断地参考,你的代码可能不完全符合我们讨论的每一个面向对象编程原则。这本书中的代码也是如此。本章的代码是为了帮助你探索和理解面向对象编程的概念。

如果你试图记住本章,你将不得不在你的大脑中腾出很多空间,你可能会忘记一些非常重要的事情,比如去工作或感谢作者告诉你不要试图记住这些东西。

一个很好的目标是尽量接近它。然后我们将开始认识到面向对象编程在实际中的应用,这样我们的理解就会更全面。然后你可以经常回顾本章来温习。

那么我们将学到的所有这些面向对象的东西是什么?实际上,我们已经学到了很多关于面向对象的知识。到目前为止,我们已经使用了诸如ButtonRandomActivity之类的类,重写了类的方法(主要是onCreate),还使用了接口;还记得在前五章中几次实现onClickListener吗?

本章只是帮助理解面向对象编程并扩展我们的理解,最后,我们将制作我们自己的类。

然后在接下来的两章中,我们将处于一个很好的位置,制作两个很酷的复古街机游戏,利用了很多其他人的辛勤工作。本章主要是理论,但也有一些使用 LogCat 的实际控制台示例,这样我们就可以看到面向对象编程在实际中的应用。

在本章中,我们将做以下事情:

  • 看看面向对象编程是什么。

  • 编写我们的第一个类。

  • 看看封装是什么,我们如何实现它,以及更深入地了解变量和不同类型。我们还将短暂休息一下,清理一下垃圾。

  • 学习继承以及我们如何在使用之前扩展甚至改进一个类。

  • 看看多态性,这是一种在编程中同时成为多种东西的方式,非常有用。

什么是面向对象编程?

面向对象编程是一种将我们的需求分解为比整体更易管理的块的编程方式。

每个块都是独立的,但也可能被其他程序重复使用,同时与其他块一起作为一个整体工作。

这些块就是我们所说的对象。当我们计划一个对象时,我们会使用一个类。一个类可以被看作是一个对象的蓝图。

我们实现了一个类的对象。这被称为类的实例。想象一下一幢房子的蓝图。你不能住在里面,但你可以根据它建造一座房子,这意味着你建造了它的一个实例。然而,面向对象编程不仅仅是这样。它还是一种定义最佳实践的方法,比如以下内容:

  • 封装:这意味着将代码的内部工作与使用它的程序的干扰隔离开来,并且只允许访问你选择的变量和方法。这意味着只要暴露的部分仍然以相同的方式被访问,你的代码就可以随时更新、扩展或改进,而不会影响使用它的程序。

  • 继承:就像它听起来的那样,继承意味着我们可以利用其他人的辛勤工作的所有特性和好处,包括封装和多态性,同时针对我们的情况对他们的代码进行特定的改进。实际上,每当我们使用extends关键字时,我们都已经这样做了。

  • 多态性:这使我们能够编写对我们试图操作的类型不太依赖的代码,使我们的代码更清晰、更高效。本章后面的一些例子将使这一点更加清晰。

提示

当我们谈论使用其他人的辛勤工作时,我们并不是在谈论一种滥用版权并逍遥法外的神奇方式。有些代码是简单明了的,是别人的财产。我们所说的是在 Java 和 Android API 的背景下,我们可以使用的大量免费代码。如果您需要执行某个特定操作的代码,它可能已经存在。我们只需要找到它,然后使用或修改它。

Java 从一开始就考虑到了所有这些,因此我们在很大程度上受到使用面向对象编程的限制。然而,这是一件好事,因为我们学会了如何使用最佳实践。

为什么要这样做?

当正确编写时,所有这些面向对象编程允许您添加新功能,而无需过多担心它们与现有功能的交互。当您必须更改一个类时,它的自包含性意味着对程序的其他部分的影响较小,或者可能为零。这就是封装的部分。

您可以使用其他人的代码,而不知道甚至不关心它是如何工作的。想想 Android 生命周期、按钮、线程等。Button类非常复杂,有近 50 个方法——我们真的想为一个按钮写这么多代码吗?

面向对象编程允许您在不费吹灰之力的情况下为高度复杂的情况编写应用程序。您可以使用继承创建类的多个相似但不同版本,而无需从头开始编写类,并且由于多态性,您仍然可以使用原始对象类型的方法来处理新对象。

真的很有道理!让我们写一些类,然后用它们制作一些对象。

我们的第一个类和第一个对象

那么究竟什么是类?类是一堆代码,可以包含方法、变量、循环和所有其他类型的 Java 语法。类是包的一部分,大多数包通常会有多个类。通常情况下,每个新类都会在自己的.java代码文件中定义,文件名与类名相同。

一旦我们编写了一个类,我们可以使用它来创建我们需要的任意多个对象。记住,类是蓝图,我们根据蓝图制作对象。房子不是蓝图,就像对象不是类;它是从类制作的对象。

这是一个类的代码。我们称之为类实现:

public class Soldier {
  int health;
  String soldierType;

  void shootEnemy(){
    //bang bang
  }

}
Soldier. There are two variables, an int variable called health and a string variable called soldierType.

还有一个名为shootEnemy的方法。该方法没有参数和void返回类型,但类方法可以是我们在第五章中讨论的任何形状或大小,游戏和 Java 基础

当我们在类中声明变量时,它们被称为字段。当类被实例化为一个真正的对象时,这些字段成为对象本身的变量,因此我们称它们为实例变量。无论它们被称为什么花哨的名字,它们只是类的变量。然而,随着我们的进展,字段和方法中声明的变量(称为局部变量)之间的区别变得更加重要。我们将在变量重访部分再次查看所有类型的变量。

记住,这只是一个类,不是一个对象。这是一个士兵的蓝图,而不是一个实际的士兵对象。这是我们如何从我们的士兵类中创建一个士兵类型的对象:

Soldier mySoldier = new Soldier();

在代码的第一部分中,Soldier mySoldier声明了一个名为mySoldierSoldier类型的新引用类型变量,而在代码的最后部分,new Soldier()创建了一个实际的Soldier对象。当然,赋值运算符=,将第二部分的结果赋给了第一部分的结果。就像常规变量一样,我们也可以像这样执行前面的步骤:

Soldier mySoldier;
mySoldier = new Soldier();

这是我们如何分配和使用变量的方式:

mySoldier.health = 100;
mySoldier.soldierType = "sniper";
//Notice that we use the object name mySoldier.
//Not the class name Soldier.
//We didn't do this:
// Soldier.health = 100; ERROR!
., is used to access the variables of the class, and this is how we would call the method. Again, we use the object name and not the class name, followed by the dot operator:
mySoldier.shootEnemy();

提示

粗略地说,类的方法是它可以的事情,它的实例变量是它知道的关于自己的事情。

我们还可以继续制作另一个Soldier对象并访问其方法和变量:

Soldier mySoldier2 = new Soldier();
mySoldier2.health = 150;
mySoldier2.soldierType = "special forces";
mySoldier2.shootEnemy();

重要的是要意识到mySoldier2是一个完全独立的对象,具有完全独立的实例变量。

还要注意,一切都是在对象本身上完成的。我们必须创建类的对象才能使它们有用。

注意

总是有例外情况,但它们是少数,我们将在本章后面再看到这些例外情况。事实上,我们在第三章中已经看到了一个例外情况,说 Java-你的第一个游戏。想想Toast

让我们更深入地探索基本类。

基本类

当我们想要一个Soldier对象的军队时会发生什么?我们将实例化多个对象。我们还将演示对变量和方法使用点运算符,并展示不同的对象具有不同的实例变量。

您可以在代码下载中获取此示例的工作项目。它位于chapter6文件夹中,简称为BasicClasses。或者继续阅读以创建您自己的工作示例:

  1. 创建一个带有空白活动的项目,就像我们在第二章中所做的那样,开始使用 Android。通过删除不必要的部分来清理代码,但这并非必要。将应用程序命名为BasicClasses

  2. 现在我们创建一个名为Soldier的新类。在项目资源管理器窗口中,右键单击com.packtpub.basicclasses文件夹。单击新建,然后单击Java 类。在名称字段中,键入Soldier,然后单击确定。新类已为我们创建,其中包含一个代码模板,准备放入我们的实现,就像以下截图中所示:Basic classes

  3. 请注意,Android Studio 已将该类放在与我们应用程序的其余部分相同的包中。现在我们可以编写其实现。在Soldier类的开头和结尾大括号内编写以下类实现代码:

public class Soldier {
    int health;
    String soldierType;

    void shootEnemy(){
        //lets print which type of soldier is shooting
        Log.i(soldierType, " is shooting");
    }
}
  1. 现在我们有了一个类,一个Soldier类型的未来对象的蓝图,我们可以开始建立我们的军队。在编辑窗口中,单击MainActivity.java选项卡。我们将像往常一样在onCreate方法中编写此代码,就在调用setContentView之后:
//first we make an object of type soldier
   Soldier rambo = new Soldier();
   rambo.soldierType = "Green Beret";
   rambo.health = 150;// It takes a lot to kill Rambo

   //Now we make another Soldier object
   Soldier vassily = new Soldier();
   vassily.soldierType = "Sniper";
   vassily.health = 50;//Snipers have less armor

   //And one more Soldier object
   Soldier wellington = new Soldier();
   wellington.soldierType = "Sailor";
   wellington.health = 100;//He's tough but no green beret

提示

现在是时候开始充分利用 Android Studio 中的自动完成功能了。注意,在声明和创建新对象之后,您只需开始输入对象的名称,所有自动完成选项都会呈现出来。

  1. 现在我们有了极其多样化且有些不太可能的军队,我们可以使用它,并验证每个对象的身份。在上一步中的代码下面输入以下代码:
Log.i("Rambo's health = ", "" + rambo.health);
Log.i("Vassily's health = ", "" + vassily.health);
Log.i("Wellington's health = ", "" + wellington.health);

rambo.shootEnemy();
vassily.shootEnemy();
wellington.shootEnemy();
  1. 现在我们可以在模拟器上运行我们的应用程序。请记住,所有输出都将显示在LogCat控制台窗口中。

这就是前面的代码是如何工作的。在第 2 步中,Android Studio 为我们的新Soldier类创建了一个模板。在第 3 步中,我们以与以前相同的方式实现了我们的类——两个变量,一个int和一个string,分别称为healthsoldierType

我们的类中还有一个名为shootEnemy的方法。让我们再次看一下并检查发生了什么:

void shootEnemy(){
        //lets print which type of soldier is shooting
        Log.i(soldierType, " is shooting");
    }

在方法的主体中,我们首先将soldierType字符串打印到控制台,然后是任意的" is shooting"字符串。这里很棒的是,根据我们调用shootEnemy方法的对象不同,soldierType字符串也会不同。

在第 4 步中,我们声明、创建和分配了三个新的Soldier类型对象。它们是rambovassilywellington。在第 5 步中,我们为每个对象初始化了不同的healthsoldierType值。

这是输出:

Rambo's health =﹕ 150
Vassily's health =﹕ 50
Wellington's health =﹕ 100
Green Beret﹕ is shooting
Sniper﹕ is shooting
Sailor﹕ is shooting

请注意,每次访问每个Soldier对象的health变量时,它都会打印出我们分配给它的值,这表明尽管这三个对象是相同类型的,它们是完全独立的个体对象。

也许更有趣的是对shootEnemy的三次调用。我们的每个Soldier对象的shootEnemy方法都被调用了一次,并且我们将soldierType变量打印到控制台。该方法对每个单独的对象都具有适当的值,进一步证明我们有三个不同的对象,尽管它们是从同一个Soldier类创建的。

我们可以用我们的第一类做更多的事情

我们可以像对待其他变量一样对待类。假设我们已经实现了我们的Soldier类,我们可以这样创建一个Soldier对象的数组:

//Declare an array called myArmy to hold 10 Soldier objects
Soldier [] myArmy = new Soldier[10];

//Then we can add the Soldier objects
//We use the familiar array notation on the left
//And the newly learnt new Soldier() syntax on the right
myArmy[0] = new Soldier();
myArmy[1] = new Soldier();
myArmy[2] = new Soldier();
myArmy[3] = new Soldier();
//Initialize more here
//..

然后我们可以使用与常规变量相同的数组表示法从数组中使用对象,就像这样:

myArmy[0].health = 125;
myArmy[0].soldierType = "Pilot";
myArmy[0].shootEnemy();
// Pilot﹕ is shooting

我们还可以在方法调用中使用类作为参数。以下是对healSoldier方法的假设调用:

healSoldier(rambo);
//Perhaps healSoldier could add to the health instance variable

提示

当然,前面的例子可能会引发问题,比如healSoldier方法应该是一个类的方法吗?

someHospitalObjectPerhaps.healSoldier(rambo);

它可能是或不是(如前面的例子所示)。这将取决于对于情况来说什么是最好的解决方案。我们将更多地了解面向对象编程,然后许多类似的难题的最佳解决方案应该更容易地呈现出来。

正如你现在可能已经预料到的,我们可以使用对象作为方法的返回值。以下是假设的healSoldier方法可能看起来像的样子:

Soldier healSoldier(Soldier soldierToBeHealed){
  soldierToBeHealed.health++;

  return soldierToBeHealed;
}

所有这些信息可能会引发一些问题。面向对象编程就是这样,因此为了尝试将所有这些类的内容与我们已经知道的内容整合起来,让我们再次看看变量和封装。

封装

到目前为止,我们真正看到的是一种代码组织约定,尽管我们确实讨论了所有这些面向对象编程的更广泛目标。现在我们将进一步探讨,并开始看到我们如何实际通过面向对象编程实现封装。

提示

封装的定义

正如我们所学到的,封装意味着保护代码的内部工作方式,使其不受使用它的程序的干扰,只允许访问你选择的变量和方法。这意味着你的代码始终可以更新、扩展或改进,而不会影响使用它的程序,只要暴露的部分仍然以相同的方式提供。它还使使用你封装的代码的代码变得更简单、更容易维护,因为任务的大部分复杂性都封装在你的代码中。

但是我不是说我们不必知道内部发生了什么吗?所以你可能会质疑到目前为止我们所看到的。如果我们不断地设置实例变量,比如rambo.health = 100;,难道不可能最终出现问题,比如以下代码行吗?

rambo.soldierType = "ballerina";

封装保护了你的类,使其不会被以不符合预期的方式使用。通过严格控制代码的使用方式,它只能做你想让它做的事情,并且你可以控制值。它不会被迫出现错误或崩溃。此外,你可以自由地更改代码的内部工作方式,而不会破坏使用旧版本代码的任何程序或你的程序的其余部分:

weighlifter.legstrength = 100;
weighlifter.armstrength = -100;
weightlifter.liftHeavyWeight();
//one typo and weightlifter rips own arms off

我们可以封装我们的类以避免这种情况,以下是方法。

使用访问修饰符控制类的使用

类的设计者控制着任何使用他们的类的程序所能看到和操作的内容。我们可以在class关键字之前添加一个访问修饰符,就像这样:

public class Soldier{
  //Implementation goes here
}

有两种类访问修饰符。让我们依次简要看一下每一个:

  • public:这很简单。声明为public的类可以被所有其他类看到。

  • default:当没有指定访问修饰符时,类具有默认访问权限。这将使其对同一包中的类公开,对所有其他类不可访问。

现在我们可以开始使用封装了。然而,即使乍一看,所描述的访问修饰符也不是非常精细。我们似乎只能完全封锁包之外的任何东西,或者完全自由。

实际上,这里的好处很容易利用。想法是设计一个完成一系列任务的包。然后包的所有复杂内部工作,那些不应该被包之外的任何人干扰的东西,应该具有默认访问权限(只能被包内的类访问)。然后我们可以提供一组精心挑选的公共类,供其他人(或程序的其他不同部分)使用。

提示

对于本书中游戏的规模和复杂性来说,几乎可以肯定地说,多个包是过度的。

类访问权限的要点

一个设计良好的应用程序可能包含一个或多个包,每个包只包含默认或默认和公共类。

除了类级别的隐私控制,Java 还给了我们非常精细的控制,但要使用这些控制,我们必须更详细地查看变量。

使用访问修饰符控制变量的使用

为了加强类的可见性控制,我们有变量访问修饰符。这里有一个使用 private 访问修饰符声明的变量:

private int myInt;

请注意,我们对变量访问修饰符的讨论也适用于对象变量。例如,这里声明、创建和分配了我们的Soldier类的一个实例。正如你所看到的,这种情况下指定的访问权限是 public:

public Soldier mySoldier = new Soldier(); 

在将修饰符应用于变量之前,您必须首先考虑类的可见性。如果类a对类b不可见,比如因为类a具有默认访问权限,而类b在另一个包中,那么在类a的变量上使用任何访问修饰符都没有任何影响;类b仍然看不到它。

因此,将一个类显示给另一个类是有意义的,但你应该只暴露需要的变量,而不是所有的变量。

我们还有更多关于访问修饰符的内容要讲,然后我们将看一些示例来帮助澄清事情。现在,这里是不同的变量访问修饰符的解释。它们比类访问修饰符更多,更精细。大多数解释都很直接,而可能引起问题的解释在我们看一个例子时会变得更清晰。

访问修改的深度和复杂性并不在于修饰符的范围,而是通过巧妙地使用它们,我们可以将它们结合起来实现封装的值得目标。以下是变量访问修饰符:

  • public:你猜对了!来自任何包的任何类或方法都可以看到这个变量。只有当你确定这就是你想要的时候才使用public

  • protected:这是在public之后的下一个最不限制的修饰符。设置为 protected 的变量可以被同一包中的任何类和任何方法看到。

  • default:这听起来不像protected那么限制,但实际上更加限制。当没有指定访问权限时,变量具有默认访问权限。default更加限制可能意味着我们应该考虑隐藏变量而不是暴露它们。在这一点上,我们需要介绍一个新概念。你还记得我们简要讨论过继承吗,以及我们如何可以快速地继承一个类的属性,然后使用extends关键字对其进行改进吗?只是为了记录,default 访问权限的变量对子类不可见。这意味着当我们像我们在Activity中所做的那样扩展一个类时,我们无法看到它的默认变量。我们将在本章后面更详细地讨论继承。

  • private:这些变量只能在它们声明的类内部看到。与默认访问一样,它们不能被子类(继承类)看到。

变量访问简而言之

一个设计良好的应用程序可能由一个或多个包组成,每个包只包含默认或默认和公共类。在这些类中,变量将具有精心选择的访问修饰符,很可能是不同的。

在我们开始实际操作之前,这些访问修改的东西还有一个小小的转折。

方法也有访问修饰符

方法是我们的类可以做的事情。我们将要控制我们的类的用户可以做什么,不能做什么。这里的一般想法是,一些方法只会在内部执行,因此不需要类的用户,而一些方法将是用户使用类的基本方法。

方法的访问修饰符与类变量的访问修饰符相同。这使得事情容易记住,但再次表明成功的封装是一种设计问题,而不是任何特定的规则。

例如,以下代码片段中提供的公共类中的方法可以被任何其他类使用:

public useMeEverybody(){
  //do something everyone needs to do here
}

然而,以下方法只能被创建它的类内部使用:

private secretInternalTask(){
  //do something that helps the class function internally
  //Perhaps, if it is part of the same class,
  //useMeEverybody could use this method...
  //On behalf of the classes outside of this class.
  //Neat!
}

下一个方法具有默认可见性,没有指定访问权限。它只能被同一包中的其他类使用。如果我们扩展包含此默认访问方法的类,该类将无法访问此方法:

fairlySecretTask(){
  //allow just the classes in the package
  //Not for external use
}

在我们继续之前,这是最后一个例子。它包含一个protected方法,只对包可见,但可以被扩展它的类使用:

protected familyTask(){
  //allow just the classes in the package
  //And you can use me if you extend me too
}

方法访问简而言之

应该选择方法访问权限以最好地执行我们已经讨论过的原则。它应该为您的类的用户提供所需的访问权限,最好是没有更多。因此,我们实现了封装目标,例如使代码的内部工作免受使用它的程序的干扰,出于我们已经讨论过的所有原因。

使用 getter 和 setter 方法访问私有变量

因此,如果将变量尽可能隐藏为私有是最佳实践,那么如何在不破坏封装的情况下允许访问它们呢?如果Hospital类的对象想要访问Soldier类型对象的health成员变量以增加它,health变量应该是私有的,对吗?

为了能够尽可能将成员变量设为私有,同时允许对其中一些进行有限访问,我们使用gettersetter。Getter 和 setter 是只获取和设置变量值的方法。

这不是我们必须学习的一些特殊或新的 Java 东西。这只是对我们已经知道的东西的使用约定。让我们以我们的SoldierHospital类为例,看看使用 getter 和 setter。

在这个例子中,我们的两个类分别在自己的文件中创建,但在同一个包中。首先,这是我们假设的Hospital类:

class Hospital{
  private void healSoldier(Soldier soldierToHeal){
    int health = soldierToHeal.getHealth();
    health = health + 10;
    soldierToHeal.setHealth(health);
  }
}

我们的Hospital类的实现只有一个方法healSoldier。它接收一个Soldier对象的引用作为参数,因此这个方法将在传入的任何Soldier对象上工作:vassilywellingtonrambo,或其他人。

它还有一个health变量。它使用这个变量来临时保存并增加士兵的健康。在同一行中,它将health变量初始化为Soldier对象的当前健康状况。Soldier对象的health是私有的,因此使用公共的 getter 方法。

然后health增加了 10,setHealth设置器方法将新的health值加载回Soldier对象。

关键在于,尽管Hospital对象可以改变Soldier对象的健康状况,但它是在 getter 和 setter 方法的范围内进行的。getter 和 setter 方法可以被编写来控制和检查潜在的错误或有害的值。

接下来是我们假设的Soldier类,具有最简单的 getter 和 setter 方法的实现:

public class Soldier{
  private int health;
  public int getHealth(){
    return health;
  }

  public void setHealth(int newHealth){
    health = newHealth;
  }
}

我们有一个名为health的实例变量,它是私有的。私有意味着它只能被Soldier类的方法更改。然后我们有一个公共的getHealth方法,不出所料地返回int类型的私有health变量中保存的值。由于这个方法是公共的,任何有权访问Soldier类的人都可以使用它。

接下来,实现了setHealth方法。同样它是公共的,但这次它以int作为参数,并将传递的任何值分配给私有的health变量。在一个更贴近生活的例子中,我们会在这里编写更多的代码来确保传递的值在我们期望的范围内。

现在我们将声明、创建并分配我们两个新类的对象,并看看我们的 getter 和 setter 是如何工作的:

Soldier mySoldier = new Soldier();
//mySoldier.health = 100;//Doesn't work private
//we can use the public setter setHealth()
mySoldier.setHealth(100);//That's better

Hospital militaryHospital = new Hospital();

//Oh no mySoldier has been wounded
mySoldier.setHealth(10);

//Take him to the hospital
//But my health variable is private
//And Hospital won't be able to access it
//I'm doomed - tell Laura I love her

//No wait- what about my public getters and setters?
//We can use the public getters and setters from another class

militaryHospital.healSoldier(mySoldier);

//mySoldiers private variable health has been increased by 10
//I'm feeling much better thanks!

我们看到我们可以直接在我们的Soldier类型的对象上调用我们的公共setHealthgetHealth方法。不仅如此,我们还可以调用Hospital对象的healSoldier方法,传递一个对Soldier对象的引用,后者可以使用公共的 getter 和 setter 来操作私有的health变量。

我们看到私有的health变量是可以被简单访问的,但完全受Soldier类的设计者控制。

如果你想尝试一下这个例子,在Chapter6文件夹的代码包中有一个可用的应用程序,名为Getters And Setters。我添加了几行代码来打印到控制台。我们故意以这种方式来覆盖它,以尽可能清晰地保留代码的关键部分。我们很快将构建一些真正的工作示例,探索类、变量和方法的访问。

注意

Getter 和 setter 有时被称为它们更正确的名称,访问器修改器。我们将坚持使用 getter 和 setter。只是想让你知道。

再一次,我们的例子和解释可能引起更多的问题。这很好!之前,我说过:

  • 类有两种访问修饰符,默认和公共

  • 类的对象是一种引用变量的类型

  • 变量(包括对象)有更多的访问可能性

我们需要更仔细地查看引用和原始变量,以及局部和实例变量。我们将在“变量重访”部分进行。在该部分,我们将进一步整合我们的信息,以更紧密地掌握这个面向对象的编程知识。首先让我们回顾一下封装的一些内容。

使用封装特性(如访问控制)就像签署一项关于如何使用和访问类、它的方法和变量的非常重要的协议。这个合同不仅仅是关于现在的协议,还隐含着对未来的保证。随着我们在本章的进展,我们将看到更多的方式来完善和加强这个合同。

注意

完全可以在不考虑封装的情况下重写本书中的每个示例。事实上,本章之外的本书中的项目对封装非常宽松。

在需要的时候使用封装,或者当然,如果你被雇主付费使用它。通常封装在小型学习项目中是多余的,比如本书中的游戏,除非你学习的主题本身就是封装。

我们学习这个 Java OOP 的知识是基于这样一个假设,即有一天你会想要在 Android 或其他使用 OOP 的平台上编写更复杂的应用程序。

使用构造函数设置我们的对象。

有了所有这些私有变量及其 getter 和 setter,这是否意味着我们需要为每个私有变量都需要一个 getter 和 setter?那么对于一个需要在开始时初始化许多变量的类呢?想想这个:

mySoldier.name
mysoldier.type
mySoldier.weapon
mySoldier.regiment
...

我们可以继续这样下去。其中一些变量可能需要 getter 和 setter,但如果我们只想在对象首次创建时设置好一切,以使对象正确运行,我们需要两个方法(一个 getter 和一个 setter)吗?

为此,我们有一个特殊的方法称为构造函数。在这里,我们创建了一个名为mySoldier的对象,并将其分配给Soldier类型的对象:

Soldier mySoldier = new Soldier();

这里没有什么新鲜事,但看一下代码行的最后部分:

...Soldier();

这看起来非常像一个方法。

我们调用了一个特殊的方法,称为构造函数,它已经被编译器自动为我们提供。

然而(这就是现在的重点),就像方法一样,我们可以重写它,这意味着我们可以在使用新对象之前做一些非常有用的事情,并且在堆栈上放置任何方法之前:

public Soldier(){
  health = 200;
  //more setup here
}

这是一个构造函数。它在语法上与方法有很多相似之处。它只能通过使用new关键字来运行。除非我们像在先前的代码中那样创建自己的构造函数,否则它会被编译器自动创建。

构造函数具有以下属性:

  • 它们没有返回类型

  • 它们与类名相同

  • 它们可以有参数

  • 它们可以被重载

我们将在下一个演示中使用构造函数。

变量重温

你可能还记得,在数学游戏项目中,我们不断更改变量的声明位置。首先,我们在onCreate中声明了一些变量,然后我们将它们移动到类声明的下方,然后我们将它们变成成员或实例变量。

因为我们没有指定访问权限,它们是默认访问权限,并且对整个类可见,由于所有操作都发生在一个类中,我们可以随处访问它们。例如,我们可以从onClick更新我们的 TextView 类型对象,但是为什么我们不能在onCreate中声明它们时就这样做呢?关于何时以及如何访问不同变量的进一步解释可能会很有用。

堆栈和堆

每个 Android 设备内部的虚拟机负责为我们的游戏分配内存。此外,它将不同类型的变量存储在不同的位置。

我们在方法中声明和初始化的变量存储在称为堆栈的内存区域中。在谈论堆栈时,我们可以沿用我们的仓库类比——几乎是。我们已经知道我们可以如何操作堆栈。

让我们谈谈堆以及存储在其中的内容。所有引用类型对象,包括对象(类)和数组,都存储在堆中。将堆视为同一仓库的另一个区域。堆有大量的地板空间用于奇形怪状的对象,用于较小对象的货架,用于数组的大量长排和较小尺寸的立方体孔,等等。这是我们的对象存储的地方。问题是我们无法直接访问堆。

让我们再次看看引用变量到底是什么。它是一个我们引用并通过引用使用的变量。引用可以宽泛地(但有用地)定义为一个地址或位置。对象的引用(地址或位置)在堆栈上。当我们使用点运算符时,我们要求 Dalvik 在引用中存储的特定位置执行任务。

注意

引用变量就是这样——一个引用。它们是访问和操作对象(变量或方法)的一种方式,但它们不是实际的变量。类比可能是,基本类型就在那里(在堆栈上),但引用是一个地址,我们说在地址上该做什么。在这个类比中,所有地址都在堆上。

我们为什么要有这样的系统?直接把对象放在堆栈上就行了!

快速休息一下,扔掉垃圾

还记得在第一章时我说 Java 比一些语言更容易学习,因为它帮助我们管理内存吗?嗯,这整个堆栈和堆的东西就是为我们做的。

正如我们所知,VM 会为我们跟踪所有的对象,并将它们存储在堆中——我们仓库的一个特殊区域。定期,VM 会扫描堆栈,或者我们仓库的常规货架,并将对象与引用进行匹配。如果它发现任何没有匹配引用的对象,它会销毁它们。在 Java 术语中,它执行垃圾回收。想象一辆非常歧视性的垃圾车驶过我们堆的中心,扫描对象以匹配引用。没有引用?你现在是垃圾!毕竟,如果一个对象没有引用变量,我们无法对其进行任何操作。这种垃圾回收系统通过释放未使用的内存帮助我们的游戏更有效地运行。

因此,在方法中声明的变量是局部的,在堆栈上,并且只能在声明它们的方法内部可见。成员变量在堆上,并且可以在有引用的任何地方引用它,前提是访问规范允许引用。

现在我们可以更仔细地看一下变量范围——从哪里可以看到什么。

关于变量还有更多的技巧和转折需要学习。在下一个演示中,我们将探索本章迄今为止学到的所有内容以及一些新的想法。

我们将研究以下主题:

  • 静态变量在类的每个实例中都是一致的(相同的)

  • 类的静态方法,可以在没有该类类型对象的情况下使用类的方法

  • 我们将演示类和局部变量的范围,以及它们在程序的不同部分中可以和不能被看到的地方

  • 我们将研究this关键字,它允许我们编写引用属于特定类实例的变量的代码,但不需要跟踪我们当前使用的实例是哪个

以下是演示。

访问、范围、this、静态和构造函数演示

我们已经看到了控制变量访问和范围的复杂方式,现在可能会对我们有所帮助,看一个实际应用的例子。这些不会是非常实际的真实世界变量使用示例,而更多是为了帮助理解类、方法和变量的访问修饰符,以及引用(或原始)和局部(或实例)等不同类型的变量的演示。然后我们将介绍静态和最终变量以及this关键字的新概念。完成的项目在代码下载的Chapter6文件夹中。它被称为AccessScopeThisAndStatic。我们现在将执行以下步骤来实现它:

  1. 创建一个新的空白活动项目,并将其命名为AccessScopeThisAndStatic

  2. 通过在项目资源管理器中右键单击现有的MainActivity类并导航到新建 | 来创建一个新类。将新类命名为AlienShip

  3. 现在我们声明我们的新类和一些成员变量。请注意,numShips是私有的和静态的。我们很快就会看到这个变量在类的所有实例中是相同的。shieldStrength变量是private的,shipNamepublic的:

public class AlienShip {
private static int numShips;
private int shieldStrength;
public String shipName;
  1. 接下来是构造函数。我们可以看到构造函数是公共的,没有返回类型,并且与类名相同,符合规则。在其中,我们递增了私有静态numShips变量。请记住,每次创建AlienShip类型的新对象时都会发生这种情况。构造函数还使用私有的setShieldStrength方法为shieldStrength私有变量设置一个值:
public AlienShip(){
  numShips++;

  //Can call private methods from here because I am part
  //of the class
  //If didn't have "this" then this call might be less clear
  //But this "this" isn't strictly necessary
  this.setShieldStrength(100);
  //Because of "this" I am sure I am setting 
  //the correct shieldStrength
}
  1. 这是公共静态 getter 方法,外部AlienShip类可以使用它来找出有多少AlienShip对象。我们还将看到我们使用静态方法的不寻常方式:
    public static int getNumShips(){
        return numShips;

    }
  1. 以下代码显示了我们的私有setShieldStrength方法。我们本可以直接从类内部设置shieldStrength,但这段代码展示了我们如何使用this关键字区分shieldStrength局部变量/参数和shieldStrength成员变量:
private void setShieldStrength(int shieldStrength){
    //"this" distinguishes between the 
    //member variable shieldStrength
    //And the local variable/parameter of the same name
    this.shieldStrength = shieldStrength;

}
  1. 接下来的方法是 getter,所以其他类可以读取但不能改变每个AlienShip对象的护盾强度:
public int getShieldStrength(){
    return this.shieldStrength;
}
  1. 现在我们有一个公共方法,每次击中AlienShip对象时都可以调用它。它只是打印到控制台,然后检查该特定对象的shieldStrength是否为零。如果为零,它调用destroyShip方法,接下来我们将看到:
public void hitDetected(){

    shieldStrength -=25;
    Log.i("Incomiming: ","Bam!!");
    if (shieldStrength == 0){
        destroyShip();
    }

}
  1. 最后,我们将看一下我们的AlienShip类的destroyShip方法。我们打印一条消息,指示哪艘飞船已被摧毁,基于它的shipName,并递增numShips静态变量,以便我们可以跟踪AlienShip类型的对象数量:
private void destroyShip(){
    numShips--;
    Log.i("Explosion: ", ""+this.shipName + " destroyed");
    }
}
  1. 现在我们切换到我们的MainActivity类,并编写一些使用我们的新AlienShip类的代码。所有的代码都放在setContentView调用之后的onCreate方法中。首先,我们创建两个名为girlShipboyShip的新的AlienShip对象:
//every time we do this the constructor runs
AlienShip girlShip = new AlienShip();
AlienShip boyShip = new AlienShip();
  1. 看看我们如何获取numShips中的值。我们使用getNumShips方法,正如我们所期望的那样。然而,仔细看语法。我们使用的是类名而不是对象。我们还可以使用非静态的方法访问静态变量。我们这样做是为了看到静态方法的运行情况:
//Look no objects but using the static method
Log.i("numShips: ", "" + AlienShip.getNumShips());
  1. 现在我们给我们的公共shipName字符串变量分配名称:
//This works because shipName is public
girlShip.shipName = "Corrine Yu";
boyShip.shipName = "Andre LaMothe";
  1. 如果我们尝试直接为私有变量分配一个值,它不起作用。因此,我们使用公共的getShieldStrength getter 方法来打印shieldStrength的值,这个值是分配给构造函数的:
//This won't work because shieldStrength is private
//girlship.shieldStrength = 999;

//But we have a public getter
Log.i("girlShip shieldStrngth: ", "" + girlShip.getShieldStrength());

Log.i("boyShip shieldStrngth: ", "" + boyShip.getShieldStrength());

//And we can't do this because it's private
//boyship.setShieldStrength(1000000);

最后,我们通过玩hitDetected方法和偶尔检查我们两个对象的护盾强度来炸毁一些东西:

//let's shoot some ships
girlShip.hitDetected();
Log.i("girlShip shieldStrngth: ", "" + girlShip.getShieldStrength());

Log.i("boyShip shieldStrngth: ", "" + boyShip.getShieldStrength());

boyShip.hitDetected();
boyShip.hitDetected();
boyShip.hitDetected();

Log.i("girlShip shieldStrngth: ", "" + girlShip.getShieldStrength());

Log.i("boyShip shieldStrngth: ", "" + boyShip.getShieldStrength());

boyShip.hitDetected();//ahhh

Log.i("girlShip shieldStrngth: ", "" + girlShip.getShieldStrength());

Log.i("boyShip shieldStrngth: ", "" + boyShip.getShieldStrength());
  1. 当我们认为我们已经摧毁了一艘飞船时,我们再次使用我们的静态getNumShips方法来检查我们的静态变量numShips是否被destroyShip方法改变了:

Log.i("numShips: ", "" + AlienShip.getNumShips());
  1. 运行演示并查看控制台输出。

以下是前面代码块的输出:

numShips:﹕ 2
girlShip shieldStrngth:﹕ 100
boyShip shieldStrngth:﹕ 100
Incomiming:﹕ Bam!!
girlShip shieldStrngth:﹕ 75
boyShip shieldStrngth:﹕ 100
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
girlShip shieldStrngth:﹕ 75
boyShip shieldStrngth:﹕ 25
Incomiming:﹕ Bam!!
Explosion:﹕ Andre LaMothe destroyed
girlShip shieldStrngth:﹕ 75
boyShip shieldStrngth:﹕ 0
numShips:﹕ 1
boyShip shieldStrngth:﹕ 0
numShips:﹕ 1

在前面的例子中,我们看到我们可以使用this关键字区分相同名称的局部变量和成员变量。我们还可以使用this关键字编写引用当前对象的代码。

我们看到静态变量,在这种情况下是numShips,在所有实例中是一致的。此外,通过在构造函数中递增它,并在我们的destroyShip方法中递减它,我们可以跟踪我们创建的AlienShip对象的数量。

我们还看到我们可以通过写类名和点运算符而不是对象来使用静态方法。

最后,我们演示了如何使用访问修饰符隐藏和暴露某些方法和变量。

在我们继续进行新的内容之前,让我们快速回顾一下栈和堆。

关于栈和堆的快速总结

让我们看看关于堆和栈的知识:

  • 你不会删除对象,但是虚拟机在认为合适时会发送垃圾收集器。通常是在对象没有活动引用时执行。

  • 局部变量和方法在栈上,局部变量是在声明它们的特定方法中局部的。

  • 实例或类变量在堆上(与它们的对象一起),但对对象的引用(地址)是栈上的局部变量。

  • 我们控制栈内的内容。我们可以使用堆上的对象,但只能通过引用它们。

  • 堆由垃圾收集器维护。

  • 当不再有有效引用指向对象时,对象将被垃圾回收。因此,当引用变量(局部或实例)从堆栈中移除时,与之相关的对象就可以进行垃圾回收,当虚拟机决定时机合适时(通常非常迅速),它将释放 RAM 内存以避免耗尽。

  • 如果我们尝试引用一个不存在的对象,我们将得到一个空指针异常,游戏将崩溃。

继承

我们已经看到了我们可以通过实例化/创建来自 Android 等 API 的类的对象来使用其他人的辛勤工作,但是这整个面向对象的东西甚至比这更深入。

如果有一个类中有大量有用的功能,但不完全符合我们的要求,我们可以从该类继承,然后进一步完善或添加其工作方式和功能。

你可能会惊讶地听到我们已经这样做了。事实上,我们在每个游戏和演示中都这样做了。当我们使用extends关键字时,我们正在继承,例如,在这行代码中:

public class MainActivity extends Activity ...

在这里,我们继承了Activity类以及其所有功能,或者更具体地说,类设计者希望我们能够访问的所有功能。以下是我们可以对我们扩展的类做的一些事情。

我们可以重写一个方法,但仍然部分依赖于我们继承的类中被重写的方法。例如,每次我们扩展Activity类时,我们都重写了onCreate方法,但我们也调用了类设计者提供的默认实现时:

super.onCreate(... 

在下一章中,我们还将重写Activity类的一些方法。具体来说,我们将重写处理生命周期的方法。

如果我们或类的设计者希望我们在使用他们的类之前强制继承,他们可以将一个类声明为抽象。然后我们就不能从中创建对象。因此,我们必须首先扩展它,然后从子类中创建对象。我们将在我们的继承示例中这样做,并在我们讨论多态性时进一步讨论它。

我们还可以声明一个方法为抽象方法,必须在扩展具有抽象方法的类的任何类中重写该方法。我们也将在我们的继承示例中这样做。

在我们的游戏项目中,我们不会设计任何需要扩展的类。在学习构建简单游戏的情况下,我们不需要那样。然而,在未来的每个游戏中,我们都将扩展他人设计的类。

我们主要讨论继承,以便了解我们周围发生的事情,并作为最终能够设计有用的类的第一步,我们或其他人可以扩展。考虑到这一点,让我们创建一些简单的类,并看看我们如何扩展它们,只是为了玩弄一下语法作为第一步,也为了能够说我们已经这样做了。当我们看这一章的最后一个主要主题,多态性时,我们还将更深入地探讨继承。

继承的例子

我们已经看过了我们可以创建类的层次结构来模拟适合我们的游戏或软件项目的系统的方式,所以让我们尝试一些使用继承的简单代码。完成的项目在代码下载的Chapter6文件夹中。它被称为InheritanceExample。我们现在将执行以下步骤:

  1. 以通常的方式创建三个新类。一个叫做AlienShip,另一个叫做Fighter,最后一个叫做Bomber

  2. 这是AlienShip类的代码。它与我们之前的AlienShip类演示非常相似。不同之处在于构造函数现在接受一个int参数,它用于设置护盾强度。构造函数还向控制台输出消息,以便我们可以看到它何时被使用。AlienShip类还有一个新方法fireWeapon,它被声明为abstract。这保证了任何继承AlienShip的类必须实现自己的fireWeapon版本。注意,该类在其声明中有abstract关键字。我们必须这样做是因为它的方法之一也使用了abstract关键字。当我们讨论这个演示和多态时,我们将解释abstract方法和abstract类:

public abstract class AlienShip {
    private static int numShips;
    private int shieldStrength;
    public String shipName;

    public AlienShip(int shieldStrength){
        Log.i("Location: ", "AlienShip constructor");
        numShips++;
        setShieldStrength(shieldStrength);
    }

    public abstract void fireWeapon();//Ahh my body

    public static int getNumShips(){
        return numShips;
    }

    private void setShieldStrength(int shieldStrength){
        this.shieldStrength = shieldStrength;
    }

    public int getShieldStrength(){
        return this.shieldStrength;
    }

    public void hitDetected(){
        shieldStrength -=25;
        Log.i("Incomiming: ", "Bam!!");
        if (shieldStrength == 0){
            destroyShip();
        }

    }

    private void destroyShip(){
        numShips--;
        Log.i("Explosion: ", "" + this.shipName + " destroyed");
    }

}
  1. 现在我们将实现Bomber类。注意调用super(100)。这将使用shieldStrength的值调用超类的构造函数。我们可以在这个构造函数中进一步初始化Bomber,但现在我们只是打印位置,以便我们可以看到Bomber构造函数何时被执行。我们还实现了一个Bomber类特定版本的抽象fireWeapon方法,因为我们必须这样做:
public class Bomber extends AlienShip {

    public Bomber(){
        super(100);
        //Weak shields for a bomber
        Log.i("Location: ", "Bomber constructor");
    }

    public void fireWeapon(){
        Log.i("Firing weapon: ", "bombs away");
    }
}
  1. 现在我们将实现Fighter类。注意调用super(400)。这将使用shieldStrength的值调用超类的构造函数。我们可以在这个构造函数中进一步初始化Fighter类,但现在我们只是打印位置,以便我们可以看到Fighter构造函数何时被执行。我们还实现了一个Fighter特定版本的抽象fireWeapon方法,因为我们必须这样做:
public class Fighter extends AlienShip{

    public Fighter(){
        super(400);
        //Strong shields for a fighter
        Log.i("Location: ", "Fighter constructor");
    }

    public void fireWeapon(){
        Log.i("Firing weapon: ", "lasers firing");
    }

}
  1. 这是我们在MainActivityonCreate方法中的代码。像往常一样,在调用setContentView之后输入此代码。这是使用我们的三个新类的代码。它看起来很普通,但没有什么新鲜的;有趣的是输出:
Fighter aFighter = new Fighter();
Bomber aBomber = new Bomber();

//Can't do this AlienShip is abstract -
//Literally speaking as well as in code
//AlienShip alienShip = new AlienShip(500);

//But our objects of the subclasses can still do
//everything the AlienShip is meant to do
aBomber.shipName = "Newell Bomber";
aFighter.shipName = "Meier Fighter";

//And because of the overridden constructor
//That still calls the super constructor
//They have unique properties
Log.i("aFighter Shield:", ""+ aFighter.getShieldStrength());
Log.i("aBomber Shield:", ""+ aBomber.getShieldStrength());

    //As well as certain things in certain ways
    //That are unique to the subclass
    aBomber.fireWeapon();
    aFighter.fireWeapon();

    //Take down those alien ships
    //Focus on the bomber it has a weaker shield
    aBomber.hitDetected();
    aBomber.hitDetected();
    aBomber.hitDetected();
    aBomber.hitDetected();

以下代码片段的输出如下:

Location:﹕ AlienShip constructor
Location:﹕ Fighter constructor
Location:﹕ AlienShip constructor
Location:﹕ Bomber constructor
aFighter Shield:﹕ 400
aBomber Shield:﹕ 100
Firing weapon:﹕ bombs away
Firing weapon:﹕ lasers firing
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Explosion:﹕ Newell Bomber destroyed

我们可以看到子类的构造函数如何调用超类的构造函数。我们还可以清楚地看到fireWeapon方法的各个实现确实按预期工作。

好像面向对象编程还不够有用!我们现在可以模拟现实世界的对象并设计它们相互交互。我们还看到了如何通过子类化/扩展/继承其他类使面向对象编程变得更加有用。我们可能想要学习的术语是被扩展的类是超类,继承超类的类是子类。我们也可以称它们为父类和子类。

提示

像往常一样,我们可能会问自己关于继承的问题:为什么?我们可以在父类中编写一次通用代码,并且可以更新该通用代码。所有继承自它的类也会被更新。此外,子类只继承公共实例变量和方法。当设计得当时,这进一步增强了封装的目标。

多态

多态大致意味着不同的形式。但对我们来说意味着什么?

用尽可能简单的话来说,任何子类都可以作为使用超类的代码的一部分。

例如,如果我们有一个动物数组,我们可以将任何属于Animal子类的对象放入Animal数组中,比如猫和狗。

这意味着我们可以编写更简单、更易于理解和修改的代码:

//This code assumes we have an Animal class
//And we have a Cat and Dog class that extends Animal
Animal myAnimal = new Animal();
Dog myDog = new Dog();
Cat myCat = new Cat();
Animal [] myAnimals = new Animal[10];
myAnimals[0] = myAnimal;//As expected
myAnimals[1] = myDog;//This is OK too
myAnimals[2] = myCat;//And this is fine as well

我们还可以为超类编写代码,并依赖于这样一个事实,即无论它被子类化多少次,在一定的参数范围内,代码仍然可以工作。让我们继续我们之前的例子:

//6 months later we need elephants
//with its own unique aspects
//As long as it extends Animal we can still do this
Elephant myElephant = new Elephant();
myAnimals[3] = myElephant;//And this is fine as well

您还可以编写具有多态返回类型和参数的方法:

Animal feedAnimal(Animal animalToFeed){
  //Feed any animal here
  return animalToFeed;
}

因此,即使今天编写代码,一周、一个月或一年后制作另一个子类,相同的方法和数据结构仍将起作用。

此外,我们可以对我们的子类强制执行一组规则,规定它们可以做什么,不能做什么,以及它们应该如何做。因此,一个阶段的良好设计可以影响其他阶段的子类。

如果你突然发现自己有一个像小鸟一样大小的现象,并且你的代码中有很多面向对象编程,那么从一开始就很容易引入雇佣帮助来推动项目的进展,并且仍然保持对项目的控制。

如果你有一个想法,想要一个有很多功能的游戏,但你想尽快推出一个简化版本的游戏,那么聪明的面向对象设计肯定是解决方案。它可以让你编写游戏的基本框架,然后逐渐扩展它。

接下来,让我们看看另一个面向对象编程的概念:抽象类。我们现在可以弄清楚AlienShip代码中发生了什么:

public abstract class AlienShip{...

抽象类

抽象类是一个不能被实例化的类,或者不能被制作成对象。我们在前面的例子中提到AlienShip是抽象的。那么它是一个永远不会被使用的蓝图吗?但这就像支付一个建筑师设计你的房子,然后永远不建造它!我有点明白抽象方法的概念,但这太愚蠢了!

一开始可能会觉得是这样的。我们通过使用abstract关键字来声明一个类为抽象类,就像这样:

abstract class someClass{
  //All methods and variables here as usual
  //Just don't try and make an object out of me!
}

但是为什么呢?

有时,我们希望一个类可以被用作多态类型,但我们需要确保它永远不能被用作对象。例如,Animal本身并没有太多意义。

我们不谈论动物;我们谈论动物的类型。我们不会说,“哦,看那只可爱的、蓬松的、白色的动物”,或者“昨天,我们去宠物店买了一只动物和一个动物床。”这太抽象了。

因此,抽象类就像一个模板,可以被任何继承它的类使用。

我们可能想要一个Worker类,并扩展为类似MinerSteelworkerOfficeWorker和当然Programmer的类。但是一个普通的Worker类到底是做什么的呢?我们为什么要实例化一个?

答案是我们不想实例化它,但我们可能想要将它用作多态类型,这样我们可以在方法之间传递多个工作子类,并且可以拥有可以容纳所有类型工作者的数据结构。

我们称这种类型的类为抽象类,当一个类有一个抽象方法时,就像AlienShip一样,它必须被声明为抽象类。正如我们所看到的,所有抽象方法必须被任何扩展抽象类的类重写。这意味着抽象类可以提供一些在其所有子类中都可用的常见功能。例如,Worker类可能有heightweightage成员变量。

它可能有getPayCheck方法,在所有子类中都是相同的,还有doWork方法,它是抽象的,必须被重写,因为所有不同类型的工作者工作方式都非常不同。

这使我们顺利地进入了另一个值得一提的多态性领域,因为我们迄今为止一直在使用它。

接口

接口就像一个类。呼!那么这里没有什么复杂的。然而,它就像一个始终是抽象的类,只有抽象方法。

我们可以把接口看作是一个完全抽象的类,它的所有方法也都是抽象的。好吧,你可能刚刚能理解抽象类,因为它至少可以传递一些功能,而它的方法不是抽象的,可以作为多态类型。

但说真的,这个接口似乎有点无聊。请耐心等待。

要定义一个接口,我们输入以下代码:

public interface myInterface{
  void someAbstractMethod();//omg I've got no body
  int anotherAbstractMethod();//Ahh! Me too

//Interface methods are always abstract and public implicitly 
//but we could make it explicit if we prefer

  public abstract explicitlyAbstractAndPublicMethod();//still no body though

}

接口的方法没有主体,因为它们是抽象的,但它们仍然可以有返回类型和参数,也可以没有。

要使用一个接口,我们在类声明后使用implements关键字。是的,我们已经为onClickListener做了几次。

public class someClass implements someInterface{

//class stuff here

//better implement the methods of the interface or the red error lines will not go away
  public void someAbstractMethod(){
    //code here if you like but just an empty implementation will do
  }

  public int anotherAbstractMethod(){
    //code here if you like but just an empty implementation will do

    //Must have a return type though as that is part of the contract
    return 1;}
}

这使我们能够使用多个来自完全不相关的继承层次结构的对象进行多态。只要它实现了一个接口,整个东西就可以像那个东西一样传递。我们甚至可以让一个类同时实现多个不同的接口。只需在每个接口之间添加逗号,并在implements关键字后列出它们。只需确保实现所有必要的方法。

让我们回到onClickListener接口。任何东西可能都想知道它何时被点击;按钮、文本视图等。我们不希望为每种类型都有不同的onClick方法。

提示

在使用 Android 时,无论是用于游戏还是更常规的基于 GUI 的应用程序(有点像我们迄今为止的应用程序),你将十有八九实现接口而不是编写自己的接口。然而,知道发生了什么是非常重要的,不是从技术意识的角度来看,因为我们刚刚看到接口指定了一个合同,编译器强制执行它,而更多的是为了理智地知道当你使用implements关键字并编写一个你没有选择的名称的方法(或方法)时实际发生了什么。

更多关于 OOP 和类

可能会有人写一本关于 OOP 的整本书,许多作者已经这样做了,但学习 OOP 的最佳方法可能是练习它;在我们学习所有理论之前就练习它。不管怎样,在我们继续一些更实际的例子之前,这里有一个稍微理论化的 OOP 例子,如果不提到的话,以后我们会对此感到困惑。

内部类

当我们查看我们的基本类演示应用程序时,我们在一个单独的文件中声明和实现了类,而不是在我们的MainActivity类中。该文件与类同名。

我们还可以在一个类中声明和实现一个类。当然,唯一剩下的问题是为什么我们要这样做?当我们实现一个内部类时,内部类可以访问外部类的成员变量,外部类也可以访问内部类的成员。我们将在接下来的两章中看到这一点。

如果你不是在建模深层或现实世界的系统,那么内部类通常是一种方法。事实上,在本书的其余部分,我们将编写的所有类都将是扩展的内部类。这意味着我们将扩展一种类型来创建我们自己的类在我们的Activity类中。这使我们的代码简单明了。

自测问题

Q1) 找出这个类声明有什么问题:

  private class someClass{
    //class implementation goes here
  }

Q2) 封装是什么?

Q3) 我并没有完全理解,实际上,我现在比章节开始时还有更多问题。我该怎么办?

总结

在本章中,我们涵盖的理论比其他任何一章都多。如果你没有记住一切,那么你已经完全成功了。如果你只是理解 OOP 是通过封装、继承和多态编写可重用、可扩展和高效的代码,那么你有成为 Java 大师的潜力。简而言之,OOP 使我们能够在那些人当时并不知道我们将要做什么的时候使用其他人的辛勤工作。你所要做的就是不断练习,所以让我们在下一章制作一个复古游戏。

第七章:复古壁球游戏

这一章是游戏开始的地方。尽管复古壁球游戏显然比最新的大型预算游戏要差一两步,但这是我们开始看一些基本原理的时候——绘图,检测我们绘制的对象何时相互碰撞,以及有我们实际控制的动画。

一旦你能够绘制一个像素并移动它,只需要一点想象力和工作,你就有了绘制任何东西的潜力。然后,当我们将这些知识与一些非常简单的数学结合起来,来模拟碰撞和重力的物理过程时,我们就接近能够实现我们的壁球游戏了。

提示

不幸的是,这本书没有时间深入讲解如何将屏幕上的点变成在三维世界中移动的逼真三维角色的数学。当然,大型预算游戏背后的技术和数学非常先进和复杂。然而,将像素转化为线条,将线条转化为三角形,给三角形贴图,用三角形构建物体,并将它们定位在三维世界中的基础知识,都可以被学过高中数学的任何人掌握。我们经常听说优秀的图形并不一定能创造出优秀的游戏,这是真的,但对我来说,优秀的图形(至少对我来说)是视频游戏中最令人兴奋的方面之一,即使它们显示在一个本身可能更有趣的游戏上。如果你想看看如何将像素变成神奇的世界,并开始欣赏顶级游戏引擎和图形库背后的工作,你可以从《计算机图形学:数学初步》、P.A. Egerton 和 W.S Hall、Prentice Hall 开始。

在本章中,我们将涵盖以下主题:

  • 探索 Android 的Canvas类,使绘图变得简单有趣

  • 编写一个简单的 Canvas 演示应用程序

  • 学习如何在屏幕上检测触摸

  • 创建复古壁球游戏

  • 实现复古的壁球游戏

使用 Android Canvas 进行绘图

到目前为止,我们一直在使用 Android UI 设计师来实现所有的图形。当我们只需要按钮和文本等对象时,这是可以的。

Android UI 元素确实比我们迄今所探索的更多。例如,我们知道我们可以用Animation类做更多的事情,我们也很简要地看到我们可以为 UI 元素中的一个分配任何我们喜欢的图像。

例如,我们可以将游戏角色(如太空飞船)分配给 UI 元素并对它们进行动画。

然而,如果我们想要平滑移动的太空飞船,准确的碰撞检测,可爱的角色,以及具有多帧、卡通般动画的可怕的坏家伙,那么我们需要摆脱预定义的 UI 元素。

我们需要开始查看和设计单个像素、线条、位图和精灵表。幸运的是,正如你可能已经猜到的那样,Android 有一些类可以让我们轻松愉快地做到这一点。我们将学习如何开始使用CanvasPaint类。

位图和精灵表将在下一章中介绍。在本章中,我们将学习如何绘制像素和线条,制作一个简单的、平滑移动的乒乓球风格的壁球游戏。

为了实现这一点,我们将学习我们用来绘制像素和线条的坐标系。然后我们将看看PaintCanvas类本身。

Android 坐标系

像素是我们可以使用PaintCanvas类来操作的最小图形元素。它本质上是一个点。如果你的设备分辨率是 1920 x 1080,就像一些新的谷歌品牌平板电脑或高端三星手机一样,那么在设备的最长长度上有 1920 个像素,在宽度上有 1080 个像素。

因此,我们可以将我们要绘制的屏幕看作一个网格。我们在虚拟画布上使用CanvasPaint类进行绘制。我们将通过在这个网格上的坐标上绘制点(像素)、线条、形状和文本来实现这一点。

坐标系统从屏幕的左上角开始。

举个例子,看看这行代码:

drawPoint(0, 0); //Not actual syntax (but very close)

在这种情况下,我们将在屏幕的左上角绘制一个单个像素。现在看看以下代码:

drawPoint(1920, 1080); //Not actual syntax (but very close)

如果我们像这样使用它,我们可以在这些高端设备的右下角绘制一个点(在横向位置时)。

我们也可以通过指定起始和结束坐标位置来绘制线条,就像这样:

drawLine(0,0,1920, 1080); //Not actual syntax (but very close)

这将从屏幕的左上角画一条线到右下角。

你可能已经注意到一些潜在的问题。首先,并非所有的 Android 设备都具有如此高的分辨率;事实上,大多数设备的分辨率要低得多。即使是分辨率很高的设备在横向或纵向位置时,坐标也会完全不同。我们将很快看到如何编写适应这些设备的代码,而不管屏幕分辨率如何。

我们的像素动画

绘制形状、线条和像素都很好,但我们如何使它们看起来移动呢?我们将使用卡通、电影和其他视频游戏中使用的相同动画技巧:

  1. 绘制一个对象。

  2. 擦掉它。

  3. 在其新位置绘制对象。

  4. 以足够快的速度重复,以欺骗玩家的大脑,使游戏对象移动。

理论使所有这些听起来比实际复杂。让我们快速看一下PaintCanvas类以及一个快速的入门演示应用程序。然后我们可以真正实现我们的复古壁球游戏。

开始使用 Canvas 和 Paint

贴切地命名的Canvas类提供了正是你所期望的——一个虚拟画布,用于绘制我们的图形。

我们可以使用Canvas类从任何 Android UI 元素创建一个虚拟画布。在我们的演示应用程序中,我们将在一个 ImageView 上绘制,当我们制作我们的游戏时,我们将直接在一个特殊类型的视图上绘制,这将带来一些额外的优势,正如我们将看到的那样。

要开始,我们需要一个视图来绘制。我们已经知道如何使用 Java 代码从我们的 UI 布局中获取视图:

ImageView ourView = (ImageView) findViewById(R.id.imageView);

这行代码获取了 UI 设计中放置的 ImageView 的引用,并将其分配给我们 Java 代码中的对象。正如我们所看到的,UI 设计中的 ImageView 具有一个名为imageView的分配 ID,而我们在 Java 代码中可控的 ImageView 对象称为ourView

现在我们需要一个位图。位图本身有一个像屏幕一样的坐标系统。我们正在创建一个位图将其转换为画布:

Bitmap ourBitmap = Bitmap.createBitmap(300,600, Bitmap.Config.ARGB_8888);

上一行代码声明并创建了一个Bitmap类型的对象。它将有 300x600 像素的大小。我们将在稍后绘制时记住这一点。

提示

createBitmap方法中的最后一个参数Bitmap.Config.ARGB_8888只是一个格式,我们可以创建一些很棒的游戏,而不用涉及位图格式的不同选项。

现在我们可以通过从中创建一个Canvas对象来准备我们的位图进行绘制:

Canvas ourCanvas = new Canvas(ourBitmap);

接下来,我们得到一个Paint类型的对象。我们可以把这个对象看作是我们虚拟画布的刷子和颜料:

Paint paint = new Paint();

在这一点上,我们准备使用我们的PaintCanvas对象进行一些绘图。在屏幕左上角绘制一个像素的实际代码将如下所示:

ourCanvas.drawPoint(0, 0, paint);//How simple is that?

现在让我们看一个工作示例。

Android Canvas 演示应用程序

让我们制作一个使用CanvasPaint类并进行一些绘图的应用程序。这个例子将是完全静态的(没有动画),所以我们可以清楚地看到如何在不用后续学习的东西的情况下使用CanvasPaint

在这个演示应用程序中,我们使用了一些概念上有帮助的变量名,以帮助我们理解每个对象所扮演的角色,但我们将在最后通过整个过程,以确保我们确切知道每个阶段发生了什么。当然,你不必输入所有这些。您可以在下载包的Chapter7文件夹中的CanvasDemo文件夹中打开已完成的代码文件:

  1. 开始一个新项目,称之为CanvasDemo。如果愿意,可以清理不必要的导入和覆盖。

  2. 在编辑器中打开activity_main.xml。从调色板中将ImageView拖动到布局中。ImageView 默认具有一个 ID,即imageView。现在我们将在我们的代码中使用这个 ID。

  3. 在编辑器中切换到MainActivity.java。首先,我们将创建我们的BitmapCanvasPaint对象,就像我们之前讨论的那样。这是代码的第一部分。在setContentView方法调用之后直接输入它:

//Get a reference to our ImageView in the layout
ImageView ourFrame = (ImageView) findViewById(R.id.imageView);

//Create a bitmap object to use as our canvas
Bitmap ourBitmap = Bitmap.createBitmap(300,600, Bitmap.Config.ARGB_8888);
Canvas ourCanvas = new Canvas(ourBitmap);

//A paint object that does our drawing, on our canvas
Paint paint = new Paint();
  1. 在这里,我们尝试绘制一些很酷的东西。在上一步中的代码之后直接输入代码:
//Set the background color
ourCanvas.drawColor(Color.BLACK);

//Change the color of the virtual paint brush
paint.setColor(Color.argb(255, 255, 255, 255));

//Now draw a load of stuff on our canvas
ourCanvas.drawText("Score: 42 Lives: 3 Hi: 97", 10, 10, paint);
ourCanvas.drawLine(10, 50, 200, 50, paint);
ourCanvas.drawCircle(110, 160, 100, paint);
ourCanvas.drawPoint(10, 260, paint);

//Now put the canvas in the frame
  ourFrame.setImageBitmap(ourBitmap);
  1. 在模拟器或设备上运行演示。

您的输出将看起来像下面的截图所示:

Android Canvas demo app

让我们再次浏览代码。在步骤 1 和 2 中,我们创建了一个新项目,并在 UI 布局上放置了一个 ID 为imageView的 ImageView 对象。

在第 3 步中,我们首先获取了布局中ImageView对象的引用。然而,我们经常这样做,通常是使用 TextView 和 Button。我们将我们的 ImageView 命名为ourFrame,因为它将容纳我们的画布:

ImageView ourFrame = (ImageView) findViewById(R.id.imageView);

然后我们创建了一个位图用于制作画布:

Bitmap ourBitmap = Bitmap.createBitmap(300,600, Bitmap.Config.ARGB_8888);
Canvas ourCanvas = new Canvas(ourBitmap);

之后,我们创建了新的Paint对象:

Paint paint = new Paint();

在第 4 步中,我们准备好绘制,并以几种不同的方式绘制。首先,我们将整个画布涂成黑色:

ourCanvas.drawColor(Color.BLACK);

然后我们选择了要绘制的颜色。(255, 255, 255, 255)是白色的数字表示,完全不透明(没有透明度):

paint.setColor(Color.argb(255, 255, 255, 255));

现在我们看到了一些新东西,但很容易理解。我们还可以将文本字符串绘制到屏幕上,并将该文本定位到精确的屏幕坐标,就像我们可以使用像素一样。

您会注意到,使用Canvas类的drawText方法和所有其他绘图方法时,我们总是将我们的Paint对象作为参数传递。为了让下一行代码中发生的事情绝对清晰,我声明"Score: 42 Lives:3 Hi: 97"是将在屏幕上绘制的字符串,10, 10是屏幕坐标,paint是我们的Paint对象:

ourCanvas.drawText("Score: 42 Lives: 3 Hi: 97", 10, 10, paint);

接下来,我们画一条线。这里的参数列表可以描述如下:(起始x坐标,起始y坐标,结束x坐标,结束y坐标,我们的Paint对象):

ourCanvas.drawLine(10, 50, 200, 50, paint);

现在我们看到我们可以绘制圆。我们还可以绘制其他形状。这里的参数列表可以描述如下:(圆的起始x坐标,起始y坐标,圆的半径,我们的Paint对象):

ourCanvas.drawCircle(110, 160, 100, paint);

然后我们画了一个不起眼的孤独像素(点)。我们使用的参数格式如下:(x坐标,y坐标,Paint对象):

ourCanvas.drawPoint(10, 260, paint);

最后,我们将我们的位图画布放在我们的 ImageView 框架上:

ourFrame.setImageBitmap(ourBitmap);

我们仍然需要更加智能地管理屏幕分辨率和方向,我们将在我们的复古挤压游戏中这样做。此外,我们需要寻找一种系统,使我们能够在一定的时间间隔内擦除和重绘我们的图像,以创造运动的错觉。实际上,我们已经知道这样的系统。想想我们如何使用线程来实现这种错觉。首先,让我们看看玩家将如何控制游戏。毕竟,我们不会有任何方便的 UI 按钮来按下这个游戏。

检测屏幕上的触摸

在我们的复古挤压游戏中,我们将没有 UI 按钮,因此我们无法使用OnClickListener接口并重写onClick方法。但是这不是问题。我们将使用另一个接口来适应我们的情况。我们将使用OnTouchListener并重写onTouchEvent方法。它的工作方式有点不同,因此在我们深入游戏代码之前,让我们看看如何实现它。

我们必须为我们想要监听触摸的活动实现OnTouchListener接口,就像这样:

public class MainActivity extends Activity implements View.OnTouchListener{

然后我们可以重写onTouchEvent方法,也许有点像这样。

@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
  float x = motionEvent.getX();
  float y = motionEvent.getY();
  //do something with the x and y values
  return false;
}

x变量将保存触摸屏幕的水平位置值,y将保存垂直位置。值得注意的是motionEvent对象参数包含大量信息,以及xy的位置,例如屏幕是否被触摸或释放。我们可以利用这些信息制作一些非常有用的开关语句,稍后我们将看到。

知道如何利用这一点来实现我们在壁球游戏中的目标需要我们首先考虑游戏的设计。

准备制作复古壁球游戏

现在我们准备讨论制作我们下一个游戏。我们实际上已经知道我们需要的一切。我们只需要考虑如何使用我们学到的不同技术。

让我们首先确切地了解我们想要实现的目标,这样我们就有了一个目标。

游戏的设计

让我们看一下游戏的屏幕截图,作为一个很好的起点。当你设计自己的游戏时,绘制游戏对象和游戏机制的草图将是设计过程中非常宝贵的一部分。在这里,我们可以通过查看最终结果来稍微作弊一下。

游戏的设计

用户界面

从顶部开始,我们有得分。每当玩家成功击中球时,就会增加一个点。接下来是生命。玩家从三条生命开始,每次让球从球拍下通过,就会失去一条生命。当玩家生命值为零时,他们的得分将被设置为零,生命值将被重新设置为三,游戏将重新开始。在这之后,我们有FPS。FPS 代表每秒帧数。如果我们能在屏幕上监视我们的屏幕每秒重绘的次数,那将是很好的,因为这是我们第一次在动画中使用我们自己的图形。

在上一张屏幕截图的中间位置大约是球。它是一个方形的球,符合传统的乒乓球风格。当你需要执行看起来真实的碰撞检测时,方形也更容易。

物理

我们将检测当球击中屏幕的四个边缘以及击中球拍时。根据球击中的物体以及碰撞时的当前方向,我们将确定球的下一步动作。以下是每种类型碰撞的大致概述:

  • 击中屏幕顶部:球将保持相同的水平(x)行进方向,但会反转垂直(y)行进方向。

  • 击中屏幕的任一侧:球将保持其y行进方向,但会反转其x行进方向。

  • 击中屏幕底部:球将消失,并在屏幕顶部重新开始,向下的y行进方向和随机的x行进方向。

  • 击中玩家的球拍:我们将检查球是否击中球拍的左侧或右侧,并改变x行进方向以匹配。我们还将反转y行进方向,将球再次发送到顶部。

通过强制执行这些粗糙的虚拟物理规则,我们可以简单地创建一个表现几乎像真实球一样的球。我们将添加一些属性,例如在击中球拍后稍微增加球的速度。这些规则在纵向或横向方向上同样有效。

玩家的球拍将是一个简单的矩形,玩家可以通过在屏幕左半部分任意位置按住左移,通过在屏幕右半部分任意位置按住右移。

为了简洁起见,我们不会制作一个主菜单屏幕来实现高分。在我们下一章开始的最终游戏中,我们将继续制作一个动画菜单屏幕、在线高分和成就。然而,当玩家生命值为零时,这个壁球游戏将简单地重新开始。

代码的结构

在这里,我们将快速理论地看一下实现中可能引起问题的一些方面。当我们最终开始实现时,我们应该会发现大部分代码非常简单,只有少数几个部分可能需要额外的解释。

我们已经讨论了我们需要知道的一切,我们也将在实现过程中讨论代码的具体内容。我们将在每个实现阶段结束时讨论代码中更棘手的部分。

和往常一样,所有已完成的代码文件都可以在下载包中找到。这个项目的所有阶段的文件都在Chapter7/RetroSquash文件夹中。

我们已经了解到,在使用类和它们的方法的应用程序中,代码的不同部分将依赖于其他部分。因此,我们不会在代码中来回跳跃,而是按顺序从第一行到最后一行进行布局。当然,我们在进行过程中也会参考相关的代码部分。我强烈建议全面研究代码,以充分理解正在发生的事情以及代码的哪些部分调用了其他部分。

为了防止这个实现变成一个庞大的待办事项列表,它已经被分成了四个阶段。这应该提供方便的停止和休息的地方。

没有布局文件,只有一个.java文件。这个文件叫做MainActivity.javaMainActivity.java文件的结构如下概述所示。我缩进了一些部分,以显示哪些部分包含在其他部分中。这是一个高层次的视图,它省略了很多细节:

Package name and various import statements
MainActivity class starts{
    Declare some member variables
    OnCreate method{
      Initialization and setup
    }
    SquashCourtView class{
      Constructor
      Multiple methods of SquashCourtView
    }
    Some Android lifecycle method overrides
}

正如之前所述,我们可以看到一切都在MainActivity.java文件中。和往常一样,在文件的顶部,我们将有一个包名和一堆导入不同类的导入。

接下来,和我们其他项目一样,我们有MainActivity类。它包含了其他所有内容,甚至SquashCourtView类。这使得SquashCourtView类成为一个内部类,因此能够访问MainActivity类的成员变量,这在实现中将是必不可少的。

然而,在SquashCourtView类之前,MainActivity类中声明了所有成员变量,然后是一个相当深入的onCreate方法。

我们可以接下来实现其他 Android 生命周期方法,欢迎你这样做。然而,一旦我们看到SquashCourtView类方法中的代码,其他 Android 生命周期方法中的代码将更有意义。

onCreate之后,我们将实现SquashCourtView类。这个类中有一些相当长的方法,所以我们将把它分成阶段 2 和 3。

最后,我们将实现剩余的 Android 生命周期方法。它们很短,但很重要。

详细介绍的四个实现阶段

在我们真正开始之前,让我们更仔细地看看实现。这是我们将如何将实现分成四个阶段,这次更详细地说明每个阶段可以期待的内容:

  • 阶段 1 - MainActivity 和 onCreate:在这个阶段,我们将创建项目本身,并实现以下步骤:

  • 我们将添加我们的导入并创建我们的MainActivity类的主体

  • 在这个阶段,我们将声明游戏需要的成员变量

  • 我们将实现我们的onCreate方法,这个方法需要做大量的设置工作,但没有什么难以理解的东西。

  • 阶段 2 - SquashCourtView 第一部分:在这个阶段,我们将开始处理我们的关键类SquashCourtView。具体来说,我们将:

  • 实现SquashCourtView类及其成员变量的声明。

  • 编写一个简单的构造函数。

  • 实现run方法来控制游戏的流程。

  • 实现冗长但相当容易理解的updateCourt方法。这个方法处理碰撞检测并跟踪我们的球和球拍。

  • 第 3 阶段-SquashCourtView 第二部分:在这个阶段,我们将通过实现以下内容完成SquashCourtView类:

  • drawCourt方法,毫不奇怪地进行所有绘制

  • controlFPS方法,使游戏在具有不同 CPU 的设备上以相似的速度运行

  • 接下来,我们将快速编写一些帮助 Android 生命周期方法的方法,这些方法具有类似的名称——pauseresume方法

  • 最后,在这个阶段,我们将通过重写我们之前查看过的onTouchEvent方法轻松处理游戏的触摸控制

  • 第 4 阶段-剩余的生命周期方法:在这个简短的阶段,我们将添加最后的修饰:

  • 通过重写onPauseonResumeonStop方法快速实现发生的情况

  • 我们还将处理玩家在手机或平板上按返回按钮时发生的情况

第 1 阶段-MainActivity 和 onCreate

既然我们已经看到了每个阶段我们将要做什么,让我们通过执行以下步骤来开始构建我们的游戏:

  1. 创建一个新项目,就像我们以前做的那样,但有一个小小的不同。这一次,在新项目对话框中,将最低要求的 SDK更改为API 13:Android 3.2(蜂巢)。将项目命名为RetroSquash。如果愿意,可以删除不必要的重写方法。

  2. 编辑AndroidManifest.xml文件,就像我们在第四章结束时所做的那样,发现循环和方法,使应用程序使用全屏。如有需要,请查看完整的详细信息。请注意,我们锁定方向,因为这个游戏在纵向和横向都很有趣。这是要添加的代码行:

android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
  1. 使用 Bfxr 制作一些音效,就像我们在第五章中所做的那样,游戏和 Java 基础。四个就足够了,但没有什么能阻止你添加更多的声音。要获得真正的 1970 年代风格的声音,请尝试下面截图中显示的Blip/Select按钮。将样本命名为sample1.oggsample2.oggsample3.oggsample4.ogg。或者你可以使用我的样本。它们在名为RetroSquash的文件夹的assets文件夹中。第 1 阶段-MainActivity 和 onCreate

  2. 在 Project Explorer 中,在main目录中创建一个名为assets的目录。将你在上一步中创建的四个声音文件复制到新创建的assets文件夹中。

  3. MainActivity.java文件的顶部但在包名之后输入以下导入语句,如下面的代码所示:

package com.packtpub.retrosquash.app;

import android.app.Activity;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.view.Display;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.util.Random;
  1. 现在输入你的类声明并声明以下成员变量。我们将在本阶段结束时详细讨论成员变量:
public class MainActivity extends Activity {

    Canvas canvas;
    SquashCourtView squashCourtView;

    //Sound
    //initialize sound variables
    private SoundPool soundPool;
    int sample1 = -1;
    int sample2 = -1;
    int sample3 = -1;
    int sample4 = -1;

    //For getting display details like the number of pixels
    Display display;
    Point size;
    int screenWidth;
    int screenHeight;

    //Game objects
    int racketWidth;
    int racketHeight;
    Point racketPosition;

    Point ballPosition;
    int ballWidth;

    //for ball movement
    boolean ballIsMovingLeft;
    boolean ballIsMovingRight;
    boolean ballIsMovingUp;
    boolean ballIsMovingDown;

    //for racket movement
    boolean racketIsMovingLeft;
    boolean racketIsMovingRight;

    //stats
    long lastFrameTime;
    int fps;
    int score;
    int lives;
  1. 接下来,我们将完整进入onCreate方法。我们正在初始化许多在上一步中声明的成员变量,以及从我们的SquashCourtView类创建一个对象,我们将在下一阶段开始实现。在这段代码块中,可能最值得注意的一行是对setContentView的略有不同的调用。看一下setContentView的参数。我们将在本阶段结束时了解更多关于这个参数的信息。这个阶段还设置了SoundPool并加载了声音样本。输入onCreate代码的第一部分:
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        squashCourtView = new SquashCourtView(this);
        setContentView(squashCourtView);

        //Sound code
        soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
        try {
            //Create objects of the 2 required classes
            AssetManager assetManager = getAssets();
            AssetFileDescriptor descriptor;

            //create our three fx in memory ready for use
            descriptor = assetManager.openFd("sample1.ogg");
            sample1 = soundPool.load(descriptor, 0);

            descriptor = assetManager.openFd("sample2.ogg");
            sample2 = soundPool.load(descriptor, 0);

            descriptor = assetManager.openFd("sample3.ogg");
            sample3 = soundPool.load(descriptor, 0);

            descriptor = assetManager.openFd("sample4.ogg");
            sample4 = soundPool.load(descriptor, 0);

        } catch (IOException e) {
            //catch exceptions here
        }
  1. 现在初始化我们之前创建的变量。请注意,有一些很好的潜在候选者可以进行一些封装。但是,为了保持代码的可读性,我们在这个阶段不会这样做。输入这段代码:
        //Could this be an object with getters and setters
        //Don't want just anyone changing screen size.
        //Get the screen size in pixels
        display = getWindowManager().getDefaultDisplay();
        size = new Point();
        display.getSize(size);
        screenWidth = size.x;
        screenHeight = size.y;

        //The game objects
        racketPosition = new Point();
        racketPosition.x = screenWidth / 2;
        racketPosition.y = screenHeight - 20;
        racketWidth = screenWidth / 8;
        racketHeight = 10;

        ballWidth = screenWidth / 35;
        ballPosition = new Point();
        ballPosition.x = screenWidth / 2;
        ballPosition.y = 1 + ballWidth;

        lives = 3;

    }

解释第 1 阶段的代码

让我们看看我们做了什么。从步骤 1 到 4,我们只是创建了一个项目和一些声音文件。然后,我们像在其他项目中一样将声音文件添加到assets文件夹中。在第 5 步,我们为将要使用的类添加了所有必要的导入。

在第 6 步中,我们创建了一大堆成员变量。让我们仔细看看它们。我们声明了一个Canvas类型的对象canvas。我们将使用这个对象来设置我们的绘图系统。我们还声明了一个SquashCourtView的实例叫做squashCourtView。这将被划线为错误,因为我们还没有实现这个类。

在这里,我们声明并初始化了变量,作为我们声音文件的引用,就像我们在其他项目中所做的那样。之后,我们做了一些新的事情:

//For getting display details like the number of pixels
Display display;
Point size;
int screenWidth;
int screenHeight;

我们声明了一个Display对象和一个Point对象。我们将在onCreate方法中看到它们的作用,再加上两个int变量,screenWidthscreenHeight。我们使用它们来获取屏幕的像素大小,以便我们的游戏可以在任何分辨率的屏幕上运行。

在这里,我们声明了一些变量,它们的目的从它们的名称中就很明显。当我们在第 8 步初始化它们并在整个SquashCourtView类中使用它们时,它们的实际用途会变得更清晰:

//Game objects
int racketWidth;
int racketHeight;
Point racketPosition;

Point ballPosition;
int ballWidth;

在这里,我们有一堆布尔变量来控制球拍和球的移动逻辑。请注意,球拍和球的每个可能方向都有一个变量。还要注意球拍可以向两个方向移动——左和右——球可以向四个方向移动。当我们在第 2 阶段编写updateCourt方法时,所有这些都将变得清晰。以下是该代码:

//for ball movement
boolean ballIsMovingLeft;
boolean ballIsMovingRight;
boolean ballIsMovingUp;
boolean ballIsMovingDown;

//for racket movement
 boolean racketIsMovingLeft;
 boolean racketIsMovingRight;

在第 6 步的最后部分,我们声明了两个相当明显的变量,livesscore。但lastFrameTimefps呢?这些将在我们在第 3 阶段编写的controlFPS方法中使用。它们将与一些局部变量一起用于测量我们的游戏循环运行的速度。然后我们可以将其锁定以以一致的速度运行,以便具有不同 CPU 速度的设备上的玩家获得类似的体验。

在第 7 步中,我们进入了onCreate方法,但这一次情况有所不同。我们将squashCourtView初始化为一个新的SquashCourtView对象。到目前为止还好,但然后我们似乎在告诉setContentView将其作为玩家将看到的整个视图,而不是通常在 Android Studio 设计师中创建的视图,这是我们已经习惯的。在这个游戏中,我们没有使用任何 Android UI 组件,因此视觉设计师及其生成的 XML 对我们没有用处。正如您将在第 2 阶段开始时看到的,我们的SquashCourtView类扩展(继承自)SurfaceView

我们创建了一个具有所有SurfaceView功能的对象。我们将对其进行自定义以玩我们的壁球游戏。很好!因此,将我们的squashCourtView对象设置为玩家将看到的整个视图是完全可以接受和合乎逻辑的:

squashCourtView = new SquashCourtView(this);
setContentView(squashCourtView);

然后我们设置了我们的音效,就像以前一样。

在第 8 步中,我们初始化了在第 6 步中声明的许多变量。让我们看看初始化的值和顺序。您可能已经注意到,我们并没有在这里初始化每个变量;有些将稍后初始化。请记住,我们不必初始化成员变量,它们也有默认值。

在接下来的代码中,我们获取设备的像素数(宽和高)。display对象在第一行执行后保存了显示的详细信息。然后我们创建了一个名为sizePoint类型的新对象。我们将size作为参数发送给display.getSize方法。Point类型有一个xy成员变量,size对象也有,现在它保存了显示的宽度和高度(以像素为单位)。然后将这些值分别赋给screenWidthscreenHeight。我们将在SquashCourtView类中广泛使用screenWidthscreenHeight

display = getWindowManager().getDefaultDisplay();
size = new Point();
display.getSize(size);
screenWidth = size.x;
screenHeight = size.y;

接下来,我们初始化了决定球和球拍大小和位置的变量。在这里,我们初始化了我们的racketPosition对象,它是Point类型的。记住它有一个x和一个y成员变量:

racketPosition = new Point();

我们将racketPosition.x初始化为当前屏幕宽度的一半,这样球拍将始终在水平和中央位置开始,而不受屏幕分辨率的影响:

racketPosition.x = screenWidth / 2;

在下一行代码中,racketPosition.y被放在屏幕底部,留有 20 像素的间隙:

racketPosition.y = screenHeight - 20;

我们将球拍的宽度设置为屏幕宽度的八分之一。当我们运行游戏时,我们会看到这是一个相当有效的大小,但我们可以通过将其除以较小的数字来使其变大,或者通过将其除以较大的数字来使其变小。关键是,无论设备的分辨率如何,它都将是screenWidth的相同部分:

racketWidth = screenWidth / 8;

在下一行代码中,我们为球拍选择了一个任意的高度:

racketHeight = 10;

然后我们将我们的球大小设置为屏幕的 1/35。同样,我们可以将其放大或缩小:

ballWidth = screenWidth / 35;

在下一行代码中,我们将创建一个新的点对象来保存球的位置:

ballPosition = new Point();

与球拍一样,我们将球放在屏幕中央,如下所示:

ballPosition.x = screenWidth / 2;

但是,我们将其设置为从屏幕顶部开始,刚好能看到球的顶部:

ballPosition.y = 1 + ballWidth;

玩家从游戏开始就有三条命:

lives = 3;

呼!那是一个相当庞大的部分。如果你愿意,可以休息一下,然后我们将继续进行第 2 阶段。

第 2 阶段 - SquashCourtView 第一部分

最后,我们来到了我们游戏的秘密武器 - SquashCourtView类。前三个方法在这里呈现,并在实现后进行更详细的解释:

  1. 这是一个扩展SurfaceView的类声明,使我们的类拥有SurfaceView的所有方法和属性。它还实现了Runnable,这使它可以在单独的线程中运行。正如你将看到的,我们将把大部分功能放在run方法中。在声明之后,我们有一个构造函数。记住构造函数是一个与类同名的方法,在我们初始化其类型的新对象时调用。构造函数中的代码初始化了一些对象,然后以随机方向发送球。在我们实现了这个阶段之后,我们将详细查看这部分。在MainActivity类的结束大括号之前输入以下代码:
class SquashCourtView extends SurfaceView implements Runnable {
        Thread ourThread = null;
        SurfaceHolder ourHolder;
        volatile boolean playingSquash;
        Paint paint;

        public SquashCourtView(Context context) {
            super(context);
            ourHolder = getHolder();
            paint = new Paint();
            ballIsMovingDown = true;

            //Send the ball in random direction
            Random randomNumber = new Random();
            int ballDirection = randomNumber.nextInt(3);
            switch (ballDirection) {
                case 0:
                    ballIsMovingLeft = true;
                    ballIsMovingRight = false;
                    break;

                case 1:
                    ballIsMovingRight = true;
                    ballIsMovingLeft = false;
                    break;

                case 2:
                    ballIsMovingLeft = false;
                    ballIsMovingRight = false;
                    break;
            }

        }
  1. 现在我们有了这个简短而甜美的run方法的重写。记住run方法包含线程的功能。在这种情况下,它有三个调用,分别是updateCourtdrawCourtcontrolFPS,这是我们类的三个关键方法。输入以下代码:
@Override
        public void run() {
            while (playingSquash) {
                updateCourt();
                drawCourt();
                controlFPS();

            }

        }
  1. 我们将在这个阶段中实现一个额外的方法(updateCourt),但它相当长。在我们输入代码之前,我们将把它分成几部分,并简要提到每部分的内容。在这部分代码中,我们处理球拍的左右移动,以及检测和反应当球击中屏幕的左侧或右侧。在上一步的代码之后输入以下代码:
public void updateCourt() {
            if (racketIsMovingRight) {
                racketPosition.x = racketPosition.x + 10;
            }

            if (racketIsMovingLeft) {
                racketPosition.x = racketPosition.x - 10;
            }

            //detect collisions

            //hit right of screen
            if (ballPosition.x + ballWidth > screenWidth) {
                ballIsMovingLeft = true;
                ballIsMovingRight = false;
                soundPool.play(sample1, 1, 1, 0, 0, 1);
            }

            //hit left of screen
            if (ballPosition.x < 0) {
                ballIsMovingLeft = false;
                ballIsMovingRight = true;
                soundPool.play(sample1, 1, 1, 0, 0, 1);
            }
  1. 在下一部分代码中,我们检查球是否击中了屏幕底部,也就是玩家未能返回球。在上一步的代码之后直接输入这段代码:
//Edge of ball has hit bottom of screen
            if (ballPosition.y > screenHeight - ballWidth) {
                lives = lives - 1;
                if (lives == 0) {
                    lives = 3;
                    score = 0;
                    soundPool.play(sample4, 1, 1, 0, 0, 1);
                }
                ballPosition.y = 1 + ballWidth;//back to top of screen

                //what horizontal direction should we use
                //for the next falling ball
                Random randomNumber = new Random();
                int startX = randomNumber.nextInt(screenWidth - ballWidth) + 1;
                ballPosition.x = startX + ballWidth;

                int ballDirection = randomNumber.nextInt(3);
                switch (ballDirection) {
                    case 0:
                        ballIsMovingLeft = true;
                        ballIsMovingRight = false;
                        break;

                    case 1:
                        ballIsMovingRight = true;
                        ballIsMovingLeft = false;
                        break;

                    case 2:
                        ballIsMovingLeft = false;
                        ballIsMovingRight = false;
                        break;
                }
            }
  1. 在这部分代码中,我们处理了球是否击中了屏幕顶部。我们还计算了本帧球的所有可能移动。现在输入以下代码:
//we hit the top of the screen
            if (ballPosition.y <= 0) {
                ballIsMovingDown = true;
                ballIsMovingUp = false;
                ballPosition.y = 1;
                soundPool.play(sample2, 1, 1, 0, 0, 1);
            }

            //depending upon the two directions we should
            //be moving in adjust our x any positions
            if (ballIsMovingDown) {
                ballPosition.y += 6;
            }

            if (ballIsMovingUp) {
                ballPosition.y -= 10;
            }

            if (ballIsMovingLeft) {
                ballPosition.x -= 12;
            }

            if (ballIsMovingRight) {
                ballPosition.x += 12;
            }
  1. 最后,我们处理了球拍和球的碰撞检测和反应。我们还关闭了updateCourt方法,这是这个阶段的最后一部分代码。在上一步的代码之后输入以下内容:
//Has ball hit racket
            if (ballPosition.y + ballWidth >= (racketPosition.y - racketHeight / 2)) {
                int halfRacket = racketWidth / 2;
                if (ballPosition.x + ballWidth > (racketPosition.x - halfRacket)
                    && ballPosition.x - ballWidth < (racketPosition.x + halfRacket)) {
                    //rebound the ball vertically and play a sound
                    soundPool.play(sample3, 1, 1, 0, 0, 1);
                    score++;
                    ballIsMovingUp = true;
                    ballIsMovingDown = false;
                    //now decide how to rebound the ball horizontally
                    if (ballPosition.x > racketPosition.x) {
                        ballIsMovingRight = true;
                        ballIsMovingLeft = false;

                    } else {
                        ballIsMovingRight = false;
                        ballIsMovingLeft = true;
                    }

                }
            }
        }
}

第 2 阶段代码解释

这个阶段的代码很长,但当我们分解它时并没有太多挑战。可能唯一的挑战在于解开一些嵌套的if语句。我们现在将这样做。

在步骤 1 中,我们声明了SquashCourView类。这实现了Runnable接口。你可能还记得来自第五章,游戏和 Java 基础Runnable为我们提供了一个线程。我们只需要重写run方法,里面的任何内容都将在一个新的线程中运行。

然后我们创建了一个名为ourThread的新Thread对象,以及一个SurfaceHolder对象来保存我们的表面并使我们能够在线程内控制或锁定我们的表面。接下来,我们有playingSquashboolean类型。这包裹了我们重写的run方法内部,以控制游戏何时运行。看起来奇怪的volatile修饰符意味着我们将能够从线程的内部和外部更改它的值。

最后,对于当前讨论的代码块,我们声明了一个Paint类型的对象,称为paint,来进行绘画:

class SquashCourtView extends SurfaceView implements Runnable {
        Thread ourThread = null;
        SurfaceHolder ourHolder;
        volatile boolean playingSquash;
        Paint paint;

接下来,我们实现了我们类的构造函数,这样当我们在onCreate中初始化一个新的SquashCourtView对象时,这就是运行的代码。首先,我们看到我们运行了超类的构造函数。然后我们使用getHolder方法初始化了ourHolder。接下来,我们初始化了我们的paint对象:

        public SquashCourtView(Context context) {
            super(context);
            ourHolder = getHolder();
            paint = new Paint();

现在,在构造函数中,我们让事情开始动起来。我们将ballIsMovingDown变量设置为true。在每场比赛开始时,我们总是希望球朝下移动。很快我们将看到updateCourt方法将执行球的移动。接下来,我们以随机的水平方向发送球。这是通过获取 0 到 2 之间的随机数来实现的。然后我们为每种可能的情况进行切换:0、1 或 2。在每种情况下,我们以不同的方式设置控制水平移动的布尔变量。在case 0中,球向左移动,在case 1case 3中,球将向右移动和向下移动。然后我们关闭我们的构造函数:

            ballIsMovingDown = true;

            //Send the ball in random direction
            Random randomNumber = new Random();
            int ballDirection = randomNumber.nextInt(3);
            switch (ballDirection) {
                case 0:
                    ballIsMovingLeft = true;
                    ballIsMovingRight = false;
                    break;

                case 1:
                    ballIsMovingRight = true;
                    ballIsMovingLeft = false;
                    break;

                case 2:
                    ballIsMovingLeft = false;
                    ballIsMovingRight = false;
                    break;
            }

        }

在第 2 步中,我们有一些非常简单的代码,但这些代码运行了其他所有内容。重写的run方法是ourThread在定义的时间间隔调用的。正如你所看到的,代码被包裹在一个由我们的playingSquash布尔类型变量控制的while块中。然后代码简单地调用updateCourt,控制移动和碰撞检测;drawCourt,将绘制一切;和controlFPS,将锁定我们的游戏到一个一致的帧速率。这就是run的全部内容。

@Override
        public void run() {
            while (playingSquash) {
                updateCourt();
                drawCourt();
                controlFPS();

            }

        }

然后在第 3 步,我们开始updateCourt方法。它非常长,所以我们将它分解成几个可管理的部分。前两个if块检查racketIsMovingRightracketIsMovingLeft布尔变量是否为 true。如果其中一个为 true,则块将从racketPosition.x中加上10或减去10。当球拍在drawCourt方法中绘制时,玩家将看到这种效果。布尔变量在onTouchEvent方法中如何操作将很快讨论:

public void updateCourt() {
            if (racketIsMovingRight) {
                racketPosition.x = racketPosition.x + 10;
            }

            if (racketIsMovingLeft) {
                racketPosition.x = racketPosition.x - 10;
            }

现在,仍然在updateCourt方法中,我们检测并处理与屏幕左侧和右侧的碰撞。检查ballPosition.x是否大于screenWidth就足以看到球是否会反弹。然而,通过更精确一些,测试ballPosition.x + ballWidth > screenWidth,我们测试了球的右边缘是否击中了屏幕的右侧。这会产生一个更加令人愉悦的效果,因为它看起来更“真实”。当发生与右侧的碰撞时,我们只需改变球的方向并播放声音。左侧检测的if代码更简单的原因是因为我们使用drawRect绘制了球,所以ballPosition.x是球的精确左侧。当球与左侧碰撞时,我们只需改变它的方向并播放一个哔哔声:

            //detect collisions

            //hit right of screen
            if (ballPosition.x + ballWidth > screenWidth) {
                ballIsMovingLeft = true;
                ballIsMovingRight = false;
                soundPool.play(sample1, 1, 1, 0, 0, 1);
            }

            //hit left of screen
            if (ballPosition.x < 0) {
                ballIsMovingLeft = false;
                ballIsMovingRight = true;
                soundPool.play(sample1, 1, 1, 0, 0, 1);
            }

在第 4 步中,我们实现了当球击中屏幕底部时会发生什么。这发生在玩家未能将球击回时,因此这里需要发生一些事情。然而,在这一部分中并没有什么特别复杂的东西。首先是碰撞测试。我们检查球的底部是否击中了屏幕底部:

//Edge of ball has hit bottom of screen
if (ballPosition.y > screenHeight - ballWidth) {

如果击中了,我们扣除一条生命。然后我们检查玩家是否失去了所有的生命:

   lives = lives - 1;
   if (lives == 0) {

如果所有生命都丢失,我们通过将生命重置为 3 并将得分重置为 0 来重新开始游戏。我们还会播放一个低音响声:

          lives = 3;
          score = 0;
          soundPool.play(sample4, 1, 1, 0, 0, 1);
       }

到目前为止,我们仍然在if块内,因为球击中了屏幕底部,但是在玩家生命值为零的if块外。无论玩家是否生命值为零或者还有一些生命值,我们都需要将球放回屏幕顶部,并使其沿着向下的轨迹和随机的水平方向移动。这段代码与我们在构造函数中看到的代码类似,用于在游戏开始时使球移动:

ballPosition.y = 1 + ballWidth;//back to top of screen
//what horizontal direction should we use
//for the next falling ball
Random randomNumber = new Random();
int startX = randomNumber.nextInt(screenWidth - ballWidth) + 1;
                ballPosition.x = startX + ballWidth;

                int ballDirection = randomNumber.nextInt(3);
                switch (ballDirection) {
                    case 0:
                        ballIsMovingLeft = true;
                        ballIsMovingRight = false;
                        break;

                    case 1:
                        ballIsMovingRight = true;
                        ballIsMovingLeft = false;
                        break;

                    case 2:
                        ballIsMovingLeft = false;
                        ballIsMovingRight = false;
                        break;
                }
            }

在第 5 步中,我们处理了球击中屏幕顶部的事件。反转ballIsMovingDownballIsMovingUp所持有的值以改变球的方向。通过tweak ballPosition.y = 1来调整球的位置。这样可以防止球被卡住,并播放一个愉悦的哔哔声:

//we hit the top of the screen
            if (ballPosition.y <= 0) {
                ballIsMovingDown = true;
                ballIsMovingUp = false;
                ballPosition.y = 1;
                soundPool.play(sample2, 1, 1, 0, 0, 1);
            }

现在,在所有这些碰撞检测和布尔变量的切换之后,我们实际上移动了球。对于每个方向为真的情况,我们相应地向ballPosition.xballPosition.y添加或减去。请注意,球向上移动的速度比向下移动的速度快。这样做是为了缩短玩家等待重新参与游戏的时间,并且粗略地模拟球被球拍击中后的加速行为:

            //depending upon the two directions we should be
            //moving in adjust our x any positions
            if (ballIsMovingDown) {
                ballPosition.y += 6;
            }

            if (ballIsMovingUp) {
                ballPosition.y -= 10;
            }

            if (ballIsMovingLeft) {
                ballPosition.x -= 12;
            }

            if (ballIsMovingRight) {
                ballPosition.x += 12;
            }

提示

您可能已经注意到,通过硬编码球移动的像素数,我们在高分辨率和低分辨率屏幕之间创建了不一致的球速度。查看本章末尾的自测问题,看看我们如何解决这个问题。

我们还有最后一点碰撞检测要做。球是否击中了球拍?这个检测分为几个阶段进行。首先,我们检查球的底部是否到达或超过了球拍的顶部:

if (ballPosition.y + ballWidth >= (racketPosition.y - racketHeight / 2)) {

如果这个条件成立,我们会进行一些额外的测试。首先,我们声明并初始化一个名为halfRacketint变量,用于保存球拍宽度的一半。我们将在即将进行的测试中使用它:

int halfRacket = racketWidth / 2;

接下来的if块检查球的右侧是否大于球拍的最左侧角,并且是否与之接触。使用 AND 运算符(&&),该块验证球的左边缘是否没有超过球拍的最右边。如果这个条件成立,我们肯定击中了,可以考虑如何处理反弹:

if (ballPosition.x + ballWidth > (racketPosition.x - halfRacket)
  && ballPosition.x - ballWidth < (racketPosition.x + halfRacket)) {

if块内的第一部分代码,确定了一个明确的击中,很简单。播放声音,增加得分,并使球向上运动,就像这样:

//rebound the ball vertically and play a sound
                    soundPool.play(sample3, 1, 1, 0, 0, 1);
                    score++;
                    ballIsMovingUp = true;
                    ballIsMovingDown = false;

现在我们有一个if-else条件,简单地检查球的左边缘是否超过了球拍的中心。如果是,我们将球发送到右侧。否则,我们将球发送到左侧:

                    //now decide how to rebound the ball horizontally
                    if (ballPosition.x > racketPosition.x) {
                        ballIsMovingRight = true;
                        ballIsMovingLeft = false;

                    } else {
                        ballIsMovingRight = false;
                        ballIsMovingLeft = true;
                    }

                }
            }
        }

第 3 阶段 - SquashCourtView 第二部分

在这个阶段,我们将完成我们的SquashCourtView类。还有两个从run方法调用的方法,drawCourtcontrolFPS。然后有一些与 Android 生命周期方法交互的短方法,我们将在第四和最后阶段实现:

  1. 以下是按照以下顺序绘制的代码:屏幕顶部的文本,球和球拍。所有这些都包含在drawCourt方法中,该方法从run方法中调用,在调用updateCourt之后立即调用。以下是drawCourt的代码。在SquashCourtView类的结束大括号之前输入以下代码:
public void drawCourt() {

            if (ourHolder.getSurface().isValid()) {
                canvas = ourHolder.lockCanvas();
                //Paint paint = new Paint();
                canvas.drawColor(Color.BLACK);//the background
                paint.setColor(Color.argb(255, 255, 255, 255));
                paint.setTextSize(45);
                canvas.drawText("Score:" + score + " Lives:" + lives + " fps:" + fps, 20, 40, paint);

                //Draw the squash racket
                canvas.drawRect(racketPosition.x - (racketWidth / 2),
                  racketPosition.y - (racketHeight / 2), racketPosition.x + (racketWidth / 2),
                      racketPosition.y + racketHeight, paint);

                //Draw the ball
                canvas.drawRect(ballPosition.x, ballPosition.y,
                        ballPosition.x + ballWidth, ballPosition.y + ballWidth, paint);

                ourHolder.unlockCanvasAndPost(canvas);
            }

        }
  1. 现在controlFPS方法将我们的帧速率锁定到某个平稳一致的值。我们很快将详细了解其确切工作原理。在上一步的代码之后输入以下代码:
public void controlFPS() {
            long timeThisFrame = (System.currentTimeMillis() - lastFrameTime);
            long timeToSleep = 15 - timeThisFrame;
            if (timeThisFrame > 0) {
                fps = (int) (1000 / timeThisFrame);
            }
            if (timeToSleep > 0) {

                try {
                    ourThread.sleep(timeToSleep);
                } catch (InterruptedException e) {
                }

            }

            lastFrameTime = System.currentTimeMillis();
        }
  1. 接下来,我们编写pauseresume的代码。这些代码由它们相关的 Android 生命周期方法(onPauseonResume)调用。我们确保在玩家完成或恢复游戏时,我们的线程能够安全地结束或启动。现在在上一步的代码之后输入以下代码:
public void pause() {
            playingSquash = false;
            try {
                ourThread.join();
            } catch (InterruptedException e) {
            }

        }

        public void resume() {
            playingSquash = true;
            ourThread = new Thread(this);
            ourThread.start();
        }
  1. 最后,我们有一个控制玩家触摸我们自定义的SurfaceView时发生的情况的方法。请记住,当我们讨论游戏的设计时,我们说屏幕左侧的任何位置按下都会将球拍移动到左侧,而屏幕右侧的任何位置按下都会将球拍移动到右侧。在上一步的代码之后输入以下代码:
@Override
        public boolean onTouchEvent(MotionEvent motionEvent) {

            switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:

                    if (motionEvent.getX() >= screenWidth / 2) {
                        racketIsMovingRight = true;
                        racketIsMovingLeft = false;
                    } else {
                        racketIsMovingLeft = true;
                        racketIsMovingRight = false;
                    }

                    break;

                case MotionEvent.ACTION_UP:
                    racketIsMovingRight = false;
                    racketIsMovingLeft = false;
                    break;
            }
            return true;
        }

    }

第 3 阶段代码解释

在第 1 步中,我们进行所有绘制。我们已经看到Canvas类的所有不同绘制方法可以做什么,它们的名称也是不言自明的。但是,我们需要解释如何得到这些坐标。首先,在drawCourt内部,我们使用ourHolder来获取绘图表面,并检查其有效性(可用性)。然后我们初始化我们的canvaspaint对象:

public void drawCourt() {

            if (ourHolder.getSurface().isValid()) {
                canvas = ourHolder.lockCanvas();
                //Paint paint = new Paint();

接下来,我们清除上一帧绘制的屏幕:

       canvas.drawColor(Color.BLACK);//the background

现在我们将画笔颜色设置为白色:

        paint.setColor(Color.argb(255, 255, 255, 255));

这是新的,但很容易解释 - 我们为文本设置了一个大小:

                paint.setTextSize(45);

现在我们可以在屏幕顶部绘制一行文本。它显示scorelives变量。我们已经看到如何控制它们的值。它还显示fps变量的值。当我们查看下一个方法controlFPS时,我们将看到如何为其分配一个值:

  canvas.drawText("Score:" + score + " Lives:" + lives + " fps:" +fps, 20, 40, paint);

然后我们绘制球拍。请注意,我们通过从racketPosition.x减去球拍宽度的一半来计算x起始位置,并通过将宽度添加到x来计算x结束位置。这使得我们的碰撞检测代码变得简单,因为racketPosition.x指的是球拍的中心:

//Draw the squash racket
  canvas.drawRect(racketPosition.x - (racketWidth / 2),
                  racketPosition.y - (racketHeight / 2), 
                  racketPosition.x + (racketWidth / 2),
                  racketPosition.y + racketHeight, paint);

接下来,我们绘制球。请注意,起始的xy坐标与ballPosition.xballPosition.y中保存的值相同。因此,这些坐标对应于球的左上角。这正是我们简单碰撞检测代码所需要的:

                //Draw the ball
                canvas.drawRect(ballPosition.x, ballPosition.y,
                  ballPosition.x + ballWidth, ballPosition.y + ballWidth, paint);

最后一行将我们刚刚完成的内容绘制到屏幕上:

                ourHolder.unlockCanvasAndPost(canvas);
            }

        }

在第 2 步中,我们基本上暂停了游戏。我们希望决定我们重新计算对象位置和重绘它们的次数。下面是它的工作原理。

首先,当从run方法调用controlFPS方法时,我们进入controlFPS方法。我们声明并初始化一个long变量,其值为毫秒,然后减去上一帧所花费的时间(以毫秒为单位)。时间是在上一次运行此方法时计算的,最后,我们将在此方法的最后看到:

public void controlFPS() {
long timeThisFrame = (System.currentTimeMillis() - lastFrameTime);

然后,我们计算我们希望在帧之间暂停多长时间,并将该值初始化为timeToSleep,一个新的长变量。计算的方法如下:15 毫秒的暂停让我们大约每秒 60 帧,这对我们的游戏效果很好,提供了非常流畅的动画。因此,15 - timeThisFrame等于我们应该暂停的毫秒数,以使帧持续 15 毫秒:

long timeToSleep = 15 - timeThisFrame; 

当然,一些设备无法应对这种速度。我们也不希望暂停负数,也不希望在timeThisFrame等于零时计算每秒帧数。接下来,我们将每秒帧数的计算包装在一个if语句中,以防止我们除以零或负数:

            if (timeThisFrame > 0) {
                fps = (int) (1000 / timeThisFrame);
            }

同样,我们将暂停线程的指令包装在类似的谨慎的if语句中:

            if (timeToSleep > 0) {

                try {
                    ourThread.sleep(timeToSleep);
                } catch (InterruptedException e) {
                }

            }

最后,我们看到我们如何初始化lastFrameTime,准备好下一次调用controlFPS

            lastFrameTime = System.currentTimeMillis();
        }

在第 3 步,我们快速实现了两种方法。它们是pauseresume。这些不应与 Android Activity 生命周期方法onPauseonResume混淆。但是,pauseresume方法是从它们的近似名称中调用的。它们分别处理停止和启动ourThread。我们应该始终清理我们的线程。否则,它们可能会在活动结束后继续运行:

public void pause() {
            playingSquash = false;
            try {
                ourThread.join();
            } catch (InterruptedException e) {
            }

        }

        public void resume() {
            playingSquash = true;
            ourThread = new Thread(this);
            ourThread.start();
        }

在第 4 步,我们处理屏幕上的触摸。这是我们如何初始化我们的racketIsMovingLeftracketIsMovingRight布尔变量的方式,updateCourt方法用于决定是向左还是向右滑动玩家的球拍,还是保持静止。我们之前已经讨论过onTouchEvent方法,但让我们看看如何在这些变量中设置值。

首先,我们重写该方法并切换以获取事件类型和事件的xy坐标:

@Override
    public boolean onTouchEvent(MotionEvent motionEvent) {

    switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {

如果事件类型是ACTION_DOWN,即屏幕已被触摸,我们进入此情况:

    case MotionEvent.ACTION_DOWN:

然后我们处理坐标。如果玩家触摸屏幕上 x 坐标大于screenWidth / 2的位置,则意味着他们触摸了屏幕的右侧,因此我们将isMovingRight设置为true,将isMovingLeft设置为falseupdateCourt方法将处理必要坐标的更改,而drawCourt方法将在适当的位置绘制球拍:

    if (motionEvent.getX() >= screenWidth / 2) {
        racketIsMovingRight = true;
        racketIsMovingLeft = false;

else语句以相反的方式设置我们的两个布尔变量,因为触摸必须发生在屏幕的左侧:

                } else {
                    racketIsMovingLeft = true;
                    racketIsMovingRight = false;
                }

                    break;

现在我们处理ACTION_UP事件的情况。但是为什么我们关心两个事件?对于按钮,我们只关心点击,但通过处理ACTION_UP事件,我们可以启用允许玩家按住屏幕向左或向右滑动的功能,就像我们在本章的游戏设计部分讨论的那样。因此,ACTION_DOWN情况设置球拍向左或向右移动,而ACTION_UP情况只是完全停止滑动:

                case MotionEvent.ACTION_UP:
                    racketIsMovingRight = false;
                    racketIsMovingLeft = false;
                    break;
            }
            return true;
        }

    }

请注意我们不关心y坐标。在左侧的任何地方我们向左走,在右侧的任何地方我们向右走。

注意

还要注意,所有代码都将在设备以纵向或横向形式保持时运行,并且无论设备的分辨率如何,都将以相同的方式运行。但是(这是一个非常重要的“但是”),在低分辨率屏幕上,游戏会稍微困难一些。这个问题的解决方案非常复杂,直到最后一章才会讨论,但它可能会帮助我们对未来学习 Android、游戏和 Java 的路径做出一些决定。

第 4 阶段-剩余的生命周期方法

我们快要完成了;再过几步,我们就会有一个可用的复古壁球游戏。我几乎能闻到怀旧的味道!由于这些剩余的方法非常简单,我们将在编写它们时进行解释。

  1. 正如我们之前学到的,当应用程序停止时,onStop方法会被 Android 系统调用。这个方法已经为我们实现了。我们在这里重写它的唯一原因是确保我们的线程被停止。我们可以通过下面的代码实现。在MainActivity类的结束大括号之前输入以下代码:
@Override
    protected void onStop() {
        super.onStop();

        while (true) {
            squashCourtView.pause();
            break;
        }

        finish();
    }
  1. 当应用程序暂停时,onPause方法会被 Android 系统调用。这也已经为我们实现了,我们在这里重写它的唯一原因是确保我们的线程被停止。我们可以通过下面的代码实现。在前面的代码之后输入这段代码:
@Override
    protected void onPause() {
        super.onPause();
        squashCourtView.pause();
    }
  1. 当应用程序恢复时,onResume方法会被 Android 系统调用。同样,这个方法已经为我们实现了。我们在这里重写它的唯一原因是确保我们的线程被恢复,我们可以通过下面的代码实现。在上一步的代码之后输入以下代码:
@Override
    protected void onResume() {
        super.onResume();
        squashCourtView.resume();
    }
  1. 最后,我们做一些全新的事情。我们处理玩家在设备上按下返回按钮时会发生什么。你可能已经猜到,我们可以重写一个方法来实现这一点——onKeyDown。我们暂停我们的线程,就像我们在重写的生命周期方法中所做的那样,然后调用finish(),这将结束活动和我们的应用程序。在上一步的代码之后输入这段代码:
public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            squashCourtView.pause();
            finish();
            return true;
        }
        return false;
    }

我们在进行这个阶段时已经覆盖了代码,这是迄今为止最短的阶段。那么为什么我们没有封装一切呢?

良好的面向对象设计

也许简单的游戏并不是展示良好面向对象设计的最佳方式,但是一个简单的代码设计,使用较少的私有变量实际上增强了项目。这无疑使教授编码游戏方面更简单易懂。

然而,当游戏变得更加复杂,更多的人参与到代码中时,面向对象编程原则变得更加必要。

自测问题

Q1) 你能解释一下如何使不同屏幕分辨率下的球速度保持相对吗?

总结

希望你喜欢给你的第一个游戏添加动画。你已经取得了很大的成就。你不仅学会了所有的 Java 主题,还学会了 Android 的不同类如何相对简单地制作游戏。

在下一章中,我们将继续进行一个新的、更复杂的游戏。希望你已经准备好了。

第八章:蛇游戏

在本章中,我们将直接着手设计和实现一个高度上瘾的Snake游戏的克隆。我们将研究游戏的设计,并学习如何为一些位图添加动画。然后我们将看一下一些新的代码方面,比如我们的坐标系统。之后,我们将快速实现游戏。最后,我们将看一下如何增强我们的游戏。

在本章中,我们将涵盖以下主题:

  • 检查我们游戏的设计

  • 查看我们的Snake游戏的坐标系统

  • 检查代码结构,以便在实现游戏时更加简单

  • 学习使用精灵表实现游戏主屏幕时的动画

  • Snake游戏的代码分解为可管理的块,并运行其完整实现

  • 稍微增强游戏

游戏设计

如果你以前没有玩过优秀的Snake游戏,这里是它的工作原理的解释。你控制一个非常小的蛇。在我们的版本中,只有一个头部,一个身体部分和一个尾巴。这是我们蛇的截图,由三个部分组成:

游戏设计

以下截图显示了三个部分:

游戏设计

现在,问题来了;我们的蛇非常饥饿,而且成长得非常快。每次他吃一个苹果,他就会长出一个身体部分。这是一个苹果的截图:

游戏设计

生活真是美好!我们的蛇只是吃东西并长大!我们游戏的玩家需要解决的问题是,蛇有点过于活跃。它永远不停止移动!加剧这个问题的是,如果蛇触碰屏幕的边缘,它就会死亡。

起初,这似乎并不是太大的问题,但随着蛇变得越来越长,他不能一直绕圈子走,因为他最终会不可避免地撞到自己。这将再次导致他的死亡:

游戏设计

每吃一个苹果,我们都会将一定数量添加到分数上。这是游戏在基本实现之后和增强之前的样子:

游戏设计

玩家通过点击屏幕的左侧或右侧来控制蛇。蛇将通过向左或向右转向做出响应。转向方向是相对于蛇行进方向的,这增加了挑战,因为玩家需要像蛇一样思考!

在本章结束时,我们还将简要介绍如何增强游戏,使用增强版在下一章中将其发布到 Google Play 商店,并添加排行榜和成就。

坐标系统

在上一章中,我们直接将所有游戏对象绘制到屏幕上的点,并使用真实的屏幕坐标来检测碰撞、反弹等。这一次,我们将做一些稍微不同的事情。这在一定程度上是出于必要,但正如我们将看到的,碰撞检测和跟踪游戏对象也会变得更简单。当我们考虑到我们的蛇可能会变得很长时,这可能会让人感到惊讶。

跟踪蛇的部分

为了跟踪所有蛇的部分,我们首先定义一个块大小来定义整个游戏区域的网格部分。每个游戏对象将驻留在(x,y)坐标处,不是基于屏幕的像素分辨率,而是基于我们虚拟网格内的位置。在游戏中,我们定义了一个宽度为 40 个方块的网格,就像这样:

//Determine the size of each block/place on the game board
 blockSize = screenWidth/40;

因此我们知道:

numBlocksWide = 40;

游戏屏幕的高度以方块为单位,将通过将屏幕的高度以像素为单位除以先前确定的blockSize的值来简单计算,减去顶部一点空间用于分数:

numBlocksHigh = ((screenHeight - topGap ))/blockSize;

然后,这使我们可以使用两个数组来跟踪我们的蛇的xy坐标,其中元素零是头部,最后使用的元素是尾部,就像这样:

//An array for our snake
snakeX = new int[200];
snakeY = new int[200];

只要我们有一个移动头部的系统,也许类似于挤压球但基于我们的新游戏网格,我们就可以做以下事情让身体跟随头部移动:

//move the body starting at the back
for(int i = snakeLength; i >0 ; i--){
  snakeX[i] = snakeX[i-1];
  snakeY[i] = snakeY[i-1];
}

前面的代码简单地从蛇的后部开始,并在网格中创建其位置,而不管前面的部分是什么。它沿着身体向上进行相同的操作,直到一切都被移动到曾经在其前面的部分的位置。

这也使得碰撞检测(即使对于非常长的蛇)变得非常简单。

检测碰撞

使用基于blockSize的网格,我们可以检测碰撞,例如与屏幕右侧的碰撞,就像这样:

if(snakeX[0] >= numBlocksWide)dead=true;

前面的代码只是检查我们的数组的第一个元素(保存蛇的x坐标)是否等于或大于游戏网格的宽度(以块为单位)。在我们实现时,试着先想出与左侧、顶部和底部碰撞的代码。

检测蛇撞到自己的事件也很快。我们只需要检查我们的数组的第一个元素(头部)是否与任何其他部分的位置完全相同,就像这样:

//Have we eaten ourselves?
for (int i = snakeLength-1; i > 0; i--) {
  if ((i > 4) && (snakeX[0] == snakeX[i]) && (snakeY[0] == snakeY[i])) {
    dead = true;
    }
}

绘制蛇

我们只需根据蛇的每个部分相对于其网格位置乘以一个块的像素大小来绘制。blockSize变量处理了使游戏在不同屏幕尺寸上运行的整个挑战,就像这样:

//loop through every section of the snake and draw it
//a block at a time.
canvas.drawBitmap(bodyBitmap, snakeX[i]*blockSize, (snakeY[i]*blockSize)+topGap, paint);

诚然,关于我们的实现将如何工作可能还有更多问题,但最好的答案可能是通过实际构建游戏来回答。

因此,我们可以通过编写代码或仅从已完成的项目中阅读来轻松跟进。让我们来看看我们代码的整体结构。

代码结构

我们将有两个活动,一个用于菜单屏幕,一个用于游戏屏幕。菜单屏幕活动将被称为MainActivity,游戏屏幕活动将被称为GameActivity。您可以在下载包的Chapter8/Snake文件夹中找到所有已完成的代码文件以及所有资产,如图像、精灵表和声音文件。

主活动

与我们的其他项目不同,菜单屏幕将不会在 Android Studio UI 设计器中设计 UI。它将由一个动画蛇头、一个标题和一个高分组成。玩家将通过在屏幕上的任何位置轻击来进入GameActivity。由于我们需要完成动画和用户交互,即使主屏幕也将有一个线程、一个视图对象和通常与我们的游戏屏幕相关的方法,就像这样:

MainActivity.java file
    Imports
    MainActivity class
        Declare some variables and objects
        onCreate
        SnakeAnimView class
            Constructor
            Run method
            Update method
            Draw method
            controlFPS method
            pause method
            resume method
            onTouchEvent method
        onStop method
        onResume method
        onPause method
        onKeyDown method

我们暂时不会深入研究菜单屏幕,因为在本节结束时,我们将逐行实现它。

游戏活动

游戏屏幕结构与我们的 Squash 游戏和菜单屏幕的结构有许多相似之处,尽管这种结构的内部差异很大(正如我们已经讨论过的和将要看到的)。在结构的末尾有一些差异,尤其是loadSound方法和configureDisplay方法。这是结构(之后我们将看到为什么有这两个额外的方法):

MainActivity.java file
    Imports
    GameActivity class
        Declare some variables and objects
        onCreate
        SnakeView class
            Constructor
            getSnake method
            getApple method
            Run method
            updateGame method
            drawGame method
            controlFPS method
            pause method
            resume method
            onTouchEvent method
        onStop method
        onResume method
        onPause method
        onKeyDown method
        loadSOund method
        configureDisplay method

整理 onCreate

当您很快实现的GameActivity类的代码时,您可能会注意到的第一件事是onCreate方法有多么简短:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        loadSound();
        configureDisplay();
        snakeView = new SnakeView(this);
        setContentView(snakeView);

    }

我们编写了两个方法,loadSoundconfigureDisplay。它们完成了我们挤压游戏中的大部分初始化和设置。这使得我们的代码更加整洁。在onCreate中只剩下初始化我们的SnakeView对象和调用setContentView

当我们实现它们时,我们将详细查看我们的loadSoundconfigureDisplay方法。

由于我们已经提前了解了结构并且有了这种实现类型的先前经验,我们将在一个阶段内完成我们游戏活动的所有实现。

让我们快速实现菜单屏幕。

动画、精灵表和蛇的主屏幕

在上一章中,我们使用位图在我们用 Java 代码创建的空白位图上绘制文本、圆圈、线条和单个像素。然后,我们使用Canvas类显示了所有涂鸦的位图。现在我们将看一种绘制二维图像的技术,有时被称为精灵。这些是由预先绘制的图像制成的。这些图像可以是简单的乒乓球,也可以是拥有肌肉定义、精美服装、武器和头发的辉煌的二维角色。

到目前为止,我们已经用不变的对象进行了动画,也就是说,我们已经将一个静态不变的图像在屏幕上移动了。在本节中,我们将看到如何不仅在屏幕上显示预先绘制的位图图像,而且不断地改变它以创建现场动画的幻觉。

当然,最终的组合将是通过同时改变图像和移动图像来实现位图动画。当我们查看本章节《Snake》游戏的增强版本时,我们将会简要看到这一点,但不会分析代码。

要在现场进行位图动画,我们需要一些位图,正如你所期望的那样。例如,要绘制蛇尾来回摆动,我们至少需要两帧动画,显示尾巴在不同位置的样子。在下面的截图中,花朵的头朝向左侧:

使用精灵表进行动画

在这个截图中,花朵已经被翻转:

动画、精灵表和蛇的主屏幕

如果两个位图连续显示,它们将创建花在风中摇摆的基本效果。当然,两帧动画不会争夺任何动画奖项,而且这些图像还有另一个问题,正如我们将要学到的,所以我们应该添加更多帧来使动画尽可能逼真。

在我们为游戏的主屏幕制作一个动画蛇头之前,我们还有一件事要讨论。我们如何让 Android 在这些位图之间切换?

使用精灵表进行动画

首先,我们需要以一种易于在代码中操作的方式呈现帧。这就是精灵表的用武之地。下面的图像显示了我们将在游戏主屏幕上使用的基本蛇头动画的一些帧。这一次,它们呈现在一条帧条中。它们都是同一图像的部分,有点像电影中的一系列图像。另外,请注意在下面的图像中,帧相对于彼此居中并且大小完全相等:

使用精灵表进行动画

如果我们实际上连续显示两个前面的花朵图像,它们不仅会摇摆,而且还会在它们的茎上来回跳动,这可能不是我们想要的效果。

因此,就蛇精灵表而言,只要我们一个接一个地显示帧,我们就会创建一个基本的动画。

那么我们如何让我们的代码从精灵表的一个部分跳到另一个部分呢?每一帧的大小都完全相同,在这种情况下是 64 x 64 像素,所以我们只需要一种方法来显示从 0 到 63 的像素,然后是 64 到 127,然后是 128 到 192,依此类推。由于精灵表图像的每一帧都略有不同,它允许我们使用一个图像文件来创建我们的动画。幸运的是,我们有一个处理这个的类,虽然不像专门的精灵表类那样奢华,但几乎一样。

提示

关于精灵表类,这样的东西确实存在,尽管不在常规的 Android 类中。专门为二维游戏设计的 API 通常会包含精灵表的类。我们将在下一章中看到这方面的例子。

Rect类保存矩形的坐标。在这里,我们创建了一个Rect类型的新对象,并将其初始化为从 0,0 开始到 63,63 结束:

Rect rectToBeDrawn = new Rect(0, 0, 63, 63);

然后,Canvas类实际上可以使用我们的Rect对象来定义先前加载的位图的一部分:

canvas.drawBitmap(headAnimBitmap, rectToBeDrawn, destRect, paint);

上述代码比看起来简单得多。首先,我们看到canvas.drawBitmap。我们使用Canvas类的drawBitmap方法,就像以前一样。然后我们将headAnimBitmap作为参数传递,它是包含我们要动画的所有帧的精灵表。rectToBeDrawn代表当前相关帧在headAnimationBitmap中的坐标。destRect简单地表示我们要绘制当前帧的屏幕坐标,当然,paintPaint类的对象。

现在我们所要做的就是改变rectToBeDrawn的坐标,并使用线程控制帧速率,我们就完成了!让我们这样做,为我们的Snake游戏创建一个动画主屏幕。

实现蛇的主屏幕

通过我们刚刚介绍的背景信息以及我们对即将编写的代码结构的详细了解,这段代码不应该有任何意外。我们将把事情分成几块,只是为了确保我们确切地知道发生了什么:

  1. 创建一个 API 级别为 13 的新项目。将其命名为Snake

  2. 像以前一样将活动设置为全屏,并将图形放入drawable/mdpi文件夹中。当然,您可以像往常一样使用我的图形。它们在Snake项目的graphics文件夹中提供了代码下载。

  3. 在这里,您将找到我们的MainActivity类声明和成员变量。注意我们的CanvasBitmap类的变量,我们声明变量来保存帧大小(宽度和高度)以及帧数。我们还有一个Rect对象来保存精灵表的当前帧的坐标。我们很快就会看到这些变量的作用。输入以下代码:

public class MainActivity extends Activity {

    Canvas canvas;
    SnakeAnimView snakeAnimView;

    //The snake head sprite sheet
    Bitmap headAnimBitmap;
    //The portion of the bitmap to be drawn in the current frame
    Rect rectToBeDrawn;
    //The dimensions of a single frame
    int frameHeight = 64;
    int frameWidth = 64;
    int numFrames = 6;
    int frameNumber;

    int screenWidth;
    int screenHeight;

    //stats
    long lastFrameTime;
    int fps;
    int hi;

    //To start the game from onTouchEvent
    Intent i;
  1. 以下是重写的onCreate方法的实现。我们以通常的方式获取屏幕尺寸。我们将我们的精灵表加载到headAnimBitmap位图中。最后,我们创建一个新的SnakeAnimView并将其设置为内容视图。在上一步的代码之后输入以下代码:
@Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

        //find out the width and height of the screen
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        screenWidth = size.x;
        screenHeight = size.y;

        headAnimBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head_sprite_sheet);

        snakeAnimView = new SnakeAnimView(this);
        setContentView(snakeAnimView);

        i = new Intent(this, GameActivity.class);

    }
  1. 这是我们的SurfaceViewSnakeAnimView的声明,以及它的成员变量。注意它扩展了SurfaceView并实现了Runnable。它的所有方法都在接下来的步骤中。在上一步的代码之后输入以下代码:
class SnakeAnimView extends SurfaceView implements Runnable {
        Thread ourThread = null;
        SurfaceHolder ourHolder;
        volatile boolean playingSnake;
        Paint paint;
  1. 这是构造函数,通过将位图宽度除以帧数得到frameWidth值,并使用getHeight方法得到frameHeight值。在上一步的代码之后输入以下代码:
public SnakeAnimView(Context context) {
    super(context);
    ourHolder = getHolder();
    paint = new Paint();
    frameWidth = headAnimBitmap.getWidth()/numFrames;
    frameHeight = headAnimBitmap.getHeight();
}
  1. 现在我们实现了简短但至关重要的run方法。它依次调用这个类的每个关键方法。这三个方法在此步骤之后的三个步骤中实现。在上一步的代码之后输入以下代码:
@Override
        public void run() {
            while (playingSnake) {
                update();
                draw();
                controlFPS();

            }

        }
  1. 这是update方法。它跟踪并选择需要显示的帧编号。每次通过update方法,我们使用frameWidthframeHeightframeNumber计算要绘制的精灵表的坐标。如果您想知道为什么我们从每个水平坐标中减去1,那是因为像屏幕坐标一样,位图从 0,0 开始它们的坐标:
public void update() {

  //which frame should we draw
  rectToBeDrawn = new Rect((frameNumber * frameWidth)-1, 0,(frameNumber * frameWidth +frameWidth)-1, frameHeight);

  //now the next frame
  frameNumber++;

  //don't try and draw frames that don't exist
  if(frameNumber == numFrames){
    frameNumber = 0;//back to the first frame
  }
}
  1. 接下来是draw方法,直到最后都没有什么新的,当它通过screenHeightscreenWidth变量除以 2 来计算在屏幕上绘制位图的位置时才有变化。然后将这些坐标保存在destRect中。destRectrectToDraw都传递给drawBitmap方法,该方法在所需位置绘制所需的帧。在上一步的代码之后输入这段代码:
public void draw() {

            if (ourHolder.getSurface().isValid()) {
                canvas = ourHolder.lockCanvas();
                //Paint paint = new Paint();
                canvas.drawColor(Color.BLACK);//the background
                paint.setColor(Color.argb(255, 255, 255, 255));
                paint.setTextSize(150);
                canvas.drawText("Snake", 10, 150, paint);
                paint.setTextSize(25);
                canvas.drawText("  Hi Score:" + hi, 10, screenHeight-50, paint);

                //Draw the snake head
                //make this Rect whatever size and location you like
                //(startX, startY, endX, endY)
                Rect destRect = new Rect(screenWidth/2-100, screenHeight/2-100, screenWidth/2+100, screenHeight/2+100);

                canvas.drawBitmap(headAnimBitmap, rectToBeDrawn, destRect, paint);

                ourHolder.unlockCanvasAndPost(canvas);
            }

        }
  1. 我们可靠的controlFPS方法确保我们的动画以合理的速度显示。这段代码的唯一变化是将timeTosleep的初始化更改为在每帧之间创建 500 毫秒的暂停。在上一步的代码之后输入以下代码:
public void controlFPS() {
            long timeThisFrame = (System.currentTimeMillis() - lastFrameTime);
            long timeToSleep = 500 - timeThisFrame;
            if (timeThisFrame > 0) {
                fps = (int) (1000 / timeThisFrame);
            }
            if (timeToSleep > 0) {

                try {
                    ourThread.sleep(timeToSleep);
                } catch (InterruptedException e) {
                }

            }

            lastFrameTime = System.currentTimeMillis();
        }
  1. 接下来是我们的pauseresume方法,它们与 Android 生命周期方法一起工作,以启动和停止我们的线程。在上一步的代码之后输入这段代码:
public void pause() {
            playingSnake = false;
            try {
                ourThread.join();
            } catch (InterruptedException e) {
            }

        }

        public void resume() {
            playingSnake = true;
            ourThread = new Thread(this);
            ourThread.start();
        }
  1. 对于我们的SnakeAnimView类和onTouchEvent方法,当屏幕的任何位置被触摸时简单地启动游戏,我们输入以下代码。显然,我们还没有GameActivity
@Override
        public boolean onTouchEvent(MotionEvent motionEvent) {

            startActivity(i);
            return true;
        }
}
  1. 最后,在MainActivity类中,我们处理了一些 Android 生命周期方法。我们还处理了当玩家按下返回按钮时会发生什么:
@Override
    protected void onStop() {
        super.onStop();

        while (true) {
            snakeAnimView.pause();
            break;
        }

        finish();
    }

    @Override
    protected void onResume() {
        super.onResume();
        snakeAnimView.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        snakeAnimView.pause();
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            snakeAnimView.pause();
            finish();
            return true;
        }
        return false;
    }
  1. 现在,您必须暂时注释掉步骤 4 中的这行代码以测试动画。这样做的原因是在我们实现GameActivity类之前,它会导致错误:
//i = new Intent(this, GameActivity.class);
  1. 测试应用程序。

  2. 当我们实现了GameActivity类时,取消步骤 14 中的注释。这是我们完成的主屏幕:实现贪吃蛇主屏幕

在这个练习中,我们设置了一个扩展了SurfaceView的类,就像我们为壁球游戏做的那样。我们有一个控制线程的run方法,以及一个计算当前动画在精灵表中坐标的update方法。draw方法只是使用update方法计算的坐标在屏幕上绘制。

就像壁球游戏一样,我们有一个onTouchUpdate方法,但这次的代码非常简单。因为我们只需要检测任何位置的任何类型的触摸,所以我们只需在方法中添加一行代码。

实现贪吃蛇游戏活动

并不是所有的代码都是新的。事实上,我们要么以前大部分都用过,要么在本章中早些时候已经讨论过。然而,我想按顺序和至少简要解释的方式向您呈现每一行,即使我们以前已经看到过。话虽如此,我没有包括导入的长列表,因为我们要么会被提示自动添加它们,要么在需要时只需按Alt + Enter

这样,我们可以在没有任何理解上的空白的情况下回顾整个过程。像往常一样,随着实现的进行,我会进行总结,并在最后深入一些细节:

  1. 添加一个名为GameActivity的活动。当询问时选择一个空活动。

  2. 像以前一样,将活动设置为全屏。

  3. 像往常一样,创建一些音效或使用我的音效。按照通常的方式在main目录中创建一个assets目录。将声音文件(sample1.oggsample2.oggsample3.oggsample4.ogg)复制粘贴到其中。

  4. 创建单独的非精灵表版本的图形,或者使用我的。将它们复制粘贴到res/drawable-mdpi文件夹中。

  5. 这是GameActivity类声明和成员变量。在这里没有什么新的,直到我们声明了贪吃蛇的数组(snakeXsnakeY)。还要注意我们用来控制游戏网格的变量(blockSizenumBlocksHighnumBlocksWide)。现在输入这段代码:

public class GameActivity extends Activity {

    Canvas canvas;
    SnakeView snakeView;

    Bitmap headBitmap;
    Bitmap bodyBitmap;
    Bitmap tailBitmap;
    Bitmap appleBitmap;

    //Sound
    //initialize sound variables
    private SoundPool soundPool;
    int sample1 = -1;
    int sample2 = -1;
    int sample3 = -1;
    int sample4 = -1;

    //for snake movement
    int directionOfTravel=0;
    //0 = up, 1 = right, 2 = down, 3= left

    int screenWidth;
    int screenHeight;
    int topGap;

    //stats
    long lastFrameTime;
    int fps;
    int score;
    int hi;

    //Game objects
    int [] snakeX;
    int [] snakeY;
    int snakeLength;
    int appleX;
    int appleY;

    //The size in pixels of a place on the game board
    int blockSize;
    int numBlocksWide;
    int numBlocksHigh;
  1. 如前所述,我们的新的小onCreate方法几乎没有什么要做的,因为大部分工作都是在loadSoundconfigureDisplay方法中完成的。在上一步的代码之后输入这段代码:
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        loadSound();
        configureDisplay();
        snakeView = new SnakeView(this);
        setContentView(snakeView);

    }
  1. 这是我们的SnakeView类的类声明,成员变量和构造函数。我们为snakeXsnakeY数组分配了 200 个int变量,并调用了getSnakegetApple方法,这将在屏幕上放置一个苹果和我们的蛇。这正是在类构造时我们想要的:
  class SnakeView extends SurfaceView implements Runnable {
      Thread ourThread = null;
      SurfaceHolder ourHolder;
      volatile boolean playingSnake;
      Paint paint;

      public SnakeView(Context context) {
          super(context);
          ourHolder = getHolder();
          paint = new Paint();

            //Even my 9 year old play tester couldn't
            //get a snake this long
            snakeX = new int[200];
            snakeY = new int[200];

            //our starting snake
            getSnake();
            //get an apple to munch
            getApple();
        }
  1. 这是我们如何在我们的坐标系统中生成蛇和苹果。在getSnake方法中,我们通过将snakeX[0]snakeY[0]初始化为高度和宽度均分之后的块数的中心位置,将蛇的头放在屏幕的大致中心。然后我们立即在后面放置一个身体段和尾部段。请注意,我们不需要为不同类型的段做任何特殊安排。只要绘图代码知道第一个段是头部,最后一个段是尾部,中间的一切都是身体,那就可以了。在getApple方法中,整数变量appleXappleY被初始化为游戏网格内的随机位置。这个方法是从构造函数中调用的,就像我们在前一步中看到的那样。每当我们的蛇设法吃掉一个苹果时,它也将被调用以放置一个新的苹果,我们将会看到。在前一步的代码之后输入此代码:
public void getSnake(){
            snakeLength = 3;
            //start snake head in the middle of screen
            snakeX[0] = numBlocksWide / 2;
            snakeY[0] = numBlocksHigh / 2;

            //Then the body
            snakeX[1] = snakeX[0]-1;
            snakeY[1] = snakeY[0];

            //And the tail
            snakeX[1] = snakeX[1]-1;
            snakeY[1] = snakeY[0];
        }

        public void getApple(){
            Random random = new Random();
            appleX = random.nextInt(numBlocksWide-1)+1;
            appleY = random.nextInt(numBlocksHigh-1)+1;
        }
  1. 接下来是run方法,它控制游戏的流程。在前一步的代码之后输入以下代码:
@Override
        public void run() {
            while (playingSnake) {
                updateGame();
                drawGame();
                controlFPS();

            }

        }
  1. 现在我们将看一下updateGame,整个应用程序中最复杂的方法。话虽如此,它可能比我们挤压游戏中的同一方法稍微复杂一些。这是因为我们的坐标系统导致了更简单的碰撞检测。这是updateGame的代码。仔细研究它,我们将在最后一行一行地剖析它:
public void updateGame() {

    //Did the player get the apple
    if(snakeX[0] == appleX && snakeY[0] == appleY){
        //grow the snake
        snakeLength++;
        //replace the apple
        getApple();
        //add to the score
        score = score + snakeLength;
        soundPool.play(sample1, 1, 1, 0, 0, 1);
    }

    //move the body - starting at the back
    for(int i=snakeLength; i >0 ; i--){
        snakeX[i] = snakeX[i-1];
        snakeY[i] = snakeY[i-1];
    }

    //Move the head in the appropriate direction
    switch (directionOfTravel){
        case 0://up
        snakeY[0]  --;
        break;

        case 1://right
        snakeX[0] ++;
        break;

        case 2://down
        snakeY[0] ++;
        break;

        case 3://left
        snakeX[0] --;
        break;
        }

        //Have we had an accident
        boolean dead = false;
        //with a wall
        if(snakeX[0] == -1)dead=true;
        if(snakeX[0] >= numBlocksWide) dead = true;
        if(snakeY[0] == -1)dead=true;
        if(snakeY[0] == numBlocksHigh) dead = true;
        //or eaten ourselves?
        for (int i = snakeLength-1; i > 0; i--) {
            if ((i > 4) && (snakeX[0] == snakeX[i]) && (snakeY[0] == snakeY[i])) {
            dead = true;
        }
    }

        if(dead){
        //start again
        soundPool.play(sample4, 1, 1, 0, 0, 1);
        score = 0;
        getSnake();

        }

        }
  1. 我们已经弄清楚了游戏对象在屏幕上的位置,现在我们可以绘制它们。这段代码很容易理解,因为我们之前大部分都见过:
public void drawGame() {

    if (ourHolder.getSurface().isValid()) {
        canvas = ourHolder.lockCanvas();
        //Paint paint = new Paint();
        canvas.drawColor(Color.BLACK);//the background
        paint.setColor(Color.argb(255, 255, 255, 255));
        paint.setTextSize(topGap/2);
        canvas.drawText("Score:" + score + "  Hi:" + hi, 10, topGap-6, paint);

        //draw a border - 4 lines, top right, bottom , left
        paint.setStrokeWidth(3);//3 pixel border
        canvas.drawLine(1,topGap,screenWidth-1,topGap,paint);
        canvas.drawLine(screenWidth-1,topGap,screenWidth-1,topGap+(numBlocksHigh*blockSize),paint);
        canvas.drawLine(screenWidth-1,topGap+(numBlocksHigh*blockSize),1,topGap+(numBlocksHigh*blockSize),paint);
        canvas.drawLine(1,topGap, 1,topGap+(numBlocksHigh*blockSize), paint);

        //Draw the snake
        canvas.drawBitmap(headBitmap, snakeX[0]*blockSize, (snakeY[0]*blockSize)+topGap, paint);
        //Draw the body
        for(int i = 1; i < snakeLength-1;i++){
            canvas.drawBitmap(bodyBitmap, snakeX[i]*blockSize, (snakeY[i]*blockSize)+topGap, paint);
        }
        //draw the tail
        canvas.drawBitmap(tailBitmap, snakeX[snakeLength-1]*blockSize, (snakeY[snakeLength-1]*blockSize)+topGap, paint);

        //draw the apple
        canvas.drawBitmap(appleBitmap, appleX*blockSize, (appleY*blockSize)+topGap, paint);

        ourHolder.unlockCanvasAndPost(canvas);
    }

        }
  1. 这是controlFPS方法,与我们的挤压游戏的controlFPS方法相同,只是我们有一个不同的目标帧率。在前一步的代码之后输入此代码:
public void controlFPS() {
    long timeThisFrame = (System.currentTimeMillis() - lastFrameTime);
    long timeToSleep = 100 - timeThisFrame;
    if (timeThisFrame > 0) {
        fps = (int) (1000 / timeThisFrame);
    }
    if (timeToSleep > 0) {

        try {
            ourThread.sleep(timeToSleep);
        } catch (InterruptedException e) {
        }

        }

        lastFrameTime = System.currentTimeMillis();
        }
  1. 这里是我们未更改的pauseresume方法。在前一步的代码之后输入以下代码:
public void pause() {
            playingSnake = false;
            try {
                ourThread.join();
            } catch (InterruptedException e) {
            }

        }

        public void resume() {
            playingSnake = true;
            ourThread = new Thread(this);
            ourThread.start();
        }
  1. 然后是onTouchEvent方法,类似于我们挤压游戏的方法。这里没有新概念,但在这个游戏中它的工作方式如下。我们打开ACTION_UP事件。这与检测点击大致相同。然后我们检查按压是在左边还是右边。如果在右边,我们增加directionOfTravel。如果在左边,我们减少directionOfTravel。如果你仔细看updateGame方法,你会发现directionOfTravel表示移动蛇的方向。记住,蛇永远不会停止。这就是为什么我们与我们的挤压游戏做法不同的原因。在前一步的代码之后输入此代码:
@Override
    public boolean onTouchEvent(MotionEvent motionEvent) {

        switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_UP:
            if (motionEvent.getX() >= screenWidth / 2) {
                //turn right
                directionOfTravel ++;
                //no such direction

                if(directionOfTravel == 4)
                //loop back to 0(up)
                directionOfTravel = 0;
            }
        } else {
            //turn left
            directionOfTravel--;
            if(directionOfTravel == -1) {//no such direction
            //loop back to 0(up)
            directionOfTravel = 3;
                        }
                    }
            }
            return true;
        }
  1. GameActivity类中,我们现在处理 Android 生命周期方法和“返回”按钮功能。在前一步的代码之后输入以下代码:
@Override
    protected void onStop() {
        super.onStop();

        while (true) {
            snakeView.pause();
            break;
        }

        finish();
    }

    @Override
    protected void onResume() {
        super.onResume();
        snakeView.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        snakeView.pause();
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {

           snakeView.pause();

            Intent i = new Intent(this, MainActivity.class);
            startActivity(i);
            finish();
            return true;
        }
        return false;
    }
  1. 这是我们的loadSound方法,它通过将所有声音初始化移动到这里,简化了onCreate方法。在前一步的代码之后输入以下代码:
public void loadSound(){
    soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
    try {
        //Create objects of the 2 required classes
        AssetManager assetManager = getAssets();
        AssetFileDescriptor descriptor;

        //create our three fx in memory ready for use
        descriptor = assetManager.openFd("sample1.ogg");
        sample1 = soundPool.load(descriptor, 0);

        descriptor = assetManager.openFd("sample2.ogg");
        sample2 = soundPool.load(descriptor, 0);

        descriptor = assetManager.openFd("sample3.ogg");
        sample3 = soundPool.load(descriptor, 0);

        descriptor = assetManager.openFd("sample4.ogg");
        sample4 = soundPool.load(descriptor, 0);

        } catch (IOException e) {
        //Print an error message to the console
        Log.e("error", "failed to load sound files);
        }
    }
  1. 然后是configureDisplay方法,它从onCreate中调用并完成了位图和屏幕大小计算的整个设置。我们稍后会更详细地看一下这个。在前一步的代码之后输入以下代码:
public void configureDisplay(){
        //find out the width and height of the screen
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        screenWidth = size.x;
        screenHeight = size.y;
        topGap = screenHeight/14;
        //Determine the size of each block/place on the game board
        blockSize = screenWidth/40;

        //Determine how many game blocks will fit into the 
        //height and width
        //Leave one block for the score at the top
        numBlocksWide = 40;
        numBlocksHigh = ((screenHeight - topGap ))/blockSize;

        //Load and scale bitmaps
        headBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head);
        bodyBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.body);
        tailBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.tail);
        appleBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.apple);

        //scale the bitmaps to match the block size
        headBitmap = Bitmap.createScaledBitmap(headBitmap, blockSize, blockSize, false);
        bodyBitmap = Bitmap.createScaledBitmap(bodyBitmap, blockSize, blockSize, false);
        tailBitmap = Bitmap.createScaledBitmap(tailBitmap, blockSize, blockSize, false);
        appleBitmap = Bitmap.createScaledBitmap(appleBitmap, blockSize, blockSize, false);

    }
  1. 现在运行应用程序。在实际设备上,游戏比在模拟器上更容易玩。

我们在进行过程中逐步介绍了代码,但像往常一样,这里是一些更复杂方法的逐步剖析,从updateGame方法开始。

首先,我们检查玩家是否吃了一个苹果。更具体地说,蛇的头部是否与苹果在同一个网格位置?if语句检查是否发生了这种情况,然后执行以下操作:

  • 增加蛇的长度

  • 通过调用getApple在屏幕上放置另一个苹果

  • 根据蛇的长度为玩家的分数增加一个值,使每个苹果的价值都比上一个更高

  • 发出蜂鸣声

这是我们刚刚描述的操作的代码:

public void updateGame() {

            //Did the player get the apple
            if(snakeX[0] == appleX && snakeY[0] == appleY){
                //grow the snake
                snakeLength++;
                //replace the apple
                getApple();
                //add to the score
                score = score + snakeLength;
                soundPool.play(sample1, 1, 1, 0, 0, 1);
            }

现在我们只是简单地移动蛇的每个部分,从后面开始,到它前面的位置。我们使用for循环来做到这一点:

            //move the body - starting at the back
            for(int i = snakeLength; i >0 ; i--){
                snakeX[i] = snakeX[i-1];
                snakeY[i] = snakeY[i-1];
            }

当然,我们最好也移动头部!我们之所以最后移动头部,是因为如果我们之前移动头部,身体的前部分会移动到错误的位置。只要在进行任何绘图之前完成整个移动,一切都会很好。我们的run方法确保这总是成立。以下是根据directionOfTravel确定的方向移动头部的代码。正如我们所看到的,directionOfTravel是由玩家在onTouchEvent方法中操作的:

            //Move the head in the appropriate direction
            switch (directionOfTravel){
                case 0://up
                    snakeY[0]  --;
                    break;

                case 1://right
                    snakeX[0] ++;
                    break;

                case 2://down
                    snakeY[0] ++;
                    break;

                case 3://left
                    snakeX[0] --;
                    break;
            }

接下来,我们检查是否与墙壁发生了碰撞。我们在之前查看碰撞检测时看到了这段代码。以下是完整的解决方案,从左墙开始,然后右墙,然后顶部,最后底部:

            //Have we had an accident
            boolean dead = false;
            //with a wall
            if(snakeX[0] == -1)dead=true;
            if(snakeX[0] >= numBlocksWide)dead=true;
            if(snakeY[0] == -1)dead=true;
            if(snakeY[0] == numBlocksHigh)dead=true;

然后我们检查蛇是否与自身发生了碰撞。最初,这似乎很尴尬,但正如我们之前看到的,我们只需循环遍历我们的蛇数组,检查任何部分是否与头部在相同的位置,无论是在x坐标还是y坐标上:

           //or eaten ourselves?
            for (int i = snakeLength-1; i > 0; i--) {
                if ((i > 4) && (snakeX[0] == snakeX[i]) && (snakeY[0] == snakeY[i])) {
                    dead = true;
                }
            }

如果我们的任何碰撞检测代码将dead设置为true,我们只需播放一个声音,将score设置为0,然后得到一个新的小蛇:

            if(dead){
                //start again
                soundPool.play(sample4, 1, 1, 0, 0, 1);
                score = 0;
                getSnake();

            }

        }

现在我们更仔细地看一下drawGame方法。首先,我们准备通过清除屏幕来绘制:

public void drawGame() {

            if (ourHolder.getSurface().isValid()) {
                canvas = ourHolder.lockCanvas();
                //Paint paint = new Paint();
                canvas.drawColor(Color.BLACK);//the background
                paint.setColor(Color.argb(255, 255, 255, 255));
                paint.setTextSize(topGap/2);

现在我们为玩家的分数绘制文本,就在我们在configureDisplay中定义的topGap上方:

    canvas.drawText("Score:" + score + "  Hi:" + hi, 10, topGap-6, paint);

现在,使用drawLine,我们在游戏网格周围绘制一个可见的边框:

 //draw a border - 4 lines, top right, bottom, left
                paint.setStrokeWidth(3);//4 pixel border
                canvas.drawLine(1,topGap,screenWidth-1,topGap,paint);
                canvas.drawLine(screenWidth-1,topGap,screenWidth-1,topGap+(numBlocksHigh*blockSize),paint);
                canvas.drawLine(screenWidth-1,topGap+(numBlocksHigh*blockSize),1,topGap+(numBlocksHigh*blockSize),paint);
                canvas.drawLine(1,topGap, 1,topGap+(numBlocksHigh*blockSize), paint);

接下来,我们绘制蛇的头部:

//Draw the snake
canvas.drawBitmap(headBitmap, snakeX[0]*blockSize, (snakeY[0]*blockSize)+topGap, paint);

蛇的头部将被所有的身体部分跟随。看一下for循环的条件。这从1开始,这意味着它不会重绘头部位置,结束于snakeLength - 1,这意味着它不会绘制尾部部分。以下是用于绘制身体部分的代码:

//Draw the body
for(int i = 1; i < snakeLength-1; i++){
    canvas.drawBitmap(bodyBitmap, snakeX[i]*blockSize, (snakeY[i]*blockSize)+topGap, paint);
}

在这里,我们绘制蛇的尾巴:

//draw the tail
canvas.drawBitmap(tailBitmap, snakeX[snakeLength-
    1]*blockSize, (snakeY[snakeLength-1]*blockSize)+topGap, paint);

最后,我们按以下方式绘制苹果:

                //draw the apple
                canvas.drawBitmap(appleBitmap, appleX*blockSize, 
                    (appleY*blockSize)+topGap, paint);

                ourHolder.unlockCanvasAndPost(canvas);
            }

        }

接下来,我们将详细介绍configureDisplay方法。

首先,我们获取屏幕分辨率,并像往常一样将结果存储在screenWidthscreenHeight中:

public void configureDisplay(){
        //find out the width and height of the screen
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        screenWidth = size.x;
        screenHeight = size.y;

在这里,我们定义了一个叫做topGap的间隙。它将是屏幕顶部的一个空间,不会成为游戏区域的一部分。这个间隙用于分数。我们在drawGame方法中相当广泛地看到了topGap的使用。之后,我们计算剩余区域的宽度和高度:

        topGap = screenHeight/14;
        //Determine the size of each block/place on the game board
        blockSize = screenWidth/40;

        //Determine how many game blocks will fit into the height and width
        //Leave one block for the score at the top
        numBlocksWide = 40;
        numBlocksHigh = (screenHeight - topGap )/blockSize;

在代码的下一部分中,我们将所有的图像文件加载到Bitmap对象中:

//Load and scale bitmaps
        headBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head);
        bodyBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.body);
        tailBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.tail);
        appleBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.apple);

最后,我们将每个位图缩放为与blockSize相同的宽度和高度:

        //scale the bitmaps to match the block size
        headBitmap = Bitmap.createScaledBitmap(headBitmap, blockSize, 
            blockSize, false);
        bodyBitmap = Bitmap.createScaledBitmap(bodyBitmap, blockSize, 
            blockSize, false);
tailBitmap = Bitmap.createScaledBitmap(tailBitmap, blockSize, 
    blockSize, false);
appleBitmap = Bitmap.createScaledBitmap(appleBitmap, 
    blockSize, blockSize, false);

    }

现在我们可以快速看一下我们可以改进游戏的几种不同方式。

增强游戏

这是一系列问题和答案,引导我们改进Snake游戏的版本。如果你无法回答一些(甚至全部)问题,也没关系。只需看一下问题和答案,然后你可以看一下新游戏和代码。

自测问题

Q1)有什么可以用来为我们的游戏屏幕提供视觉改进?我们可以使用漂亮的浅绿色草地背景,而不仅仅是黑色吗?

Q2)怎么样一些漂亮的花?

Q3)如果你感到勇敢,让花朵摇曳。想想我们对精灵表学到的东西。理论上与动画蛇头的理论完全相同。我们只需要几行代码来控制帧速率,与游戏帧速率分开。

Q4) 我们可以设置另一个计数器,并在 GameActivity 中使用我们的蛇头动画,但这并不是很有用,因为在较小的尺寸下,细微的舌头运动几乎不可见。但我们能摆动尾部吗?

Q5) 这是一个稍微棘手的增强。你会注意到,当蛇精灵朝着四个可能的方向中的三个时,它们看起来不对劲。你能修复这个问题吗?

总结

这是又一个成功的游戏项目的结束。现在你知道如何创建和动画精灵表,以增加我们游戏的逼真感。现在我们有了一个增强版的 Snake 游戏。

在下一章中,我们将看到添加排行榜和成就是多么简单。这将使我们的游戏变得社交化和引人入胜,让玩家看到他们朋友的高分和成就,并将其与自己的进行比较。

第九章:让您的游戏成为下一个大事件

终于到了我们可以发布第一个游戏的时候了。这一章,虽然比其他章节短,但可能是完成时间最长的一章。在真正开始之前,浏览一下不同的练习内容是个好主意。大多数这些教程都不适合在您最喜欢的电视节目的广告时间或者下班后感到非常疲倦时进行。

阅读本章并制定何时执行每个阶段的计划。这些阶段安排得很好,所以您应该能够在每个阶段之间离开项目。如果您真的下定决心,已经理解了到目前为止的所有代码,对文件和文件夹感到自信,并且没有中断,您可能可以在大约一天内完成本章的工作。

如往常一样,完成的代码位于下载包中相关文件夹中,这种情况下是Chapter9文件夹。

注意

请注意,由于我无法分享我的开发者帐户的登录凭据,必须使用一系列黑线在代码中掩盖一些 ID 号码。在本章的代码中,当谈到ids.xml文件时,您会看到这些。由于其机密性质,该文件包中没有包含该文件。但是,正如您将在设置蛇项目以准备实施部分中看到的那样,很容易获得您自己的 ID 代码。还要注意,本章的许多工作涉及在您的开发者控制台中进行的设置。在您完成必要的步骤之前,排行榜和成就将无法正常工作。但是,您可以在Chapter9文件夹中查看整个代码,并从第八章下载游戏的增强版本,蛇游戏,其中包括来自第九章的工作排行榜和成就,让您的游戏成为下一个大事件,从play.google.com/store/apps/details?id=com.packtpub.enhancedsnakegame.enhancedsnakegame下载。

如果您想自己实现所有内容,并且还想从包含上一章自测问题的增强版游戏开始,那么请从Chapter8文件夹中获取EnhancedSnakeGame代码,并从Chapter8更新您的工作项目。

在本章中,您将学习以下主题:

  • 如何发布您的应用程序

  • 营销您的应用程序,包括通过排行榜和公共成就使其社交化

  • 使用 Google Play 游戏服务 API 实施排行榜和成就

  • 根据您想要实现的目标,查看接下来要做什么

如何发布您的应用程序

本指南中的一些步骤涉及编写描述和提供截图,因此您可能希望在实施任何步骤之前阅读整个指南:

  1. 创建一个图标。如何设计图标超出了本书的范围,但简单来说,您需要为每个 Android 屏幕密度类别创建一个漂亮的图像。这比听起来要容易。使用简单的图像,比如蛇头位图,您可以定制并从romannurik.github.io/AndroidAssetStudio/icons-launcher.html下载一组图标。有许多网站提供类似的免费服务。当然,您也可以直接使用增强版蛇项目中的图像,跳过这一步和下一步。

  2. 一旦您从上述链接下载了.zip文件,您可以简单地将下载包中的res文件夹复制到项目资源管理器中的main文件夹中。现在,所有密度的所有图标都将被更新。

  3. 在我们进一步进行之前,您可能需要准备一些游戏的屏幕截图。您将被提示为几种屏幕类型上传截图,但由于游戏在所有屏幕类型上几乎相同,一张图片就可以了。您还需要一张尺寸为 512 x 512 的高分辨率图标和一张尺寸为 1024 x 500 的特色图形。它们不需要很好,但您需要它们才能继续。创建您自己的图形,或者在Chapter9文件夹中获取我的非常简单的图形的副本。

  4. 现在,不幸的是,您需要花费 25 美元开设一个 Google Play 账户。您可以在play.google.com/apps/publish/注册。

  5. 一旦您注册了,您可以使用与上一步中提到的相同 URL 登录到您的开发者控制台。

  6. 一旦进入控制台,点击+添加新应用程序按钮:如何发布您的应用程序

  7. 添加新应用程序对话框中,输入应用程序的名称,例如Snake Game。现在点击上传 APK

  8. 我们现在需要将我们的应用程序制作成发布版本。打开AndroidManifest.xml文件,并在指定位置添加代码中的突出显示行:

<application
        android:debuggable="false"
        android:allowBackup="true"
  1. 重新构建您的已签名 APK,以获取Snake游戏的最新版本,如第二章中所讨论的,开始使用 Android

  2. 现在点击将您的第一个 APK 上传到生产环境

  3. 现在转到您的 Snake 游戏 APK。

  4. 等待 APK 上传完成。您现在可以看到游戏摘要屏幕。请注意下一张图片左上角的突出显示的进度指示器。我们有一个绿色的勾号,表示 APK 已成功上传:如何发布您的应用程序

  5. 我们需要做的下一件事是配置我们的商店列表,所以点击商店列表链接,就在 APK 链接下面。

  6. 写一个简短的描述和一个长描述。还要上传你的屏幕截图、特色图形和高分辨率图标。

  7. 应用程序类型下拉菜单中,选择游戏。在类别下拉菜单中,街机可能是最合适的。对于内容评级,选择所有人,对于隐私政策,点击此时不提交隐私政策的复选框。

  8. 将您的网站和电子邮件地址添加到相应的框中。

  9. 回到网页顶部,点击保存按钮。

  10. 现在我们已经到达本指南的最后阶段。点击定价和分发链接。它就在第 13 步的商店列表链接下面。

  11. 点击页面顶部的免费按钮。

  12. 点击您希望您的游戏在其中列出的所有国家的复选框。

  13. 滚动到页面底部,点击内容指南美国出口法的复选框。

  14. 点击页面顶部的保存

  15. 最后,从页面右上角的准备发布下拉菜单中,点击发布此应用程序,然后您就完成了。如何发布您的应用程序

恭喜!您的游戏将在 5 分钟至 24 小时之间在 Google Play 上线。

推广您的应用程序

在这个阶段的诱惑是坐下来等待我们的游戏登顶畅销应用。这永远不会发生。

为了确保我们的应用程序发挥其全部潜力,我们需要不断进行以下操作:

改进它

我们已经对Snake游戏进行了很多改进,但还有许多其他改进,比如难度设置、音乐、调试(您有没有看到偶尔有点不稳定的身体段?)、设置菜单等等。您可以支付专业人员设计背景和精灵,或者添加更多音效。当您进一步提高您的 Android 和 Java 技能时,您可以使用更流畅的引擎重新编写整个游戏,并称其为版本 2。

推广它

这可能是另一本书的主题,但我们可以通过很多方式来宣传我们的应用。我们可以在所有社交媒体网站上创建页面/个人资料,比如 Facebook、Twitter、Flickr 等等。定期更新、公告、挑战(参见强迫)。我们可以创建一个网站来推广我们的应用,并以所有我们推广任何其他网站的方式来推广它。我们可以在应用程序本身中添加一条消息,要求玩家对其进行评分,也许在他们获得高分或成就后弹出一条消息。我们可以要求我们认识的每个人和访问我们社交媒体/网站的每个人给予评分并留下评论。还有许多其他推广应用的方式。它们的秘诀是:不断地进行。例如,不要创建一个 Facebook 页面然后期望它自行流行起来。不断增加所有推广渠道。

保持玩家的强迫水平

除了以我们简要提到的方式改进游戏之外,我们需要给玩家一个有力的理由继续回到我们的游戏。一种方法可能是添加新的关卡。例如,在我们的贪吃蛇游戏中实现关卡不难。每个关卡可以在不同的地方设置墙壁,布局可以变得越来越具有挑战性。我们需要做的就是制作一个障碍物数组,将它们绘制在屏幕上,并检测碰撞。然后为每个关卡设置蛇长度的目标,当达到目标时进入下一个关卡。

我们可以提供不同的蛇设计来解锁特定挑战。玩家可以将他们收集的所有苹果保存为一种货币,然后有策略地花费这种货币来获得在死后继续的机会,这怎么样?

提供限时挑战怎么样?例如,本月底完成第 10 关以获得一千个额外的苹果。也许,我们可以想出更多苹果可以用来消费的东西。酷炫的蛇配饰或只能用苹果解锁的关卡。关键是所有这些强迫力都可以在我们上传改进的同时添加和更新。在这次讨论中提到的关于强迫力的任何事情都不是我们迄今为止学到的技能无法实现的。

可能,我们可以为我们的游戏增加的最引人入胜的方面是在线排行榜和成就,这样玩家可以将自己与朋友和全世界的其他玩家进行比较。谷歌意识到了这一点,并已经做了很多工作,使向游戏中添加排行榜和成就变得尽可能容易。我们将看看我们如何再次利用其他人的辛勤工作。

而且,玩家在游戏中获得的所有成就都会被记录在他们的 Google Play 个人资料中。这是我相当糟糕的 Google Play 成就个人资料的截图:

营销您的应用

你可能已经注意到了一些贪吃蛇成就。这个功能使你的游戏可能更加引人入胜。

提示

让我们进行一个快速的现实检查——我并不是在建议你花费大量时间来尝试将我们的谦逊的贪吃蛇游戏变成一个真正的业务。它只是作为一个讨论的有用例子。此外,如果我们能为一个如此古老和简单的游戏想出这么多点子,那么我们肯定可以为一个我们热爱的游戏想出一些真正令人惊叹的东西。当你有一个你热爱的想法时,那就是去扩展我们讨论过的简短营销计划的时候了。

添加排行榜和成就

所以我们知道为什么排行榜和成就是一件好事。我们需要做的第一件事是规划我们的成就。排行榜就是一个高分榜,就是这样!我们无法做太多事情来使它们不同。然而,成就值得讨论一番。

规划贪吃蛇成就

起初,似乎一个非常简单的游戏,比如我们的游戏,实现起来并不深刻,可能没有很多,甚至没有任何成就。接下来是一个快速的头脑风暴会话,关于成就的想法:

  • 得分 10、25、50、100 等等:简单地在不同水平的高分解锁成就。

  • 蛇长度:简单地在不同的蛇长度解锁成就。

  • 食人族:第一次玩家与自己的尾部碰撞时解锁成就。

  • 总共收集 x 个苹果:记录所有收集的苹果,并在重要的里程碑上解锁成就。

  • 玩 10、25、50、100 场比赛:奖励玩家继续前进。无论他们输赢,都会因努力而解锁成就。

  • 寻宝:如果每个游戏中都有一个隐藏的地点怎么办?这可能会给玩家一个探索每个级别的理由。他们可以获得积分和苹果的奖励。然后他们可以解锁真正的成就,也许是每发现 5、10 或 20 个隐藏地点。

一些成就表明我们需要记录玩家的进度。令人惊讶的是,Google Play 游戏服务实际上可以为我们做到这一点。这些被称为递增成就。总共收集的苹果数量就是递增成就的一个很好的例子。其他,比如蛇长度,只取决于玩家在任何一场比赛中的表现。

我们将实现总苹果数和蛇长度成就,以便了解如何实现这两种类型。

我们可以为达到以下每个蛇长度设定五个成就:5、10、20、35 和 50。还可以为总苹果数设定五个递增成就。具体来说,玩家将在收集 10、25、50、100、150 和 250 个苹果时获得成就。很快,我们将看到如何做到这一点。

最后,我们需要决定每个成就的价值,每个游戏的上限为 1,000 点。由于我可能会回来添加更多成就,我打算将 250 点分配给苹果的成就,就像这样:

吃掉的苹果数量 成就点数
10 10
20 30
50 40
100 70
250 100

我还将在下表中分配 250 点给蛇长度成就,如下所示:

蛇的长度 成就点数
5 10
10 30
25 40
35 70
50 100

一旦您了解如何在代码和开发者控制台中实现这些成就,设计和实现您自己的不同成就将会相当简单。

逐步排行榜和成就

这可能是完成本书最长的部分。但是,一旦您完成了这个过程,下次再做就会容易得多。

在您的 PC 上安装 Google Play 服务 API

首先,我们需要添加使用游戏服务类所需的工具和库。这在 Android Studio 中非常简单:

  1. 点击 Android Studio 工具栏中的 SDK 管理器图标:在您的 PC 上安装 Google Play 服务 API

  2. SDK 管理器将启动。它看起来有点像这样:在您的 PC 上安装 Google Play 服务 API

  3. 滚动到最底部,在额外下面,您会看到Google Play 服务。通过点击下面的截图中显示的突出显示的框来勾选它:在您的 PC 上安装 Google Play 服务 API

  4. 现在点击Google 存储库复选框,就在Google Play 服务下面。

  5. 点击安装软件包,等待软件包下载和安装。

  6. 保存您的项目并重新启动 Android Studio。

我们现在已经安装了开始开发 Google Play 游戏服务应用所需的工具。接下来,我们需要设置我们的开发者控制台,以便与我们的应用进行通信,为我们即将编写代码的功能做好准备。

配置 Google Play 开发者控制台

在这里,我们将通过创建一个新的游戏服务应用来准备您的开发者控制台。这可能听起来有点违反直觉;毕竟,Snake 就是我们的应用,不是吗?是的,但 Google Play 的结构是这样的,您需要创建一个游戏服务应用,实际的游戏(在这种情况下是 Snake)将与该应用进行通信。游戏服务应用将拥有我们将从 Snake 游戏中授予和显示的成就和排行榜:

  1. 登录到play.google.com/apps/publish/的 Google Play 开发者控制台。

  2. 点击网页左侧的游戏服务选项卡。配置 Google Play 开发者控制台

  3. 现在点击添加新游戏按钮。

  4. Snake输入为游戏名称,并从类别下拉菜单中选择街机。现在点击继续。所有这些都显示在下一个截图中:配置 Google Play 开发者控制台

  5. 现在我们可以配置我们的游戏。在描述字段中输入游戏描述,并添加与上传游戏时添加的相同的高分辨率图标和特色图形。

  6. 点击屏幕顶部的保存按钮。

  7. 现在我们将把 Snake 游戏服务应用与我们实际的Snake游戏进行关联。在网页左侧,点击已关联的应用选项卡。配置 Google Play 开发者控制台

  8. Google Play 游戏服务可以与几乎任何平台一起使用,甚至是苹果。我们在这里使用它来开发 Android 应用,所以点击Android按钮。配置 Google Play 开发者控制台

  9. 在这个屏幕上,我们只需要点击包名称搜索框,然后点击我们的Snake 游戏选项。配置 Google Play 开发者控制台

  10. 点击屏幕顶部的保存并继续

  11. 我们接近这个阶段的结束。点击立即授权您的应用并查看信息。配置 Google Play 开发者控制台

  12. 最后,点击继续配置 Google Play 开发者控制台

我们现在已经设置了一个 Google 游戏服务应用,并与我们的Snake游戏进行了关联。

在 Google Play 开发者控制台中实现排行榜

现在我们需要在开发者控制台中创建我们的排行榜,以便稍后在我们的 Java 代码中与之交互:

  1. 登录到您的开发者控制台。

  2. 点击游戏服务,然后点击Snake,然后点击排行榜

  3. 现在点击添加排行榜。这是新排行榜屏幕:在 Google Play 开发者控制台中实现排行榜

  4. 这可能看起来像是一项漫长的任务,但我们只需要在名称字段中输入一个名称(Snake就可以了),然后就完成了。可能会觉得奇怪为我们的排行榜输入一个名称,但这是因为一个游戏可以有多个排行榜。

  5. 阅读所有选项。您会发现它们对我们来说都是合适的,不需要进一步操作。点击保存

我们的排行榜现在已准备好与我们的Snake应用进行通信。

在 Google Play 开发者控制台中实现成就

在这里,我们将在开发者控制台中设置我们之前讨论过的成就。

您可能希望准备一些图形来代表这些成就。它们需要每个 512 x 512 像素。或者,您可以使用一个放大的苹果位图,也许是一个蛇身段,分别用于苹果和蛇长度成就:

  1. 登录到开发者控制台。点击游戏服务,然后点击,再点击成就

  2. 点击添加成就,你将看到新成就屏幕:在 Google Play 开发者控制台中实施成就

  3. 因为我们正在实施增量苹果成就,首先要做的是在新成就表单中输入内容。在名称字段中输入苹果食客 1

  4. 描述字段中输入吃掉 10 个苹果

  5. 点击添加图标按钮,并选择你喜欢的 512 x 512 图像。

  6. 点击增量成就复选框,并在需要多少步骤字段中输入5。这是因为第一个成就是吃掉 5 个苹果。此步骤显示在下一个截图中:在 Google Play 开发者控制台中实施成就

  7. 点数字段中输入10

  8. 点击保存,并重复步骤 2 到 7 四次,为所有苹果成就变化名称描述需要多少步骤点数字段,根据我们的计划和成就数值表。

  9. 现在我们可以继续进行蛇长度成就。点击新成就。在名称字段中输入超级蛇 1

  10. 描述字段中输入让你的蛇长到 5 节

  11. 点击添加图标按钮,并浏览到你喜欢的图片。

  12. 最后,在点数字段中输入10

  13. 点击保存,并重复步骤 9 到 13 四次,为每个蛇长度成就变化名称描述点数字段,根据我们的计划和成就数值表。

我们现在已经设置好了成就,准备在代码中实施。

设置好蛇项目以准备实施

在本节中,我们将准备我们的应用与 Google Play 服务器通信:

  1. 将这段高亮代码添加到AndroidManifest.xml文件中,就在闭合的</application>标签之前:
<meta-data android:name="com.google.android.gms.games.APP_ID"
 android:value="@string/app_id" />
<meta-data android:name="com.google.android.gms.version"
 android:value="@integer/google_play_services_version"/>

</application>
  1. 在项目资源管理器中的values文件夹中创建ids.xml文件。现在你需要获取你的游戏的唯一代码放入这个文件中。登录到开发者控制台,点击游戏服务,然后点击。现在点击成就

  2. 在你的成就列表下方有一个小的获取资源链接:设置好蛇项目以准备实施

  3. 点击获取资源链接。设置好蛇项目以准备实施

  4. 复制并粘贴代码到ids.xml文件中。然后点击开发者控制台中的完成按钮。

  5. 现在我们需要从 Google Play 游戏服务 GitHub 存储库中获取四个代码文件。我们将直接复制并粘贴这些文件到我们的项目中。

  6. java文件夹中创建三个新的空文件。在项目资源管理器中右键点击GameActivity,然后导航到新建 | Java 类文件。将新文件命名为BaseGameActivity。重复此步骤,将文件命名为GameHelper。再重复一次,将文件命名为GameHelperUtils

  7. 现在我们将获取 Java 代码,复制到我们刚刚创建的三个文件中。要获取BaseGameActivity.java的代码,请访问github.com/playgameservices/android-basic-samples/tree/master/BasicSamples/libraries/BaseGameUtils/src/main/java/com/google/example/games/basegameutils,在那里你可以看到进一步链接到我们在步骤 7 中创建的三个文件的代码:设置好蛇项目以准备实施

  8. 点击BaseGameActivity.java,如前面的截图所示。选择所有代码,然后复制粘贴到我们在 Android Studio 中创建的同名文件中。请注意,当我们创建文件时,Android Studio 创建了一些基本模板代码。我们需要删除所有这些代码,除了顶部的包名。当我们粘贴复制的代码时,需要删除 Google 的包名。

  9. 点击GameHelper.java,如前面的截图所示,并重复第 9 步。

  10. 点击GameHelperUtils.java,如前面的截图所示,并重复第 9 步。

  11. 还有一个文件要创建。在项目资源管理器中右键单击values文件夹。导航到New | File。将文件命名为gamehelper_strings.xml

  12. 以与前三个 Java 文件相同的方式获取此文件所需的代码,但是从此链接获取:github.com/playgameservices/android-basic-samples/blob/master/BasicSamples/libraries/BaseGameUtils/src/main/res/values/gamehelper_strings.xml

  13. 将代码粘贴到我们在第 12 步中创建的gamehelper_strings.xml中。

  14. 现在修改MainActivity.java文件中的MainActivity声明。

考虑一下这段代码:

public class MainActivity extends Activity {

将其更改为以下代码,这样我们现在可以扩展处理游戏服务 API 的 Activity 版本:

public class MainActivity extends BaseGameActivity {
  1. 现在查看GameActivity.java文件中的代码:
public class GameActivity extends Activity {

将前面的代码更改为以下代码,这样我们现在可以扩展处理游戏服务 API 的 Activity 版本:

public class GameActivity extends BaseGameActivity {
  1. 注意,对于这两个 Activity,我们在刚刚输入的类声明中有一个错误。如果将鼠标悬停在我们在上一步中输入的代码上,就可以看到错误的原因。我们需要实现一些我们正在使用的类的抽象方法。回想一下第六章中的内容,OOP – 使用他人的辛勤工作,如果一个类中的方法被声明为抽象的,那么扩展它的类必须实现它。这就是我们!现在先进行一个空的实现。右键单击带有错误的代码行,导航到Generate | Implement Methods。现在点击OK。对MainActivity.java文件和GameActivity.java文件执行此步骤。我们的空方法现在已经准备好了。我们将在下一个教程中编写代码。

  2. 接下来,使用项目资源管理器找到build.gradle文件。要小心,有两个同名的文件。我们需要找到的文件在AndroidManifest.xml文件的下面几行。在下一个截图中有高亮显示。双击build.gradle文件打开它:设置好 Snake 项目以准备实施

  3. 找到这里显示的代码部分,并添加高亮显示的行。这样我们在上一篇指南中下载的所有类都可以在我们的Snake游戏中使用:

dependencies {
    compile 'com.google.android.gms:play-services:+'
    compile 'com.android.support:appcompat-v7:+'
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

好的,我同意那是一个相当艰难的教程,但是现在我们已经准备好在三个最后步骤中实现我们的代码了:

  1. 玩家登录和按钮。

  2. 排行榜。

  3. 成就。

然后我们就可以上传我们更新的应用程序并使用我们的新排行榜和成就了。

实现玩家的登录、成就和排行榜按钮

通过本节结束时,玩家将能够通过游戏登录到我们的空排行榜和成就。接下来的指南将使排行榜和成就真正起作用。

  1. 首先,让我们启用游戏服务。到目前为止,在开发者控制台中所做的所有工作都需要在我们可以使用它之前发布。登录到您的开发者控制台。导航至游戏服务 | Snake | 准备发布 | 发布游戏。然后会显示一个发布您的游戏按钮。点击它。最后,阅读简要的免责声明,然后点击立即发布

  2. 现在我们需要构建一个具有登录登出排行榜成就按钮的 UI。打开layout_main.xml文件,并在编辑器窗口的文本选项卡上通过添加以下代码进行编辑。显然,需要输入很多内容。您可能希望从Chapter9\EnhancedSnakeGame\layout的下载包中复制并粘贴代码。以下是代码。输入或复制并粘贴它:

<RelativeLayout 

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.packtpub.enhancedsnakegame.enhancedsnakegame.MainActivity">

    <Button

        android:id="@+id/llPlay"
        android:layout_width="140dp"
        android:layout_height="wrap_content"
        android:text="Leaderboards"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:visibility="gone"/>

    <Button
        android:id="@+id/awardsLink"
        android:layout_width="140dp"
        android:layout_height="wrap_content"
        android:text="Achievements"
        android:layout_gravity="center_vertical"
        android:layout_alignTop="@+id/llPlay"
        android:layout_toLeftOf="@+id/llPlay"
        android:visibility="gone"/>

    <!-- sign-in button -->
    <com.google.android.gms.common.SignInButton
        android:id="@+id/sign_in_button"
        android:layout_width="140dp"
        android:layout_gravity="center_horizontal"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
         />

    <!-- sign-out button -->
    <Button
        android:id="@+id/sign_out_button"
        android:layout_width="140dp"
        android:layout_height="wrap_content"
        android:text="Sign Out"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:layout_gravity="center_horizontal"
        android:visibility="gone"
        />

</RelativeLayout>
  1. 逐行解释代码超出了本书的范围,但这与我们在第二章开始使用 Android时使用 UI 设计器自动生成的代码并没有太大不同。上一步中的代码块中的每个代码块定义了一个按钮及其在屏幕上的位置。您可以切换到设计选项卡并移动按钮以适应自己。请注意,设计师中某些按钮不可见的原因是它们在玩家登录之前是隐藏的。我们之所以这样做的原因是为了确保我们以恰当的方式实现登录按钮。请注意每个按钮的id属性。接下来我们将在我们的 Java 代码中操作它们。将一些按钮设置为visibility = gone后,我们看到类似以下内容:实现玩家的登录、成就和排行榜按钮

  2. 将一些按钮设置为visibility = visible后,我们看到类似以下截图所示的内容:实现玩家的登录、成就和排行榜按钮

  3. 您可能会想知道为什么我们要设计一个 UI,而SnakeAnimView是用户看到的内容。我们本可以使用位图实现所有自己的按钮,并使用它们的屏幕坐标来检测按压,但现在我们将在SnakeAnimView上加载我们的 UI,这将大大简化事情。切换到编辑器窗口中的MainActivity选项卡。

  4. 首先,我们想要实现onClickListener接口来处理我们的按钮点击。为实现这一点,将类声明更改为以下内容:

public class MainActivity extends BaseGameActivity implements View.OnClickListener{
  1. 现在,我们可以通过右键单击类声明,导航至添加 | 实现方法,然后点击确定,让 Android Studio 快速实现所需的onClick方法。

  2. 在上一行代码之后,我们立即声明了我们的四个新按钮。在上一步中的代码之后添加此代码:

//Our google play buttons
    Button llPlay;
    Button awardsLink;
    com.google.android.gms.common.SignInButton sign_in_button;
    Button sign_out_button;
  1. onCreate方法中,在调用setContent视图之后,我们使用LayoutInflater类的对象在我们的SnakeAnimView上加载我们的 UI。在调用setContentView之后添加突出显示的代码:
setContentView(snakeAnimView);

 //Load our UI on top of our SnakeAnimView
 LayoutInflater mInflater = LayoutInflater.from(this);
 View overView = mInflater.inflate(R.layout.activity_main, null);
 this.addContentView(overView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

  1. 在上一步中的代码之后,我们可以引用所有的按钮并以通常的方式监听点击:
//game services buttons
        sign_in_button = (com.google.android.gms.common.SignInButton)findViewById(R.id.sign_in_button);
        sign_in_button.setOnClickListener(this);
        sign_out_button = (Button)findViewById(R.id.sign_out_button);
        sign_out_button.setOnClickListener(this);
        awardsLink = (Button) findViewById(R.id.awardsLink);
        awardsLink.setOnClickListener(this);
        llPlay = (Button)findViewById(R.id.llPlay);
        llPlay.setOnClickListener(this);
  1. 请记住,在上一篇指南中,当我们扩展BaseGameActivity类时,我们覆盖了两个抽象方法。现在我们将在它们的实现中放入一些代码。代码非常简单。当登录失败时,我们隐藏登出按钮并显示登录按钮;当登录成功时,我们隐藏登录按钮并显示其他三个按钮。以下是这两种方法的全部内容。在所示的方法中输入突出显示的代码:
@Override
    public void onSignInFailed() {
        // Sign in failed. So show the sign-in button.
 sign_in_button.setVisibility(View.VISIBLE);
 sign_out_button.setVisibility(View.GONE);
    }

    @Override
    public void onSignInSucceeded() {
        // show sign-out button, hide the sign-in button
 sign_in_button.setVisibility(View.GONE);
 sign_out_button.setVisibility(View.VISIBLE);
 llPlay.setVisibility(View.VISIBLE);
 awardsLink.setVisibility(View.VISIBLE);
    }
  1. 现在我们处理onClick方法以及玩家点击我们四个按钮中的任何一个时会发生什么。首先,我们输入我们的 switch 块的代码。我们将在下一步填写case语句:
switch (v.getId()) {

}
  1. 在这里,我们处理登录按钮。我们只需调用beginUserInitiatedSignIn方法。这在BaseGameActivity类中已经为我们实现了。在前一步的switch块中输入这段代码:
    case R.id.sign_in_button:
                // start the sign
                beginUserInitiatedSignIn();
                break;
  1. 现在我们处理玩家登出时会发生什么。我们只需调用signOut,这在BaseGameActivity类中已经为我们实现了。然后隐藏所有按钮,再次显示登录按钮。在上一步的代码后面输入以下代码:
case R.id.sign_out_button:
                // sign out.
                signOut();

                // show sign-in button, hide the sign-out button
                sign_in_button.setVisibility(View.VISIBLE);
                sign_out_button.setVisibility(View.GONE);
                llPlay.setVisibility(View.GONE);
                awardsLink.setVisibility(View.GONE);
                break;
  1. 接下来,我们处理玩家点击成就按钮时会发生什么。一行代码为我们提供了所有成就功能。这就是面向对象编程的全部意义——别人的辛勤工作为我们做了一切。在前面的代码后面输入这段代码:
case R.id.awardsLink:

                startActivityForResult(Games.Achievements.getAchievementsIntent(getApiClient()), 0);

                break;
  1. 最后,我们处理了玩家点击排行榜按钮时会发生什么。再次,一行代码为我们提供了所有排行榜的功能:
case R.id.llPlay:
                        startActivityForResult(Games.Leaderboards.getLeaderboardIntent(getApiClient(), getResources().getString(R.string.leaderboard_snake)),0);
                        break;

我们在进行时解释了代码,但让我们总结一下:

  1. 我们设计了一个简单的用户界面。

  2. 我们在SnakeAnimView上加载了用户界面。

  3. 我们得到了对我们的四个按钮的引用,并监听了点击事件。

  4. 我们处理了当人们点击我们的按钮时会发生什么,这只不过是根据需要隐藏和显示按钮,从BaseGameActivity调用方法,并使用Intent类来实现我们所有的排行榜和成就功能。

你实际上可以运行Snake游戏并查看排行榜和成就界面。当然,此时还没有人有任何成就或高分。我们现在将修复这个问题。

在代码中实现排行榜

再次,我们将见证使用其他人设计良好的代码的简单性。诚然,要达到这一点确实有一些复杂性,但一旦你设置好了,那么你下一个游戏将只需要花费你设置的一小部分时间:

  1. 我们希望在游戏结束时向排行榜提交分数。Google Play 将处理检查是否为高分。Google Play 甚至会确定这是否是本周或本月的新高分。在代码编辑器窗口中打开GameActivity.java文件。

  2. 找到updateGame方法,并在游戏结束时(当dead等于true时)在所有其他操作中添加高亮显示的代码。我们只需在检查中包装一行代码,以确保当前玩家已登录:

if(dead){
 if (isSignedIn()) {
 Games.Leaderboards.submitScore(getApiClient(),
 getResources().getString(R.string.leaderboard_snake), 
 score);
 }

  1. 就是这样!构建游戏并在真正的 Android 设备上玩游戏。现在你可以访问 Google Play 上的排行榜,看到你的最高分。

这很简单。在这里,我们可以看到登录界面:

在代码中实现排行榜

然后是欢迎消息和我们的成就排行榜按钮,如下截图所示:

在代码中实现排行榜

最后,我们可以看到我们的新排行榜只有一个玩家——我。

在代码中实现排行榜

以防你想知道,我可以做得比 39 好得多。

在代码中实现成就

这个简短的教程首先会设置我们的游戏,以便发布苹果成就的增量更新和蛇段长度的一次性成就:

  1. GameActivity.java文件中,在类声明后添加一个applesMunchedThisTurn变量,如下所示:
public class GameActivity extends BaseGameActivity {

    int applesMunchedThisTurn;
  1. 找到updateGame方法。

  2. 每次吃一个苹果时,添加一行代码来增加applesMunchedThisTurn,在显示的地方添加高亮显示的代码行:

//Did the player get the apple
            if(snakeX[0] == appleX && snakeY[0] == appleY){
                applesMunchedThisTurn++;
                //grow the snake
                snakeLength++;
                //replace the apple
                getApple();
                //add to the score
                score = score + snakeLength;
                soundPool.play(sample1, 1, 1, 0, 0, 1);
            }
  1. 注意我们将这一突出显示的行放在了玩家死亡时执行的代码中(if(dead)块)。我们可以在玩家吃苹果的时候做这件事,但是如果我们每次玩家吃苹果时向 Google Play 服务器发送五条消息,我们可能会冒着被 Google 认为是垃圾邮件的风险。我们只是通过已吃的苹果数量逐个增加每个成就,然后将applesMunchedThisTurn变量重置为零。我们在成就方法调用中加入了一个检查,即玩家已登录并且applesMunchedThisTurn大于零。现在添加突出显示的代码:
if(dead){
//start again
if (isSignedIn())
if(applesMunchedTisTurn > 0){//can't increment zero
    Games.Achievements.increment(getApiClient(), getResources().getString(R.string.achievement_apple_muncher_1), applesMunchedThisTurn);
 Games.Achievements.increment(getApiClient(), getResources().getString(R.string.achievement_apple_muncher_2), applesMunchedThisTurn);
 Games.Achievements.increment(getApiClient(), getResources().getString(R.string.achievement_apple_muncher_3), applesMunchedThisTurn);
 Games.Achievements.increment(getApiClient(), getResources().getString(R.string.achievement_apple_muncher_4), applesMunchedThisTurn);
 Games.Achievements.increment(getApiClient(), getResources().getString(R.string.achievement_apple_muncher_5), applesMunchedThisTurn);
 applesMunchedThisTurn = 0;
}//end if(applesMunchedThisTurn > 0)

    Games.Leaderboards.submitScore(getApiClient(), getResources().getString(R.string.leaderboard_snake),score);

}//end if(isSignedIn)
               soundPool.play(sample4, 1, 1, 0, 0, 1);
                score = 0;
                getSnake();

            }

        }
  1. 现在我们将处理分段长度成就。在updateGame方法中,在玩家吃苹果时执行的代码部分,就在增加snakeLength的代码行之后,我们测试是否达到了任何值得获得超级贪吃蛇成就的长度。当达到所需长度(5、10、25、35 或 50 段)时,我们要求 Google Play 授予它(如果尚未授予)。我们在成就方法调用中加入了一个检查,即玩家已登录并且至少吃了一个苹果。突出显示的代码是要添加的新代码:
//grow the snake
snakeLength++;
if (isSignedIn()){
if(applesMunchedThisTurn > 0) {//can't increment by zero
 //Are we long enough for a new SuperSnake achievement?
 if(snakeLength == 5){
 Games.Achievements.unlock(getApiClient(), getResources().getString(R.string.achievement_super_snake_1));
 }
 if(snakeLength == 10){
 Games.Achievements.unlock(getApiClient(), getResources().getString(R.string.achievement_super_snake_2));
 }
 if(snakeLength == 25){
 Games.Achievements.unlock(getApiClient(), getResources().getString(R.string.achievement_super_snake_3));
 }
 if(snakeLength == 35){
 Games.Achievements.unlock(getApiClient(), getResources().getString(R.string.achievement_super_snake_4));
 }
 if(snakeLength == 50){
 Games.Achievements.unlock(getApiClient(), getResources().getString(R.string.achievement_super_snake_5));
 }
}

  1. 就是这样!现在你可以玩游戏并获得成就:在代码中实现成就

同样,这很简单。你可能可以看到在本章前面讨论的所有其他成就想法要实现起来有多简单。让我们继续更新我们在 Google Play 上的游戏。

将更新后的贪吃蛇游戏上传到 Google Play

这很简单,操作如下:

  1. 首先,我们需要让 Google Play 知道这是一个新版本。我们通过更改版本号来实现这一点。打开Build.gradle文件,找到以下代码行:
versionCode 1
        versionName "1.0"
Change them to the following:
versionCode 2
        versionName "1.1"
  1. 以通常的方式构建你的 APK。

  2. 登录到你的开发者控制台。

  3. 点击贪吃蛇游戏 1.0,然后点击APK,然后点击上传新 APK 到生产

  4. 转到你的新更新的 APK。

  5. 此版本的新功能字段中输入添加排行榜和成就

  6. 点击立即发布到生产

从现在开始,每个下载你的游戏的人都将获得更新的版本。有了我们的第一个游戏,包括精灵表动画、排行榜和成就,是时候休息一下,进行一些理论学习了。

接下来呢?

如果这是你的第一次尝试编程,你应该为你迄今为止的创作感到自豪。如果一些概念、语法或项目仍然不清楚,那么考虑休息一下后再重新学习。

我们还没有讨论的一件事是我们需要进一步提高的新技能。这是因为这在很大程度上取决于你最初阅读本书的动机。

获得程序员的工作

如果你想成为一名 Java 员工,也就是说,全职在中型或大型公司以专业能力工作,那么你可能需要大学学位,希望这本书能给你一个对编程和 Java 本身世界的一瞥。如果这描述了你,那么在进一步学习时,你可以考虑更正式的 Java 书籍,然后是关于面向对象分析和设计的纯 OOP 书籍。然后你可以继续学习设计模式。

符合这些类别的一些最好的书籍是Head First Object-Oriented Analysis and Design: A Brain Friendly Guide to OOA&D, Brett McLaughlin and Gary Pollice; Head First Design Patterns; Eric Freeman and Elisabeth Robson, O'Reilly; 和 Design Patterns CD: Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Addison Wesley。前两本书非常适合初学者。后者备受推崇,但对初学者来说是一本更具挑战性的阅读。

我猜你之所以选择了一本关于游戏和 Java 的初学者书,很可能不是因为你正朝着那个方向前进,所以让我们来考虑一下到目前为止我们的巅峰之作——我们的贪吃蛇游戏。

构建更大更好的游戏。

如果你把我们的“贪吃蛇”游戏与现代的专业游戏进行比较,甚至是一个二维游戏,更不用说现代的大型预算第一人称射击游戏(FPS),那么我们仍然有很多需要学习的地方。让我们考虑一下我们的贪吃蛇游戏与专业游戏相比的一些不足之处。

想想我们的花朵和尾巴动画。它们之所以有效是因为我们在controlFPS方法中建立了一个粗糙的定时系统。但如果我们有十几个或更多需要进行动画处理的游戏对象呢?

那么如果它们都有不同的帧数和帧速率呢?如果一些动画需要在循环中工作,而其他动画需要在每个帧之间倒转再重新开始,我们可以进一步复杂化事情。

现在想象一个角色需要跳跃。当玩家跳跃时,我们如何同步显示的任何帧呢?

实际上,所有这些问题以及更多问题都可以通过快速的网络搜索和一些学习来解决。问题是事情开始变得相当复杂,而我们只谈到了动画。

物理学呢?当我们的未来游戏中的物体弹跳时会有什么行为?我们在“弹球”游戏中能够作弊,因为环境和物体都很少而且简单。如果球是圆的,有很多不同大小和形状的物体,有些移动快,有些静止,我们该如何模拟这个物理模型呢?

答案都在那里,但它们增加了复杂性。其他环境因素如光和阴影呢?当我们的屏幕需要左右滚动时会发生什么?上下呢?

现在考虑所有这些问题,并想象在虚拟的三维世界中实现解决方案。再次强调,解决方案都在那里,但一个决心的初学者可能需要花费数月时间来使用涉及三维计算的原始数学来实现他们自己的解决方案。

接下来,想象一下你希望你的新的三维、基于物理的、动画效果出色的游戏可以在 Android、Apple 和 PC 平台上使用。

如果我打消了你寻找这些解决方案的积极性,但你对找到答案很感兴趣,那么我的建议是去找到答案。这绝对会是一段迷人的旅程,并让你成为一个更好的游戏开发者。然而,在实现任何这些东西时,三思而后行,除非出于好奇心、自我提高或者乐趣之外的任何原因。

原因是因为我们不是第一个遇到这些问题和许多其他问题的人——解决方案已经被实现。猜猜?我们可以使用这些解决方案,通常不需要花费。

例如,有一个名为 OpenGL 的库,它有一个目的——在三维坐标系统中绘制。它有你所需要的一切类和方法。甚至有一个移动版的 OpenGL,叫做 OpenGL ES,你可以用 Java 来编程。的确,OpenGL 本身也有一些复杂性,但可以从简单到困难以逻辑和直接的方式学习。

如果你已经读到了这本书的这一部分,请快速复习一下第六章,“OOP – 使用他人的辛勤工作”,然后获取一本《OpenGL ES2 for Android》,作者 K. Brothaler,Pragmatic Bookshelf 的副本。这本书探讨了代码库以及一些背后的数学知识,因此它应该能够满足对知识好奇和纯粹实用的读者。或者,你可以在www.learnopengles.com/上查看大量免费教程。

如果你只是想制作更多的游戏,并且对三维特性并不特别在意,那么下一个合乎逻辑的步骤将是一个基于 Java 的游戏库。有很多,但其中一个特别使用纯 Java 在 Android、iPhone、PC 和 Web 上构建游戏。

事实上,你可以构建一个 Java 游戏,它将在世界上几乎任何设备上运行,甚至是一个网页。它还有一些简化前述 OpenGL ES 使用的类。这个库叫做 LibGDX,我在学习 Libgdx 游戏开发www.packtpub.com/game-development/learning-libgdx-game-development)中跟着做了一个平台游戏,玩得很开心。LibGDX 还解决了所有我们的动画、滚动和物理难题,而无需任何数学,尽管它实际上并没有涉及三维特性。

提示

请注意,这两本书都涉及一些相当深入的面向对象编程,但如果你理解了第六章 OOP – Using Other People's Hard Work,并且有决心,这并不是难以掌握的。

如果你想立即进入 3D,那么一个非常有趣的选择就是虚幻引擎。虚幻引擎在许多真正大型预算的游戏中使用,并且可能涉及另一种编程语言中的巨大复杂性。然而,要在 GUI 开发环境中制作二维和三维游戏,它可能是无与伦比的。虚幻引擎 4 使用了一个叫做蓝图的系统,你可以拖放流程图式的元素,而不是编码。它仍然使用了面向对象编程的所有概念,以及循环和分支,但你可以在没有一行真正代码的情况下完成大量工作。看看虚幻引擎版本的 Flappy Bird,它是在没有一行代码的情况下创建的,网址是play.google.com/store/apps/details?id=com.epicgames.TappyChicken

虚幻引擎也可以为多个平台构建游戏,但不幸的是,这里有一个小额的月费,而且最具限制性的是,你制作的任何商业项目都将受到协议的约束。在这里,你需要支付 30%给 Epic 游戏,但对于学习和娱乐来说,这可能是无法超越的。

或者,看看我的博客(www.gamecodeschool.com),我经常添加针对初学者到中级游戏程序员的文章和有趣的游戏构建指南。我的博客讨论了许多不同的编程语言、目标平台,以及之前提到的所有工具,以及更多。

自测问题

Q1) 尝试在设备上实现本地高分。

Q2) 有多少著名的计算机科学家在本书的代码中出现过客串?

Q3) 作为最后的挑战,尝试打破我的贪吃蛇排行榜上的最高分。

总结

在本章中,我们涵盖了很多内容。我们在 Google Play 上发布了我们的贪吃蛇游戏。然后我们添加了一些在线排行榜和成就。我们还更新了我们的出版物。这个过程展示了如何使用 API 将诸如互联网通信之类的非常复杂的任务变得非常简单。

在完成这本书的最后修改时,我看了一段 YouTube 视频,是由软件传奇人物约翰·卡马克(John Carmack)讲的一场讲座。他是Doom游戏开发中的关键工程师,该游戏于 1995 年 6 月发布。当他解释说在学校时,他觉得自己错过了技术革命,等到他够大可以工作时,一切都结束了,我不禁笑了起来,他的听众也是如此。

许多技术革命的确已经来临,许多也已经过去。至少,许多早期采用者的机会已经消失。约翰·卡马克解释说,总会有另一个革命就在拐角处。

所以你可能会发展你的技能,并关注下一个大事件。或者,你只是想用任何语言为任何平台编程任何东西,玩得开心。

希望你喜欢我们一起学习 Android 和 Java 的旅程,并且希望你也会继续这个学习之旅。无论你选择什么样的未来道路,我真诚地祝愿你一切顺利。欢迎你来到www.gamecodeschool.com分享你的经验和知识。这本书的完美续集将于 2015 年中期出版,名为《通过示例学习 Android 游戏编程》。

附录 A. 自测问题和答案

在这里,我们包含了一些你可以问自己的问题,看看你是否理解了每一章。不要担心!答案也包括在内。

第二章

Q1) 如果所有这些关于生命周期、类和方法的讨论让你有点困惑,你应该怎么办?

A) 不要担心它们。理解是一点点来的,如果在这个阶段它们不是完全清楚的,它不会阻碍你彻底学习 Java,随着我们的进展,一切都会变得更清晰。

Q2) 什么是 Java 类?

A) 类是 Java 程序的基本构建块。它们就像我们 Java 代码的容器,我们甚至可以使用其他人的类来简化我们编写的程序,即使没有看到或理解这些类中包含的代码。

Q3) 方法和类之间有什么区别?

A) 方法包含在类中,代表类的特定功能,就像容器中的另一个容器。例如,从游戏中,我们可能有一个Tank类,其中包括shootdriveselfDestruct方法。我们可以通过制作我们自己的类来使用类及其方法,就像我们将在第六章中所做的那样,或者通过使用@import语句,就像我们在本章早些时候所做的那样。

Q4) 查看 Android 开发者网站及其有关生命周期阶段的更多技术解释,网址为developer.android.com/reference/android/app/Activity.html。你能看到我们没有讨论过的阶段及其相关的方法吗?在应用程序中它会在什么时候被触发?从创建到销毁,活动会经历怎样的确切路径?

A) 这是重新启动阶段。它对应的方法是onRestart。当应用程序停止然后重新启动时,它会被触发。我们在本书中不需要onRestart方法,但这个练习有望帮助澄清生命周期的概念。确切的路径会有所不同;我们只需要处理与我们的游戏相关的阶段。到目前为止,我们只是尝试了onCreate

第三章

Q1) 这段代码是做什么的?

// setContentView(R.layout.activity_main);

A) 什么都没有,因为它被//注释掉了。

Q2) 这些行中哪一行会导致错误?

String a = "Hello";
String b = " Vinton Cerf";
int c = 55;
a = a + b
c = c + c + 10;
a = a + c;
c = c + a;

A) 第四行a = a + b没有分号,所以会导致错误。最后一行c = c + a;也会导致错误,因为你不能将字符串赋给int值。

Q3) 我们谈了很多关于运算符以及如何将不同的运算符组合在一起构建复杂的表达式。表达式乍一看有时会让代码看起来复杂。然而,仔细看时,它们并不像看起来那么难。通常,只是将表达式分成较小的部分来弄清楚发生了什么。这里有一个比本书中你会遇到的任何其他东西都更加复杂的表达式。作为挑战,你能计算出x会是什么吗?

int x = 10;
int y = 9;
boolean isTrueOrFalse = false;
isTrueOrFalse = (((x <=y)||(x == 10))&&((!isTrueOrFalse) || (isTrueOrFalse)));

A) 你可以在代码包的Chapter3文件夹中运行SelfTestC3Q3项目来在控制台中查看答案,但isTrueOrFalse会评估为 true;原因如下。

首先,让我们把这个糟糕的行分解成由括号定义的可管理的部分:

((x <=y)||(x == 10))

之前,我们问的问题是,“x是否小于或等于y,或者x是否恰好等于10?”。显然,x既不等于也不小于y,但x恰好等于10,所以我们在中间使用的逻辑或运算符||导致整个表达式评估为true

&&

&&运算符的两侧必须都为true,整个表达式才为true。所以让我们看看另一边:

((!isTrueOrFalse) || (isTrueOrFalse)))

好吧,isTrueOrFalse是一个布尔值。它只能是真或假,所以表达式的这一部分必须是真,因为我们实质上是在问,“isTrueOrFalse是假还是isTrueOrFalse是真?”。它必须是其中之一。因此,无论我们如何初始化isTrueOrFalse,表达式的最后一部分都将是真。

因此,整个表达式评估为true,并且true被分配给isTrueOrFalse

第四章

Q1)这个方法有什么问题?

void doSomething(){
  return 4;
}

A)它返回一个值,但具有void返回类型。

Q2)在这段代码片段结束时,x将等于多少?

int x=19;
do{
  x=11;
  x++;
}while(x<20)

A)好吧,这是一个稍微棘手的问题。无论x的值如何,do块总是至少执行一次。然后x设置为11,之后递增为12。因此,当评估while表达式时,它是真的,do块再次执行。再一次,x设置为11,然后递增为12。程序陷入了一个永无止境(无限)的循环。这段代码很可能是一个错误。

第五章

Q1)假设我们想要进行一个测验,问题可能是关于总统的名字、首都等。我们如何使用多维数组来实现这一点?

A)我们只需使内部数组保存三个字符串,可能是这样:

String[][] countriesCitiesAndPresidents;
//now allocate like this
countriesAndCities = new String[5][3];
//and initialize like this
countriesCitiesAndPresidents [0][0] = "United Kingdom";
countriesCitiesAndPresidents [0][1] = "London";
countriesCitiesAndPresidents [0][3] = "Cameron";//at time of writing

Q2)在我们的持久性示例中,我们将一个不断更新的字符串保存到文件中,以便在应用程序关闭和重新启动后保持。这就像要求用户单击保存按钮。在您对第二章的所有知识的基础上,您能想出一种在用户退出应用程序时保存字符串而不是通过按钮点击保存它的方法吗?

A)覆盖onPause生命周期方法,并将保存字符串的代码放在其中,就像这样:

@Override
    protected void onPause() {
        editor.putString(stringName, currentString);
        editor.commit();
    }

Q3)除了增加难度级别,我们如何使记忆游戏更难?

A)我们可以简单地修改线程执行中的暂停时间,将数字降低,给玩家更少的思考时间,就像这样:

myHandler.sendEmptyMessageDelayed(0, 450);
//This halves the players thinking time

Q4)使用普通的 Android UI 和沉闷的灰色按钮并不是很令人兴奋。看看可视化设计师中的 UI 元素。您能想出如何在我们的按钮背景中使用图像吗?

A)只需向drawable-mdpi文件夹添加一些.png图形,然后在选择按钮时在属性窗口中找到背景属性。单击以通常的方式编辑属性,并选择您添加到drawable-mdpi文件夹的图形。

第六章

Q1)什么是封装?

A)封装是我们以一种方式打包我们的变量、代码和方法,使得我们只向我们想要的应用程序部分(或使用我们类的任何应用程序)公开部分和功能。

Q2)我不太明白这一切,实际上,我现在比章节开始时还有更多问题。我该怎么办?

A)您已经了解足够的面向对象编程知识,可以在游戏和任何其他类型的 Java 编程中取得重大进展。如果您现在急于了解更多面向对象编程知识,那么有很多评价很高的书籍专门讨论面向对象编程。然而,练习和熟悉语法将对实现相同目标产生很大帮助,并且可能更有趣。现在是否急于学习面向对象编程的复杂细节,实际上取决于您的个人目标以及将来想要用编程技能做什么。阅读第九章的最后几页,使您的游戏成为下一个大事件,以获取更多讨论。

第七章

Q1)球的速度是以像素为单位计算的。不同的设备具有不同数量的像素。您能解释如何使不同屏幕分辨率上的球速度大致相同吗?

A) 适应不同屏幕分辨率的一个简单方法是设计一个系统,考虑屏幕的像素数量。我们已经为球拍和球的大小做到了这一点。我们可以声明一个成员变量,如下所示:

int pixelsPerFrameX;
int pixelsPerFrameY;

然后在我们获得屏幕尺寸之后,在onCreate中初始化这些变量:

pixelsPerFrameX = screenWidth/50;
pixelsPerFrameY = screenHeight/50;

然后我们可以稍微移动我们的球,就像这样:

//moving in adjust our x any positions
            if (ballIsMovingDown) {
                ballPosition.y += pixelsPerFrameX;
            }

            //etc...

第八章

Q1) 我们的游戏屏幕有什么视觉改进,也许是一个漂亮的浅绿色草地背景,而不仅仅是黑色?

A) 您可以使用大多数图形程序(如 Gimp 或 Photoshop)获取漂亮的浅绿色草地的 RGB 值。或者,您可以使用在线颜色选择器,例如www.colorpicker.com/。然后查看我们drawGame方法中的这一行:

canvas.drawColor(Color.BLACK);//the background

将其更改为以下行:

canvas.drawColor(Color.argb(255,186,230,177));//the background

Q2) 在背景中添加一些漂亮的花怎么样?

A) 这是做到这一点的方法。创建一个花位图(或使用我的),在configureDisplay方法中加载并按照通常的方式进行缩放。决定绘制多少朵花。在SnakeView构造函数中选择并存储板上的位置(或编写并调用一个特殊的方法,也许是plantFlowers)。

drawGame方法中的蛇和苹果之前绘制它们。这将确保它们永远不会隐藏苹果或蛇的一部分。您可以在提到的方法中看到我的具体实现,并在Chapter8文件夹的EnhancedSnakeGame项目中复制花的位图。

Q3) 如果您感到勇敢,可以让花摇摆。想想精灵表。理论上与动画蛇头的理论完全相同。我们只需要几行代码来控制帧速率,与游戏帧速率分开。

A) 查看controlFPS方法中的新代码。我们只需为花动画设置一个新的计数器,以便在每六个游戏帧中切换花帧。您还可以从Chapter8文件夹中的EnhancedSnakeGame项目中复制精灵表。

Q4) 我们可以设置另一个计数器并使用我们的蛇头动画,但这并不是很有用,因为由于尺寸较小,细微的舌头运动几乎不可见。尽管如此,我们可以相当容易地摆动尾部段。

A) 在Chapter8文件夹的EnhancedSnakeGame项目中有一个两帧尾部位图。由于这也是两帧,我们可以使用与花相同的帧定时器。查看Chapter8文件夹中EnhancedSnakeGame项目中的实现。唯一需要更改的是configureDisplaydrawGame

Q5) 这是一个稍微棘手的增强。您可能已经注意到,当蛇精灵朝着四个可能的方向中的三个方向时,它们看起来不正确。您能修复这个问题吗?

A) 我们需要根据它们的前进方向进行旋转。Android 有一个Matrix类,它允许我们轻松旋转位图,而Bitmap类有一个重载版本的createBitmap方法,它以Matrix对象作为参数。

因此,我们可以为我们需要处理的每个角度创建一个矩阵,如下所示:

Matrix matrix90 = new Matrix();
matrix90.postRotate(90);

然后我们可以使用以下代码旋转位图:

rotatedBitmap = Bitmap.createBitmap(regularBitmap , 0, 0, regularBitmap .getWidth(), regularBitmap .getHeight(), matrix90, true);

另一个问题是,当蛇扭曲和转弯时,我们如何跟踪每个部分的个体方向?我们已经有了一个方向查找方案:0 是向上,1 是向右,依此类推。因此,我们可以为snakeXsnakeY数组中的身体部分创建另一个方向数组。然后,我们所需要做的就是确保头部有正确的方向,并且在每一帧上从后面更新,就像我们为蛇的坐标所做的那样。您可以在Chapter8文件夹的EnhancedSnakeGame项目中看到这一实现。

Chapter8文件夹的EnhancedSnakeGame项目中有一些更多的增强功能的完成项目。这是我们将在下一章和最后一章中使用的起点版本。您还可以从 Google Play 下载游戏play.google.com/store/apps/details?id=com.packtpub.enhancedsnakegame.enhancedsnakegame

第八章第八章

第九章

Q1)尝试在设备上实现本地高分。

A)你已经知道如何做到这一点。如果不确定,只需返回第五章,游戏和 Java 基础。该实现也在本章项目的代码中。

Q2)有多少著名的计算机科学家在整本书中的代码中客串出现?

A)9

阿达·洛芙莱斯

查尔斯·巴贝奇

艾伦·图灵

文顿·瑟夫

杰夫·明特

科琳娜·于

安德烈·拉莫特

盖布·纽维尔

西德·梅尔

为什么不在网上搜索一下这些名字?每个人都有一些有趣的故事。

posted @ 2024-05-22 15:15  绝不原创的飞龙  阅读(11)  评论(0编辑  收藏  举报