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

百度收录网站线上销售渠道有哪几种

百度收录网站,线上销售渠道有哪几种,wordpress页面瀑布流,滨州正规网站建设公司原文:Beginning Android Games 协议:CC BY-NC-SA 4.0 零、简介 大家好,欢迎来到 Android 游戏开发的世界。你来这里是为了学习 Android 上的游戏开发,我们希望成为让你实现自己想法的人。 我们将一起涵盖相当广泛的材质和主题:A…

原文:Beginning Android Games

协议:CC BY-NC-SA 4.0

零、简介

大家好,欢迎来到 Android 游戏开发的世界。你来这里是为了学习 Android 上的游戏开发,我们希望成为让你实现自己想法的人。

我们将一起涵盖相当广泛的材质和主题:Android 基础知识,音频和图形编程,一点数学和物理,OpenGL ES,Android 原生开发工具包(NDK)介绍,最后,出版,营销,从你的游戏中赚钱。基于所有这些知识,我们将开发三个不同的游戏,其中一个甚至是 3D 的。

如果你知道自己在做什么,游戏编程会很容易。因此,我们试图以这样一种方式呈现这些材质,不仅给你有用的代码片段供你重用,而且实际上向你展示游戏开发的全貌。理解潜在的原则是解决越来越复杂的游戏想法的关键。你不仅能够编写与本书中开发的游戏相似的游戏,而且你还将具备足够的知识去上网或逛书店,自己开发新的游戏领域。

这本书是给谁的

这本书首先面向游戏编程的完全初学者。你不需要任何关于主题的先验知识;我们会教你所有的基本知识。然而,我们需要假设你对 Java 有一点了解。如果你对这个问题感到生疏,我们建议你读一读布鲁斯·埃凯尔(Prentice Hall,2006 年)的《用 Java 思考》(Thinking in Java )来刷新你的记忆,这是一本优秀的编程语言入门书籍。除此之外,没有其他要求。没有必要事先接触 Android 或 Eclipse!

这本书也是针对那些想接触 Android 的中级游戏程序员的。虽然有些材质对你来说可能已经是旧闻了,但仍然有许多技巧和提示值得一读。Android 有时是一只奇怪的野兽,这本书应该被视为你的战斗指南。

这本书的结构

这本书采用了一种迭代的方法,我们将缓慢但肯定地从绝对的基础工作到硬件加速游戏编程的深奥高度。在本章的过程中,我们将建立一个可重用的代码库,你可以用它作为大多数类型游戏的基础。

如果你阅读这本书纯粹是作为一个学习练习,我们建议从第一章开始按顺序阅读这几章。每一章都建立在前一章的基础上,这是一次很好的学习经历。

如果你读这本书的目的是想在最后发布一款新游戏,我们强烈建议你跳到第十四章,学习如何设计你的游戏,使其适销对路并赚钱,然后回到起点开始开发。

当然,更有经验的读者可以跳过他们认为有把握的部分。请务必通读您浏览过的部分的代码清单,这样您就会理解在后续更高级的部分中如何使用这些类和接口。

下载代码

这本书是完全独立的;包含了运行示例和游戏所需的所有代码。然而,将书中的清单复制到 Eclipse 很容易出错,而且游戏不仅仅由代码组成,还包含一些您不能轻易从书中复制出来的素材。我们非常小心地确保本书中的所有列表都没有错误,但是小精灵们总是在努力工作。

为了使这一过程顺利进行,我们创建了一个谷歌代码项目,为您提供以下内容:

  • 从项目的 Subversion 存储库中可以获得完整的源代码和素材。该代码根据 Apache License 2.0 获得许可,因此可以在商业和非商业项目中免费使用。这些素材由-SA 3.0 根据知识共享协议授予许可。您可以为您的商业项目使用和修改它们,但是您必须将您的素材置于相同的许可之下!
  • 一个快速入门指南,向您展示如何以文本形式将项目导入到 Eclipse 中,以及同样的视频演示。
  • 一个问题跟踪器,允许您报告您发现的任何错误,无论是在书本身还是在书附带的代码中。一旦您在问题跟踪器中提交了一个问题,我们就可以在 Subversion 存储库中合并任何修复。这样,您将始终拥有本书代码的最新(希望)无错误版本,其他读者也可以从中受益。
  • 一个讨论组,每个人都可以自由加入并讨论书的内容。当然,我们也会在那里。

对于包含代码的每一章,Subversion 存储库中都有一个等价的 Eclipse 项目。这些项目并不相互依赖,因为我们将在本书的过程中反复改进一些框架类。因此,每个项目都是独立的。第五章和第六章的代码都包含在ch06-mrnom项目中。

谷歌代码项目可以在http://code.google.com/p/beginnginandroidgames2.找到

联系作者

如果您有任何问题或意见——或者甚至发现您认为我们应该知道的错误——您可以通过注册帐户并在http://badlogicgames.com/forum/viewforum.php?f=21发帖联系 Mario Zechner,或者通过访问www.rbgrn.net/contact.联系 Robert Green

我们更喜欢通过论坛联系。这样其他读者也会受益,因为他们可以查找已经回答的问题或参与讨论!

一、每个家庭都有一个安卓

作为 80 年代和 90 年代的孩子,我们很自然地伴随着值得信赖的任天堂游戏机和世嘉游戏机长大。我们花了无数时间帮助马里奥营救公主,在俄罗斯方块中获得最高分,并通过链接电缆在超级 RC Pro-Am 中与我们的朋友比赛。我们带着这些很棒的硬件去任何我们能去的地方。我们对游戏的热情让我们想要创造自己的世界,并与我们的朋友分享。我们开始在 PC 上编程,但很快意识到我们无法将我们的小杰作转移到可用的便携式游戏机上。随着我们继续成为热情的程序员,随着时间的推移,我们对实际玩视频游戏的兴趣消退了。此外,我们的游戏男孩最终打破了。。。

快进到今天。智能手机和平板电脑已经成为这个时代的新移动游戏平台,与任天堂 3DS 和 PlayStation Vita 等经典的专用手持系统竞争。这一发展重新激起了我们的兴趣,我们开始研究哪些移动平台适合我们的开发需求。苹果的 iOS 似乎是我们游戏编码技能的一个很好的候选。然而,我们很快意识到这个系统不是开放的,只有在苹果公司允许的情况下,我们才能与他人分享我们的工作,我们需要一台 Mac 来开发 iOS。然后我们发现了 Android 。

我们俩立刻就爱上了 Android。它的开发环境可以在所有主要平台上运行——没有任何附加条件。它有一个充满活力的开发人员社区,乐意帮助您解决遇到的任何问题,并提供全面的文档。你可以与任何人分享你的游戏,而不必为此付费,如果你想将你的作品货币化,你可以在几分钟内轻松地向拥有数百万用户的全球市场发布你最新最伟大的创新。

剩下的唯一事情就是弄清楚如何为 Android 编写游戏,以及如何将我们的 PC 游戏开发知识转移到这个新系统中。在接下来的章节中,我们希望与您分享我们的经验,并帮助您开始 Android 游戏开发。当然,这在一定程度上是一个自私的计划:我们想在旅途中玩更多的游戏!

让我们从了解我们的新朋友 Android 开始。

Android 简史

Android 首次公开露面是在 2005 年,当时谷歌收购了一家名为 Android Inc .的小型初创公司,这引发了人们对谷歌有意进入移动设备领域的猜测。2008 年,Android 1.0 版本的发布结束了所有的猜测,Android 继续成为移动市场上新的挑战者。自那以后,Android 一直在与已经建立的平台竞争,如 iOS(当时称为 iPhone OS)、黑莓 OS 和 Windows Phone 7。Android 的增长是惊人的,因为它每年都获得越来越多的市场份额。虽然移动技术的未来总是在变化,但有一点是肯定的:Android 将会继续存在。

由于 Android 是开源的,使用新平台的手机制造商进入门槛很低。他们可以生产所有价格段的设备,修改 Android 本身以适应特定设备的处理能力。因此,Android 不仅限于高端设备,还可以部署在低成本设备中,从而覆盖更广泛的受众。

Android 成功的一个关键因素是 2007 年末开放手机联盟(OHA)的成立。OHA 包括 HTC、高通、摩托罗拉和英伟达等公司,它们都合作开发移动设备的开放标准。尽管 Android 的代码主要是由谷歌开发的,但所有 OHA 成员都以这样或那样的形式为其源代码做出了贡献。

Android 本身是一个基于 Linux 内核版本 2.6 和 3.x 的移动操作系统和平台,它可以免费用于商业和非商业用途。OHA 的许多成员为他们的设备开发了用户界面经过修改的定制版 Android,比如 HTC 的 Sense 和摩托罗拉的 MOTOBLUR。Android 的开源特性也使得爱好者能够创建和分发他们自己的版本。这些通常被称为 mods固件rom。在撰写本文时,最著名的 rom 是由 Steve Kondik(也称为 Cyanogen)和许多贡献者开发的。它旨在为各种 Android 设备带来最新最好的改进,并为那些被抛弃或陈旧的设备带来新鲜空气。

自 2008 年发布以来,Android 已经收到了许多重大的版本更新,都是以甜点命名的(Android 1.1 除外,如今已经无关紧要了)。Android 平台的大多数版本都添加了新功能,通常以应用编程接口(API)或新开发工具的形式出现,这些功能在某种程度上与游戏开发者相关:

  • 1.5 版本(Cupcake) :增加了在 Android 应用中包含原生库的支持,之前仅限于纯 Java 编写。在最关心性能的情况下,本机代码非常有用。
  • 1.6 版本(甜甜圈) :引入了对不同屏幕分辨率的支持。我们将在本书中多次重温这一发展,因为它对我们如何为 Android 编写游戏有一些影响。
  • 2.0 版本(克莱尔) :增加了对多点触控屏幕的支持。
  • 2.2 版(Froyo) :在 Dalvik 虚拟机(VM)上增加了即时(JIT)编译,这是一款为 Android 上所有 Java 应用提供动力的软件。JIT 大大加快了 Android 应用的执行速度——根据不同的场景,速度提高了 5 倍。
  • 2.3 版本(姜饼) :在 Dalvik VM 中增加了一个新的并发垃圾收集器。
  • 3.0 版本(蜂巢) :创造了一个平板电脑版本的 Android。Honeycomb 于 2011 年初推出,包含了比迄今为止发布的任何其他单一 Android 版本更多的重大 API 变化。到了 3.1 版本,Honeycomb 增加了对分割和管理大型高分辨率平板电脑屏幕的广泛支持。它增加了更多类似 PC 的功能,如 USB 主机支持和 USB 外设支持,包括键盘、鼠标和操纵杆。这个版本唯一的问题是它只针对平板电脑。小屏幕/智能手机版本的 Android 还停留在 2.3 版本。
  • Android 4.0(冰激凌三明治【ICS】):将 Honeycomb (3.1)和 Gingerbread (2.3)合并成一套通用的功能,在平板电脑和手机上都运行良好。
  • Android 4.1(果冻豆) :改进了 UI 的合成方式,以及一般的渲染。这一努力被称为“黄油项目”;首款搭载果冻豆的设备是谷歌自己的 Nexus 7 平板电脑。

ICS 对最终用户来说是一个巨大的推动,它对 Android UI 和内置应用(如浏览器、电子邮件客户端和照片服务)进行了大量改进。对于开发人员来说,除了其他事情之外,IC 还融入了蜂窝 UI APIs,为手机带来了大屏幕功能。ICS 还合并了 Honeycomb 的 USB 外围支持,这使制造商可以选择支持键盘和操纵杆。至于新的 API,ICS 增加了一些,比如 Social API,它为联系人、个人资料、状态更新和照片提供了一个统一的存储。对 Android 游戏开发者来说幸运的是,ICS 在其核心保持了良好的向后兼容性,确保了一个正确构建的游戏将与旧版本如 Cupcake 和艾克蕾尔保持良好的兼容性。

注意我们都经常被问到新版本的 Android 会给游戏带来哪些新功能。答案经常让人们感到惊讶:自 2.1 版本以来,除了原生开发工具包(NDK)之外,实际上没有新的游戏特定功能被添加到 Android 中。从那个版本开始,Android 已经包含了你所需要的一切,你可以构建任何你想要的游戏。大多数新特性都添加到了 UI API 中,所以只需关注 2.1 版,你就可以开始了。

碎片化

Android 的巨大灵活性是有代价的:选择开发自己用户界面的公司必须赶上新版本 Android 发布的快速步伐。这可能导致推出不到几个月的手机变得过时,因为运营商和手机制造商拒绝创建包含新 Android 版本改进的更新。这个过程的结果是一个叫做碎片化的大怪物。

碎片化有很多面。对于最终用户来说,这意味着由于被旧版本的 Android 卡住而无法安装和使用某些应用和功能。对于开发人员来说,这意味着在创建能够在所有版本的 Android 上运行的应用时必须小心谨慎。虽然为早期版本的 Android 编写的应用通常在新版本上运行良好,但反之则不然。当然,新版本的 Android 增加的一些功能在旧版本上是不可用的,比如多点触摸支持。开发者因此被迫为不同版本的 Android 创建不同的代码路径。

2011 年,许多著名的 Android 设备制造商同意支持最新的 Android 操作系统,设备寿命为 18 个月。这似乎不是很长的时间,但这是帮助减少碎片化的一大步。这也意味着 Android 的新功能,比如冰激凌三明治中的新 API,可以更快地在更多手机上使用。一年后,这个承诺似乎没有兑现。很大一部分市场仍在运行旧的 Android 版本,主要是姜饼。如果一款游戏的开发者想要得到大众市场的认可,这款游戏将需要在至少六个不同版本的 Android 上运行,分布在 600 多种设备上(还在增加!).

但是不要害怕。虽然这听起来很可怕,但事实证明,为适应多个版本的 Android 而必须采取的措施是很少的。大多数情况下,你甚至可以忘记这个问题,假装只有一个版本的 Android。作为游戏开发者,我们不太关心 API 的差异,而更关心硬件能力。这是一种不同形式的碎片化,这也是 iOS 等平台的问题,尽管没有那么明显。在这本书里,我们将讨论相关的碎片问题,当你为 Android 开发下一个游戏时,这些问题可能会妨碍你。

谷歌的作用

尽管 Android 官方上是开放手机联盟的产物,但在实现 Android 本身以及为其发展提供必要的生态系统方面,谷歌显然是领导者。

Android 开源项目

谷歌的努力总结在 Android 开源项目中。大多数代码都是在 Apache License 2 下授权的,与其他开源许可证(如 GNU 通用公共许可证(GPL ))相比,Apache License 2 是非常开放和无限制的。每个人都可以自由地使用这个源代码来构建自己的系统。然而,宣称兼容 Android 的系统首先必须通过 Android 兼容性计划,这一过程确保与开发者编写的第三方应用的基线兼容性。兼容系统被允许参与 Android 生态系统,其中还包括 Google Play

Google Play

Google Play(原名 Android Market )于 2008 年 10 月由谷歌向公众开放。这是一个在线商店,用户可以购买音乐、视频、书籍和第三方应用,或在他们的设备上消费的应用。Google Play 主要在 Android 设备上提供,但也有一个 web 前端,用户可以在那里搜索、评级、下载和安装应用。这不是必需的,但大多数 Android 设备都默认安装了 Google Play 应用。

Google Play 允许第三方开发者免费或付费发布他们的程序。在许多国家都可以购买付费应用,集成的购买系统使用 Google Checkout 处理汇率。Google Play 还提供了为每个国家的应用手动定价的选项。

用户在建立谷歌账户后就可以进入商店。应用可以通过信用卡购买,通过谷歌结帐或使用运营商计费。买家可以在购买后 15 分钟内决定退回申请,获得全额退款。以前,退款窗口是 24 小时,但为了减少对系统的利用,退款窗口被缩短了。

开发者需要向谷歌注册一个 Android 开发者账户,一次性支付 25 美元,才能在商店上发布应用。注册成功后,开发人员可以在几分钟内开始发布新的应用。

Google Play 没有审批流程,而是依靠许可系统。在安装应用之前,会向用户提供一组必需的权限,这些权限处理对电话服务、网络、安全数字(SD)卡等的访问。用户可能因为权限而选择不安装应用,但是用户当前没有能力简单地不允许应用具有特定的权限。整体上是“要么接受,要么放弃”。这种方法旨在让应用诚实地知道他们将使用设备做什么,同时为用户提供他们需要的信息,以决定信任哪些应用。

为了销售应用,开发人员还必须注册一个免费的 Google Checkout 商家帐户。所有的金融交易都通过这个账户处理。谷歌也有一个应用内购买系统,它与 Android Market 和谷歌 Checkout 集成在一起。开发人员可以使用单独的 API 来处理应用内购买交易。

谷歌输入输出

一年一度的谷歌 I/O 大会是每个安卓开发者每年都期待的一件大事。在 Google I/O 上,展示了最新最伟大的 Google 技术和项目,其中 Android 近年来获得了特殊的地位。谷歌 I/O 通常会有多个关于 Android 相关主题的会议,这些会议也可以在 YouTube 的谷歌开发者频道上以视频形式获得。在谷歌 I/O 2011 上,三星和谷歌向所有常规与会者分发了 Galaxy Tab 10.1 设备。这标志着谷歌开始大举进军平板电脑市场。

Android 的功能和架构

Android 不仅仅是另一个面向移动设备的 Linux 发行版。在为 Android 开发时,你不太可能遇到 Linux 内核本身。Android 面向开发者的一面是一个平台,它抽象出底层的 Linux 内核,并通过 Java 编程。从高层次来看,Android 拥有几个不错的特性:

  • 一个应用框架,它为创建各种类型的应用提供了丰富的 API。它还允许重用和替换平台和第三方应用提供的组件。
  • Dalvik 虚拟机,负责在 Android 上运行应用。
  • 一套用于 2D 和 3D 编程的图形库
  • 媒体支持常见的音频、视频和图像格式,如 Ogg Vorbis、MP3、MPEG-4、H.264 和 PNG。甚至有一个专门的 API 来播放声音效果,这将在你的游戏开发冒险中派上用场。
  • 用于访问外设的 API,如摄像头、全球定位系统(GPS)、指南针、加速度计、触摸屏、轨迹球、键盘、控制器和操纵杆。注意,并不是所有的 Android 设备都有这些外设——硬件碎片化在起作用。

当然,Android 的功能远不止刚刚提到的几个。但是,对于您的游戏开发需求,这些功能是最相关的。

Android 的架构由堆叠的组件组组成,每一层都建立在其下一层的组件之上。图 1-1 给出了 Android 主要组件的概述。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-1。 Android 架构概述

内核

从堆栈的底部开始,您可以看到 Linux 内核为硬件组件提供了基本的驱动程序。此外,内核负责诸如内存和进程管理、网络等日常事务。

运行时和 Dalvik

Android 运行时构建在内核之上,它负责生成和运行 Android 应用。每个 Android 应用都在自己的进程中运行,有自己的 Dalvik VM。

Dalvik 以 Dalvik 可执行(DEX)字节码格式运行程序。通常,你转换普通的 Java。使用软件开发工具包(SDK)提供的名为 dx 的特殊工具将类文件转换成 DEX 格式。与经典 Java 相比,DEX 格式的内存占用更小。类文件。这是通过大量压缩、表和多个。类文件。

Dalvik VM 与核心库接口,核心库提供向 Java 程序公开的基本功能。核心库通过使用 Apache Harmony Java 实现的子集,提供了 Java Standard Edition (SE)中可用的一些类,但不是全部。这也意味着没有可用的 Swing 或抽象窗口工具包(AWT ),也没有可以在 Java Micro Edition (ME)中找到的任何类。然而,只要小心,您仍然可以在 Dalvik 上使用许多可用于 Java SE 的第三方库。

在 Android 2.2 (Froyo)之前,所有的字节码都是解释的。Froyo 引入了一个跟踪 JIT 编译器,它可以动态地将部分字节码编译成机器码。这大大提高了计算密集型应用的性能。JIT 编译器可以使用专门为特殊计算定制的 CPU 特性,例如专用浮点单元(FPU)。几乎每一个新版本的 Android 都改进了 JIT 编译器并提高了性能,通常是以消耗内存为代价的。不过,这是一个可扩展的解决方案,因为新设备包含越来越多的标准 RAM。

Dalvik 还有一个集成的垃圾收集器(GC) ,在早期版本中,它有时会让开发人员有点抓狂。不过,只要注意一些细节,你就可以在日常游戏开发中与 GC 和平共处。从 Android 2.3 开始,Dalvik 采用了改进的并发 GC,这减轻了一些痛苦。在本书的后面,您将更详细地研究 GC 问题。

Dalvik VM 实例中运行的每个应用总共至少有 16 MB 的堆内存可用。较新的设备,特别是平板电脑,有更高的堆限制,以促进更高分辨率的图形。不过,玩游戏很容易耗尽所有的内存,所以当你处理图像和音频资源时,你必须记住这一点。

系统库

除了提供一些 Java SE 功能的核心库之外,还有一组本地 C/C++ 库(图 1-1 中的第二层),它们为应用框架构建了基础(图 1-1 中的第三层)。这些系统库主要负责计算量大的任务,这些任务不太适合 Dalvik VM,比如图形渲染、音频回放和数据库访问。API 由应用框架中的 Java 类包装,当你开始编写游戏时,你将会利用这些 API。您将以某种形式使用以下库:

  • Skia 图形库(Skia) :这款 2D 图形软件用于渲染 Android 应用的 UI。您将使用它来绘制您的第一个 2D 游戏。
  • 嵌入式系统 OpenGL(OpenGL ES):这是硬件加速图形渲染的行业标准。OpenGL ES 1.0 和 1.1 在所有版本的 Android 上都暴露给 Java。OpenGL ES 2.0 将着色器带到了桌面上,仅从 Android 2.2 (Froyo)开始受支持。应该提到的是,Froyo 中 OpenGL ES 2.0 的 Java 绑定是不完整的,并且缺少一些重要的方法。幸运的是,这些方法是在 2.3 版本中添加的。此外,许多仍然占市场一小部分份额的旧仿真器图像和设备不支持 OpenGL ES 2.0。出于您的目的,请坚持使用 OpenGL ES 1.0 和 1.1,以最大化兼容性并允许您轻松进入 Android 3D 编程的世界。
  • OpenCore :这是一个音频和视频的媒体回放和录制库。它支持 Ogg Vorbis、MP3、H.264、MPEG-4 等格式的良好混合。您将主要处理音频部分,它不直接暴露给 Java 端,而是包装在几个类和服务中。
  • 这是一个用来加载和渲染位图和矢量字体的库,最著名的是 TrueType 格式。FreeType 支持 Unicode 标准,包括阿拉伯语和类似特殊文本的从右到左字形呈现。与 OpenCore 一样,FreeType 并不直接暴露于 Java 端,而是包装在几个方便的类中。

这些系统库覆盖了游戏开发者的很多领域,并完成了大部分繁重的工作。这就是为什么你可以用普通的 Java 编写游戏的原因。

注意尽管 Dalvik 的功能通常足以满足您的需求,但有时您可能需要更高的性能。这可能是非常复杂的物理模拟或繁重的 3D 计算的情况,为此您通常会求助于编写本机代码。我们将在本书的后一章对此进行探讨。已经有几个 Android 的开源库可以帮助你保持在 Java 方面。参见http://code.google.com/p/libgdx/中的示例。

应用框架

应用框架将系统库和运行时联系在一起,创建了 Android 的用户端。该框架管理应用,并提供应用在其中运行的精细结构。开发人员通过一组 Java APIs 为这个框架创建应用,这些 API 涵盖了 UI 编程、后台服务、通知、资源管理、外设访问等领域。Android 提供的所有开箱即用的核心应用,比如邮件客户端,都是用这些 API 编写的。

应用,无论是 ui 还是后台服务,都可以将它们的能力传达给其他应用。这种通信使应用能够重用其他应用的组件。一个简单的例子是,一个应用需要拍摄一张照片,然后在照片上执行一些操作。应用向系统查询提供该服务的另一个应用的组件。然后,第一个应用可以重用该组件(例如,内置的照相机应用或照片库)。这大大减轻了程序员的负担,也使你能够定制 Android 行为的方方面面。

作为游戏开发人员,您将在这个框架内创建 UI 应用。因此,您会对应用的架构和生命周期以及它与用户的交互感兴趣。后台服务通常在游戏开发中起的作用很小,这也是不详细讨论的原因。

软件开发工具包

要为 Android 开发应用,您将使用 Android 软件开发工具包(SDK)。SDK 由一套全面的工具、文档、教程和示例组成,可以帮助您快速入门。还包括为 Android 创建应用所需的 Java 库。这些包含应用框架的 API。所有主要的桌面操作系统都支持作为开发环境。

SDK 的突出特点如下:

  • 调试器,能够调试在设备或仿真器上运行的应用。
  • 一个内存和性能概要文件来帮助你发现内存泄漏和识别缓慢的代码。
  • 设备模拟器虽然有时有点慢,但很准确,它基于 QEMU(一个用于模拟不同硬件平台的开源虚拟机)。有一些选项可用于加速仿真器,如英特尔硬件加速执行管理器(HAXM),我们将在第二章中讨论。
  • 与设备通信的命令行工具
  • 构建脚本和工具来打包和部署应用。

SDK 可以与 Eclipse 集成,Eclipse 是一种流行的、功能丰富的开源 Java 集成开发环境(IDE)。集成是通过 Android 开发工具(ADT)插件 实现的,该插件为 Eclipse 添加了一组新功能,目的如下:创建 Android 项目;在仿真器或设备上执行、分析和调试应用;并打包 Android 应用以部署到 Google Play。注意,SDK 也可以集成到其他 ide 中,比如 NetBeans。然而,对此没有官方支持。

第二章讲述了如何用 SDK 和 Eclipse 来设置 IDE。

Eclipse 的 SDK 和 ADT 插件不断更新,添加新的特性和功能。因此,保持更新是一个好主意。

任何好的 SDK 都有大量的文档。Android 的 SDK 在这方面并不逊色,它包含了很多示例应用。您还可以在http://developer.android.com/guide/index.html找到开发人员指南和应用框架所有模块的完整 API 参考。

除了 Android SDK,使用 OpenGL 的游戏开发人员可能希望安装和使用高通、PowerVR、英特尔和 NVIDIA 的各种分析器。与 Android SDK 中的任何东西相比,这些分析器提供了更多关于游戏在设备上的需求的数据。我们将在第二章中更详细地讨论这些分析器。

开发者社区

Android 成功的部分原因是它的开发者社区,他们聚集在网络的各个地方。开发者交流最频繁的网站是位于http://groups.google.com/group/android-developers的 Android 开发者小组。当你偶然发现一个看似无法解决的问题时,这里是你提问或寻求帮助的首选之地。各种各样的 Android 开发人员都会访问这个小组,从系统程序员到应用开发人员,再到游戏程序员。偶尔,负责 Android 部分的谷歌工程师也会提供有价值的见解。注册是免费的,我们强烈建议你现在就加入这个小组!除了为你提供一个提问的地方,它也是一个搜索以前回答过的问题和问题解决方案的好地方。所以,在提问之前,先检查一下是否已经有人回答了。

另一个信息和帮助来源是http://www.stackoverflow.com的堆栈溢出。可以通过关键词搜索,也可以通过标签浏览最新的安卓问题。

每个称职的开发者社区都有一个吉祥物。Linux 有企鹅 Tux,GNU 有它的。。。好吧,gnu,Mozilla Firefox 也有它时髦的 Web 2.0 fox。安卓也没什么不同,选了一个绿色小机器人做吉祥物。图 1-2 给你看那个小恶魔。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-2。 Android 机器人

这个机器人已经出演了一些流行的安卓游戏。它最引人注目的出现在 Replica Island,这是一个免费的开源平台,由前谷歌开发者倡导者 Chris Pruett 创建,是一个 20%的项目。(术语百分之二十项目代表谷歌员工每周有一天可以花在他们自己选择的项目上。)

装置,装置,装置!

Android 没有被锁定在一个单一的硬件生态系统中。许多著名的手机制造商,如 HTC、摩托罗拉、三星和 LG,已经加入了 Android 的行列,他们提供了大量运行 Android 的设备。除了手机,还有一系列基于 Android 的平板设备。不过,一些关键概念是所有设备都共享的,这将使你作为游戏开发者的生活变得更容易一些。

硬件

Google 最初发布了以下最低硬件规格。几乎所有可用的 Android 设备都满足,并且经常大大超过这些建议:

  • 128 MB RAM :这个规格是最低的。目前的高端设备已经包括 1 GB RAM,如果摩尔定律得以实现,这种上升趋势不会很快结束。
  • 256 MB 闪存:这是存储系统映像和应用所需的最小内存量。长期以来,缺乏足够的内存是 Android 用户最大的抱怨,因为第三方应用只能安装到闪存中。随着 Froyo 的发布,这种情况发生了变化。
  • 迷你或微型 SD 卡存储:大多数设备都带有几千兆字节的 SD 卡存储,用户可以将其替换为更高容量的 SD 卡。一些设备,如三星 Galaxy Nexus,已经取消了可扩展的 SD 卡插槽,只集成了闪存。
  • 16 位彩色四分之一视频图形阵列(QVGA)薄膜晶体管液晶显示器(TFT-LCD) :在 Android 版本之前,操作系统只支持半尺寸 VGA (HVGA)屏幕(480 × 320 像素)。从版本 1.6 开始,支持更低和更高分辨率的屏幕。目前的高端手机都有宽 VGA (WVGA)屏幕(800 × 480、848 × 480 或 852 × 480 像素),一些低端设备支持 QVGA 屏幕(320 × 280 像素)。平板电脑屏幕有各种尺寸,通常约为 1280 × 800 像素,谷歌电视支持高清电视的 1920 × 1080 分辨率!虽然许多开发人员喜欢认为每个设备都有触摸屏,但事实并非如此。Android 正在向机顶盒和带有传统显示器的类似 PC 的设备进军。这些设备类型都没有与手机或平板电脑相同的触摸屏输入。
  • 专用硬件按键:这些按键用于导航。设备总是会提供按钮,或者作为软键,或者作为硬件按钮,专门映射到标准导航命令,例如 home 和 back,通常与屏幕触摸命令分开。Android 的硬件范围很大,所以不要做任何假设!

当然,大多数 Android 设备配备的硬件比最低规格要求的要多得多。几乎所有的手机都有 GPS ,一个加速计,和一个指南针。许多还具有接近和光传感器。这些外设为游戏开发者提供了新的方式让用户与游戏互动;我们将在本书的后面使用其中的一些。一些设备甚至有完整的 QWERTY 键盘和轨迹球。后者最常见于 HTC 设备。摄像头也几乎在目前所有的便携设备上都有。一些手机和平板电脑有两个摄像头:一个在背面,一个在正面,用于视频聊天。

专用的*图形处理单元(GPU)*对于游戏开发尤为关键。最早运行 Android 的手机已经有一个符合 OpenGL ES 1.0 的 GPU。较新的便携式设备的 GPU 性能与较旧的 Xbox 或 PlayStation 2 相当,支持 OpenGL ES 2.0。如果没有可用的图形处理器,该平台以称为 PixelFlinger 的软件渲染器的形式提供后备。许多低预算手机依赖于软件渲染器,这对于大多数低分辨率屏幕来说已经足够快了。

除了图形处理器,任何当前可用的 Android 设备也有专用的音频硬件。许多硬件平台包括解码不同媒体格式(如 H.264)的特殊电路。通过硬件组件为移动电话、Wi-Fi 和蓝牙提供连接。Android 设备中的所有硬件模块通常都集成在单个片上系统(SoC) 中,这种系统设计也出现在嵌入式硬件中。

设备的范围

一开始,有 G1。开发人员急切地等待更多的设备,几款略有不同的手机很快问世,这些手机被认为是“第一代”。多年来,硬件变得越来越强大,现在已经有了手机、平板电脑和机顶盒,从具有 2.5 英寸 QVGA 屏幕、仅在 500 MHz ARM CPU 上运行软件渲染器的设备,一直到具有双 1 GHz CPUs、支持 HDTV 的非常强大的 GPU 的机器。

我们已经讨论了碎片问题,但是开发人员还需要处理如此大范围的屏幕尺寸、功能和性能。做到这一点的最佳方法是了解最小硬件,并使其成为游戏设计和性能测试的最小公分母。

最低实际目标

截至 2012 年年中,不到 3%的 Android 设备运行的是 2.1 之前的 Android 版本。这很重要,因为这意味着你现在开始的游戏将只需要支持最低 7 (2.1)的 API 级别,并且当它完成时,它仍将达到所有 Android 设备的 97%(按版本)。这并不是说你不能使用最新的新功能!你当然可以,我们会告诉你怎么做。你只需要设计一些后备机制来兼容 2.1 版的游戏。当前数据可在http://developer.android.com/resources/dashboard/platform-versions.html通过谷歌获得,2012 年 8 月收集的图表显示在图 1-3 中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-3。2012 年 8 月 1 日 Android 版本发布

那么,作为最低目标,什么是好的基线设备呢?回到发布的第一款 Android 2.1 设备:原版摩托罗拉 Droid ,如图图 1-4 。虽然 droid 已经更新到 Android 2.2,但它仍然是一款广泛使用的设备,在 CPU 和 GPU 性能方面都相当出色。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-4。摩托罗拉 Droid

最初的 Droid 被称为第一个“第二代”设备,它是在第一套基于高通 MSM7201A 的模型(包括 G1、Hero、MyTouch、厄里斯和许多其他模型)大约一年后发布的。Droid 是第一款拥有分辨率高于 480 × 320 的屏幕和独立 PowerVR GPU 的手机,也是第一款原生多点触摸 Android 设备(尽管它有一些多点触摸问题,但稍后会有更多)。

支持 Droid 意味着您支持具有以下规格的设备:

  • CPU 速度在 550 MHz 和 1 GHz 之间,支持硬件浮点运算
  • 支持 OpenGL ES 1.x 和 2.0 的可编程 GPU
  • WVGA 屏风
  • 多点触摸支持
  • Android 版本 2.1 或 2.2 以上

Droid 是一个优秀的最小目标,因为它运行 Android 2.2 并支持 OpenGL ES 2.0。它的屏幕分辨率为 854 × 480,与大多数基于手机的手机相似。如果一款游戏在 Droid 上运行良好,那么它很可能在 90 %的 Android 手机上运行良好。仍然会有一些旧的,甚至一些新的设备的屏幕尺寸为 480 × 320,所以最好为它做好计划,至少在它们上面进行测试,但从性能角度来看,你不太可能需要比 Droid 支持的少得多,以抓住绝大多数 Android 观众。

Droid 也是一款出色的测试设备,可以模拟许多涌入亚洲市场的廉价中国手机的功能,由于价格低廉,这些手机也进入了一些西方市场。

尖端设备

Honeycomb 推出了非常可靠的平板电脑支持,平板电脑显然是一个不错的游戏平台。随着 NVIDIA Tegra 2 芯片在 2011 年初引入设备,手机和平板电脑都开始接收快速的双核 CPU,甚至更强大的 GPU 也成为了标准。在写一本书的时候,很难讨论什么是现代,因为它变化如此之快,但在撰写本文的时候,设备到处都有超高速处理器、大量存储、大量内存、高分辨率屏幕、十点多点触摸支持,甚至在一些型号中有 3D 立体显示,这变得非常普遍。

Android 设备中最常见的 GPU 是 Imagination Technologies 的 PowerVR 系列,高通的集成 Adreno GPUs 的骁龙,NVIDIA 的 Tegra 系列,以及许多三星芯片中内置的 Mali 系列。PowerVR 目前有几个版本:530、535、540 和 543。不要被型号之间的小增量所迷惑;与其前辈相比,540 绝对是速度极快的 GPU,它在三星 Galaxy S 系列和谷歌 Galaxy Nexus 中都有搭载。543 目前配备在最新的 iPad 和 PlayStation Vita 中,比 540 快几倍!虽然它目前没有安装在任何主要的 Android 设备上,但我们不得不假设 543 将很快出现在新的平板电脑上。较旧的 530 在 Droid 中,535 分散在几个型号中。也许最常用的 GPU 是高通的,几乎在每一个 HTC 设备中都能找到。Tegra GPU 的目标是平板电脑,但也在几款手机中使用。三星的许多新手机都在使用 Mali GPU,取代了以前使用的 PowerVR 芯片。所有这四种竞争芯片架构都具有很强的可比性和强大的功能。

三星的 Galaxy Tab 2 10.1(见图 1-5 )很好地代表了最新的 Android 平板电脑产品。它具有以下特点:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-5。三星银河 Tab 2 10.1

  • 双核 1 GHz CPU/GPU
  • 支持 OpenGL ES 1.x 和 2.0 的可编程 GPU
  • 1280 × 800 像素的屏幕
  • 十点多点触控支持
  • 安卓冰淇淋三明治 4.0

支持 Galaxy Tab 2 10.1 级平板电脑对于维持越来越多的用户接受这项技术非常重要。从技术上来说,支持它和支持任何其他设备没有区别。平板电脑大小的屏幕是在设计阶段可能需要额外考虑的另一个方面,但你会在本书的后面找到更多相关信息。

未来:下一代

设备制造商试图尽可能长时间地对他们的最新手机保密,但一些规格总是被泄露。

所有未来设备的总体趋势是更多的内核、更多的内存、更好的 GPU、更高的屏幕分辨率和每英寸像素。竞争对手的芯片不断出现,不断吹嘘更大的数量,而 Android 本身也在发展和成熟,这既通过提高性能,也通过在几乎每个后续版本中增加功能。硬件市场竞争异常激烈,而且没有任何放缓的迹象。

虽然 Android 始于一部手机,但它已经迅速发展到可以在不同类型的设备上运行,包括电子书阅读器、机顶盒、平板电脑、导航系统和插入坞站成为个人电脑的混合手机。为了创造一个可以在任何地方工作的 Android 游戏,开发者需要考虑 Android 的本质;也就是说,一个无处不在的操作系统可以嵌入到几乎任何东西上。人们不应该认为 Android 将简单地停留在当前类型的设备上。自 2008 年以来,它的增长如此之快,覆盖面如此之广,以至于对于 Android 来说,很明显天空是无限的。

无论未来会发生什么,Android 将永远存在!

兼容所有设备

在所有这些关于手机、平板电脑、芯片组、外设等等的讨论之后,很明显,支持 Android 设备市场与支持 PC 市场没有什么不同。屏幕尺寸从微小的 320 × 240 像素一直到 1920 × 1080(在 PC 显示器上可能更大!).在最低端的第一代设备上,你只有微不足道的 500 MHz ARM5 CPU 和非常有限的 GPU,没有太多的内存。另一方面,您有一个高带宽、多核 1–2 GHz CPU,带有大规模并行 GPU 和大量内存。第一代手机有一个不确定的多点触摸系统,无法检测离散的触摸点。新的平板电脑可以支持 10 个独立的触摸点。机顶盒根本不支持任何触摸!开发者该怎么做?

首先,所有这些都是明智的。Android 本身有一个兼容性程序,规定了 Android 兼容设备各部分的最低规格和值范围。如果设备不符合标准,则不允许捆绑 Google Play 应用。唷,那就放心了!兼容性程序在http://source.android.com/compatibility/overview.html可用。

Android 兼容性计划在兼容性定义文档(CDD)中进行了概述,该文档可在兼容性计划网站上获得。该文档针对 Android 平台的每个版本进行更新,硬件制造商必须更新和重新测试他们的设备以保持合规。

CDD 规定的与游戏开发者相关的一些项目如下:

  • 最小音频延迟(各不相同)
  • 最小屏幕尺寸(目前为 2.5 英寸)
  • 最小屏幕密度(目前为 100 dpi)
  • 可接受的长宽比(目前为 4:3 到 16:9)
  • 3D 图形加速(需要 OpenGL ES 1.0)
  • 输入设备

即使你不能理解上面列出的一些项目,也不用担心。在本书的后面部分,您将会更详细地了解这些主题。从这个列表中可以看出,有一种方法可以设计一款游戏,使其能够在绝大多数 Android 设备上运行。通过规划游戏中的用户界面和一般视图等内容,以便它们可以在不同的屏幕大小和长宽比上工作,并通过了解您不仅需要触摸功能,还需要键盘或其他输入方法,您可以成功开发一个非常兼容的游戏。不同的游戏需要不同的技术来在不同的硬件上实现良好的用户体验,所以不幸的是没有解决这些问题的灵丹妙药。但是,请放心:随着时间的推移和一点适当的规划,你将能够获得良好的结果。

手机游戏不同

早在 iPhone 和 Android 出现之前,游戏就已经是一个巨大的市场了。然而,随着这些新形式的混合设备的出现,情况开始发生变化。游戏不再是书呆子们的专利。人们看到严肃的商务人士在公共场合用他们的手机玩最新的流行游戏,报纸报道成功的小游戏开发商在手机应用市场上发财的故事,而老牌游戏发行商很难跟上移动领域的发展。游戏开发者必须认识到这种变化,并做出相应的调整。让我们看看这个新的生态系统能提供什么。

每个口袋里都有一台游戏机

移动设备无处不在。这可能是从本节中得出的关键陈述。由此,你可以很容易地推导出手机游戏的所有其他事实。

随着硬件价格不断下降,新设备的计算能力不断增强,它们也成为游戏的理想选择。现在手机是必需品,所以市场渗透率很大。许多人正在用新一代智能手机替换他们的旧的、经典的手机,并发现他们可以使用的各种新的应用。

以前,如果你想玩视频游戏,你必须有意识地决定购买视频游戏系统或游戏 PC。现在,您可以在手机、平板电脑和其他设备上免费获得该功能。没有额外的费用(至少如果你不计算你可能需要的数据计划),你的新游戏设备随时可供你使用。只需从您的口袋或钱包中取出它,您就可以开始使用了-无需随身携带单独的专用系统,因为一切都集成在一个包中。

除了只需携带一台设备来满足电话、互联网和游戏需求的好处之外,另一个因素使更多的观众可以轻松地在手机上玩游戏:你可以在你的设备上启动一个专用的市场应用,选择一个看起来有趣的游戏,然后立即开始玩。没有必要去商店或者通过你的电脑下载一些东西,例如,你没有把游戏传输到你的手机上所需的 USB 线。

当代设备处理能力的提高也对你作为游戏开发者的潜力产生了影响。即使是中产阶级的设备也能够产生类似于旧 Xbox 和 PlayStation 2 系统的游戏体验。有了这些强大的硬件平台,你也可以开始探索具有物理模拟的复杂游戏,这是一个具有巨大创新潜力的领域。

新的设备带来了新的输入方法,这一点我们已经提到过了。一些游戏已经利用了大多数 Android 设备中的 GPS 和/或指南针。使用加速度计已经是许多游戏的必备功能,多点触摸屏为用户提供了与游戏世界互动的新方式。已经讨论了很多内容,但是仍然有新的方法以创新的方式使用所有这些功能。

始终连接

Android 设备通常与数据计划一起出售。这使得网络流量越来越大。智能手机用户很可能在任何给定时间连接到网络(不考虑硬件设计故障导致的接收不良)。

永久连接为手机游戏打开了一个全新的世界。用户可以挑战地球另一端的对手,进行一场快速的国际象棋比赛,探索有真人居住的虚拟世界,或者在一场绅士之死比赛中尝试击碎来自另一个城市的最好的朋友。此外,所有这一切都发生在旅途中——在公共汽车上,在火车上,或者在当地公园最受欢迎的角落里。

除了多人游戏功能,社交网络也开始影响手机游戏。游戏提供自动将您的最新高分直接发布到您的 Twitter 帐户的功能,或者通知朋友您在你们都喜欢的赛车游戏中获得的最新成就。虽然传统游戏世界中存在越来越多的社交网络(例如,Xbox Live 或 PlayStation Network),但脸书和 Twitter 等服务的市场渗透率要高得多,因此用户可以免去同时管理多个网络的负担。

休闲与硬核

绝大多数用户采用移动设备也意味着从未接触过 NES 控制器的人突然发现了游戏世界。他们对好游戏的想法往往与铁杆游戏玩家相差甚远。

根据手机的使用案例,典型用户倾向于更休闲的游戏,他们可以在公交车上或快餐店排队时玩几分钟。这些游戏相当于 PC 上那些令人上瘾的小 flash 游戏,每当他们感觉到身后有人时,就会迫使许多职场人疯狂地按 Alt + Tab。问问你自己:你愿意每天花多少时间在手机上玩游戏?你能想象在这样的设备上玩“快速”的文明游戏吗?

当然,如果他们可以在手机上玩他们心爱的高级龙与地下城游戏,可能会有认真的游戏玩家愿意献出他们的第一个孩子。但这个群体是少数,iPhone 应用商店和 Google Play 中最畅销的游戏就证明了这一点。最畅销的游戏通常本质上非常休闲,但他们有一个巧妙的锦囊妙计:玩一轮游戏的平均时间在几分钟内,但这些游戏通过使用各种邪恶的计划让你回来。一个游戏可能会提供一个复杂的在线成就系统,让你可以虚拟地吹嘘你的技能。另一个可能实际上是一个伪装的硬核游戏。为用户提供一个简单的方法来保存他们的进展,你是在卖一个可爱的益智游戏史诗 RPG!

大市场,小开发商

移动游戏市场的低进入门槛是吸引许多爱好者和独立开发者的主要因素。在 Android 的情况下,这个障碍特别低:只要让你自己的 SDK 和程序离开。你甚至不需要一个设备;只需使用模拟器(尽管建议至少有一个开发设备)。Android 的开放性也导致了网络上的大量活动。关于系统编程的所有方面的信息都可以在网上免费找到。没有必要签署一份保密协议,或者等待某个权威机构批准你进入他们神圣的生态系统。

最初,市场上许多最成功的游戏都是由一个人的公司和小团队开发的。各大出版社很长时间没有涉足这个市场,至少没有成功。智乐就是一个最好的例子。尽管 Gameloft 在 iPhone 上很大,但在很长一段时间内无法在 Android 上立足,因此决定在自己的网站上销售他们的游戏。智乐可能不喜欢缺少数字版权管理方案(现在安卓上有了)。最终,Gameloft 与 Zynga 或 Glu Mobile 等其他大公司一起,再次开始在 Google Play 上发布内容。

Android 环境也允许大量的实验和创新,因为无聊的人在 Google Play 上搜索小宝石,包括新的想法和游戏机制。在经典游戏平台(如 PC 或游戏机)上进行的实验经常会失败。然而,Google Play 能让你接触到大量愿意尝试实验性新想法的观众,而且不费吹灰之力就能接触到他们。

当然,这并不意味着你不必推销你的游戏。一种方法是在网上的各种博客和专门的网站上发布你的最新游戏。许多安卓用户都是狂热爱好者,经常光顾这样的网站,查看下一个大热门。

接触大量受众的另一种方式是在 Google Play 中出现。当用户启动 Google Play 应用时,您的应用将出现在用户列表中。许多开发人员报告下载量大幅增加,这与在 Google Play 中获得功能直接相关。不过,如何成为特色有点神秘。无论你是一个大出版商还是一个小的个人商店,拥有一个令人敬畏的想法并以最完美的方式执行它是你最好的选择。

最后,仅仅通过简单的口口相传,社交网络就可以大大提高你的应用的下载量和销量。病毒游戏通常通过直接整合脸书或推特让这个过程变得更加容易。让一款游戏像病毒一样传播是一种黑艺术,通常与在正确的时间出现在正确的地点比计划更有关系。

摘要

Android 是一个令人兴奋的野兽。您已经看到了它的构成,并对它的开发者生态系统有了一些了解。从开发的角度来看,它在软件和硬件方面为您提供了一个非常有趣的系统,鉴于免费提供的 SDK,进入的门槛非常低。这些设备本身对于手持设备来说非常强大,它们将使你能够向你的用户呈现视觉上丰富的游戏世界。使用传感器,如加速度计,让你创造新的用户互动的创新游戏的想法。完成游戏开发后,您可以在几分钟内将它们部署给数百万潜在的游戏玩家。听起来很刺激?是时候动手编写一些代码了!

二、Android SDK 的第一步

Android SDK 提供了一套工具,使您能够在短时间内创建应用。本章将指导你使用 SDK 工具构建一个简单的 Android 应用。这包括以下步骤:

  1. 设置开发环境。
  2. 在 Eclipse 中创建新项目并编写代码。
  3. 在模拟器或设备上运行应用。
  4. 调试和分析应用。

我们将通过研究有用的第三方工具来结束本章。让我们从设置开发环境开始。

设置开发环境

Android SDK 非常灵活,可以很好地与多种开发环境集成。纯粹主义者可能会选择使用命令行工具。不过,我们希望事情变得更舒适一点,所以我们将使用 IDE(集成开发环境)走更简单、更可视化的路线。

以下是您需要按照给定顺序下载并安装的软件列表:

  1. Java 开发工具包(JDK) ,版本 5 或 6。我们建议用 6。在撰写本文时,JDK 7 在 Android 开发方面存在问题。必须指示编译器为 Java 6 编译。
  2. Android 软件开发工具包(Android SDK)。
  3. Eclipse for Java Developers,3.4 版或更新版本。
  4. Eclipse 的 Android 开发工具(ADT)插件。

让我们来看一下正确设置所需的步骤。

注意由于网络是一个移动的目标,我们在这里不提供具体的下载网址。启动你最喜欢的搜索引擎,找到合适的地方找到上面列出的物品。

设置 JDK

下载适用于您的操作系统的指定版本之一的 JDK。在大多数系统中,JDK 都包含在一个安装程序或包中,所以不应该有任何障碍。一旦安装了 JDK,您应该添加一个名为 JDK_HOME 的新环境变量,指向 JDK 安装的根目录。此外,您应该将$JDK _ HOME/bin(% Windows 上的 JDK_HOME%\bin)目录添加到 PATH 环境变量中。

设置 Android SDK

Android SDK 也适用于三种主流桌面操作系统。为您的平台选择版本并下载。SDK 以 ZIP 或 tar gzip 文件的形式出现。解压到一个方便的文件夹就行了(比如 Windows 上的 c:\android-sdk 或者 Linux 上的/opt/android-sdk)。SDK 附带了几个命令行工具,位于 tools/文件夹中。创建一个名为 ANDROID_HOME 的环境变量,指向 SDK 安装的根目录,并将$ ANDROID _ HOME/tools(% ANDROID _ HOME % \ tools,在 Windows 上)添加到 PATH 环境变量中。这样,如果需要的话,您可以很容易地从 shell 中调用命令行工具。

注意对于 Windows,你也可以下载一个合适的安装程序,它会为你设置好一切。

在执行了前面的步骤之后,您将拥有一个由创建、编译和部署 Android 项目所需的基本命令行工具、SDK 管理器(一个用于安装 SDK 组件的工具)和 AVD 管理器(负责创建仿真器使用的虚拟设备)组成的基本安装。仅仅这些工具不足以开始开发,所以您需要安装额外的组件。这就是 SDK 管理器的用武之地。管理器是一个包管理器,很像 Linux 上的包管理工具。管理器允许您安装以下类型的组件:

  • Android 平台:对于每一个正式的 Android 版本,都有一个 SDK 平台组件,包括运行时库、仿真器使用的系统映像和任何特定于版本的工具。
  • SDK 附加组件:附加组件通常是不特定于平台的外部库和工具。一些例子是允许你在应用中集成谷歌地图的谷歌 API。
  • Windows 的 USB 驱动程序:这个驱动程序是在 Windows 的物理设备上运行和调试应用所必需的。在 Mac OS X 和 Linux 上,你不需要特殊的驱动程序。
  • 样本:对于每个平台,也有一组特定于平台的样本。这些是了解如何使用 Android 运行时库实现特定目标的很好的资源。
  • 文档:这是最新 Android 框架 API 文档的本地副本。

作为贪婪的开发人员,我们希望安装所有这些组件,以便拥有所有这些功能。因此,首先我们必须启动 SDK 管理器。在 Windows 上,SDK 的根目录下有一个名为 SDK manager.exe 的可执行文件。在 Linux 和 Mac OS X 上,您只需在 SDK 的工具目录中启动脚本 android。

在第一次启动时,SDK 管理器将连接到包服务器并获取可用包的列表。然后,管理器将向您显示如图 2-1 所示的对话框,允许您安装单独的软件包。只需单击选择旁边的新链接,然后单击安装按钮。您将看到一个对话框,要求您确认安装。选中全部接受复选框,然后再次单击安装按钮。接下来,给自己泡杯好茶或咖啡。管理器需要一段时间来安装所有的软件包。安装程序可能会要求您提供某些软件包的登录凭据。您可以安全地忽略这些,只需点击取消。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-1。第一次与 SDK 经理联系

您可以随时使用 SDK 管理器来更新组件或安装新组件。一旦安装过程完成,您就可以进入设置开发环境的下一步。

安装 Eclipse

Eclipse 有几种不同的风格。对于 Android 开发者,我们建议使用 Eclipse for Java Developers 版本 3.7.2,代码名为“Indigo”。与 Android SDK 类似,Eclipse 以 ZIP 或 tar gzip 包的形式出现。只需将其提取到您选择的文件夹中。一旦包被解压缩,您就可以在桌面上创建一个快捷方式,指向 eclipse 安装根目录下的 Eclipse 可执行文件。

第一次启动 Eclipse 时,会提示您指定一个工作区目录。图 2-2 显示了该对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-2。选择工作空间

工作区是 Eclipse 对包含一组项目的文件夹的概念。您是为所有项目使用单个工作区,还是将几个项目组合在一起的多个工作区,完全由您决定。本书附带的示例项目都组织在一个单独的工作空间中,您可以在该对话框中指定该工作空间。现在,我们将简单地在某个地方创建一个空的工作区。

然后 Eclipse 会用一个欢迎屏幕来欢迎您,您可以安全地忽略并关闭它。这将为您留下默认的 Eclipse Java 透视图。在后面的小节中,您将对 Eclipse 有一点了解。目前,让它运行就足够了。

安装 ADT Eclipse 插件

我们的设置难题的最后一部分是安装 ADT Eclipse 插件。Eclipse 基于一个插件架构,用于通过第三方插件来扩展其功能。ADT 插件 将 Android SDK 中的工具与 Eclipse 的强大功能结合在一起。有了这个组合,我们可以完全忘记调用所有的命令行 Android SDK 工具;ADT 插件将它们透明地集成到我们的 Eclipse 工作流中。

为 Eclipse 安装插件可以手动完成,通过将插件 ZIP 文件的内容放入 Eclipse 的 plug-ins 文件夹,或者通过与 Eclipse 集成的 Eclipse 插件管理器。这里我们将选择第二条路线:

  1. Go to Help 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Install New Software, which opens the installation dialog. In this dialog, you can choose the source from which to install a plug-in. First, you have to add the plug-in repository from the ADT plug-in that is fetched. Click the Add button. You will be presented with the dialog shown in Figure 2-3.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 2-3。添加存储库

  2. 在第一个文本字段中,输入存储库的名称;类似“ADT 存储库”的东西就可以了。第二个文本字段指定存储库的 URL。对于 ADT 插件,该字段应为https://dl-ssl.google.com/android/eclipse/。请注意,对于较新的版本,此 URL 可能会有所不同,因此请查看 ADT 插件网站以获取最新链接。

  3. 单击 OK,您将返回到安装对话框,现在应该会获取存储库中可用插件的列表。选中开发工具复选框,然后单击下一步按钮。

  4. Eclipse 会计算所有必要的依赖项,然后向您呈现一个新的对话框,其中列出了将要安装的所有插件和依赖项。单击“下一步”按钮进行确认。

  5. Another dialog pops up prompting you to accept the license for each plug-in to be installed. You should, of course, accept those licenses and then initiate the installation by clicking the Finish button.

    注意在安装过程中,会要求您确认未签名软件的安装。别担心,插件只是没有经过验证的签名。同意安装以继续该过程。

  6. Eclipse 会询问您是否应该重启以应用更改。您可以选择完全重启或不重启就应用更改。为了安全起见,选择 Restart Now,这将按预期重启 Eclipse。

Eclipse 重启后,您将看到和以前一样的 Eclipse 窗口。工具栏提供了几个 Android 特有的新按钮,允许您直接从 Eclipse 中启动 SDK 和 AVD 管理器,并创建新的 Android 项目。图 2-4 显示了新的工具栏按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-4。 ADT 工具栏按钮

左侧的前两个按钮允许您分别打开 SDK 管理器和 AVD 管理器。看起来像复选框的按钮让你运行 Android lint,它检查你的项目是否有潜在的 bug。下一个按钮是新的 Android 应用项目按钮,这是创建新的 Android 项目的快捷方式。最右边的两个按钮分别使您能够创建一个新的单元测试项目或 Android 清单文件(我们不会在本书中使用该功能)。

作为完成 ADT 插件安装的最后一步,您必须告诉插件 Android SDK 的位置:

  1. 打开窗口外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传偏好设置,并在出现的对话框的树形视图中选择 Android。
  2. 在右侧,单击浏览按钮选择 Android SDK 安装的根目录。
  3. 单击“确定”按钮关闭对话框。现在,您将能够创建您的第一个 Android 应用。

快速游览 Eclipse

Eclipse 是一个开源的 IDE,可以用来开发用各种语言编写的应用。通常,Eclipse 与 Java 开发结合使用。鉴于 Eclipse 的插件架构,已经创建了许多扩展,因此也有可能开发纯 C/C++、Scala 或 Python 项目。可能性是无穷无尽的;例如,甚至存在编写 LaTeX 项目的插件——这与您通常的代码开发任务略有相似。

Eclipse 的一个实例使用一个包含一个或多个项目的工作区。以前,我们在启动时定义了一个工作区。您创建的所有新项目都将存储在工作区目录中,以及定义使用工作区时 Eclipse 外观的配置。

Eclipse 的用户界面(UI) 围绕着两个概念:

  • 一个视图,一个单一的 UI 组件,比如源代码编辑器、输出控制台或者项目浏览器。
  • 一个透视图,一组特定的视图,您很可能需要它们来完成特定的开发任务,比如编辑和浏览源代码、调试、分析、与版本控制库同步等等。

Eclipse for Java Developers 附带了几个预定义的透视图。我们最感兴趣的是 Java 和 Debug。Java 透视图如图 2-5 所示。它的特点是左边是 Package Explorer 视图,中间是 Source Code 视图(它是空的,因为我们还没有打开一个源文件),右边是 Task List 视图,一个 Outline 视图,以及一个选项卡式视图,其中包含称为 Problems 视图、Javadoc 视图、Declaration 视图和 Console 视图的子视图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-5。 Eclipse 在行动 Java 的视角

通过拖放,您可以自由地重新安排透视图中任何视图的位置。您也可以调整视图的大小。此外,您可以在透视图中添加和删除视图。要添加视图,进入窗口外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传显示视图,从显示的列表中选择一个或选择其他以获得所有可用视图的列表。

要切换到另一个透视图,您可以转到窗口外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传打开透视图并选择您想要的那个。Eclipse 的左上角提供了一种在已经打开的透视图之间切换的更快方法。在那里,您将看到哪些透视图已经打开,哪些透视图是活动的。在图 2-5 中,注意 Java 透视图是打开的并且是活动的。这是目前唯一开放的视角。一旦您打开附加的透视图,它们也会显示在 UI 的那个部分。

图 2-5 中显示的 工具栏也只是视图。根据您当时所处的视角,工具栏也可能会发生变化。回想一下,安装 ADT 插件后,工具栏中出现了几个新按钮。这是插件的常见行为:一般来说,它们会添加新的视图和视角。对于 ADT 插件,除了标准的 Java Debug 透视图之外,我们现在还可以访问一个名为 DDMS 的透视图(Dalvik Debugging Monitor Server,它专用于调试和分析 Android 应用,这将在本章后面介绍)。ADT 插件还添加了几个新视图,包括 LogCat 视图,它显示来自任何连接的设备或仿真器的实时日志记录信息。

一旦您熟悉了透视图和视图概念,Eclipse 就不那么可怕了。在下面的小节中,我们将探索一些我们将用来编写 Android 游戏的视角和视图。我们不可能涵盖使用 Eclipse 开发的所有细节,因为它是如此庞大。因此,如果需要的话,我们建议您通过其广泛的帮助系统来学习更多关于 Eclipse 的知识。

有用的 Eclipse 快捷键

每个新的 IDE 都需要一些时间来学习和适应。在使用 Eclipse 多年后,我们发现以下快捷方式可以显著加快软件开发。这些快捷键使用 Windows 术语,因此 Mac OS X 用户应该在适当的地方替换命令和选项:

  • 将光标放在函数或字段上时,按 Ctr + Shift + G 组合键将在工作区中搜索对该函数或字段的所有引用。例如,如果你想知道某个函数在哪里被调用,只需点击鼠标将光标移动到该函数上,然后按 Ctrl + Shift + G。
  • 将光标放在调用 into 函数上的 F3 将跟随该调用,并将您带到声明和定义该函数的源代码。将此热键与 Ctrl + Shift + G 结合使用,可以轻松导航 Java 源代码。在类名或字段上做同样的事情将会打开它的声明。
  • Ctr +空格键自动完成您当前键入的函数或字段名称。输入几个字符后,开始键入并按快捷键。当有多种可能性时,会出现一个框。
  • Ctr + Z 无效。
  • Ctr + X 削减。
  • ctrl+c 副本。
  • ctrl+v 蛋糕。
  • Ctr + F11 运行应用。
  • F11 调试应用。
  • Ctr + Shift + O 组织当前源文件的 Java 导入。
  • Ctr + Shift + F 格式化当前源文件。
  • Ctr + Shift + T 跳转到任何 Java 类。
  • Ctr + Shift + R 跳转到任意资源文件;即图像、文本文件等等。
  • Alt + Shift + T 调出当前选择的重构菜单。
  • Ctrl + O 让您跳转到当前打开的 Java 类中的任何方法或字段。

Eclipse 中有许多更有用的特性,但是掌握这些基本的键盘快捷键可以显著加快游戏开发,让 Eclipse 中的生活稍微好一点。Eclipse 也是非常可配置的。这些键盘快捷键中的任何一个都可以在偏好设置中重新分配给不同的键。

在 Eclipse 中创建新项目并编写代码

有了我们的开发设置,我们现在可以在 Eclipse 中创建我们的第一个 Android 项目。ADT 插件安装了几个向导,使得创建新的 Android 项目变得非常容易。

创建项目

创建新的 Android 项目有两种方法。第一种是在包资源管理器视图中右键单击(见图 2-5 ,然后从弹出菜单中选择新建外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传项目。在新建对话框中,选择 Android 类别下的 Android 项目。如您所见,在该对话框中有许多其他项目创建选项。这是在 Eclipse 中创建任何类型的新项目的标准方法。在对话框中单击确定后,Android 项目向导将打开。

第二种方法要简单得多:只需点击新的 Android 应用项目工具栏按钮(如前面的图 2-4 所示),这也会打开向导。

一旦你进入 Android 项目向导对话框,你必须做出一些决定。请遵循以下步骤:

  1. 定义应用名称。这是 Android 上的启动器中显示的名称。我们会用“你好世界”

  2. 指定项目名称。这是您的项目在 Eclipse 中将被引用的名称。习惯上使用全部小写字母,所以我们将输入“helloworld”

  3. 指定包名。这是您所有 Java 代码所在的包的名称。向导试图根据您的项目名来猜测您的包名,但是您可以根据自己的需要随意修改它。在本例中,我们将使用“com.helloworld”。

  4. 指定生成 SDK。选择安卓 4.1。这允许我们使用最新的 API。

  5. Specify the minimum required SDK. This is the lowest Android version your application will support. We’ll choose Android 1.5 (Cupcake, API level 3).

    注意在第一章中,你看到 Android 的每个新版本都向 Android 框架 API 添加了新的类。构建 SDK 指定了您希望在应用中使用这个 API 的哪个版本。例如,如果您选择 Android 4.1 build SDK,您将获得最新、最棒的 API 特性。但是,这也有风险:如果您的应用运行在使用较低 API 版本的设备上(比如说,运行 Android 版本的设备),那么如果您访问仅在 4.1 版本中可用的 API 特性,您的应用就会崩溃。在这种情况下,您需要在运行时检测支持的 SDK 版本,并在您确定设备上的 Android 版本支持该版本时,仅访问 4.1 特性。这听起来可能很糟糕,但是正如你将在第五章中看到的,给定一个好的应用架构,你可以很容易地启用和禁用某些特定于版本的功能,而没有崩溃的风险。

  6. 单击下一步。您将看到一个对话框,让您定义您的应用的图标。我们将保持一切不变,因此只需单击“下一步”。

  7. 在下一个对话框中,询问您是否想要创建一个空白活动。接受此选择,然后单击“下一步”继续。

  8. 在最后一个对话框中,您可以修改向导将为您创建的空白活动的一些属性。我们将活动名称设置为“HelloWorldActivity”,标题设置为“Hello World”单击“完成”将创建您的第一个 Android 项目。

注意设置所需的最低 SDK 版本有一些含义。该应用只能在 Android 版本等于或高于您指定的最低 SDK 版本的设备上运行。当用户通过 Google Play 应用浏览 Google Play 时,只会显示具有适当最低 SDK 版本的应用。

探索项目

在包浏览器中,您现在应该看到一个名为“helloworld”的项目如果你展开它和它的所有子节点,你会看到类似于图 2-6 的东西。这是大多数 Android 项目的一般结构。让我们稍微探索一下。

  • src/包含了你所有的 Java 源文件。请注意,这个包与您在 Android 项目向导中指定的包同名。
  • gen/包含 Android 构建系统生成的 Java 源文件。您不应该修改它们,因为它们会自动重新生成。
  • Android 4.1 告诉我们,我们正在以 Android 4.1 版本为目标进行构建。这实际上是一个标准 JAR 文件形式的依赖项,它保存了 Android 4.1 API 的类。
  • Android Dependencies 向我们展示了我们的应用链接到的任何支持库,同样是以 JAR 文件的形式。作为游戏开发者,我们不关心这些。
  • assets/是存储应用所需文件的地方(例如配置文件、音频文件等)。这些文件与您的 Android 应用打包在一起。
  • bin/保存已编译的代码,准备部署到设备或仿真器。与 gen/文件夹一样,我们通常不关心这个文件夹中发生了什么。
  • libs/保存我们希望应用依赖的任何额外的 JAR 文件。如果我们的应用使用 C/C++ 代码,它还包含本机共享库。我们将在第十三章中探讨这个问题。
  • RES/hold 应用需要的资源,例如图标、国际化字符串和通过 XML 定义的 UI 布局。像素材一样,资源也与应用打包在一起。
  • AndroidManifest.xml 描述了您的应用。它定义了应用包含哪些活动和服务,应用运行的最低和目标 Android 版本(假设),以及它需要哪些权限(例如,访问 SD 卡或网络)。
  • project.properties 和 proguard-project.txt 保存构建系统的各种设置。我们不会触及这一点,因为 ADT 插件会在必要时负责修改这些文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-6。 Hello World 项目结构

我们可以很容易地在 Package Explorer 视图中添加新的源文件、文件夹和其他资源,方法是右键单击我们想要放置新资源的文件夹,并选择 new 和我们想要创建的相应资源类型。但是现在,我们会让一切保持原样。接下来,让我们看看如何修改我们的基本应用设置和配置,以便它能够兼容尽可能多的 Android 版本和设备。

使应用兼容所有 Android 版本

之前我们创建了一个项目,指定 Android 1.5 作为我们的最低 SDK。遗憾的是,ADT 插件有一个小错误,它忘记为 Android 1.5 上的应用创建包含图标图像的文件夹。下面是我们解决这个问题的方法:

  1. 在 res/目录下创建一个名为 drawable/的文件夹。您可以在 Package Explorer 视图中直接这样做,方法是右键单击 res/目录,并从上下文菜单中选择 New 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Folder。
  2. 将 ic_launcher.png 文件从 res/drawable-mdpi/文件夹复制到新的 assets/drawable/文件夹。Android 1.5 需要这个文件夹,而更高版本会根据屏幕大小和分辨率在其他文件夹中查找图标和其他应用资源。我们将在第四章中讨论这个问题。

有了这些改变,你的应用可以在目前所有的 Android 版本上运行!

编写应用代码

我们还没有写一行代码,所以让我们改变一下。Android 项目向导为我们创建了一个名为 HelloWorldActivity 的模板活动类,当我们在模拟器或设备上运行应用时,它将会显示出来。在 Package Explorer 视图中双击文件,打开类的源代码。我们将用清单 2-1 中的代码替换模板代码。

**清单 2-1。**HelloWorldActivity.java

package com.helloworld;import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;public class HelloWorldActivity extends Activityimplements View.OnClickListener {Button button;int touchCount;@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);button = new Button( this );button.setText( "Touch me!" );button.setOnClickListener( this );setContentView(button);}public void onClick(View v) {touchCount++;button.setText("Touched me " + touchCount + " time(s)");}
}

让我们剖析一下清单 2-1 ,这样你就能理解它在做什么。我们将把本质细节留给后面的章节。我们只想知道发生了什么。

源代码文件从标准的 Java 包声明和几个导入开始。大多数 android 框架类都位于 Android 包中。

package com.helloworld;import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

接下来,我们定义我们的 HelloWorldActivity,并让它扩展基类 Activity,这是由 Android 框架 API 提供的。活动很像传统桌面用户界面中的一个窗口,限制是活动总是充满整个屏幕(除了 Android 用户界面顶部的通知栏)。此外,我们让活动实现 OnClickListener 接口。如果您有使用其他 UI 工具包的经验,您可能会看到接下来会发生什么。一会儿会有更多的内容。

public class HelloWorldActivity extends Activityimplements View.OnClickListener {

我们让我们的活动有两个成员:一个按钮和一个计算按钮被触摸频率的 int。

    Button button;int touchCount;

每个 Activity 子类都必须实现抽象方法 Activity.onCreate(),当 Activity 第一次启动时,Android 系统会调用它一次。这取代了通常用来创建类实例的构造函数。必须调用基类 onCreate()方法作为方法体中的第一条语句。

    @Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);

接下来,我们创建一个按钮并设置它的初始文本。按钮是 Android 框架 API 提供的众多小部件之一。UI 小部件在 Android 上被称为视图。注意,button 是我们的 HelloWorldActivity 类的成员。我们以后需要参考它。

        button = new Button( this );button.setText( "Touch me!" );

onCreate()中的下一行设置按钮的 OnClickListener。OnClickListener 是一个回调接口,只有一个方法 OnClickListener.onClick(),单击按钮时会调用该方法。我们希望在点击时得到通知,所以我们让 HelloWorldActivity 实现该接口,并将其注册为按钮的 OnClickListener。

        button.setOnClickListener( this );

onCreate()方法中的最后一行将按钮设置为活动的内容视图。视图可以嵌套,活动的内容视图是这个层次结构的根。在我们的例子中,我们简单地将按钮设置为由活动显示的视图。为了简单起见,我们不会详细讨论在给定内容视图的情况下如何安排活动。

        setContentView(button);}

下一步只是实现 OnClickListener.onClick()方法,接口要求我们的活动使用该方法。每次单击按钮时都会调用此方法。在这个方法中,我们增加了 touchCount 计数器,并将按钮的文本设置为一个新的字符串。

    public void onClick(View v) {touchCount++;button.setText("Touched me" + touchCount + "times");}

因此,为了总结我们的 Hello World 应用,我们构造了一个带有按钮的活动。每次点击按钮时,我们相应地设置它的文本。这可能不是这个星球上最令人兴奋的应用,但它将用于进一步的演示目的。

注意,我们从来不需要手动编译任何东西。每当我们添加、修改或删除一个源文件或资源时,ADT 插件和 Eclipse 都会重新编译项目。这个编译过程的结果是一个 APK 文件,可以部署到仿真器或 Android 设备上。APK 文件位于项目的 bin/文件夹中。

在接下来的小节中,您将使用这个应用来学习如何在模拟器实例和设备上运行和调试 Android 应用。

在设备或仿真器上运行应用

一旦我们编写了应用代码的第一个迭代,我们希望运行并测试它来识别潜在的问题,或者只是对它的辉煌感到惊讶。我们有两种方法可以实现这一点:

  • 我们可以在通过 USB 连接到开发 PC 的真实设备上运行我们的应用。
  • 我们可以启动 SDK 中包含的模拟器,并在那里测试我们的应用。

在这两种情况下,我们都必须做一些设置工作,然后才能最终看到我们的应用在运行。

连接设备

在连接设备进行测试之前,我们必须确保操作系统能够识别它。在 Windows 上,这涉及到安装适当的驱动程序,这是我们之前安装的 SDK 的一部分。只需连接您的设备并遵循 Windows 的标准驱动程序安装项目,将过程指向 SDK 安装根目录中的驱动程序/文件夹。对于某些设备,您可能需要从制造商的网站上获取驱动程序。许多设备可以使用 SDK 附带的 Android ADB 驱动程序;但是,通常需要一个过程来将特定的设备硬件 ID 添加到 INF 文件中。在谷歌上快速搜索设备名称和“Windows ADB ”,通常会获得与该特定设备连接所需的信息。

在 Linux 和 Mac OS X 上,你通常不需要安装任何驱动程序,因为它们是操作系统自带的。根据您的 Linux 风格,您可能需要稍微调整一下您的 USB 设备发现,通常是为 udev 创建一个新的规则文件。这因设备而异。快速的网络搜索应该会为你的设备带来一个解决方案。

创建 Android 虚拟设备

SDK 附带了一个模拟器,可以运行 Android 虚拟设备(avd)。一个 Android 虚拟设备由一个特定 Android 版本的系统映像、一个皮肤和一组属性组成,包括屏幕分辨率、SD 卡大小等等。

要创建一个 AVD,你必须启动 Android 虚拟设备管理器。您可以按照之前在 SDK 安装步骤中描述的方式来完成这项工作,也可以通过单击工具栏中的 AVD Manager 按钮直接在 Eclipse 中完成这项工作。你可以使用现有的 avd。相反,让我们来看一下创建自定义 AVD 的步骤:

  1. Click the New button on the right side of the AVD Manager screen, which opens the Edit Android Virtual Device (AVD) dialog, shown in Figure 2-7.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 2-7。编辑 Android 虚拟设备(AVD)对话框

  2. 每个 AVD 都有一个名称,您可以通过它来引用它。你可以自由选择任何你想要的名字。

  3. 目标指定 AVD 应该使用的 Android 版本。对于我们简单的“hello world”项目,您可以选择一个 Android 4.0.3 目标。

  4. CPU/ABI 指定 AVD 应该模拟哪种 CPU 类型。在此选择手臂。

  5. 您可以通过皮肤设置中的选项指定 AVDsd 卡的大小以及屏幕大小。让这些字段保持原样。对于实际测试,您通常会希望创建多个 avd,覆盖您希望应用处理的所有 Android 版本和屏幕尺寸。

  6. 启用快照选项将在您关闭时保存模拟器的状态。下次启动时,模拟器将加载保存状态的快照,而不是引导。这可以在启动新的模拟器实例时节省一些时间。

  7. 硬件选项更先进。我们将在下一节中探究其中的一些。它们允许您修改仿真器设备和仿真器本身的低级属性,例如仿真器的图形输出是否应该进行硬件加速。

注意除非你有几十个不同 Android 版本和屏幕尺寸的不同设备,否则你应该使用仿真器对 Android 版本/屏幕尺寸组合进行额外测试。

安装高级仿真器功能

现在有一些硬件虚拟化实现支持 Android 模拟器,英特尔是其中之一。如果您有英特尔 CPU,您应该能够安装英特尔硬件加速执行管理器(HAXM) ,它与 x86 仿真器映像结合使用,将虚拟化您的 CPU,并且运行速度明显快于普通的完全仿真映像。与此同时,启用 GPU 加速将(理论上)提供一个合理的性能测试环境。我们对这些工具当前状态的经验是,它们仍然有一点缺陷,但事情看起来很有希望,所以请确保关注 Google 的官方声明。同时,让我们做好准备:

  1. 从英特尔下载并安装 HAXM 软件,可在http://software.intel.com/en-us/articles/intel-hardware-accelerated-execution-manager/获得。

  2. Once installed, you will need to make sure you have installed the specific AVD called Intel x86 Atom System Image. Open the SDK Manager, navigate to the Android 4.0.3 section, and check if the image is installed (see Figure 2-8). If it is not installed, check the entry, then click “Install packages . . .”

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 2-8。为 ICS 选择 x86 Atom 系统映像

  3. Create a specific AVD for the x86 image. Follow the steps to create a new AVD described in the last section, but this time make sure to select the Intel Atom (x86) CPU. In the Hardware section, add a new property called GPU emulation and set its value to yes, as shown in Figure 2-9.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 2-9。创建启用 GPU 仿真的 x86 AVD

现在您已经准备好了新的模拟器映像,我们需要让您了解一些注意事项。在测试时,我们得到了一些混合的结果。图 2-10 中的图像来自一款 2D 游戏,该游戏使用 OpenGL 1.1 多重纹理在角色上获得微妙的灯光效果。如果你仔细观察这幅图像,你会发现敌人的脸是横着的,上下颠倒。正确的渲染总是让它们正面朝上,所以这肯定是个错误。另一个更复杂的游戏崩溃了,无法运行。这并不是说硬件加速的 AVD 没有用,因为对于更基本的渲染来说,它可能工作得很好,如果你注意到右下角的数字 61,那基本上意味着它每秒运行 60 帧(FPS)——这是这台测试 PC 上 Android 模拟器上 GL 的新纪录!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-10。快速 OpenGL ES 1.1 仿真,但有一些渲染错误

图 2-11 中的图像显示了运行 OpenGL ES 2.0 的演示的主屏幕。虽然演示渲染正确,但帧速率开始一般,最后相当糟糕。这个菜单中并没有渲染太多东西,已经降低到 45FPS 了。主要的演示游戏运行速度为 15 到 30FPS,而且非常简单。很高兴看到 ES 2.0 运行,但显然还有一些改进的空间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-11。 OpenGL ES 2.0 工作正常,但帧速率较低

尽管我们在本节中概述了这些问题,但新的模拟器加速是 Android SDK 的一个受欢迎的补充,如果您选择不在设备上专门测试,我们建议您在游戏中试用它。有很多情况下它会工作得很好,你可能会发现你有更快的周转时间测试,这就是它的全部。

运行应用

现在您已经设置了您的设备和 avd,您终于可以运行 Hello World 应用了。在 Eclipse 中,您可以通过在 Package Explorer 视图中右键单击“hello world”项目,然后选择 Run As 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Android Application(或者您可以单击工具栏上的 Run 按钮)来轻松实现这一点。然后,Eclipse 将在后台执行以下步骤:

  1. 如果自上次编译以来有任何文件发生了更改,则将项目编译为 APK 文件。
  2. 为 Android 项目创建一个新的运行配置(如果尚不存在的话)。(我们稍后将了解运行配置。)
  3. 通过使用合适的 Android 版本启动或重用已经运行的仿真器实例,或者通过在连接的设备上部署和运行应用来安装和运行应用(该设备还必须至少运行您在创建项目时指定为最低必需 SDK 级别的最低 Android 版本)。

注意第一次在 Eclipse 中运行 Android 应用时,会询问您是否希望 ADT 对设备/仿真器输出中的消息做出反应。因为您总是需要所有的信息,所以只需单击 OK。

如果您没有连接设备,ADT 插件将启动您在 AVD 管理器窗口中看到的一个 AVD。输出应该看起来像图 2-12 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-12。Hello World 应用正在运行

模拟器的工作方式几乎与真实设备完全一样,您可以通过鼠标与它进行交互,就像用手指在设备上操作一样。以下是真实设备和模拟器之间的一些差异:

  • 模拟器仅支持单点触摸输入。简单地使用你的鼠标光标,假装它是你的手指。
  • 模拟器缺少一些应用,例如 Google Play 应用。
  • 要更改设备在屏幕上的方向,请不要倾斜显示器。相反,使用数字键盘上的 7 键来更改方向。您必须先按下数字键盘上方的 Num Lock 键来禁用其数字功能。
  • 模拟器非常慢。不要通过在模拟器上运行来评估应用的性能。
  • 4.0.3 之前的模拟器版本仅支持 OpenGL ES 1.x. OpenGL ES 2.0 及更高版本的模拟器支持 OpenGL ES 2.0。我们将在第七章中讨论 OpenGL ES。模拟器将很好地为我们的基本测试工作。一旦我们深入到 OpenGL,你会想要得到一个真实的设备来测试,因为即使我们使用了最新的模拟器,OpenGL 实现(虚拟化和软件化)仍然有一点缺陷。现在,请记住不要在模拟器上测试任何 OpenGL ES 应用。

玩一会儿,感觉舒服点。

注意启动一个新的仿真器实例需要相当长的时间(根据您的硬件,最长可达 10 分钟)。您可以让模拟器在整个开发会话期间一直运行,这样您就不必重复重新启动它,或者您可以在创建或编辑 AVD 时检查 Snapshot 选项,这将允许您保存和恢复虚拟机(VM)的快照,从而实现快速启动。

有时,当我们运行 Android 应用时,ADT 插件执行的自动仿真器/设备选择是一个障碍。例如,我们可能连接了多个设备/仿真器,我们希望在一个特定的设备/仿真器上测试我们的应用。为了解决这个问题,我们可以在 Android 项目的运行配置中关闭自动设备/仿真器选择。那么,什么是运行配置呢?

当您告诉 Eclipse 运行应用时,运行配置提供了一种方式来告诉 Eclipse 应该如何启动您的应用。运行配置通常允许您指定传递给应用的命令行参数、VM 参数(在 Java SE 桌面应用的情况下)等等。Eclipse 和第三方插件为特定类型的项目提供了不同的运行配置。ADT 插件将 Android 应用运行配置添加到可用运行配置集中。当我们在本章前面第一次运行我们的应用时,Eclipse 和 ADT 在后台用默认参数为我们创建了一个新的 Android 应用运行配置。

要获得 Android 项目的运行配置,请执行以下操作:

  1. 在 Package Explorer 视图中右键单击项目,并选择 Run As外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传Run configuration。
  2. 从左侧的列表中,选择“hello world”项目。
  3. 在对话框的右侧,您现在可以修改运行配置的名称,并更改 Android、Target 和 Commons 选项卡上的其他设置。
  4. 要将自动部署更改为手动部署,请单击目标选项卡并选择手动。

当您再次运行应用时,系统会提示您选择一个兼容的仿真器或设备来运行应用。图 2-13 显示了该对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-13。选择运行应用的仿真器/设备

该对话框显示所有正在运行的仿真器和当前连接的设备,以及所有其他当前未运行的 avd。您可以选择任何模拟器或设备来运行您的应用。注意连接设备旁边的红色 × 。这通常表明应用不能在这个设备上运行,因为它的版本低于我们指定的目标 SDK 版本(在本例中是 14 对 15)。然而,因为我们指定了最低 SDK 版本 3 (Android 1.5),所以我们的应用实际上也可以在这个设备上工作。

调试和分析应用

有时,您的应用会以意想不到的方式运行或崩溃。为了找出到底哪里出错了,您希望能够调试您的应用。

Eclipse 和 ADT 为我们提供了极其强大的 Android 应用调试工具。我们可以在源代码中设置断点,检查变量和当前堆栈跟踪,等等。

通常,在调试之前设置断点,以检查程序中某些点的程序状态。要设置断点,只需在 Eclipse 中打开源文件,双击要设置断点的行前面的灰色区域。出于演示目的,对 HelloWorldActivity 类中的第 23 行执行此操作。这将使调试器在您每次单击该按钮时停止。双击该行后,源代码视图会在该行前显示一个小圆圈,如图 2-14 所示。您可以通过在源代码视图中再次双击断点来移除断点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-14。设置断点

如前一节所述,开始调试非常类似于运行应用。在 Package Explorer 视图中右键单击项目,并选择 Debug As 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Android Application。这将为您的项目创建一个新的调试配置,就像简单地运行应用一样。您可以通过从上下文菜单中选择 Debug As 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Debug Configurations 来更改该调试配置的默认设置。

注意您可以使用 Run 菜单来运行和调试应用并访问配置,而不是在 Package Explorer 视图中浏览项目的上下文菜单。

如果您开始第一个调试会话,并且命中了一个断点(例如,您在我们的应用中点击按钮),Eclipse 会询问您是否想要切换到调试透视图,您可以确认这一点。先来看看那个视角。图 2-15 显示了我们开始调试 Hello World 应用后的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-15。调试视角

如果您还记得我们对 Eclipse 的快速浏览,那么您会知道有几个不同的透视图,它们由一组特定任务的视图组成。调试透视图与 Java 透视图看起来非常不同。

  • 左上角的 Debug 视图显示了所有当前正在运行的应用,以及它们所有线程的堆栈跟踪(如果应用在调试模式下运行并被挂起)。
  • 调试视图下面是源代码视图,它也出现在 Java 透视图中。
  • 控制台视图也出现在 Java 透视图中,它打印出来自 ADT 插件的消息,告诉我们它正在做什么。
  • 任务列表视图(控制台视图旁边带有标签“Tasks”的选项卡与 Java 透视图中的相同。我们通常不需要它,你可以关闭它。
  • LogCat view 将是您旅途中最好的朋友之一。这个视图显示了运行应用的仿真器/设备的日志输出。日志输出来自系统组件、其他应用和您自己的应用。LogCat 视图将在应用崩溃时向您显示堆栈跟踪,并允许您在运行时输出自己的日志消息。在下一节中,我们将进一步了解 LogCat。
  • Outline 视图也出现在 Java 透视图中,但在 Debug 透视图中不是很有用。您通常会关心断点和变量,以及在调试时程序被挂起的当前行。我们经常从 Debug 透视图中删除 Outline 视图,以便为其他视图留出更多空间。
  • Variables 视图对于调试特别有用。当调试器遇到断点时,您将能够在程序的当前范围内检查和修改变量。
  • 断点视图显示了到目前为止您已经设置的断点列表。

如果您很好奇,您可能已经在运行的应用中单击了按钮,以查看调试器的反应。它将在第 23 行停止,正如我们在那里设置断点所指示的那样。您还会注意到,Variables 视图现在显示了当前范围内的变量,包括活动本身(this)和方法的参数(v)。您可以通过展开这些变量来进一步深入研究它们。

Debug 视图向您显示当前堆栈的堆栈跟踪,一直到您当前所在的方法。请注意,您可能有多个线程正在运行,并且可以在 Debug 视图中随时暂停它们。

最后,请注意,我们设置断点的那一行被突出显示,表明程序当前在代码中暂停的位置。

您可以指示调试器执行当前语句(通过按 F6),单步执行当前方法中调用的任何方法(通过按 F5),或者继续正常执行程序(通过按 F8)。或者,您可以使用“运行”菜单上的项目来实现同样的目的。此外,请注意,除了我们刚刚提到的那些,还有更多步进选项。和所有事情一样,我们建议你尝试一下,看看什么对你有用,什么没用。

注意好奇心是成功开发 Android 游戏的基石。您必须熟悉您的开发环境,才能充分利用它。这种范围的书不可能解释 Eclipse 的所有本质细节,所以我们敦促您进行实验。

洛卡特和 DDMS

ADT Eclipse 插件安装了许多将在 Eclipse 中使用的新视图和透视图。最有用的视图之一是 LogCat 视图,我们在上一节中简要地提到了它。

LogCat 是 Android 事件记录系统,它允许系统组件和应用输出关于各种记录级别的记录信息。每个日志条目都由时间戳、日志记录级别、日志来自的进程 ID、由日志记录应用本身定义的标记以及实际的日志记录消息组成。

LogCat 视图从连接的仿真器或设备收集并显示这些信息。图 2-16 显示了 LogCat 视图的一些示例输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-16。log cat 视图

请注意,在 LogCat 视图的左上角和右上角有许多按钮:

  • 加号和减号按钮允许您添加和删除过滤器。已经有一个过滤器只显示来自我们应用的日志消息。
  • 减号按钮右侧的按钮允许您编辑现有的过滤器。
  • 下拉列表框允许您选择消息必须在下面的窗口中显示的日志级别。
  • 下拉列表框右侧的按钮允许您(按从左到右的顺序)保存当前日志输出、清除日志控制台、切换左侧过滤器窗口的可见性以及停止更新控制台窗口。

如果当前连接了几个设备和模拟器,那么 LogCat 视图将只输出其中一个的日志数据。为了获得更细粒度的控制和更多的检查选项,您可以切换到 DDMS 透视图。

DDMS (Dalvik 调试监控服务器)提供了大量关于所有连接设备上运行的进程和 Dalvik 虚拟机的深入信息。您可以随时通过窗口外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传打开视角外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传其他外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 DDMS 切换到 DDMS 视角。图 2-17 显示了 DDMS 视角通常的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-17。 DDMS 在行动

和往常一样,几种特定的观点适合我们手头的任务。在这种情况下,我们希望收集关于所有进程、它们的虚拟机和线程、堆的当前状态、关于特定连接设备的 LogCat 信息等信息。

  • Devices 视图显示所有当前连接的仿真器和设备,以及在其上运行的所有进程。通过该视图的工具栏按钮,您可以执行各种操作,包括调试选定的进程、记录堆和线程信息以及截图。

  • LogCat 视图与 Debug 透视图中的相同,不同之处在于它将显示 Devices 视图中当前所选设备的输出。

  • 模拟器控件视图允许您改变正在运行的模拟器实例的行为。例如,您可以强制模拟器伪造 GPS 坐标进行测试。

  • 图 2-17 中的所示的线程视图显示了在设备视图中当前选择的进程上运行的线程的信息。仅当您还启用了线程跟踪时,线程视图才会显示此信息,这可以通过单击设备视图中左起第五个按钮来实现。

  • 堆视图提供了设备上堆的状态信息。与线程信息一样,您必须通过单击左边第二个按钮,在 Devices 视图中显式启用堆跟踪。

  • 分配跟踪器视图显示了哪些类在最近几分钟内被分配得最多。这个视图提供了一个寻找内存泄漏的好方法。

  • 网络状态视图允许您跟踪通过连接的 Android 设备或模拟器的网络连接发送的传入和传出字节数。

  • 文件浏览器视图允许您修改连接的 Android 设备或仿真器实例上的文件。您可以像使用标准操作系统文件资源管理器一样,将文件拖放到该视图中。

DDMS 实际上是一个通过 ADT 插件与 Eclipse 集成的独立工具。你也可以从$ANDROID _ HOME/tools 目录*(Windows 上的* %ANDROID_HOME%/tools)作为独立应用启动 DDMS。DDMS 并不直接连接设备,而是使用 Android Debug Bridge (ADB),这是 SDK 中包含的另一个工具。让我们来看看 ADB,以完善您对 Android 开发环境的了解。

使用 ADB

ADB 允许您管理连接的设备和仿真器实例。它实际上由三部分组成:

  • 运行在开发机器上的客户机,您可以通过发出 adb 命令从命令行启动它(如果您按照前面的描述设置了环境变量,它应该可以工作)。当我们谈到 ADB 时,我们指的是这个命令行程序。
  • 也在您的开发机器上运行的服务器。服务器作为后台服务安装,它负责 ADB 程序实例和任何连接的设备或仿真器实例之间的通信。
  • ADB 守护进程,它也作为后台进程在每个仿真器和设备上运行。ADB 服务器连接到这个守护进程进行通信。

通常,我们通过 DDMS 透明地使用 ADB,而忽略它作为命令行工具的存在。有时,ADB 可以在小任务中派上用场,所以让我们快速浏览一下它的一些功能。

查看 Android 开发者网站上的 ADB 文档,获取可用命令的完整参考列表。

使用 ADB 执行的一个非常有用的任务是查询所有连接到 ADB 服务器的设备和仿真器(以及您的开发机器)。为此,请在命令行上执行以下命令(注意>不是该命令的一部分):

> adb devices

这将打印出所有连接的设备和仿真器的列表,以及它们各自的序列号,类似于以下输出:

List of devices attached
HT97JL901589    device
HT019P803783    device

设备或仿真器的序列号用于指定后续命令。以下命令将在序列号为 HT019P803783 的设备上安装位于开发机器上的名为 myapp.apk 的 APK 文件:

> adb –s HT019P803783 install myapp.apk

–s 参数可以与任何执行针对特定设备的操作的 ADB 命令一起使用。

还存在将文件复制到设备或仿真器以及从设备或仿真器复制文件的命令。以下命令将名为 myfile.txt 的本地文件复制到序列号为 HT019P803783 的设备的 SD 卡上:

> adb –s HT019P803783 push myfile.txt  /sdcard/myfile.txt

要从 SD 卡中提取名为 myfile.txt 的文件,您可以发出以下命令:

> abd pull /sdcard/myfile.txt myfile.txt

如果当前只有一个设备或仿真器连接到 ADB 服务器,您可以省略序列号。adb 工具将自动为您定位连接的设备或仿真器。

也可以通过网络(没有 USB)使用 ADB 调试设备。这被称为 ADB 远程调试,在某些设备上是可能的。要检查您的设备是否可以做到这一点,找到开发者选项,看看“ADB over network”是否在选项列表中。如果是这样,你很幸运。只需在您的设备上启用这个远程调试选项,然后运行以下命令:

> adb connect ipaddress

连接后,设备将显示为通过 USB 连接。如果不知道 IP 地址,通常可以通过触摸当前接入点名称在 Wi-Fi 设置中找到。

当然,ADB 工具提供了更多的可能性。大多数是通过 DDMS 暴露的,我们通常使用它而不是命令行。但是,对于快速任务,命令行工具是理想的。

有用的第三方工具

Android SDK 和 ADT 可能提供了大量的功能,但是还有许多非常有用的第三方工具,下面列出了其中的一些,它们可以在以后的开发中帮助您。这些工具可以监视 CPU 的使用情况,告诉您 OpenGL 渲染的情况,帮助您找到内存或文件访问中的瓶颈,等等。您需要将设备中的芯片与芯片制造商提供的工具相匹配。以下列表包括制造商和 URL,以帮助您进行匹配。排名不分先后:

  • Adreno Profiler :用于高通/骁龙设备(主要是 HTC,但也有很多其他);https://developer.qualcomm.com/mobile-development/mobile-technologies/gaming-graphics-optimization-adreno/tools-and-resources
  • PVRTune/PVRTrace :用在 PowerVR 芯片上(三星,LG,等);http://www.imgtec.com/powervr/insider/powervr-utilities.asp
  • NVidia PerfHUD ES :用在 Tegra 芯片上(LG、三星、摩托罗拉等);http://developer.nvidia.com/mobile/perfhud-es

我们不会详细讨论安装或使用这些工具的细节,但是当你准备认真对待你的游戏性能时,请务必回到这一部分并深入研究。

摘要

Android 开发环境有时可能有点吓人。幸运的是,您只需要可用选项的一个子集就可以开始了,本章末尾的“使用 ADB”一节应该已经为您提供了足够的信息来开始一些基本的编码。

从这一章中学到的最重要的一课是如何将这些部分组合在一起。JDK 和 Android SDK 为所有 Android 开发提供了基础。它们提供了在仿真器实例和设备上编译、部署和运行应用的工具。为了加快开发速度,我们将 Eclipse 与 ADT 插件结合使用,该插件完成了我们原本必须使用 JDK 和 SDK 工具在命令行上完成的所有繁重工作。Eclipse 本身建立在几个核心概念之上:工作区,它管理项目;视图,提供特定的功能,如源代码编辑或 LogCat 输出;透视图,它将特定任务(如调试)的视图联系在一起;以及运行和调试配置,这些配置允许您指定运行或调试应用时使用的启动设置。

掌握这一切的秘诀是实践,尽管这听起来很枯燥。在整本书中,我们将实现几个项目,这些项目会让你对 Android 开发环境更加熟悉。然而,在一天结束的时候,这取决于你是否能更进一步。

有了这些信息,你就可以继续你最初阅读这本书的原因:开发游戏。

三、游戏开发 101

游戏开发很难——不是因为它是火箭科学,而是因为在你真正开始编写你梦想的游戏之前,有大量的信息需要消化。在编程方面,您必须担心诸如文件输入/输出(I/O)、用户输入处理、音频和图形编程以及网络代码之类的日常事务。而这些只是基础!最重要的是,你会想要建立你真正的游戏机制。代码也需要结构,如何创建游戏的架构并不总是显而易见的。你实际上必须决定如何让你的游戏世界移动。你能不使用物理引擎,而使用你自己的简单模拟代码吗?你的游戏世界设定的单位和尺度是什么?如何翻译到屏幕上?

实际上还有另一个许多初学者忽略的问题,那就是,在你开始动手之前,你实际上必须首先设计你的游戏。数不清的项目从未公开,并陷入技术演示阶段,因为对游戏实际上应该如何运行从来没有任何清晰的想法。我们不是在谈论你的普通第一人称射击游戏的基本游戏机制。这是最简单的部分:WASD 移动键加鼠标,你就完成了。你应该问自己这样的问题:有闪屏吗?它过渡到什么?主菜单屏幕上有什么?实际游戏画面上有哪些平视显示元素?如果我按下暂停按钮会发生什么?设置屏幕上应该提供什么选项?我的 UI 设计在不同的屏幕尺寸和长宽比下会怎样?

有趣的是没有灵丹妙药;没有处理所有这些问题的标准方法。我们不会假装给你开发游戏的终极解决方案。相反,我们将尝试说明我们通常是如何设计游戏的。你可以决定完全适应它或者修改它以更好地满足你的需要。没有规则——对你有效的就行。然而,你应该总是努力寻找一个简单的解决方案,无论是在代码上还是在纸上。

流派:适合每个人的口味

在你的项目开始时,你通常决定你的游戏将属于哪种类型。除非你想出一些全新的、前所未见的东西,否则你的游戏创意很有可能会符合当前流行的广泛类型之一。大多数流派都建立了游戏机制标准(例如,控制方案、特定目标等等)。偏离这些标准可以让游戏大受欢迎,因为游戏玩家总是渴望新的东西。不过,这也是一个很大的风险,所以要仔细考虑你的新平台玩家/第一人称射击游戏/即时战略游戏是否真的有观众。

让我们来看看 Google Play 上更受欢迎的流派的一些例子。

休闲游戏

可能 Google Play 上最大的游戏部分是所谓的休闲游戏。那么到底什么是休闲游戏呢?这个问题没有具体的答案,但是休闲游戏有一些共同的特点。通常,它们具有很好的可访问性,因此即使非游戏玩家也可以很容易地使用它们,这极大地增加了潜在玩家的数量。一场游戏最多只需要几分钟。然而,休闲游戏的简单性容易让人上瘾,经常让玩家沉迷几个小时。实际的游戏机制从极其简单的益智游戏到一键平台游戏,再到像把纸团扔进篮子这样简单的事情。由于休闲风格的模糊定义,这种可能性是无穷无尽的。

神庙逃亡(见图 3-1 ),由伊玛吉工作室制作,是完美的休闲游戏范例。你引导一个人物通过充满障碍的多条轨迹。整个输入方案是基于滑动的。如果你向左或向右滑动,角色会向那个方向转弯(假设前面有一个十字路口)。如果你向上滑动,角色会跳跃,而向下滑动会使角色滑到障碍物下面。一路上你可以获得各种奖励和动力。易于理解的控制、明确的目标和漂亮的 3D 图形使这款游戏在苹果应用商店和谷歌 Play 上一炮而红。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-1。伊曼吉工作室的《神庙逃亡》

宝石矿工:挖得更深(见图 3-2 ),由一人军 Psym 机动,是完全不同的动物。这是同一家公司大获成功的宝石矿工 的续集。它只是稍微迭代了一下原文。你扮演一名矿工,试图在随机产生的矿中找到有价值的矿石、金属和宝石。这些宝藏可以用来交换更好的设备,以挖掘更深的地方,找到更有价值的宝藏。它利用了一个事实,即许多人喜欢研磨的概念:没有太多的努力,你就不断地得到新的噱头,让你玩下去。这个游戏的另一个有趣的方面是地雷是随机产生的。这极大地增加了游戏的重玩价值,而没有增加额外的游戏机制。为了增加趣味,游戏提供了具有具体目标的挑战关卡,完成后你可以获得奖牌。这是一个非常轻量级的成就系统。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-2。宝石矿工:深入挖掘,Psym Mobile

这个游戏更有趣的一面是它的赚钱方式。尽管目前的趋势是“免费增值”游戏(游戏本身是免费的,而额外的内容可以以经常是荒谬的价格购买),它使用的是“老派”付费模式。大约 2 美元一张,超过 100,000 次下载,这对于一个非常简单的游戏来说是相当大的一笔钱。这种销售数字在 Android 上很少见,尤其是 Psym Mobile 基本上没有为游戏做任何广告。前作的成功及其庞大的玩家基础很大程度上保证了续集的成功。

休闲游戏类别的所有可能的子类别的列表将会占据本书的大部分。在这个流派中可以找到许多更具创新性的游戏概念,值得在市场上查看各自的类别以获得一些灵感。

益智游戏

益智游戏无需介绍。我们都知道一些很棒的游戏,比如俄罗斯方块和 ?? 宝石迷阵。他们是安卓游戏市场的重要组成部分,在所有人群中都很受欢迎。与基于 PC 的益智游戏(通常只涉及将三个颜色或形状的物体放在一起)相比,Android 上的许多益智游戏偏离了经典的 match-3 公式,使用了更复杂的基于物理的谜题。

切绳子(见图 3-3 ),作者 ZeptoLab,是一个物理学难题的极好例子。游戏的目标是给每个屏幕上的小生物喂糖果。这块糖必须通过切断它所系的绳子,将它放入气泡中以便它可以向上漂浮,绕过障碍物等等来引导它。每个游戏对象在某种程度上都是物理模拟的。这款游戏由 2D 物理引擎 Box2D 驱动。割绳子在 iOS 应用商店和 Google Play 上都获得了瞬间的成功,甚至已经被移植到浏览器中运行!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-3。割断绳子,由 ZeptoLab

器械(见图 3-4 ),由 Bithack(另一个一人公司)制作,深受老牌 Amiga 和 PC 经典不可思议机器的影响。像切断绳子,这是一个物理难题,但它给了玩家更多的控制她解决每个难题的方式。各种积木,如可以钉在一起的简单木头、绳子、马达等等,可以以创造性的方式组合起来,将蓝色的球从关卡的一端带到目标区域。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-4。仪器,由 Bithack

除了有预制关卡的战役模式,还有一个沙盒环境,在那里你可以发挥你的创造力。更好的是,你的定制装置可以很容易地与他人分享。设备的这个方面保证了即使玩家已经完成了游戏,仍然有大量的额外内容需要探索。

当然,你也可以在市场上找到各种各样的俄罗斯方块克隆版,match-3 游戏,以及其他标准公式。

动作和街机游戏

动作和街机游戏通常会释放 Android 平台的全部潜力。其中许多都具有令人惊叹的 3D 视觉效果,展示了在当前这一代硬件上的可能性。这种类型有许多子类别,包括赛车游戏、射击游戏、第一和第三人称射击游戏以及平台游戏。在过去的几年里,随着大型游戏工作室开始将其游戏移植到 Android 上,Android 市场的这一部分已经获得了很大的吸引力。

SHADOWGUN (见图 3-5 ),由 MADFINGER Games 出品,是一款视觉效果惊人的第三人称射击游戏,展示了最近的 Android 手机和平板电脑的计算能力。与许多 AAA 游戏一样,它在 Android 和 iOS 上都可以使用。 SHADOWGUN 利用跨平台游戏引擎 Unity,是 Unity 在移动设备上的力量的典型代表之一。游戏性方面,它是一个双模拟棍射击游戏,甚至允许躲在板条箱和其他通常在手机动作游戏中找不到的漂亮机械装置后面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-5。 SHADOWGUN,由 MADFINGER 游戏

虽然很难获得确切的数字,但 Android 市场的统计数据似乎表明, SHADOWGUN 的下载量与之前讨论过的宝石矿大致相当。这表明,创造一款成功的 Android 游戏并不一定需要一个庞大的 AAA 团队。

坦克英雄:激光战争(见图 3-6 ) 是坦克英雄的续集,由一个名为 Clapfoot Inc .的非常小的独立团队创作。你指挥一辆坦克,你可以装备越来越多疯狂的附件,如射线枪、声波炮等等。关卡非常小,限制在平坦的战场上,周围散布着互动元素,你可以利用这些元素来消灭游戏场上所有其他的敌方坦克。通过简单地触摸敌人或游戏场地来控制坦克,作为回应,它将采取适当的行动(分别是射击或移动)。虽然它在视觉上还没有达到 SHADWOGUN 的水平,但它仍然拥有相当好看的动态照明系统。这里要吸取的教训是,即使是小团队,如果他们对内容加以约束,比如限制比赛场地的大小,也可以创造出视觉上令人愉悦的体验。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-6。《坦克英雄:激光战争》,克拉普富特公司出品。

龙,飞吧!(见图 3-7 ),by Four pixel,改编自 Andreas Illiger 的极其成功的游戏 Tiny Wings ,在撰写本文时该游戏仅在 iOS 上可用。你控制一条小龙在几乎无限多的斜坡上上下下,同时收集各种宝石。如果加速足够快,小龙可以起飞和飞行。这是通过在下坡时触摸屏幕来实现的。机制非常简单,但随机生成的世界和对更高分数的渴望使人们回来寻求更多。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-7。龙,飞!,四个像素

龙,飞吧!很好地说明了一个现象:通常,特定的手机游戏流派会出现在 iOS 上。即使有巨大的需求,原创者也不经常把他们的游戏移植到 Android 上。其他游戏开发商可以介入,为 Android 市场提供替代版本。这也可能完全适得其反,如果“灵感”游戏太过抄袭,就像 Zynga 对一款名为小塔的游戏所做的那样。推广一个创意通常会受到好评,而公然抄袭另一个游戏通常会遭到恶语相向。

【马克思·佩恩】(见图 3-8)由 Rockstar Games 出品,是一款 2001 年出版的老牌 PC 游戏的移植。我们把它放在这里是为了说明一个不断增长的趋势,即 AAA 出版商把他们的旧知识产权移植到移动环境中。《马克思·佩恩》讲述了一名警察的家庭被贩毒集团谋杀的故事。马克斯暴跳如雷,为妻子和孩子报仇。所有这一切都嵌入了黑色电影风格的叙事中,通过连环漫画和短片场景来展示。最初的游戏严重依赖于我们在 PC 上玩射击游戏时习惯使用的标准鼠标/键盘组合。Rockstar Games 成功创造了基于触摸屏的控件。虽然控制不如 PC 上的精确,但它们仍然足以让游戏在触摸屏上变得令人愉快。

*外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-8。马克思·佩恩,由摇滚明星游戏公司出品

动作和街机类型在市场上仍然有点代表性不足。玩家渴望好的动作游戏,所以那可能是你的专长!

塔防游戏

鉴于他们在 Android 平台上的巨大成功,我们觉得有必要将塔防游戏作为他们自己的类型来讨论。塔防游戏作为由 modding 社区开发的 PC 即时战略游戏的变体变得流行起来。这个概念很快就被翻译成了单机游戏。塔防游戏目前是 Android 上最畅销的游戏类型。

在一个典型的塔防游戏中,一些主要是邪恶的力量在所谓的波浪中派出生物来攻击你的城堡/基地/水晶/你能想到的。你的任务是通过放置射击来袭敌人的防御炮塔来保卫游戏地图上的那个特殊地方。对于每一个你杀死的敌人,你通常会得到一些钱或点数,你可以投资在新的炮塔或升级上。这个概念非常简单,但是要在这种类型的游戏中找到平衡是非常困难的。

DroidHen 的是 Google Play 上最受欢迎的免费游戏之一,但它使用了 flash 游戏玩家所熟知的简单的塔防旋转。你有一个玩家控制的塔,而不是建造多个塔,它可以接受许多升级,从攻击力增加到分裂箭。除了主要武器,还有不同的科技法术树可以用来消灭入侵的敌军。这个游戏的好处在于它很简单,容易理解,而且很精致。图形都很干净,主题也很好,DroidHen 得到了恰到好处的平衡,这往往会让你玩得比你计划的时间长得多。这款游戏在赚钱方面很聪明,因为你可以获得许多免费升级,但对于没有耐心的人来说,你总是可以用真钱提前购买一些东西,并获得即时满足。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-9。防御者,由 DroidHen

防御者只有一个等级,但是它把坏人混在一起进行一波又一波的攻击。就好像你没有注意到它只有一个等级,因为它看起来很漂亮,会让你把更多的注意力放在坏人、你的武器和你的法术上。总的来说,这应该是一个小开发团队在合理的时间内开发出的游戏类型的好灵感,一个休闲玩家会喜欢的游戏。

社交游戏

你不会以为我们会跳过社交游戏吧?如果有什么不同的话,“社交”这个词是我们现代技术集体中最大的热门话题(也是最大的赚钱机器之一)。什么是社交游戏?在游戏中,你可以与朋友和熟人分享经验,通常以病毒式反馈循环的方式相互交流。这是惊人的强大,如果做得好,它可以滚雪球般变成雪崩式的成功。

Zynga 的 Words with Friends (见图 3-10 )将回合制游戏添加到已经建立的基于磁贴的单词创建类型中。《??》与朋友的对话的真正创新之处在于整合了聊天和多个同时进行的游戏。你可以同时玩很多游戏,这样就不用等待一个游戏了。一篇著名的评论(由约翰·梅耶撰写)称,“‘和朋友聊天’应用是新的 Twitter。”这很好地概括了 Zynga 如何很好地利用社交空间,并将其与一款非常容易上手的游戏相结合。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-10。Zynga 的《与朋友的话》

画东西(见图 3-11 ),由 OMGPOP 出品,是一款让玩家一笔一划猜测某人在画什么的游戏。这不仅很有趣,而且其他玩家也将自己的作品提交给朋友,这是众包内容的神奇之处。 Draw Something 乍一看像是一个基本的手指绘画应用,但仅仅几分钟后,游戏的精髓就真正显现出来了,因为你想立即提交你的猜测,猜测另一个,然后画出你自己的,并让你的朋友一起分享乐趣。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-11。画点东西,由 OMGPOP

超越流派

许多新游戏、创意、流派和应用一开始看起来并不是游戏,但它们确实是。因此,当进入 Google Play 时,很难真正明确指出现在有什么创新。我们见过这样的游戏,其中平板电脑被用作游戏主机,然后连接到电视,电视又通过蓝牙连接到多个 Android 手机,每个手机都被用作控制器。休闲、社交游戏已经做得很好了,许多在苹果平台上开始的热门游戏现在都移植到了 Android 上。一切可能的都已经做了吗?不可能!对于那些愿意冒险尝试一些新游戏创意的人来说,总会有尚未开发的市场和游戏创意。硬件变得越来越快,这开启了全新的可能性领域,以前由于缺乏 CPU 马力,这些可能性是不可行的。

所以,现在你已经知道 Android 上已经有什么可用的了,我们建议你启动 Google Play 应用,看看之前展示的一些游戏。注意它们的结构(例如,哪些屏幕通向其他哪些屏幕,哪些按钮做什么,游戏元素如何相互交互等等)。用分析的心态玩游戏,实际上可以获得对这些事情的感觉。暂且抛开娱乐因素,专心解构游戏。一旦你完成了,回来继续读下去。我们要在纸上设计一个非常简单的游戏。

游戏设计:笔比代码更强大

正如我们前面所说的,启动 IDE 并拼凑出一个不错的技术演示是很诱人的。如果你想建立实验游戏机制的原型,看看它们是否真的有效,这是可以的。然而,一旦你这样做了,就扔掉原型。拿起一支笔和一些纸,坐在一把舒适的椅子上,仔细思考你的游戏的所有高级方面。先不要专注于技术细节,你以后会做的。现在,你想专注于设计游戏的用户体验。做到这一点的最好方法是画出以下内容:

  • 核心游戏机制,包括关卡概念(如果适用的话)
  • 主要人物的粗略背景故事
  • 一系列的物品,能量或者其他可以改变角色,机械或者环境的东西
  • 基于背景故事和人物的图形风格草图
  • 所有相关屏幕的草图,屏幕之间的转换图,以及转换触发器(例如,游戏结束状态)

如果你看过目录,你就会知道我们将在 Android 上实现 Snake《蛇》是手机市场上最受欢迎的游戏之一。如果你还不知道,在继续阅读之前,在网上查一下。与此同时,我们将在这里等着。。。

欢迎回来。所以,现在你知道 Snake 是关于什么的了,让我们假装是我们自己想出了这个主意,并开始为它设计。让我们从游戏机制开始。

核心游戏机制

在我们开始之前,这里有一份我们需要的清单:

  • 一把剪刀
  • 用来写字的东西
  • 很多纸

在我们游戏设计的这个阶段,一切都是移动的目标。我们建议你用纸创建基本的构建模块,并在桌子上重新排列它们,直到它们合适为止,而不是用 Paint、Gimp 或 Photoshop 精心制作精美的图像。你可以很容易地从物理上改变事情,而不必应付一个愚蠢的鼠标。一旦你确定了你的纸张设计,你就可以拍照或扫描设计供将来参考。让我们从创建核心游戏屏幕的那些基本块开始。图 3-12 向你展示了我们的核心游戏机制需要什么。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-12。游戏设计积木

最左边的矩形是我们的屏幕,大约是 Nexus One 屏幕的大小。这是我们放置其他元素的地方。下一个构建模块是两个箭头按钮,我们将使用它们来控制蛇。最后,还有蛇头、几条尾巴和一块它可以吃的东西。我们还写了一些数字,并把它们剪了下来。这些将用于显示分数。图 3-13 展示了我们对初始竞争环境的愿景。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-13。最初的比赛场地

让我们来定义游戏机制:

  • 这条蛇沿着它的头指向的方向前进,拖着它的尾巴。头部和尾部由大小相等的部分组成,在视觉上没有太大的区别。
  • 如果蛇走出屏幕边界,它会从另一边重新进入屏幕。
  • 如果按下右箭头或左箭头按钮,蛇将顺时针(右)或逆时针(左)旋转 90 度。
  • 如果蛇撞到自己(比如尾巴的一部分),游戏就结束了。
  • 如果蛇用头撞上了一个棋子,这个棋子就消失了,分数增加 10 分,场上出现一个新的棋子,位置不是蛇自己占据的。蛇还长了一个尾巴。新的尾巴部分附在蛇的末端。

对于这样一个简单的游戏来说,这是一个相当复杂的描述。请注意,我们按照复杂性升序对项目进行了排序。当蛇在游戏场上吃掉一块时游戏的行为可能是最复杂的。当然,更复杂的游戏无法用如此简洁的方式描述。通常,您会将这些拆分成单独的部分,并单独设计每个部分,在流程结束时的最终合并步骤中将它们连接起来。

最后一个游戏力学项目有这样的暗示:游戏最终会结束,因为屏幕上的所有空间都将被蛇用尽。

既然我们完全原创的游戏力学想法看起来不错,让我们试着为它想出一个背景故事。

一个故事和一种艺术风格

虽然有僵尸、宇宙飞船、矮人和大量爆炸的史诗故事会很有趣,但我们必须意识到我们的资源是有限的。我们的绘图技巧,如图 3-12 所示,有些欠缺。如果我们的生命取决于僵尸,我们就不能画它。所以我们做了任何有自尊的独立游戏开发者都会做的事情:诉诸涂鸦风格,并相应地调整设置。

进入诺姆先生的世界。Nom 先生是一条纸蛇,总是渴望吃掉从不明来源掉落在他的纸地上的墨滴。Nom 先生非常自私,他只有一个不那么高尚的目标:成为世界上最大的墨水纸蛇!

这个小小的背景故事让我们可以定义更多的东西:

  • 艺术风格是 doodly。我们将在以后扫描我们的构建模块,并在我们的游戏中使用它们作为图形素材。
  • 由于 Nom 先生是一个个人主义者,我们将稍微修改他的块状性质,给他一个适当的蛇脸。和一顶帽子。
  • 可消化的部分将被转化成一组墨水污迹。
  • 我们将通过让诺姆先生每次吃到墨水渍时发出咕噜声来解决游戏的音频问题。
  • 与其选择“涂鸦蛇”这样无聊的标题,不如把这个游戏叫做“Nom 先生”,一个更有趣的标题。

图 3-14 显示了 Nom 先生的全盛时期,以及一些将取代原块的墨迹。我们还画了一个很棒的 Nom 先生标志,可以在整个游戏中重复使用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-14。Nom 先生,他的帽子,墨水渍,还有商标

屏幕和过渡

随着游戏机制、背景故事、人物和艺术风格的固定,我们现在可以设计我们的屏幕和它们之间的过渡。然而,首先重要的是要准确理解屏幕是由什么组成的:

  • 屏幕是填充整个显示器的原子单位,它只负责游戏的一部分(例如,主菜单、设置菜单或动作发生的游戏屏幕)。
  • 一个屏幕可以由多个组件组成(例如,按钮、控件、平视显示器或游戏世界的渲染)。
  • 屏幕允许用户与屏幕的元素进行交互。这些交互可以触发屏幕转换(例如,按下主菜单上的新游戏按钮可以将当前活动的主菜单屏幕与游戏屏幕或级别选择屏幕交换)。

有了这些定义,我们就可以开动脑筋,设计 Nom 先生游戏的所有屏幕。

我们的游戏首先呈现给玩家的是主菜单屏幕。什么是好的主菜单屏幕?

  • 原则上,显示我们游戏的名字是一个好主意,所以我们会放上 Nom 先生的标志。

  • 为了让事情看起来更一致,我们还需要一个背景。为此,我们将重复使用运动场背景。

  • 玩家通常会想玩这个游戏,所以让我们加入一个游戏按钮。这将是我们的第一个交互组件。

  • Players want to keep track of their progress and awesomeness, so we’ll also add a high-score button as shown in Figure 3-15, another interactive component.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 3-15。主菜单屏幕

  • 可能有人不知道蛇。让我们以帮助按钮的形式给他们一些帮助,帮助按钮将转换到帮助屏幕。

  • 虽然我们的音效设计会很可爱,但有些玩家可能还是喜欢安静地玩。给他们一个象征性的切换按钮来启用和禁用声音就可以了。

我们实际上如何在屏幕上布置这些组件是一个品味问题。你可以开始研究计算机科学的一个分支,叫做人机界面(HCI ),以获得关于如何向用户展示你的应用的最新科学观点。不过,对 Nom 先生来说,这可能有点过头了。我们采用了图 3-15 所示的简单设计。

请注意,所有这些元素(徽标、菜单按钮等)都是独立的图像。

从主菜单屏幕开始,我们获得了一个直接的优势:我们可以直接从交互组件中获得更多的屏幕。在 Nom 先生的例子中,我们需要一个游戏屏幕、一个高分屏幕和一个帮助屏幕。我们不包括设置屏幕,因为唯一的设置(声音)已经出现在主菜单屏幕上。

让我们暂时忽略游戏屏幕,先把注意力集中在高分屏幕上。我们决定高分将存储在本地的 Nom 先生,所以我们将只跟踪单个玩家的成就。我们还决定只记录五个最高分。因此,高分屏幕将看起来像图 3-16 ,在顶部显示“高分”文本,随后是五个最高分和一个带箭头的按钮,指示您可以过渡回某个内容。我们将再次重复使用运动场的背景,因为我们喜欢它便宜。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-16。高分屏幕

接下来是帮助屏幕。它将告知玩家背景故事和游戏机制。所有这些信息在一个屏幕上显示有点太多了。因此,我们将帮助屏幕分成多个屏幕。这些屏幕中的每一个都将向用户呈现一条必不可少的信息:Nom 先生是谁,他想要什么,如何控制 Nom 先生让他吃墨迹,以及 Nom 先生不喜欢什么(即吃自己)。总共有三个帮助屏幕,如图图 3-17 所示。请注意,我们在每个屏幕上添加了一个按钮,以表明还有更多信息需要阅读。我们一会儿就把这些屏幕连接起来。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-17。帮助屏幕

最后,是我们的游戏屏幕,我们已经看到了。不过,我们忽略了一些细节。第一,游戏不应该马上开始;我们应该给运动员一些时间准备。因此,屏幕将开始请求触摸屏幕以开始咀嚼。这并不保证单独的屏幕;我们将直接在游戏屏幕中实现初始暂停。

说到暂停,我们还将添加一个按钮,允许用户暂停游戏。一旦暂停,我们还需要给用户一个恢复游戏的方法。在这种情况下,我们将只显示一个大的 Resume 按钮。在暂停状态下,我们还将显示另一个按钮,允许用户返回主菜单屏幕。一个额外的退出按钮让用户返回到主菜单。

万一 Nom 先生咬到自己的尾巴,我们需要通知玩家游戏结束了。我们可以实现一个单独的游戏结束屏幕,或者我们可以留在游戏屏幕内,只覆盖一个大的“游戏结束”信息。在这种情况下,我们将选择后者。为了使事情圆满,我们还将显示玩家获得的分数,以及一个返回主菜单的按钮。

把游戏屏幕的这些不同状态想象成子屏幕。我们有四个子屏幕:初始就绪状态、正常游戏状态、暂停状态和游戏结束状态。图 3-18 显示了这些子屏幕。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-18。游戏画面及其四种不同状态

现在是时候把屏幕连在一起了。每个屏幕都有一些交互组件,用于转换到另一个屏幕。

  • 从主菜单屏幕,我们可以通过相应的按钮进入游戏屏幕、高分屏幕和帮助屏幕。
  • 从游戏屏幕,我们可以通过暂停状态的按钮或游戏结束状态的按钮返回到主菜单屏幕。
  • 从高分屏幕,我们可以回到主菜单屏幕。
  • 从第一个帮助屏幕,我们可以转到第二个帮助屏幕;从第二个到第三个;从第三个到第四个;从第四个开始,我们将返回到主菜单屏幕。

这就是我们所有的转变!看起来没那么糟,是吧?图 3-19 直观地总结了所有的转换,箭头从每个交互组件指向目标屏幕。我们还放入了组成屏幕的所有元素。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-19。所有设计元素和过渡

我们现在已经完成了第一个完整的游戏设计。剩下的就是实现了。我们如何把这个设计变成一个可执行的游戏?

注意我们刚刚使用的游戏设计方法对于小游戏来说是很好的。这本书叫做开始安卓游戏,所以这是一个合适的方法论。对于较大的项目,你最有可能在一个团队中工作,每个团队成员专攻一个方面。虽然您仍然可以在该上下文中应用这里描述的方法,但是您可能需要对它进行一点调整,以适应不同的环境。您还将更加迭代地工作,不断完善您的设计。

代码:本质细节

这里还有一个先有鸡还是先有蛋的情况:我们只想了解与游戏编程相关的 Android APIs。然而,我们仍然不知道如何实际编程一个游戏。我们有一个如何设计的想法,但将其转化为可执行文件对我们来说仍然是巫术。在下面的小节中,我们想给你一个游戏元素的概述。我们将查看一些接口的伪代码,稍后我们将使用 Android 提供的功能来实现这些代码。接口令人敬畏有两个原因:它们允许我们专注于语义而不需要知道实现细节,并且它们允许我们稍后交换实现(例如,代替使用 2D CPU 渲染,我们可以利用 OpenGL ES 在屏幕上显示 Nom 先生)。

每一个游戏都需要一个基本的框架来抽象和减轻与底层操作系统通信的痛苦。通常这被分成如下模块:

  • 应用和窗口管理:这是负责创建一个窗口,并处理像关闭窗口或暂停/恢复 Android 中的应用这样的事情。
  • 输入:这与窗口管理模块有关,它跟踪用户输入(即触摸事件、击键、外围和加速度计读数)。
  • 文件输入/输出(File I/O):这允许我们从磁盘中获取我们的素材字节到我们的程序中。
  • 图形:这可能是除了实际游戏之外最复杂的模块了。它负责加载图形并将它们绘制在屏幕上。
  • 音频:这个模块负责加载和播放一切会撞击我们耳朵的东西。
  • 游戏框架(Game framework):这将上述所有内容联系在一起,为编写游戏提供了一个易于使用的基础。

每个模块都由一个或多个接口组成。每个接口至少有一个具体的实现,它基于底层平台(在我们的例子中是 Android)提供的东西应用接口的语义。

注意是的,我们故意在前面的列表中遗漏了网络。我们不会在本书中实现多人游戏。这是一个相当高级的话题,取决于游戏的类型。如果你对这个话题感兴趣,你可以在网上找到一系列的教程。是一个开始的好地方。)

在下面的讨论中,我们将尽可能地与平台无关。这些概念在所有平台上都是相同的。

应用和窗口管理

游戏就像任何其他有用户界面的计算机程序一样。它包含在某种窗口中(如果底层操作系统的 UI 范例是基于窗口的,这是所有主流操作系统的情况)。窗口作为一个容器,我们基本上认为它是一个画布,我们从中绘制游戏内容。

除了触摸客户区或按键之外,大多数操作系统允许用户以一种特殊的方式与窗口交互。在桌面系统上,你通常可以拖动窗口,调整它的大小,或者最小化到某种任务栏。在 Android 中,调整大小被适应方向变化所取代,最小化类似于通过按下 home 键或对来电的反应将应用放在后台。

应用和窗口管理模块还负责实际设置窗口,并确保它由单个 UI 组件填充,我们稍后可以渲染该组件,该组件以触摸或按键的形式接收来自用户的输入。UI 组件可以通过 CPU 呈现,也可以是硬件加速的,就像 OpenGL ES 一样。

应用和窗口管理模块没有一组具体的接口。稍后我们会将它与游戏框架合并。我们必须记住的是我们必须管理的应用状态和窗口事件:

  • Create :当窗口(以及应用)启动时调用一次
  • 暂停:当应用被某种机制暂停时调用
  • Resume :当应用恢复并且窗口再次在前台时调用

注意此时,一些安卓迷可能会翻白眼。为什么只使用单一窗口(Android speak 中的活动)?为什么不在游戏中使用一个以上的 UI 小部件呢——比如说,实现我们的游戏可能需要的复杂 UI?主要原因是我们想要完全控制我们游戏的外观和感觉。它还允许我们专注于 Android 游戏编程,而不是 Android UI 编程,关于这个主题有更好的书籍——例如,马克·墨菲的优秀开始 Android 3 (Apress,2011)。

投入

用户肯定会想以某种方式与我们的游戏互动。这就是输入模块的用武之地。在大多数操作系统上,诸如触摸屏幕或按键之类的输入事件被分派到当前聚焦的窗口。然后,窗口将进一步将事件分派给具有焦点的 UI 组件。调度过程通常对我们是透明的;我们唯一关心的是从聚焦的 UI 组件中获取事件。操作系统的 UI APIs 提供了一种挂钩到事件调度系统的机制,以便我们可以轻松地注册和记录事件。这种事件的挂钩和记录是输入模块的主要任务。

我们可以用记录的信息做什么?有两种操作方式:

  • 轮询:通过轮询,我们只检查输入设备的当前状态。当前检查和上一次检查之间的任何状态都将丢失。例如,这种输入处理方式适用于检查用户是否触摸了特定的按钮。它不适合跟踪文本输入,因为键事件的顺序丢失了。
  • 基于事件的处理(Event-based handling):这为我们提供了自上次检查以来发生的事件的完整历史记录。它是执行文本输入或任何其他依赖于事件顺序的任务的合适机制。检测手指第一次接触屏幕或抬起的时间也很有用。

我们想要处理什么输入设备?在 Android 上,我们有三种主要的输入方式:触摸屏、键盘/轨迹球和加速度计。前两种方法适用于轮询和基于事件的处理。加速度计通常只是被轮询。触摸屏可以产生三个事件:

  • 向下触摸:手指触摸屏幕时会发生这种情况。
  • 触摸拖动:手指在屏幕上拖动时会出现这种情况。在拖拽之前,总会有一个向下的事件。
  • Touch up :手指从屏幕上抬起时会出现这种情况。

每个触摸事件都有附加信息:相对于 UI 组件原点的位置,以及在多点触摸环境中用于识别和跟踪不同手指的指针索引。

键盘可以产生两种类型的事件:

  • 按键按下:这种情况发生在按键被按下的时候。
  • 向上键:当一个键被抬起时会发生这种情况。此事件之前总是有一个按键事件。

关键事件也携带附加信息。按键事件存储被按下的按键的代码。按键事件存储按键的代码和实际的 Unicode 字符。按键代码和按键事件生成的 Unicode 字符是有区别的。在后一种情况下,还会考虑其他键的状态,例如 Shift 键。例如,通过这种方式,我们可以在按键事件中获得大写和小写字母。对于按键事件,我们只知道某个键被按下了;我们不知道按键实际上会产生哪个字符。

寻求使用自定义 usb 硬件(包括操纵杆、模拟控制器、特殊键盘、触摸板或其他 android 支持的外围设备)的开发人员可以通过使用 android.hardware.usb 包 API 来实现这一点,这些 API 在 API level 12 (Android 3.1)中引入,并通过 com.android.future.usb 包向后移植到 Android 2 . 3 . 4。USB API 使 Android 设备能够在主机模式下运行,这允许外围设备连接到 Android 设备并由其使用,或者在附件模式下运行,这允许设备作为另一个 USB 主机的附件。这些 API 不是初学者的材质,因为设备访问级别非常低,为 USB 附件提供数据流 I/O,但重要的是要注意功能确实存在。如果你的游戏设计围绕一个特定的 USB 附件,你肯定会想为该附件开发一个通信模块,并使用它制作原型。

最后,还有加速度计。尽管几乎所有的手机和平板电脑都将加速度计作为标准硬件,但包括机顶盒在内的许多新设备可能没有加速度计,因此请始终计划使用多种输入模式,这一点很重要。

为了使用加速度计,我们将总是轮询加速度计的状态。加速度计报告地球重力在加速度计三个轴之一上施加的加速度。轴被称为 x、y 和 z。图 3-20 描述了每个轴的方向。每个轴上的加速度用米每秒平方(m/s 2 表示。从物理课上,我们知道一个物体在地球上自由落体时会以大约 9.8 米/秒 2 的速度加速。其他星球引力不同,所以加速度常数也不同。为了简单起见,我们在这里只讨论地球。当一个轴指向远离地心的方向时,最大的加速度作用在它上面。如果一个轴指向地球的中心,我们得到一个负的最大加速度。例如,如果你在纵向模式下将手机直立,那么 y 轴将报告 9.8 米/秒的加速度 2 。在图 3-20 中,z 轴将报告加速度为 9.8 米/秒 2 ,x 轴和 y 轴将报告加速度为零。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-20。安卓手机上的加速度计轴。z 轴指向手机之外

现在,让我们定义一个接口,它给我们提供对触摸屏、键盘和加速度计的轮询访问,也给我们提供对触摸屏和键盘的基于事件的访问(见清单 3-1 )。

清单 3-1。 输入界面以及 KeyEvent 和 TouchEvent 类

package com.badlogic.androidgames.framework;import java.util.List;public interface Input {public static class KeyEvent {public static final int *KEY_DOWN* = 0;public static final int *KEY_UP* = 1;public int type;public int keyCode;public char keyChar;}public static class TouchEvent {public static final int *TOUCH_DOWN* = 0;public static final int *TOUCH_UP* = 1;public static final int *TOUCH_DRAGGED* = 2;public int type;public int x, y;public int pointer;}public boolean isKeyPressed(int keyCode);public boolean isTouchDown(int pointer);public int getTouchX(int pointer);public int getTouchY(int pointer);public float getAccelX();public float getAccelY();public float getAccelZ();public List<KeyEvent> getKeyEvents();public List<TouchEvent> getTouchEvents();
}

我们的定义由两个类开始,KeyEvent 和 TouchEvent。KeyEvent 类定义了编码 KeyEvent 类型的常量;TouchEvent 类也是如此。如果事件的类型是 KEY_UP,则 KeyEvent 实例记录其类型、键的代码和 Unicode 字符。

TouchEvent 代码类似,它保存 TouchEvent 的类型、手指相对于 UI 组件原点的位置以及触摸屏驱动程序赋予手指的指针 ID。只要手指在屏幕上,该手指的指针 ID 就会保持不变。如果两个手指放下,手指 0 抬起,那么手指 1 只要接触屏幕就保持其 ID。新手指将获得第一个空闲 ID,在本例中为 0。指针 id 通常是按顺序分配的,但并不保证会这样。例如,索尼 Xperia Play 使用 15 个 id,并以循环方式将它们分配给 touches。不要在代码中对新指针的 ID 做任何假设——只能使用索引读取指针的 ID 并引用它,直到指针被抬起。

接下来是输入接口的轮询方法,这应该是不言自明的。Input.isKeyPressed()接受一个键码,并返回相应的键当前是否被按下。Input.isTouchDown()、Input.getTouchX()和 Input.getTouchY()返回给定指针是否按下,以及其当前的 x 和 y 坐标。请注意,如果相应的指针没有实际接触屏幕,坐标将是未定义的。

Input.getAccelX()、Input.getAccelY()和 Input.getAccelZ()返回每个加速度计轴各自的加速度值。

最后两种方法用于基于事件的处理。它们返回自我们上次调用这些方法以来记录的 KeyEvent 和 TouchEvent 实例。事件根据发生的时间进行排序,最新的事件位于列表的末尾。

有了这个简单的接口和这些助手类,我们可以满足所有的输入需求。让我们继续处理文件。

注意虽然带有公共成员的可变类令人厌恶,但我们可以在这种情况下摆脱它们,原因有两个:Dalvik 在调用方法(在这种情况下是 getters)时仍然很慢,事件类的可变性对输入实现的内部工作没有影响。请注意,这通常是不好的风格,但是出于性能原因,我们偶尔会采用这种快捷方式。

文件输入输出

读写文件对于我们的游戏开发工作来说是非常重要的。假设我们在 Java 领域,我们主要关心的是创建 InputStream 和 OutputStream 实例,这是从特定文件读取数据和向特定文件写入数据的标准 Java 机制。在我们的例子中,我们主要关心的是读取游戏中打包的文件,比如关卡文件、图像和音频文件。写文件是我们很少做的事情。通常,如果我们想保持高分或游戏设置,或者保存一个游戏状态,以便用户可以从他们离开的地方继续,我们就只写文件。

我们想要尽可能简单的文件访问机制。清单 3-2 显示了我们对简单接口的建议。

清单 3-2。 文件 I/O 接口

package com.badlogic.androidgames.framework;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;public interface FileIO {public InputStream readAsset(String fileName)throws IOException;public InputStream readFile(String fileName)throws IOException;public OutputStream writeFile(String fileName)throws IOException;
}

那是相当精简和卑鄙的。我们只是指定一个文件名,然后得到一个流作为回报。正如我们在 Java 中通常做的那样,我们将抛出一个 IOException 以防出错。当然,我们在哪里读写文件取决于实现。素材将从我们的应用的 APK 文件中读取,文件将从 SD 卡(也称为外部存储)中读取和写入。

返回的 InputStreams 和 OutputStreams 是普通的 Java 流。当然,一旦我们用完它们,我们必须把它们关上。

声音的

虽然音频编程是一个相当复杂的话题,但我们可以用一个非常简单的抽象来摆脱它。我们不会做任何高级音频处理;我们只是回放从文件中加载的声音效果和音乐,就像我们在图形模块中加载位图一样。

不过,在我们深入模块接口之前,让我们停下来,了解一下声音实际上是什么,以及它是如何以数字形式表示的。

声音的物理学

声音通常被建模为在空气或水等介质中传播的一组波。波不是实际的物理对象,而是分子在介质中的运动。想象一个小池塘,你往里面扔一块石头。当石头撞击池塘表面时,它将推开池塘内的大量水分子,这些被推开的分子将把它们的能量转移给它们的邻居,邻居也将开始移动和推动。最终,你会看到圆形的波浪从石头击中池塘的地方出现。

声音产生的时候也会发生类似的情况。你得到的不是圆周运动,而是球形运动。从你童年可能进行过的高度科学的实验中你可能知道,水波是可以相互作用的;它们可以相互抵消或相互加强。声波也是如此。当你听音乐时,环境中的所有声波结合起来形成你听到的音调和旋律。声音的音量取决于移动和推动的分子对其邻居并最终对你的耳朵施加了多少能量。

录制和回放

录制和回放音频的原理在理论上非常简单。为了记录,我们记录下形成声波的分子对空间中的某个区域施加一定压力的时间点。回放这些数据仅仅是让扬声器周围的空气分子像我们记录时一样摆动和移动。

在实践中,当然要复杂一点。音频通常以两种方式录制:模拟或数字。在这两种情况下,声波都被某种麦克风记录下来,麦克风通常由一层薄膜组成,将分子的推动转化为某种信号。信号的处理和存储方式决定了模拟录音和数字录音的区别。我们正在数字化工作,所以让我们看看那个案例。

以数字方式记录音频意味着以离散的时间步长测量并存储麦克风膜片的状态。根据周围分子的推动,膜可以相对于中性状态向内或向外推动。这个过程被称为采样,因为我们在离散的时间点采集膜状态样本。我们每单位时间内采集的样本数称为采样率。通常时间单位以秒为单位,单位称为赫兹(Hz)。每秒采样越多,音频质量就越高。CD 以 44,100Hz 或 44.1KHz 的采样率回放。例如,当通过电话线传输语音时,采样率较低(在这种情况下通常为 8KHz)。

采样率只是决定录音质量的一个因素。我们存储每个膜状态样本的方式也起作用,它也受数字化的影响。让我们回忆一下膜的实际状态是什么:它是膜离中性状态的距离。因为膜是被向内推还是向外推是有区别的,所以我们记录了带符号的距离。因此,特定时间步的膜状态是单个负数或正数。我们可以用多种方式存储这个有符号数:作为有符号的 8 位、16 位或 32 位整数,作为 32 位浮点数,甚至作为 64 位浮点数。每种数据类型都有有限的精度。一个 8 位有符号整数可以存储 127 个正距离值和 128 个负距离值。32 位整数提供了更高的分辨率。当存储为浮点型时,膜状态通常归一化为 1 到 1 之间的范围。最大的正值和最小的负值代表了膜离开其中性状态的最远距离。膜状态也被称为振幅。它代表撞击它的声音的响度。

使用单个麦克风,我们只能录制单声道声音,这将丢失所有空间信息。通过两个麦克风,我们可以测量空间中不同位置的声音,从而获得所谓的立体声。例如,您可以将一个麦克风放在发声物体的左侧,另一个放在右侧,从而获得立体声。当声音通过两个扬声器同时播放时,我们可以合理地再现音频的空间分量。但这也意味着当存储立体声音频时,我们需要存储两倍数量的样本。

回放最终是一件简单的事情。一旦我们获得了数字形式的音频样本,并且具有特定的采样速率和数据类型,我们就可以将这些数据发送到音频处理单元,它会将信息转换为信号,供连接的扬声器使用。扬声器解释这个信号,并将其转化为薄膜的振动,这又会导致周围的空气分子移动并产生声波。这正是为记录所做的,只是颠倒了!

音频质量和压缩

哇,好多理论。我们为什么关心?如果您注意了,现在您可以根据采样率和用于存储每个样本的数据类型来判断音频文件是否是高质量的。采样率越高,数据类型精度越高,音频质量就越好。然而,这也意味着我们需要更多的存储空间来存放音频信号。

想象一下,我们以 60 秒的长度录制相同的声音,但我们录制了两次:一次是以 8KHz 的采样率、每样本 8 位,另一次是以 44KHz 的采样率、16 位精度。我们需要多少内存来存储每个声音?在第一种情况下,每个样本需要 1 个字节。将其乘以 8,000Hz 的采样率,我们需要每秒 8000 字节。对于我们完整的 60 秒录音,这是 480,000 字节,或大约半兆字节(MB)。我们更高质量的录音需要更多的内存:每个样本 2 字节,每秒 44,000 字节的 2 倍。也就是每秒 88000 字节。将此乘以 60 秒,我们得到 5,280,000 字节,或 5MB 多一点。你通常的 3 分钟流行歌曲会占用超过 15MB 的质量,这只是一个单声道录音。对于立体声录音,你需要两倍的内存。对于一首愚蠢的歌来说相当多的字节!

许多聪明人想出了减少录音所需字节数的方法。他们发明了相当复杂的心理声学压缩算法,分析未压缩的音频记录,并输出较小的压缩版本。压缩通常是有损,意味着原始音频的一些次要部分被省略。当你播放 MP3 或 OGGs 时,你实际上是在听压缩的有损音频。因此,使用 MP3 或 OGG 等格式将有助于我们减少存储音频所需的磁盘空间。

回放压缩文件的音频怎么样?虽然存在用于各种压缩音频格式的专用解码硬件,但是普通的音频硬件通常只能处理未压缩的样本。在实际向声卡输入样本之前,我们必须先读入样本并解压缩。我们可以这样做一次,将所有未压缩的音频样本存储在内存中,或者只在需要时从音频文件的分区中流过。

在实践中

您已经看到,即使是 3 分钟的歌曲也会占用大量内存。因此,当我们播放游戏音乐时,我们会实时输入音频样本,而不是将所有音频样本预加载到内存中。通常,我们只有一个音乐流在播放,所以我们只需访问磁盘一次。

对于短暂的声音效果,如爆炸或枪声,情况略有不同。我们经常想要同时播放多次声音效果。对于声音效果的每个实例,从磁盘流式传输音频样本不是一个好主意。不过,我们很幸运,因为短音不会占用太多内存。因此,我们将把音效的所有样本读入内存,这样我们就可以直接同时播放它们。

我们有以下要求:

  • 我们需要一种方法来加载音频文件,以便进行流式播放和从内存中播放。
  • 我们需要一种方法来控制流式音频的回放。
  • 我们需要一种方法来控制满载音频的回放。

这直接转化为音频、音乐和声音接口(分别显示在清单 3-3 到 3-5、中)。

清单 3-3。 音频接口

package com.badlogic.androidgames.framework;public interface Audio {public Music newMusic(String filename);public Sound newSound(String filename);
}

音频接口是我们创建新的音乐和声音实例的方式。一个音乐实例代表一个流式音频文件。一个声音实例代表一个简短的声音效果,我们将它完全保存在内存中。Audio.newMusic()和 Audio.newSound()方法都将文件名作为参数,并在加载过程失败时抛出 IOException(例如,当指定的文件不存在或损坏时)。文件名指的是我们的应用的 APK 文件中的素材文件。

清单 3-4。 音乐界面

package com.badlogic.androidgames.framework;public interface Music {public void play();public void stop();public void pause();public void setLooping(boolean looping);public void setVolume(float volume);public boolean isPlaying();public boolean isStopped();public boolean isLooping();public void dispose();
}

音乐界面稍微复杂一点。它具有开始播放音乐流、暂停和停止音乐流的方法,并将其设置为循环播放,这意味着当它到达音频文件的结尾时,它将自动从头开始播放。此外,我们可以将音量设置为 0(静音)到 1(最大音量)范围内的浮动值。还提供了一些 getter 方法,允许我们轮询 Music 实例的当前状态。一旦我们不再需要音乐实例,我们就必须处理它。这将关闭所有系统资源,例如音频流所来自的文件。

清单 3-5。 声音界面

package com.badlogic.androidgames.framework;public interface Sound {public void play(float volume);
,,,public void dispose();
}

声音界面更简单。我们需要做的就是调用它的 play()方法,该方法再次接受一个 float 参数来指定音量。我们可以随时调用 play()方法(例如,当 Nom 先生吃了一个墨迹)。一旦我们不再需要 Sound 实例,我们就必须释放它来释放样本使用的内存,以及其他可能相关的系统资源。

注意虽然我们在本章中讲述了很多内容,但关于音频编程还有很多内容需要学习。我们简化了一些内容,以保持这一部分简洁明了。例如,通常你不会线性地指定音量。在我们的背景下,可以忽略这个小细节。只是要意识到还有更多!

制图法

我们游戏框架核心的最后一个模块是图形模块。你可能已经猜到了,它将负责把图像(也称为位图)绘制到我们的屏幕上。这听起来可能很容易,但如果你想要高性能的图形,你至少要知道图形编程的基本知识。让我们从 2D 图形的基础开始。

我们需要问的第一个问题是这样的:图像到底是如何输出到我的显示器上的?答案相当复杂,我们不一定需要知道所有的细节。我们将快速回顾一下我们的计算机和显示器内部发生了什么。

栅格、像素和帧缓冲区

今天的显示器是基于光栅的。光栅是一个所谓图片元素的二维网格。你可能知道它们是像素,我们将在随后的文本中这样称呼它们。光栅网格的宽度和高度是有限的,我们通常用每行和每列的像素数来表示。如果你觉得勇敢,你可以打开你的电脑,试着在你的显示器上辨认出单个的像素。请注意,我们对您的眼睛造成的任何损害概不负责。

一个像素有两个属性:在网格中的位置和颜色。像素的位置以离散坐标系中的二维坐标给出。 离散是指一个坐标总是在一个整数位置。坐标是在施加于网格上的欧几里得坐标系中定义的。坐标系的原点是网格的左上角。正 x 轴指向右侧,y 轴指向下方。最后一项是最让人困惑的。我们一会儿会回来。出现这种情况的原因很简单。

忽略愚蠢的 y 轴,我们可以看到,由于我们坐标的离散性,原点与网格中左上角的像素重合,位于(0,0)。原点像素右边的像素位于(1,0),原点像素下面的像素位于(0,1),依此类推(见图 3-21 左侧)。显示器的光栅网格是有限的,因此有意义的坐标数量有限。负坐标在屏幕外。大于或等于栅格宽度或高度的坐标也在屏幕之外。请注意,最大的 x 坐标是栅格的宽度减 1,最大的 y 坐标是栅格的高度减 1。这是因为原点与左上角的像素重合。一个接一个的错误是图形编程中常见的挫折来源。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-21。显示光栅网格和 VRAM,过于简化

显示器从图形处理器接收恒定的信息流。它按照控制屏幕绘制的程序或操作系统的指定,对显示器光栅中每个像素的颜色进行编码。显示器将每秒刷新其状态几十次。确切的速率称为刷新率。它用赫兹表示。液晶显示器的刷新率通常为每秒 60Hz 阴极射线管(CRT)显示器和等离子显示器通常具有更高的刷新率。

图形处理器可以访问一个称为视频随机存取存储器(VRAM)的特殊内存区域。在 VRAM 中,有一个保留区域用于存储屏幕上显示的每个像素。这个区域通常被称为帧缓冲区。一幅完整的屏幕图像因此被称为一帧。对于显示器光栅网格中的每个像素,在保存像素颜色的帧缓冲区中都有相应的内存地址。当我们想改变屏幕上显示的内容时,我们只需改变 VRAM 内存区域中像素的颜色值。

现在是时候解释为什么显示器坐标系中的 y 轴指向下方了。内存,无论是 VRAM 还是普通 RAM,都是线性一维的。把它想象成一个一维数组。那么我们如何将二维像素坐标映射到一维内存地址呢?图 3-21 显示了一个相当小的 3×2 像素的显示光栅网格,以及它在 VRAM 中的表示。(我们假设 VRAM 仅由帧缓冲存储器组成。)由此,我们可以很容易地推导出下面的公式来计算一个像素在(x,y)处的内存地址:

int address = x + y * rasterWidth;

我们也可以反过来,从地址到像素的 x 和 y 坐标:

int x = address % rasterWidth;
int y = address / rasterWidth;

因此,由于 VRAM 中像素颜色的内存布局,y 轴指向下方。这实际上是从早期计算机图形学继承下来的遗产。监视器将更新屏幕上每个像素的颜色,从左上角开始,移动到右边,在下一行回到左边,直到它们到达屏幕的底部。将 VRAM 内容以易于将颜色信息传输到监视器的方式进行布局是很方便的。

注意如果我们可以完全访问帧缓冲区,我们可以使用前面的等式编写一个完整的图形库来绘制像素、线条、矩形、加载到内存的图像等等。由于各种原因,现代操作系统不允许我们直接访问帧缓冲区。相反,我们通常绘制到一个内存区域,然后由操作系统复制到实际的帧缓冲区。不过,一般概念在这种情况下也适用!如果你对如何有效地做这些低级的事情感兴趣,在网上搜索一个叫 Bresenham 的家伙和他的画线和画圆算法。

垂直同步和双缓冲

现在,如果你还记得关于刷新率的那一段,你可能已经注意到刷新率似乎相当低,我们可以比显示器刷新更快地写入帧缓冲区。这是有可能的。更糟糕的是,我们不知道显示器何时从 VRAM 获取最新的帧副本,如果我们正在画东西,这可能是一个问题。在这种情况下,显示器将显示旧帧缓冲区内容的一部分和新状态的一部分,这是不希望的情况。你可以在许多 PC 游戏中看到这种效果,它表现为撕裂(屏幕同时显示上一帧的部分和新帧的部分)。

这个问题解决方案的第一部分叫做双缓冲。图形处理单元(GPU)实际上管理两个帧缓冲区,而不是单个帧缓冲区:前端缓冲区和后端缓冲区。将从中提取像素颜色的前缓冲区可供显示器使用,后缓冲区可用于绘制我们的下一帧,同时显示器很高兴地从前缓冲区获取数据。当我们完成绘制当前帧时,我们告诉 GPU 将两个缓冲区相互交换,这通常意味着只交换前后缓冲区的地址。在图形编程文献和 API 文档中,您可能会发现术语翻页缓冲区交换,它们指的就是这个过程。

但是,仅仅双缓冲并不能完全解决问题:当屏幕正在刷新内容时,交换仍然会发生。这就是垂直同步(也称为 vsync )发挥作用的地方。当我们调用 buffer swap 方法时,GPU 会一直阻塞,直到显示器发出信号,表示它已经完成了当前的刷新。如果发生这种情况,GPU 可以安全地交换缓冲区地址,一切都会好起来。

幸运的是,如今我们几乎不需要关心这些烦人的细节。VRAM 以及双缓冲和垂直同步的细节对我们是安全隐藏的,因此我们无法对它们进行破坏。相反,我们被提供了一组 API,这些 API 通常限制我们操作应用窗口的内容。其中一些 API,如 OpenGL ES,公开了硬件加速,它基本上只不过是用图形芯片上的专用电路操纵 VRAM。看,这不是魔法!至少在高层次上,您应该了解内部工作原理的原因是,它允许您了解应用的性能特征。当 vsync 启用时,你永远不能超过屏幕的刷新率,如果你所做的只是绘制一个像素,这可能会令人困惑。

当我们使用非硬件加速的 API 进行渲染时,我们不会直接处理显示器本身。相反,我们在窗口中绘制一个 UI 组件。在我们的例子中,我们处理一个扩展到整个窗口的 UI 组件。因此,我们的坐标系不会延伸到整个屏幕,而只会延伸到我们的 UI 组件。UI 组件实际上变成了我们的显示器,拥有自己的虚拟帧缓冲区。然后,操作系统将管理所有可见窗口内容的合成,并确保它们的内容被正确地传输到它们在实际帧缓冲区中覆盖的区域。

什么是颜色?

你会注意到,到目前为止,我们已经很方便地忽略了颜色。我们在图 3-21 中虚构了一种叫做颜色的类型,并假装一切正常。让我们看看什么是真正的颜色。

从物理上来说,颜色是你的视网膜和视觉皮层对电磁波的反应。这种波的特征是它的波长和强度。我们可以看到波长大约在 400 到 700 纳米之间的波。电磁波谱的这个子波段也被称为可见光光谱。彩虹显示了可见光光谱的所有颜色,从紫色到蓝色到绿色到黄色,然后是橙色,最后是红色。显示器所做的只是为每个像素发射特定的电磁波,我们感受到的是每个像素的颜色。不同类型的显示器使用不同的方法来实现这一目标。这个过程的一个简化版本是这样的:屏幕上的每个像素都是由三种不同的荧光粒子组成的,它们会发出红色、绿色或蓝色中的一种颜色的光。当显示器刷新时,每个像素的荧光粒子将通过某种方式发光(例如,在 CRT 显示器的情况下,像素的粒子被一束电子击中)。对于每个粒子,显示器可以控制它发出多少光。例如,如果一个像素完全是红色的,那么只有红色的粒子会被全强度的电子击中。如果我们想要三种基色之外的颜色,我们可以通过混合基色来实现。混合是通过改变每个粒子发出颜色的强度来完成的。电磁波在到达我们视网膜的途中会相互叠加。我们的大脑将这种混合解释为一种特定的颜色。因此,颜色可以由基色红、绿、蓝的混合强度来确定。

颜色模型

我们刚刚讨论的被称为颜色模型,特别是 RGB 颜色模型。当然,RGB 代表红色、绿色和蓝色。我们可以使用更多的颜色模型,例如 YUV 和 CMYK。然而,在大多数图形编程 API 中,RGB 颜色模型几乎是标准的,所以我们在这里只讨论它。

RGB 颜色模型被称为加色颜色模型,因为最终颜色是通过混合加色原色红、绿和蓝而获得的。你可能在学校尝试过混合原色。图 3-22 向你展示了一些 RGB 颜色混合的例子,让你回忆一下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-22。享受混合红、绿、蓝三原色的乐趣

当然,通过改变红色、绿色和蓝色成分的强度,我们可以生成比图 3-22 所示更多的颜色。每个分量可以具有介于 0 和某个最大值(比如 1)之间的强度值。如果我们将每个颜色分量解释为一个三维欧几里得空间的三个轴中的一个值,我们可以绘制出一个所谓的色立方体,如图图 3-23 所示。如果我们改变每种成分的强度,就有更多的颜色可供选择。颜色以三元组(红、绿、蓝)给出,其中每个分量的范围在 0.0 和 1.0 之间(0.0 表示该颜色没有强度,1.0 表示完全强度)。黑色位于原点(0,0,0),白色位于原点(1,1,1)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-23。强大的 RGB 颜色立方体

数字编码颜色

我们如何在计算机内存中对 RGB 颜色三元组进行编码?首先,我们必须定义颜色组件要使用的数据类型。我们可以使用浮点数,并将有效范围指定为 0.0 到 1.0 之间。这将为每个组件提供相当多的分辨率,并为我们提供许多不同的颜色。遗憾的是,这种方法占用了大量空间(每像素 3 乘以 4 或 8 字节,这取决于我们使用的是 32 位还是 64 位浮点)。

我们可以做得更好——以失去一些颜色为代价——这完全没问题,因为显示器通常只能发出有限的颜色。我们可以使用无符号整数,而不是对每个组件使用浮点数。现在,如果我们对每个分量使用 32 位整数,我们没有得到任何东西。相反,我们对每个分量使用一个无符号字节。每个分量的强度范围从 0 到 255。因此,对于 1 个像素,我们需要 3 个字节,即 24 位。这是 2 的 24 次方(16,777,216)种不同的颜色。这对我们的需要来说足够了。

我们能再降低一点吗?是的,我们可以。我们可以将每个组件打包成一个 16 位字,因此每个像素需要 2 个字节的存储空间。红色用 5 位,绿色用 6 位,蓝色用剩下的 5 位。绿色获得 6 位的原因是我们的眼睛可以看到更多的绿色阴影,而不是红色或蓝色。所有的位加在一起构成 2 的 16 次方(65,536)种我们可以编码的不同颜色。图 3-24 显示了如何用上述三种编码对颜色进行编码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-24。粉红色的颜色编码(抱歉,在这本书的印刷本中将是灰色的)

在浮点数的情况下,我们可以使用三个 32 位的 Java 浮点数。在 24 位编码的情况下,我们有一个小问题:Java 中没有 24 位整数类型,所以我们可以将每个组件存储在一个字节中,或者使用 32 位整数,剩下的高 8 位不用。在 16 位编码的情况下,我们也可以使用两个单独的字节,或者将各个部分存储在一个短值中。注意 Java 没有无符号类型。由于二进制补码的强大功能,我们可以安全地使用有符号整数类型来存储无符号值。

对于 16 位和 24 位整数编码,我们还需要指定在短整型值中存储三个部分的顺序。通常使用两种方法:RGB 和 BGR。图 3-23 使用 RGB 编码。蓝色分量位于最低的 5 或 8 位,绿色分量使用接下来的 6 或 8 位,红色分量使用最高的 5 或 8 位。BGR 编码正好颠倒了这个顺序。绿色的位留在原处,红色和蓝色的位交换位置。我们将在整本书中使用 RGB 顺序,因为 Android 的图形 API 也使用这种顺序。让我们总结一下到目前为止讨论的颜色编码:

  • 32 位浮点 RGB 编码的每个像素有 12 个字节,亮度在 0.0 和 1.0 之间变化。
  • 24 位整数 RGB 编码的每个像素有 3 或 4 个字节,亮度在 0 到 255 之间变化。组件的顺序可以是 RGB 或 BGR。在某些圈子里,这也被称为 RGB888 或 BGR888,其中 8 表示每个元件的位数。
  • 16 位整数 RGB 编码对于每个像素有 2 个字节;红色和蓝色的强度介于 0 和 31 之间,绿色的强度介于 0 和 63 之间。组件的顺序可以是 RGB 或 BGR。在某些圈子中,这也被称为 RGB565 或 BGR565,其中 5 和 6 指定相应元件的位数。

我们使用的编码类型也被称为色深。我们创建并存储在磁盘或内存中的图像具有定义的颜色深度,实际图形硬件和显示器本身的帧缓冲区也是如此。现在的显示器通常有一个默认的 24 位色深,在某些情况下可以配置得更少。图形硬件的帧缓冲区也相当灵活,它可以使用许多不同的颜色深度。当然,我们自己的图像也可以有我们喜欢的任何颜色深度。

注意对每像素颜色信息进行编码的方式还有很多。除了 RGB 颜色,我们还可以有灰度像素,它只有一个单一的组成部分。由于这些不常用,我们在这一点上忽略它们。

图像格式和压缩

在我们游戏开发过程中的某个时刻,我们的美工会给我们提供用 Gimp、Paint.NET 或 Photoshop 等图形软件制作的图像。这些图像可以以各种格式存储在磁盘上。为什么首先需要这些格式?难道我们不能将栅格数据作为字节块存储在磁盘上吗?

嗯,我们可以,但是让我们检查一下那会占用多少内存。假设我们想要最好的质量,所以我们选择以每像素 24 位的 RGB888 编码我们的像素。该图像的大小为 1,024 × 1,024 像素。这是 3MB 的一个微不足道的形象!使用 RGB565,我们可以将其降至大约 2MB。

就像音频一样,有很多关于如何减少存储图像所需内存的研究。像往常一样,采用压缩算法,专门为存储图像和尽可能多地保留原始颜色信息的需要而定制。两种最流行的格式是 JPEG 和 PNG。JPEG 是一种有损格式。这意味着一些原始信息在压缩过程中被丢弃。PNG 是一种无损格式,它将再现百分之百真实的原始图像。有损格式通常表现出更好的压缩特性,并且占用更少的磁盘空间。因此,我们可以根据磁盘内存的限制来选择使用哪种格式。

与音效类似,当我们将图像加载到内存中时,我们必须对其进行完全解压缩。因此,即使你的图像在磁盘上压缩了 20KB,你仍然需要 RAM 中的全宽乘以高乘以色深的存储空间。

一旦加载并解压缩,图像将以像素颜色数组的形式可用,与 VRAM 中的帧缓冲区布局方式完全相同。唯一的区别是像素位于普通 RAM 中,颜色深度可能不同于帧缓冲区的颜色深度。载入的图像也有一个类似 framebuffer 的坐标系,原点在左上角,x 轴指向右边,y 轴指向下面。

一旦图像被加载,我们可以简单地通过将图像中的像素颜色传输到帧缓冲区中的适当位置,将它绘制到 RAM 中的帧缓冲区。我们不用手来做这件事;相反,我们使用提供该功能的 API。

Alpha 合成和混合

在我们开始设计我们的图形模块接口之前,我们必须处理另外一件事:图像合成。为了便于讨论,假设我们有一个可以渲染的帧缓冲区,以及一组加载到 RAM 中的图像,我们将在帧缓冲区中抛出这些图像。图 3-25 显示了一个简单的背景图像,还有鲍勃,一个杀僵尸的女人缘。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-25。一个简单的背景和鲍勃,宇宙的主人

要绘制 Bob 的世界,我们首先将背景图像绘制到 framebuffer,然后在 framebuffer 中的背景图像上绘制 Bob。这个过程被称为合成,因为我们将不同的图像合成为最终的图像。我们绘制图像的顺序是相关的,因为任何新的绘制操作都会覆盖帧缓冲区中的当前内容。那么,我们合成的最终结果会是什么呢?图 3-26 给你看。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-26。将背景和鲍勃合成到帧缓冲区中(这不是我们想要的)

哎哟,这不是我们想要的。在图 3-26 中,注意 Bob 被白色像素包围。当我们在背景上绘制 Bob 到 framebuffer 时,那些白色像素也被绘制,有效地覆盖了背景。如何绘制 Bob 的图像,使得只绘制 Bob 的像素,忽略白色背景像素?

进入阿尔法混合。在 Bob 的例子中,这在技术上被称为 alpha 蒙版,但这只是 alpha 混合的一个子集。图形软件通常让我们不仅指定像素的 RGB 值,还指示其半透明性。可以把它看作是像素颜色的另一个组成部分。我们可以对它进行编码,就像我们对红色、绿色和蓝色分量进行编码一样。

我们之前暗示过,我们可以在 32 位整数中存储 24 位 RGB 三元组。在这个 32 位整数中有 8 个未使用的位,我们可以抓取并在其中存储我们的 alpha 值。然后我们可以指定一个像素的半透明度从 0 到 255,其中 0 是完全透明的,255 是不透明的。根据组件的顺序,这种编码称为 ARGB8888 或 BGRA8888。当然还有 RGBA8888 和 ABGR8888 格式。

在 16 位编码的情况下,我们有一个小问题:我们的 16 位短整型的所有位都被颜色分量占用了。让我们模仿 ARGB8888 格式,类似地定义一个 ARGB4444 格式。我们的 RGB 值总共剩下 12 位,每个颜色分量 4 位。

我们可以很容易地想象完全半透明或不透明的像素渲染方法是如何工作的。在第一种情况下,我们只需忽略 alpha 分量为零的像素。在第二种情况下,我们只需覆盖目标像素。然而,当一个像素既没有完全半透明也没有完全不透明的 alpha 分量时,事情会变得稍微复杂一点。

当以正式的方式谈论混合时,我们必须定义一些事情:

  • 混合有两个输入和一个输出,每个都表示为 RGB 三元组©加上 alpha 值(α)。
  • 这两个输入被称为目的地。源是我们要在目标图像(即帧缓冲区)上绘制的图像像素。目标像素是我们将要用源像素(部分)过度绘制的像素。
  • 输出再次是表示为 RGB 三元组和 alpha 值的颜色。不过,通常我们会忽略 alpha 值。为了简单起见,我们将在本章中这样做。
  • 为了简化数学,我们将 RGB 和 alpha 值表示为 0.0 到 1.0 范围内的浮点数。

有了这些定义,我们可以创建所谓的混合方程。最简单的等式是这样的:

red = src.red * src.alpha + dst.red * (1 – src.alpha)
blue = src.green * src.alpha + dst.green * (1 – src.alpha)
green = src.blue * src.alpha + dst.blue * (1 – src.alpha)

src 和 dst 是我们想要彼此混合的源和目标的像素。我们将这两种颜色按分量混合。请注意,在这些混合等式中缺少目标 alpha 值。让我们尝试一个例子,看看它做了什么:

src = (1, 0.5, 0.5), src.alpha = 0.5, dst = (0, 1, 0)
red = 1 * 0.5 + 0 * (10.5) = 0.5
blue = 0.5 * 0.5 + 1 * (10.5) = 0.75
red = 0.5 * 0.5 + 0 * (10.5) = 0.25

图 3-27 说明了前面的等式。我们的源颜色是粉红色,目标颜色是绿色。这两种颜色对最终输出颜色的贡献相等,导致绿色或橄榄色有点脏。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-27。混合两个像素

两位名叫波特和达夫的绅士提出了一系列混合方程式。不过,我们将坚持前面的等式,因为它涵盖了我们的大多数用例。试着在纸上或你选择的图形软件中进行实验,感受一下混合会对你的作品产生什么样的影响。

勾兑是一个很广的领域。如果你想充分利用它的潜力,我们建议你在网上搜索波特和达夫在这个问题上的原创作品。然而,对于我们将要编写的游戏,前面的等式就足够了。

请注意,前面的等式中包含了大量乘法运算(准确地说是六次)。乘法是昂贵的,我们应该尽可能避免它们。在混合的情况下,我们可以通过将源像素颜色的 RGB 值与源 alpha 值相乘来消除其中的三个乘法。大多数图形软件支持图像的 RGB 值与相应的 alphas 值相乘。如果不支持,可以在加载时在内存中实现。然而,当我们使用图形 API 绘制混合图像时,我们必须确保使用正确的混合公式。我们的图像仍然包含 alpha 值,所以前面的等式会输出不正确的结果。源 alpha 不得与源颜色相乘。幸运的是,所有 Android 图形 API 都允许我们完全指定我们想要如何混合我们的图像。

在 Bob 的例子中,我们只是在首选的图形软件程序中将所有白色像素的 alpha 值设置为零,加载 ARGB8888 或 ARGB4444 格式的图像,可能会预乘 alpha,并使用一种绘图方法,使用正确的混合公式进行实际的 alpha 混合。结果看起来像图 3-28 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-28。 Bob blended 在左边,Bob in Paint。NET .在右边。棋盘显示白色背景像素的 alpha 为零,因此背景棋盘会发光

注意JPEG 格式不支持存储每个像素的 alpha 值。在这种情况下,请使用 PNG 格式。

在实践中

有了这些信息,我们终于可以开始设计图形模块的接口了。让我们来定义这些接口的功能。注意,当我们提到 framebuffer 时,我们实际上是指我们所绘制的 UI 组件的虚拟 framebuffer。我们只是假装直接绘制到真正的帧缓冲区。我们需要能够执行以下操作:

  • 从磁盘加载图像,并将其存储在内存中,以便以后绘制。
  • 用一种颜色清除帧缓冲区,这样我们就可以清除最后一帧中仍然存在的内容。
  • 将帧缓冲区中特定位置的像素设置为特定颜色。
  • 向帧缓冲区绘制线条和矩形。
  • 将先前加载的图像绘制到帧缓冲区。我们希望能够画出完整的图像或图像的一部分。我们还需要能够绘制混合和不混合的图像。
  • 获取帧缓冲区的尺寸。

我们提出两个简单的接口:图形和位图。让我们从图形界面开始,如清单 3-6 所示。

清单 3-6。 图形界面

package com.badlogic.androidgames.framework;public interface Graphics {public static enum PixmapFormat {*ARGB8888*,*ARGB4444*,*RGB565*}public Pixmap newPixmap(String fileName, PixmapFormat format);public void clear(int color);public void drawPixel(int x, int y, int color);public void drawLine(int x, int y, int x2, int y2, int color);public void drawRect(int x, int y, int width, int height, int color);public void drawPixmap(Pixmap pixmap, int x, int y, int srcX, int srcY,int srcWidth, int srcHeight);public void drawPixmap(Pixmap pixmap, int x, int y);public int getWidth();public int getHeight();
}

我们从一个名为 PixmapFormat 的公共静态枚举开始。它编码了我们将支持的不同像素格式。接下来,我们有我们的图形界面的不同方法:

  • Graphics.newPixmap()方法将加载 JPEG 或 PNG 格式的图像。我们为生成的位图指定一个期望的格式,这是对加载机制的一个提示。产生的位图可能有不同的格式。我们这样做是为了在某种程度上控制已加载图像的内存占用(例如,通过将 RGB888 或 ARGB8888 图像加载为 RGB565 或 ARGB4444 图像)。文件名指定了我们的应用的 APK 文件中的一个素材。
  • Graphics.clear()方法用给定的颜色清除整个帧缓冲区。我们的小框架中的所有颜色将被指定为 32 位 ARGB8888 值(当然,Pixmaps 可能有不同的格式)。
  • Graphics.drawPixel()方法会将 framebuffer 中(x,y)处的像素设置为给定的颜色。屏幕外的坐标将被忽略。这叫做削波
  • Graphics.drawLine()方法类似于 Graphics.drawPixel()方法。我们指定线条的起点和终点,以及颜色。位于帧缓冲区栅格之外的线的任何部分都将被忽略。
  • Graphics.drawRect()方法在 framebuffer 中绘制一个矩形。(x,y)指定帧缓冲区中矩形左上角的位置。参数 width 和 height 指定 x 和 y 的像素数,矩形将从(x,y)开始填充。我们在 y 方向向下填充。颜色参数是用来填充矩形的颜色。
  • Graphics.drawPixmap()方法将 Pixmap 的矩形部分绘制到 framebuffer 中。(x,y)坐标指定了帧缓冲区中位图目标位置的左上角位置。参数 srcX 和 srcY 指定了矩形区域的相应左上角,该矩形区域是从像素图中使用的,在像素图自己的坐标系中给出。最后,srcWidth 和 srcHeight 指定了我们从位图中获取的部分的大小。
  • 最后,Graphics.getWidth()和 Graphics.getHeight()方法以像素为单位返回 framebuffer 的宽度和高度。

除 Graphics.clear()之外的所有绘制方法都会自动对它们接触的每个像素执行混合,如前一节所述。我们可以根据具体情况禁用混合来加快绘制速度,但这会使我们的实现变得复杂。通常,对于像 Nom 先生这样的简单游戏,我们可以一直启用混合。

列表 3-7 中的给出了 Pixmap 接口。

清单 3-7。 点阵图界面

package com.badlogic.androidgames.framework;import com.badlogic.androidgames.framework.Graphics.PixmapFormat;public interface Pixmap {public int getWidth();public int getHeight();public PixmapFormat getFormat();public void dispose();
}

我们保持它非常简单和不可变,因为合成是在帧缓冲区中完成的:

  • Pixmap.getWidth()和 Pixmap.getHeight()方法以像素为单位返回 Pixmap 的宽度和高度。
  • 方法返回 Pixmap 存储在 RAM 中的 PixelFormat。
  • 最后,还有 Pixmap.dispose()方法。Pixmap 实例会耗尽内存和潜在的其他系统资源。如果我们不再需要它们,我们应该用这种方法处理它们。

有了这个简单的图形模块,我们以后可以很容易地实现 Nom 先生。让我们以对游戏框架本身的讨论来结束这一章。

游戏框架

在我们做了所有的基础工作之后,我们终于可以谈论如何实现游戏本身了。为此,让我们确定我们的游戏必须执行哪些任务:

  • 游戏被分成不同的屏幕。每个屏幕执行相同的任务:评估用户输入,将输入应用到屏幕状态,以及渲染场景。一些屏幕可能不需要任何用户输入,只是在一段时间后转换到另一个屏幕(例如,闪屏)。
  • 屏幕需要以某种方式进行管理(也就是说,我们需要跟踪当前屏幕,并有办法过渡到新屏幕,这可以归结为销毁旧屏幕并将新屏幕设置为当前屏幕)。
  • 游戏需要授予屏幕对不同模块(图形、音频、输入等)的访问权限,以便它们可以加载资源、获取用户输入、播放声音、渲染到帧缓冲区,等等。
  • 由于我们的游戏将是实时的(这意味着事物将不断移动和更新),我们必须使当前屏幕更新其状态,并尽可能经常地呈现它自己。我们通常会在一个叫做主循环的循环中这样做。当用户退出游戏时,循环将终止。这个循环的单次迭代被称为。我们可以计算的每秒帧数(FPS)被称为帧率
  • 说到时间,我们还需要记录自上一帧以来已经过去的时间跨度。这是用于独立于帧的运动,我们将在一分钟内讨论。
  • 游戏需要跟踪窗口状态(即它是暂停还是恢复),并将这些事件通知当前屏幕。
  • 游戏框架将处理设置窗口和创建 UI 组件,我们渲染和接收输入。

让我们将其归结为一些伪代码,暂时忽略暂停和恢复等窗口管理事件:

createWindowAndUIComponent();Input input = new Input();
Graphics graphics = new Graphics();
Audio audio = new Audio();
Screen currentScreen = new MainMenu();
Float lastFrameTime = currentTime();while ( !userQuit() ) {float deltaTime = currentTime() – lastFrameTime;lastFrameTime = currentTime();currentScreen.updateState(input, deltaTime);currentScreen.present(graphics, audio, deltaTime);
}cleanupResources();

我们首先创建游戏的窗口和 UI 组件,我们向其渲染并从其接收输入。接下来,我们实例化完成底层工作所需的所有模块。我们实例化我们的开始屏幕并使它成为当前屏幕,我们记录当前时间。然后我们进入主循环,如果用户表示他或她想要退出游戏,主循环将终止。

在游戏循环内,我们计算所谓的 delta 时间。这是从最后一帧开始所经过的时间。然后我们记录下当前帧开始的时间。增量时间和当前时间通常以秒为单位。对于屏幕,delta time 表示自上次更新以来已经过了多长时间——如果我们想要进行独立于帧的移动(我们稍后将回到这一点),则需要该信息。

最后,我们简单地更新当前屏幕的状态并呈现给用户。更新取决于增量时间以及输入状态;因此,我们将它们提供给屏幕。该演示包括将屏幕状态呈现到帧缓冲区,以及回放屏幕状态所需的任何音频(例如,由于上次更新中发射的一个镜头)。表示方法可能还需要知道自上次调用以来已经过了多长时间。

当主循环终止时,我们可以清理并释放所有资源,关闭窗口。

这也是几乎所有游戏的高级工作方式:处理用户输入,更新状态,将状态呈现给用户,并无限重复(或者直到用户厌倦了我们的游戏)。

现代操作系统上的 UI 应用通常不能实时工作。它们使用基于事件的范例,其中操作系统通知应用输入事件,以及何时呈现自身。这是通过应用在启动时向操作系统注册的回调来实现的;然后,它们负责处理收到的事件通知。所有这些都发生在所谓的 UI 线程—UI 应用的主线程中。尽可能快地从回调中返回通常是一个好主意,所以我们不想在其中一个回调中实现我们的主循环。

相反,我们将游戏的主循环放在一个单独的线程中,当游戏启动时,我们将产生这个线程。这意味着当我们想要接收 UI 线程事件时,例如输入事件或窗口事件,我们必须采取一些预防措施。但是这些都是我们以后在为 Android 实现游戏框架时要处理的细节。请记住,我们需要在某些时候同步 UI 线程和游戏的主循环线程。

游戏和屏幕界面

综上所述,让我们试着设计一个游戏界面。下面是这个接口的实现必须做的事情:

  • 设置窗口和 UI 组件,并挂钩回调,以便我们可以接收窗口和输入事件。
  • 启动主循环线程。
  • 跟踪当前屏幕,并告诉它在每次主循环迭代中更新和呈现自己(也称为帧)。
  • 将任何窗口事件(例如,暂停和恢复事件)从 UI 线程转移到主循环线程,并将它们传递到当前屏幕,以便它可以相应地更改其状态。
  • 授权访问我们之前开发的所有模块:输入、文件、图形和音频。

作为游戏开发人员,我们希望不知道我们的主循环运行在什么线程上,以及我们是否需要与 UI 线程同步。我们只是想在低级模块和一些窗口事件通知的帮助下实现不同的游戏屏幕。因此,我们将创建一个非常简单的游戏界面,隐藏所有这些复杂性,以及一个抽象的屏幕类,我们将使用它来实现我们所有的屏幕。清单 3-8 显示游戏界面。

清单 3-8。 游戏界面

package com.badlogic.androidgames.framework;public interface Game {public Input getInput();public FileIO getFileIO();public Graphics getGraphics();public Audio getAudio();public void setScreen(Screen screen);public Screen getCurrentScreen();public Screen getStartScreen();
}

正如预期的那样,有几个 getter 方法可以返回我们底层模块的实例,游戏实现将实例化和跟踪这些模块。

Game.setScreen()方法允许我们设置游戏的当前屏幕。这些方法将被实现一次,连同所有的内部线程创建、窗口管理和主循环逻辑,它们将不断要求当前屏幕呈现并更新自身。

Game.getCurrentScreen()方法返回当前活动的屏幕实例。

稍后我们将使用一个名为 AndroidGame 的抽象类来实现游戏接口,它将实现除 Game.getStartScreen()方法之外的所有方法。这个方法将是一个抽象方法。如果我们为实际游戏创建 AndroidGame 实例,我们将扩展它并覆盖 Game.getStartScreen()方法,将实例返回到游戏的第一个屏幕。

为了让你对设置我们的游戏有多简单有个印象,这里有个例子(假设我们已经实现了 AndroidGame 类):

public class MyAwesomeGameextends AndroidGame {public Screen getStartScreen () {return new MySuperAwesomeStartScreen(this );}
}

太棒了,不是吗?我们所要做的就是实现我们想要用来启动游戏的屏幕,AndroidGame 类会为我们完成剩下的工作。从这一点开始,我们的 MySuperAwesomeStartScreen 将被主循环线程中的 AndroidGame 实例请求更新和呈现。注意,我们将 MyAwesomeGame 实例本身传递给屏幕实现的构造函数。

注意如果你想知道实际上是什么实例化了我们的 MyAwesomeGame 类,我们给你一个提示:AndroidGame 将从 Activity 派生,当用户启动我们的游戏时,它将由 Android 操作系统自动实例化。

拼图的最后一块是抽象类屏幕。我们让它成为一个抽象类,而不是一个接口,这样我们就可以实现一些簿记。这样,在抽象 Screen 类的实际实现中,我们必须编写更少的样板代码。清单 3-9 显示了抽象的屏幕类。

清单 3-9。 屏幕类

package com.badlogic.androidgames.framework;public abstract class Screen {protected final Game game;public Screen(Game game) {this .game = game;}public abstract void update(float deltaTime);public abstract void present(float deltaTime);public abstract void pause();public abstract void resume();public abstract void dispose();
}

事实证明,记账并没有那么糟糕。构造函数接收游戏实例,并将其存储在所有子类都可以访问的最终成员中。通过这种机制,我们可以实现两件事:

  • 我们可以访问游戏界面的底层模块来回放音频、在屏幕上绘图、获取用户输入以及读写文件。
  • 我们可以在适当的时候通过调用 Game.setScreen()来设置一个新的当前屏幕(例如,当按下一个按钮触发到新屏幕的转换时)。

第一点非常明显:我们的屏幕实现需要访问这些模块,这样它才能真正做一些有意义的事情,比如渲染大量患有狂犬病的独角兽。

第二点允许我们在屏幕实例本身中容易地实现我们的屏幕转换。每个屏幕可以根据其状态(例如,当按下菜单按钮时)决定何时转换到其他屏幕。

方法 Screen.update()和 Screen.present()现在应该是不言自明的了:它们将更新屏幕状态并相应地显示出来。游戏实例将在主循环的每次迭代中调用它们一次。

当游戏暂停或恢复时,将调用 Screen.pause()和 Screen.resume()方法。这同样由游戏实例完成,并应用于当前活动的屏幕。

如果调用 Game.setScreen(),游戏实例将调用 Screen.dispose()方法。游戏实例将通过这个方法释放当前屏幕,从而给屏幕一个机会来释放它的所有系统资源(例如,存储在 Pixmaps 中的图形资源),以便在内存中为新屏幕的资源腾出空间。对 Screen.dispose()方法的调用也是屏幕确保保存任何需要持久性的信息的最后机会。

简单的例子

继续我们的 MySuperAwesomeGame 示例,这里是 MySuperAwesomeStartScreen 类的一个非常简单的实现:

public class MySuperAwesomeStartScreen extends Screen {Pixmap awesomePic;int x;public MySuperAwesomeStartScreen(Game game) {super (game);awesomePic = game.getGraphics().newPixmap("data/pic.png",PixmapFormat.*RGB565*);}@Overridepublic void update(float deltaTime) {x += 1;if (x > 100)x = 0;}@Overridepublic void present(float deltaTime) {game.getGraphics().clear(0);game.getGraphics().drawPixmap(awesomePic, x, 0, 0, 0,awesomePic.getWidth(), awesomePic.getHeight());}@Overridepublic void pause() {// nothing to do here}@Overridepublic void resume() {// nothing to do here}@Overridepublic void dispose() {awesomePic.dispose();}
}

让我们看看这个类,结合 MySuperAwesomeGame 类,将会做什么:

  1. 当 MySuperAwesomeGame 类被创建时,它将设置窗口、我们向其呈现和从其接收事件的 UI 组件、接收窗口和输入事件的回调以及主循环线程。最后,它将调用自己的 mysuperawesomegay . getstartscreen()方法,该方法将返回 MySuperAwesomeStartScreen()类的一个实例。
  2. 在 MySuperAwesomeStartScreen 构造函数中,我们从磁盘加载一个位图,并将其存储在一个成员变量中。这就完成了我们的屏幕设置,控制权交还给了 MySuperAwesomeGame 类。
  3. 主循环线程现在将不断调用我们刚刚创建的实例的 mysuperawesomestartscreen . update()和 mysuperawesomestartscreen . present()方法。
  4. 在 mysuperawesomestartscreen . update()方法中,我们每帧增加一个名为 x 的成员。这个成员持有我们想要渲染的图像的 x 坐标。当 x 坐标值大于 100 时,我们将其重置为 0。
  5. 在 mysuperawesomestartscreen . present()方法中,我们用黑色(0x00000000 = 0)清除帧缓冲区,并在位置(x,0)呈现我们的位图。
  6. 主循环线程将重复步骤 3 到 5,直到用户按下设备上的后退按钮退出游戏。游戏实例将调用 mysuperawesomestarscreen . dispose()方法,该方法将释放位图。

这是我们第一个(不那么)激动人心的游戏!用户只会看到图像在屏幕上从左向右移动。这并不是一个令人愉快的用户体验,但我们稍后会解决这个问题。请注意,在 Android 上,游戏可以暂停,并在任何时间点恢复。然后,我们的 MyAwesomeGame 实现将调用 mysuperawesomestartscreen . pause()和 mysuperawesomestartscreen . resume()方法。只要应用本身暂停,主循环线程就会暂停。

还有最后一个我们必须要说的问题:帧率——独立运动。

帧速率-独立运动

让我们假设用户的设备可以以 60FPS 的速度运行上一节中的游戏。我们的 Pixmap 将在 100 帧中前进 100 个像素,因为我们每帧将 MySuperAwesomeStartScreen.x 成员增加 1 个像素。在 60FPS 的帧速率下,到达位置(100,0)大约需要 1.66 秒。

现在让我们假设第二个用户在不同的设备上玩我们的游戏。那个设备能够以每秒 30 帧的速度运行我们的游戏。每秒,我们的位图前进 30 个像素,所以到达位置(100,0)需要 3.33 秒。

这很糟糕。它可能不会对我们的简单游戏所产生的用户体验产生影响,但是用超级马里奥代替像素地图,并考虑以依赖于帧的方式移动他将意味着什么。假设我们按住右边的 D-pad 按钮,马里奥就会跑到右边。在每一帧中,我们将他推进 1 个像素,就像我们在像素图中所做的那样。在能以 60 FPS 运行游戏的设备上,马里奥的运行速度将是以 30 FPS 运行游戏的设备的两倍!这将完全改变用户体验,取决于设备的性能。我们需要解决这个问题。

这个问题的解决方案叫做独立于帧速率的运动。我们不是每帧固定移动我们的点阵图(或马里奥),而是指定每秒单位的移动速度。假设我们希望我们的位图每秒前进 50 个像素。除了每秒 50 像素的值之外,我们还需要关于自从我们上次移动位图以来已经过了多长时间的信息。这就是这个奇怪的 delta 时间发挥作用的地方。它告诉我们自上次更新以来已经过去了多长时间。因此,我们的 mysuperawesomestartscreen . update()方法应该如下所示:

@Override
public void update(float deltaTime) {x += 50 * deltaTime;if(x > 100)x = 0;
}

如果我们的游戏以恒定的 60FPS 运行,传递给该方法的增量时间将始终是 1/60 0.016 秒。因此,在每一帧中,我们前进 50×0.016 \u 0.83 像素。在 60FPS 下,我们推进 60×0.83∾50 像素!我们用 30FPS 来测试一下这个:50×1/30∾1.66。乘以 30FPS,我们再次每秒移动 50 个像素。因此,无论运行我们游戏的设备执行游戏的速度有多快,我们的动画和动作将始终与实际的挂钟时间保持一致。

如果我们真的用前面的代码来尝试,我们的位图根本不会以 60FPS 的速度移动。这是因为我们代码中的一个错误。我们会给你一些时间来发现它。这很微妙,但却是游戏开发中常见的陷阱。我们用来增加每一帧的 x 成员实际上是一个整数。整数加 0.83 不会有任何影响。要解决这个问题,我们只需将 x 存储为浮点数而不是整数。这也意味着我们在调用 Graphics.drawPixmap()时,必须向 int 添加一个强制转换。

注意虽然 Android 上的浮点计算通常比整数运算慢,但影响几乎可以忽略不计,所以我们可以不用使用更昂贵的浮点运算。

这就是我们游戏框架的全部内容。我们可以直接把 Mr. Nom 设计的屏幕翻译成我们的类和框架的接口。当然,一些实现细节仍然需要注意,但是我们将把它留到后面的章节。现在,你可以为自己感到骄傲。你坚持读完这一章,现在你已经准备好成为 Android(和其他平台)的游戏开发者了!

摘要

大约 50 页高度浓缩和信息丰富的内容之后,你应该对创建一个游戏有一个很好的想法。我们在 Google Play 上查看了一些最受欢迎的流派,并得出了一些结论。我们从头开始设计了一个完整的游戏,只用了剪刀、一支笔和一些纸。最后,我们探索了游戏开发的理论基础,我们甚至创建了一组接口和抽象类,我们将在本书中使用它们来实现基于这些理论概念的游戏设计。如果你觉得你想超越这里所涵盖的基础知识,那么尽一切办法在网上寻找更多的信息。你手里握着所有的关键词。理解这些原则是开发稳定且性能良好的游戏的关键。也就是说,让我们为 Android 实现我们的游戏框架吧!*

四、面向游戏开发者的 Android

Android 的应用框架非常庞大,有时会令人困惑。对于你能想到的每一个可能的任务,都有一个你可以使用的 API。当然,你必须先学习 API。幸运的是,我们游戏开发者只需要非常有限的一组 API。我们想要的只是一个有单一 UI 组件的窗口,我们可以在其中绘图,从那里我们可以接收输入,以及播放音频的能力。这涵盖了我们实现游戏框架的所有需求,我们在第三章中设计了这个框架,并且是以一种平台无关的方式。

在这一章中,你将学到实现 Nom 先生所需的最少数量的 Android APIs。您会惊讶地发现,要实现这个目标,您实际上只需要了解这些 API。让我们回忆一下我们需要哪些原料:

  • 窗口管理
  • 投入
  • 文件输入输出
  • 声音的
  • 制图法

对于这些模块中的每一个,在应用框架 API 中都有一个对应的模块。我们将挑选处理这些模块所需的 API,讨论它们的内部结构,最后实现我们在第三章设计的游戏框架的各个接口。

如果你碰巧来自 iOS/Xcode 背景,我们在本章末尾有一小段将提供一些翻译和指导。然而,在我们深入 Android 上的窗口管理之前,我们必须回顾一下我们在第二章中简单讨论过的东西:通过清单文件定义我们的应用。

定义 Android 应用:清单文件

一个 Android 应用 可以由大量不同的组件组成:

  • Activities :这些是面向用户的组件,提供一个可以与之交互的 UI。
  • 服务:这些是在后台工作的进程,没有可见的 UI。例如,服务可能负责轮询邮件服务器以获取新的电子邮件。
  • 内容提供者(Content providers):这些组件使您的应用数据的一部分对其他应用可用。
  • 意图:这些是系统或应用自己创建的消息。然后,它们被传递给任何感兴趣的一方。意图可能会通知我们系统事件,如 SD 卡被移除或 USB 电缆被连接。意图也被系统用来启动我们的应用的组件,比如活动。我们还可以触发自己的意图,要求其他应用执行某个操作,比如打开照片库来显示图像,或者启动相机应用来拍照。
  • 广播接收器:这些接收器对特定的意图做出反应,它们可能会执行一个动作,比如开始一个特定的活动或者向系统发出另一个意图。

Android 应用没有单一的入口点,就像我们习惯在桌面操作系统上拥有的那样(例如,以 Java 的 main()方法的形式)。取而代之的是,Android 应用的组件被启动或被要求执行特定意图的特定动作。

应用的清单文件中定义了我们的应用由哪些组件组成,以及这些组件对哪些意图做出反应。Android 系统使用这个清单文件来了解我们的应用是由什么组成的,比如应用启动时显示的默认活动。

注意我们只关心本书中的活动,所以我们只讨论这种类型组件的清单文件的相关部分。如果你想让自己晕头转向,你可以在 Android 开发者网站上了解更多关于 manifest 文件的信息(【http://developer.android.com】??)。

清单文件不仅仅用于定义应用的组件。以下列表总结了游戏开发环境中清单文件的相关部分:

  • 在 Google Play 上显示和使用的应用版本
  • 我们的应用可以运行的 Android 版本
  • 我们的应用需要的硬件配置文件(即多点触摸、特定的屏幕分辨率或对 OpenGL ES 2.0 的支持)
  • 使用特定组件的权限,例如写入 SD 卡或访问网络堆栈

在接下来的小节中,我们将创建一个模板清单文件,我们可以以稍微修改的方式在本书的所有项目中重用它。为此,我们将浏览定义应用所需的所有相关 XML 标记。

元素

标签是 AndroidManifest.xml 文件的根元素。这里有一个基本的例子:

<manifest xmlns:android="*[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)*"package="*com*.*helloworld*"android:versionCode="*1*"android:versionName="*1*.*0*"android:installLocation="*preferExternal*">
...
</manifest>

我们假设您以前使用过 XML,所以您应该熟悉第一行。标签指定了一个名为 android 的名称空间,该名称空间在清单文件的其余部分中使用。package 属性定义了我们的应用的根包名。稍后,我们将引用与这个包名相关的应用的特定类。

versionCode 和 versionName 属性以两种形式指定应用的版本。versionCode 属性是一个整数,每次我们发布应用的新版本时,它都必须递增。Google Play 使用它来跟踪我们应用的版本。当 Google Play 的用户浏览我们的应用时,会向他们显示 versionName 属性。我们可以在这里使用任何我们喜欢的字符串。

只有当我们在 Eclipse 中将 Android 项目的构建目标设置为 Android 2.2 或更新版本时,installLocation 属性才可用。它指定了我们的应用应该安装在哪里。字符串 preferExternal 告诉系统我们希望我们的应用安装到 SD 卡上。这只适用于 Android 2.2 或更高版本,所有早期的 Android 应用都会忽略该字符串。在 Android 2.2 或更高版本中,应用总是会尽可能地安装到内部存储中。

清单文件中 XML 元素的所有属性通常都以 android 名称空间为前缀,如前所示。为了简洁起见,在下面的部分中,当谈到特定的属性时,我们将不指定名称空间。

在元素中,我们定义了应用的组件、权限、硬件配置文件和支持的 Android 版本。

元素

与元素的情况一样,让我们以示例的形式讨论元素:

<application android:icon="@*drawable*/*icon*" android:label="@*string*/*app*_*name*">
...
</application>

这看起来是不是有点奇怪?@drawable/icon 和@string/app_name 字符串是怎么回事?在开发一个标准的 Android 应用时,我们通常会编写大量的 XML 文件,每个文件都定义了应用的一个特定部分。这些部分的完整定义要求我们还能够引用 XML 文件中没有定义的资源,比如图像或国际化字符串。这些资源位于 res/文件夹的子文件夹中,正如我们在 Eclipse 中剖析 Hello World 项目时在第二章中讨论的那样。

为了引用资源,我们使用前面的符号。@指定我们想要引用在别处定义的资源。下面的字符串标识了我们想要引用的资源的类型,它直接映射到 RES/目录中的一个文件夹或文件。最后一部分指定了资源的名称。在前面的例子中,这是一个名为 icon 的图像和一个名为 app_name 的字符串。对于图像,它是我们指定的实际文件名,可以在 res/drawable-xxx/文件夹中找到。请注意,图像名称没有像这样的后缀。png 或. jpg. Android 会根据 res/drawable-xxx/文件夹里的内容自动推断后缀。app_name 字符串在 res/values/strings.xml 文件中定义,该文件将存储应用使用的所有字符串。字符串的名称是在 strings.xml 文件中定义的。

注意Android 上的资源处理非常灵活,但也很复杂。对于这本书,我们决定跳过大部分资源处理,原因有两个:这对游戏开发来说完全是大材小用,我们想完全控制我们的资源。Android 有修改放置在 res/文件夹中的资源的习惯,尤其是图片(称为 drawables)。这是我们作为游戏开发者不希望看到的。我们建议 Android 资源系统在游戏开发中的唯一用途是国际化字符串。我们不会在本书中深入探讨这一点;相反,我们将使用更有利于游戏开发的资源/文件夹,这不会影响我们的资源,并允许我们指定自己的文件夹层次结构。

现在,元素属性的含义应该变得更清楚了。icon 属性指定 res/drawable/文件夹中的图像用作应用的图标。该图标将显示在 Google Play 以及设备上的应用启动器中。它也是我们在元素中定义的所有活动的默认图标。

label 属性指定在应用启动器中为我们的应用显示的字符串。在前面的例子中,它引用了 res/values/string.xml 文件中的一个字符串,这是我们在 Eclipse 中创建 Android 项目时指定的。我们也可以将它设置为一个原始字符串,比如我的超级棒的游戏。该标签也是我们在元素中定义的所有活动的默认标签。标签将显示在我们的应用的标题栏中。

我们只讨论了可以为元素指定的很小一部分属性。但是,这些对于我们的游戏开发需求来说已经足够了。如果你想知道更多,你可以在 Android 开发者网站上找到完整的文档。

元素包含所有应用组件的定义,包括活动和服务,以及使用的任何附加库。

元素

现在越来越有趣了。下面是我们的提名先生游戏的一个假设的例子:

<activity android:name=".*MrNomActivity*"android:label="*Mr*.*Nom*"android:screenOrientation="*portrait*">android:configChanges="*keyboard*|*keyboardHidden*|*orientation*"><intent-filter><action android:name="*android*.*intent*.*action*.*MAIN*" /><category android:name="*android*.*intent*.*category*.*LAUNCHER*" /></intent-filter>
</activity>

让我们先来看看标签的属性:

  • name:这指定了相对于我们在元素中指定的包属性的活动类的名称。您也可以在这里指定一个完全限定的类名。
  • 标签:我们已经在元素中指定了相同的属性。该标签显示在活动的标题栏中(如果有)。如果我们定义的活动是应用的入口点,标签也将用作应用启动器中显示的文本。如果我们不指定它,将使用来自元素的标签。请注意,我们在这里使用了原始字符串,而不是对 string.xml 文件中的字符串的引用。
  • screenOrientation:该属性指定活动将使用的方向。这里我们为我们的提名先生游戏指定了肖像,它只能在肖像模式下工作。或者,如果我们想在横向模式下运行,我们可以指定横向。这两种配置都将强制活动的方向在活动的生命周期中保持不变,不管设备实际上是如何定向的。如果我们忽略这个属性,那么活动将使用设备的当前方向,通常基于加速度计数据。这也意味着无论何时设备方向改变,活动都将被破坏并重新开始——这在游戏中是不可取的。我们通常将游戏活动的方向固定为横向模式或纵向模式。
  • 配置更改:重新定位设备或滑出键盘被视为配置更改。在这种变化的情况下,Android 将销毁并重新启动我们的应用来适应这种变化。这在游戏中是不可取的。元素的 configChanges 属性可以解决这个问题。它允许我们指定我们想要自己处理的配置更改,而不需要破坏和重新创建我们的活动。可以通过使用|字符连接多个配置更改来指定它们。在前面的例子中,我们自己处理键盘、隐藏键盘和方向的变化。

与元素一样,当然,您可以为元素指定更多的属性。对于游戏开发来说,我们摆脱了刚才讨论的四个属性。

现在,您可能已经注意到,元素不是空的,但是它包含另一个元素,该元素本身又包含两个元素。这些是干什么用的?

正如我们之前指出的,Android 上的应用没有单一的主入口点。相反,我们可以有多个活动和服务形式的入口点,这些入口点是为了响应系统或第三方应用发出的特定意图而启动的。不知何故,我们需要与 Android 沟通,我们的应用的哪些活动和服务将对特定意图做出反应(以及以何种方式)。这就是元素发挥作用的地方。

在前面的例子中,我们指定了两种类型的意图过滤器:一个和一个。元素告诉 Android 我们的活动是应用的主要入口。元素指定我们希望将该活动添加到应用启动器中。这两个元素一起允许 Android 推断,当应用启动器中的图标被按下时,它应该开始特定的活动。

对于和元素,唯一指定的是 name 属性,它标识活动将对其做出反应的意图。intent android . intent . action . main 是一个特殊的 intent,Android 系统使用它来启动应用的主活动。intent android . intent . category . launcher 用于告诉 Android 应用的特定活动是否应该在应用启动器中有一个条目。

通常,我们只有一个活动指定这两个意图过滤器。然而,一个标准的 Android 应用几乎总是有多个活动,这些活动也需要在 manifest.xml 文件中定义。下面是这种子活动的定义示例:

<activity android:name=".*MySubActivity*"android:label="*Sub Activity Title*"android:screenOrientation="*portrait*">android:configChanges="*keyboard*|*keyboardHidden*|*orientation*"/>

这里没有指定意图过滤器——只有我们前面讨论的活动的四个属性。当我们像这样定义一个活动时,它只对我们自己的应用可用。我们带着一种特殊的意图以编程方式开始这种类型的活动;比方说,当在一个活动中按下一个按钮来打开一个新的活动时。我们将在后面的章节中看到如何以编程方式启动一个活动。

总而言之,我们为一个活动指定了两个意图过滤器,这样它就成为了我们应用的主要入口点。对于所有其他活动,我们省略了意图过滤器规范,这样它们就在我们的应用内部。我们将以编程方式启动这些。

如前所述,我们在游戏中只会有一个活动。该活动将具有与前面所示完全相同的意图过滤器规范。我们讨论如何指定多个活动的原因是,我们将在一分钟内创建一个具有多个活动的特殊示例应用。别担心,这很容易。

元素

我们现在离开元素,回到我们通常定义为元素的子元素的元素。其中一个元素是元素。

Android 有一个复杂的安全模型。每个应用都在自己的进程和虚拟机(VM)中运行,有自己的 Linux 用户和组,它不能影响其他应用。Android 还限制系统资源的使用,如网络设施、SD 卡和录音硬件。如果我们的应用想要使用这些系统资源,我们必须请求许可。这是通过元素完成的。

权限总是具有以下形式,其中字符串指定我们想要被授予的权限的名称:

<uses-permission android:name="*string*"/>

以下是一些可能会派上用场的权限名称:

  • Android . permission . record _ AUDIO:这允许我们访问录音硬件。
  • android.permission.INTERNET:这授予我们访问所有网络 API 的权限,因此我们可以从互联网上获取图像或上传高分。
  • Android . permission . write _ EXTERNAL _ STORAGE:这允许我们读写外部存储上的文件,通常是设备的 SD 卡。
  • android.permission.WAKE_LOCK:这允许我们获得一个唤醒锁。有了这个唤醒锁,如果屏幕有一段时间没有被触摸,我们可以防止设备进入睡眠状态。例如,这可能发生在仅由加速度计控制的游戏中。
  • Android . permission . access _ COARSE _ LOCATION:这是一个非常有用的权限,因为它允许您获得非 GPS 级别的访问权限,例如用户所在的国家,这对于语言默认和分析非常有用。
  • android.permission.NFC:这允许应用通过近场通信(NFC)执行 I/O 操作,这对于涉及少量信息快速交换的各种游戏功能非常有用。

为了访问网络 API,我们将下面的元素指定为元素的子元素:

<uses-permission android:name="*android*.*permission*.*INTERNET*"/>

对于任何额外的权限,我们只需添加更多的元素。您可以指定更多的权限;我们再次建议您参考 Android 官方文档。我们只需要刚才讨论过的那套。

忘记添加访问 SD 卡等权限是常见的错误来源。它在设备日志中显示为一条消息,因此由于日志中杂乱的信息,它可能不会被发现。在随后的部分中,我们将更详细地描述日志。考虑游戏需要的权限,并在最初创建项目时指定它们。

另一件要注意的事情是,当用户安装您的应用时,他或她将首先被要求检查您的应用需要的所有权限。许多用户会跳过这些,高兴地安装他们能找到的任何东西。一些用户对他们的决定更有意识,会详细检查权限。如果你请求可疑的权限,比如发送昂贵的短信或获取用户位置的能力,当你的应用在 Google Play 上时,你可能会在评论区收到用户的一些讨厌的反馈。如果你必须使用那些有问题的权限,你的应用描述也应该告诉用户你为什么使用它。最好的办法是首先避免这些权限,或者提供合法使用它们的功能。

元素

如果你自己是一个 Android 用户,并且拥有一个像 1.5 这样的旧 Android 版本的旧设备,你会注意到一些很棒的应用不会出现在你设备上的 Google Play 应用中。其中一个原因可能是在应用的清单文件中使用了元素。

Google Play 应用将根据您的硬件配置文件过滤所有可用的应用。使用元素,应用可以指定它需要哪些硬件特性;比如多点触控或者支持 OpenGL ES 2.0。任何不具备指定功能的设备都将触发该过滤器,因此最终用户首先不会看到该应用。

一个元素具有以下属性:

<uses-feature android:name="*string*" android:required=["*true*" | "*false*"]
android:glEsVersion="*integer*" />

name 属性指定了要素本身。required 属性告诉过滤器我们是否真的在所有情况下都需要这个特性,或者它只是一个很好的特性。最后一个属性是可选的,仅在需要特定的 OpenGL ES 版本时使用。

对于游戏开发者来说,以下功能最为重要:

  • Android . hardware . touchscreen . multi touch:这要求设备具有多点触摸屏幕,能够进行基本的多点触摸交互,如挤压缩放等。这些类型的屏幕在独立跟踪多个手指方面存在问题,所以你必须评估这些功能是否足以满足你的游戏。
  • Android . hardware . touch . multi touch . distinct:这是最后一个功能的老大哥。这需要完整的多点触摸功能,适合于实现像屏幕上的虚拟双操纵杆这样的控制。

我们将在本章的后半部分研究多点触摸。现在,只要记住,当我们的游戏需要多点触摸屏幕时,我们可以通过指定一个具有前面的功能名称的元素来剔除所有不支持该功能的设备,就像这样:

<uses-feature android:name="*android*.*hardware*.*touchscreen*.*multitouch*" android:required="*true*"/>

游戏开发者要做的另一件有用的事情是指定需要哪个 OpenGL ES 版本。在本书中,我们将关注 OpenGL ES 1.0 和 1.1。对于这些,我们通常不指定元素,因为它们彼此没有太大的不同。然而,任何实现 OpenGL ES 2.0 的设备都可以被认为是图形发电站。如果我们的游戏在视觉上很复杂,需要大量的处理能力,我们可以要求 OpenGL ES 2.0,以便游戏只在能够以可接受的帧速率呈现令人惊叹的视觉效果的设备上显示。注意,我们没有使用 OpenGL ES 2.0,我们只是通过硬件类型进行过滤,以便我们的 OpenGL ES 1.x 代码获得足够的处理能力。我们可以这样做:

<uses-feature android:glEsVersion="*0x00020000*"android:required="*true*"/>

这将使我们的游戏只能在支持 OpenGL ES 2.0 的设备上显示,因此被认为具有相当强大的图形处理器。

注意一些设备错误地报告了这个特性,这将使你的应用对其他完美的设备不可见。慎用。

假设您希望为您的游戏提供可选的 USB 外设支持,以便设备可以成为 USB 主机,并连接控制器或其他外设。正确的处理方式是添加以下内容:

<uses-feature android:name="*android*.*hardware*.*usb*.*host*" android:required="*false*"/>

将“android:required”设置为 false 会对 Google Play 说,“我们可能会使用这个功能,但没有必要下载并运行游戏。”设置可选硬件功能的使用是一种很好的方法,可以让你的游戏在各种你还没有遇到过的硬件上经得起时间考验。它允许制造商将应用限制在那些声明支持其特定硬件的应用中,如果你声明支持它,你将被包括在可以为该设备下载的应用中。

现在,你在硬件方面的每一个具体要求都有可能减少可以安装游戏的设备数量,这将直接影响你的销售。在指定以上任何一项之前,请三思。例如,如果你的游戏的标准模式需要多点触摸,但你也可以想办法让它在单点触摸设备上工作,你应该努力有两个代码路径——每个硬件配置文件一个——以便你的游戏可以部署到更大的市场。

元素

我们将放入清单文件的最后一个元素是元素。它是元素的子元素。当我们在第二章中创建 Hello World 项目时,我们定义了这个元素,并确保我们的 Hello World 应用从 Android 1.5 开始通过一些手动修改就可以工作。那么这个元素是做什么的呢?这里有一个例子:

<uses-sdk android:minSdkVersion="*3*" android:targetSdkVersion="*16*"/>

正如我们在第二章中讨论的,每个 Android 版本都有一个整数,也称为 SDK 版本。< uses-sdk >元素指定了我们的应用支持的最低版本和我们的应用的目标版本。在这个例子中,我们定义我们的最低版本为 Android 1.5,目标版本为 Android 4.1。该元素允许我们将使用仅在较新版本中可用的 API 的应用部署到安装了较低版本的设备上。一个突出的例子是多点触摸 API,它从 SDK 版本 5 (Android 2.0)开始就受到支持。当我们在 Eclipse 中建立我们的 Android 项目时,我们使用一个支持该 API 的构建目标;比如 SDK 第 5 版或更高版本(我们通常设置为最新的 SDK 版本,编写时为 16)。如果我们希望我们的游戏也能在安装了 SDK version 3 (Android 1.5)的设备上运行,我们像以前一样在 manifest 文件中指定 minSdkVersion。当然,我们必须注意不要使用任何在较低版本中不可用的 API,至少在 1.5 设备上是这样。在更高版本的设备上,我们也可以使用更新的 API。

对于大多数游戏来说,前面的配置通常是合适的(除非您不能为更高版本的 API 提供单独的回退代码路径,在这种情况下,您会希望将 minSdkVersion 属性设置为您实际支持的最低 SDK 版本)。

八个简单步骤中的 Android 游戏项目设置

现在让我们结合前面的所有信息,开发一个简单的逐步方法,在 Eclipse 中创建新的 Android 游戏项目。以下是我们希望从我们的项目中得到的:

  • 它应该能够使用最新 SDK 版本的功能,同时保持与一些设备仍在运行的最低 SDK 版本的兼容性。那意味着我们要支持 Android 1.5 及以上版本。
  • 如果可能的话,应该将它安装到 SD 卡上,这样我们就不会填满设备的内部存储空间。
  • 它应该有一个单独的主活动,自己处理所有的配置更改,这样当硬件键盘暴露或者设备的方向改变时,它就不会被破坏。
  • 活动应固定为纵向或横向模式。
  • 它应该允许我们访问 SD 卡。
  • 它应该能让我们得到一个唤醒锁。

利用你刚刚获得的信息,这些是一些容易实现的目标。以下是步骤:

  1. 通过打开 new Android project 向导,在 Eclipse 中创建新的 Android 项目,如第二章中所述。
  2. 创建项目后,打开 AndroidManifest.xml 文件。
  3. 要让 Android 在 SD 卡上安装游戏(如果有的话),需要将 installLocation 属性添加到元素中,并将其设置为 preferExternal。
  4. 要固定活动的方向,将 screenOrientation 属性添加到元素,并指定您想要的方向(纵向或横向)。
  5. 要告诉 Android 我们想要处理键盘、keyboardHidden 和 orientation 配置更改,请将元素的 configChanges 属性设置为 keyboard | keyboard hidden | orientation。
  6. 在元素中添加两个元素,并指定名称属性 Android . permission . write _ external stage 和 android.permission.WAKE_LOCK。
  7. 设置元素的 minSdkVersion 和 targetSdkVersion 属性(例如,minSdkVersion 设置为 3,targetSdkVersion 设置为 16)。
  8. 在 res/文件夹中创建一个名为 drawable/的文件夹,将 RES/drawable-mdpi/IC _ launcher . png 文件复制到这个新文件夹中。这是 Android 1.5 将搜索启动器图标的位置。如果不想支持 Android 1.5,可以跳过这一步。

这就是了。八个简单的步骤将生成一个完全定义的应用,该应用将安装到 SD 卡上(在 Android 2.2 及更高版本上),具有固定的方向,不会在配置更改时爆炸,允许您访问 SD 卡和唤醒锁,并将在从 1.5 到最新版本的所有 Android 版本上工作。以下是执行上述步骤后的最终 AndroidManifest.xml 内容:

<?xml version="*1*.*0*" encoding="*utf*-*8*"?>
<manifest xmlns:android="*[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)*"package="*com*.*badlogic*.*awesomegame*"android:versionCode="*1*"android:versionName="*1*.*0*"android:installLocation="*preferExternal*"><application android:icon="@*drawable*/*icon*"android:label="*Awesomnium*"android:debuggable="*true*"><activity android:name=".*GameActivity*"android:label="*Awesomnium*"android:screenOrientation="*landscape*"android:configChanges="*keyboard*|*keyboardHidden*|*orientation*"><intent-filter><action android:name="*android*.*intent*.*action*.*MAIN*" /><category android:name="*android*.*intent*.*category*.*LAUNCHER*" /></intent-filter></activity></application><uses-permission android:name="*android*.*permission*.*WRITE*_*EXTERNAL*_*STORAGE*"/><uses-permission android:name="*android*.*permission*.*WAKE*_*LOCK*"/><uses-sdk android:minSdkVersion="*3*" android:targetSdkVersion="*16*"/>
</manifest>

如您所见,我们去掉了和元素的标签属性中的@string/app_name。这不是真正必要的,但是最好将应用定义放在一个地方。从现在开始,一切都是为了代码!或者是?

Google Play 过滤器

有这么多不同的 Android 设备,有这么多不同的功能,硬件制造商有必要只允许兼容的应用下载并在他们的设备上运行;否则,用户会有尝试运行与设备不兼容的应用的糟糕体验。为了解决这个问题,Google Play 从特定设备的可用应用列表中过滤掉不兼容的应用。例如,如果你有一个没有摄像头的设备,而你搜索一个需要摄像头的游戏,它就不会出现。不管是好是坏,对你这个用户来说,就好像这个应用不存在一样。

我们之前讨论的许多清单元素都被用作过滤器,包括、和。以下是您应该记住的另外三个特定于过滤的元素:

  • 这允许你声明游戏可以运行的屏幕尺寸和密度。理想情况下,你的游戏可以在所有屏幕上运行,我们将向你展示如何确保这一点。但是,在清单文件中,您可能希望明确声明支持每种屏幕尺寸。
  • :这允许你在设备上声明对输入配置类型的显式支持,比如硬键盘、QWERTY 专用键盘、触摸屏或者轨迹球导航输入。理想情况下,您将支持以上所有内容,但如果您的游戏需要非常具体的输入,您将需要研究并在 Google Play 上使用此标签进行过滤。
  • 这允许声明你的游戏所依赖的第三方库必须存在于设备上。例如,你可能需要一个非常大的文本到语音转换库,但是对于你的游戏来说非常普通。用这个标签声明这个库可以确保只有安装了这个库的设备才能看到和下载你的游戏。这样做的一个常见用途是允许基于 GPS/地图的游戏只能在安装了谷歌地图库的设备上运行。

随着 Android 的发展,可能会有更多的过滤器标签可用,所以请确保在部署之前查看 http://developer.android.com/guide/google/play/filters.html 的官方 Google Play 过滤器页面,以获得最新信息。

定义你游戏的图标

当你把你的游戏部署到一个设备上,打开应用启动器,你会看到它的入口有一个漂亮的,但不是真正唯一的,Android 图标。你的游戏在 Google Play 上会显示同样的图标。如何将它更改为自定义图标?

仔细看看元素。在那里,我们定义了一个名为 icon 的属性。它引用了 res/drawable-xxx 目录中一个名为 icon 的图像。所以,应该很明显要做什么:用你自己的图标图像替换 drawable 文件夹中的图标图像。

按照创建 Android 项目的八个简单步骤,你会在 res/文件夹中看到类似于图 4-1 的东西。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1。我的 res/ folder 怎么了?

我们在第一章中看到设备有不同的尺寸,但我们没有谈到 Android 如何处理这些不同的尺寸。事实证明,Android 有一个复杂的机制,允许你为一组屏幕密度定义图形素材。屏幕密度是物理屏幕尺寸和屏幕像素数量的组合。我们将在第五章中更详细地探讨这个话题。现在,知道 Android 定义了四种密度就足够了:低密度屏幕的 ldpi、标准密度屏幕的 mdpi、高密度屏幕的 hdpi 和超高密度屏幕的 xhdpi。对于低密度的屏幕,我们通常使用较小的图像;对于更高密度的屏幕,我们使用高分辨率的素材。

因此,对于我们的图标,我们需要提供四个版本:每个密度一个。但是每个版本应该有多大呢?幸运的是,我们在 res/drawable 文件夹中已经有了默认图标,可以用来重新设计我们自己图标的大小。res/drawable-ldpi 中的图标分辨率为 36×36 像素,res/drawable-mdpi 中的图标分辨率为 48×48 像素,res/drawable-hdpi 中的图标分辨率为 72×72 像素,res/drawable-xhdpi 中的图标分辨率为 96×96 像素。我们所要做的就是用相同的分辨率创建自定义图标的版本,并用我们自己的 icon.png 文件替换每个文件夹中的 icon.png 文件。我们可以保持清单文件不变,只要我们把我们的图标图像文件称为 icon.png。请注意,清单文件中的文件引用区分大小写。为了安全起见,在资源文件中总是使用小写字母。

为了真正兼容 Android 1.5,我们需要添加一个名为 res/drawable/的文件夹,并将 res/drawable-mdpi/文件夹中的图标图像放在那里。Android 1.5 不知道其他可绘制的文件夹,所以它可能找不到我们的图标。

最后,我们准备完成一些 Android 编码。

对于来自 iOS/Xcode 的用户

Android 的环境与苹果的环境有很大不同。在苹果控制非常严格的地方,Android 依赖于来自不同来源的许多不同模块,这些模块定义许多 API,控制格式,并规定哪些工具最适合特定任务,例如构建应用。

Eclipse/ADT 与。x mode(x mode)-x mode(x mode)-x mode(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)

Eclipse 是一个多项目、多文档的界面。您可以在一个工作区中拥有许多 Android 应用,它们都列在您的 Package Explorer 视图下。您还可以在源代码视图中从这些项目中打开多个文件。就像 Xcode 中的前进/后退一样,Eclipse 有一些工具栏按钮来帮助导航,甚至还有一个名为 Last Edit Location 的导航选项,可以将您带回上次所做的更改。

Eclipse 为 Java 提供了许多 Xcode 为 Objective-C 所没有的语言特性,而在 Xcode 中你必须点击“跳转到定义”,在 Eclipse 中你只需按 F3 或点击 Open Declaration。另一个最喜欢的是参考搜索功能。想知道什么调用特定的方法吗?只需点击选择它,然后按 Ctrl+Shift+G 或选择搜索外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传引用外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传工作空间。所有的重命名或移动操作都被归类为“重构”操作,所以在您因为看不到任何重命名类或文件的方法而沮丧之前,请看一下重构选项。因为 Java 没有单独的头文件和实现文件,所以没有“跳转到头文件/实现”的快捷方式。如果您启用了项目外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传自动构建,Java 文件的编译是自动的。启用该设置后,每次进行更改时,您的项目都会被增量编译。要自动完成,只需按 Ctrl+Space。

作为一名新的 Android 开发人员,您首先会注意到的一件事是,要在设备上部署,除了启用设置之外,您不必做太多其他事情。Android 上的任何可执行代码仍然需要用私钥签名,就像在 iOS 中一样,但密钥不需要由像苹果这样的可信机构颁发,所以 IDE 实际上是在您在设备上运行测试代码时为您创建了一个“调试”密钥。这个密钥将不同于您的生产密钥,但是不必为了进行应用测试而弄乱任何东西是非常有用的。密钥位于名为的子目录下的用户主目录中。android/debug.keystore。

像 Xcode 一样,Eclipse 支持 Subversion (SVN),尽管您需要安装一个插件。最常见的插件叫做 Subclipse,可以在subclipse.tigris.org获得。所有的 SVN 功能都可以在团队上下文菜单选项下获得,或者通过选择窗口外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传显示视图外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传其他外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 SVN 来打开视图。首先检查那里,以访问您的存储库,并开始签出或共享项目。

Eclipse 中的大多数东西都是上下文相关的,所以您需要右键单击(或者双击/Ctrl-click)项目、文件、类、方法以及其他任何东西的名称,看看有哪些选项。例如,第一次运行一个项目最好的方法就是右键单击项目名称,然后选择 Run As 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Android Application。

定位和配置目标

Xcode 可以有一个包含多个目标的项目,如 My Game Free 和 My Game Full,它们有不同的编译时选项,可以基于这些选项生成不同的应用。Android 在 Eclipse 中没有这种东西,因为 Eclipse 是以非常扁平化的方式面向项目的。要在 Android 中做同样的事情,你需要有两个不同的项目,它们共享所有的代码,除了那个项目的一段特殊的配置代码。共享代码非常容易,使用 Eclipse 简单的“链接源代码”特性就可以做到。

如果你习惯了 Xcode 列表和页面配置,你会很高兴听到你在 Android 中可能需要的几乎所有东西都位于以下两个位置之一:AndroidManifest.xml(本章介绍)和项目的属性窗口。Android manifest 文件涵盖了非常特定于应用的内容,就像 Xcode 目标的摘要和信息一样,项目的属性窗口涵盖了 Java 语言的特性(例如链接了哪些库,类位于何处,等等。).右键单击该项目并选择 Properties,会显示许多类别供您配置。Android 和 Java 构建路径类别处理库和源代码依赖性,很像 Xcode 中的许多构建设置、构建阶段和构建规则标签选项。事情肯定会有所不同,但是了解到哪里可以节省大量的时间。

其他有用的花絮

当然 XCode 和 Eclipse 之间有更多的区别。下面的列表告诉你那些我们认为最有用的。

  • Eclipse 显示了实际的文件系统结构,但是缓存了关于它的许多东西,所以请充分利用 F5/refresh 特性来获得项目文件的最新情况。
  • 文件位置确实很重要,而且没有相当于组的位置虚拟化。这就好像所有文件夹都是文件夹引用,不包括文件的唯一方法是设置排除过滤器。
  • 设置是基于每个工作空间的,因此您可以有多个工作空间,每个工作空间都有不同的设置。当你既有个人项目又有专业项目,并且想把它们分开时,这是非常有用的。
  • Eclipse 有多个透视图,当前透视图由 Eclipse 窗口右上角的活动图标标识,默认情况下是 Java。正如在第二章中所讨论的,透视图是一组预配置的视图和一些相关的上下文设置。如果事情在任何一点上看起来变得奇怪,检查以确保你处于正确的角度。
  • 本书涵盖了部署,但它不像在 Xcode 中那样改变方案或目标。这是一个完全独立的操作,您可以通过项目的右键上下文菜单来完成(Android Tools 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传导出签名的应用包)。
  • 如果代码编辑似乎没有生效,很可能是您的自动构建设置被关闭了。您通常希望为期望的行为启用它(项目外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传自动构建)。
  • XIB 没有直接的对等物。最接近的是 Android 布局,但 Android 不像 XIB 那样做插座,所以只要假设你会一直使用 id 惯例。大部分游戏不需要在意多种布局,但是记住就好。
  • Eclipse 在项目目录中主要使用基于 XML 的配置文件来存储项目设置。检查“点”文件,如。如果需要手动进行更改或构建自动化系统,请使用。这个加上 AndroidManifest.xml 非常类似于 Xcode 中的 project.pbxproj 文件。

Android API 基础

在这一章的剩余部分,我们将集中精力使用那些与我们游戏开发需求相关的 Android API。为此,我们将做一些相当方便的事情:我们将建立一个测试项目,该项目将包含我们将要使用的不同 API 的所有小测试示例。我们开始吧。

创建测试项目

从上一节中,我们已经知道了如何设置我们所有的项目。因此,我们要做的第一件事是执行前面列出的八个步骤。创建一个名为 ch04–Android-basics 的项目,使用名为 com.badlogic.androidgames 的包以及一个名为 AndroidBasicsStarter 的主活动。我们将使用一些旧的和一些新的 API,因此我们将最低 SDK 版本设置为 3 (Android 1.5),将构建 SDK 版本设置为 16 (Android 4.1)。您可以为其他设置填入您喜欢的任何值,例如应用的标题。从现在开始,我们要做的就是创建新的活动实现,每个实现展示 Android API 的一部分。

但是,请记住,我们只有一个主要活动。那么,我们的主要活动是什么样的呢?我们希望有一种方便的方式来添加新的活动,我们希望能够轻松地开始一个特定的活动。对于一个主要的活动,应该清楚的是,这个活动将会以某种方式为我们提供一个方法来开始一个特定的测试活动。如前所述,main 活动将被指定为清单文件中的主入口点。我们添加的每一个额外的活动都将在没有子元素的情况下被指定。我们将从主活动中以编程方式启动它们。

AndroidBasicsStarter 活动

Android API 为我们提供了一个名为 ListActivity 的特殊类,它来自我们在 Hello World 项目中使用的 Activity 类。ListActivity 类是一种特殊类型的活动,它的唯一目的是显示一个事物列表(例如,字符串)。我们使用它来显示我们的测试活动的名称。当我们触摸其中一个列表项时,我们将以编程方式启动相应的活动。清单 4-1 显示了我们的 AndroidBasicsStarter 主活动的代码。

***清单 4-1。***AndroidBasicsStarter.java,我们的主要活动负责列出并开始我们所有的测试

package com.badlogic.androidgames;import android.app.ListActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;public class AndroidBasicsStarter extends ListActivity {String tests[] = { "LifeCycleTest", "SingleTouchTest", "MultiTouchTest","KeyTest", "AccelerometerTest", "AssetsTest","ExternalStorageTest", "SoundPoolTest", "MediaPlayerTest","FullScreenTest", "RenderViewTest", "ShapeTest", "BitmapTest","FontTest", "SurfaceViewTest" };public void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);setListAdapter(new ArrayAdapter<String>(this ,android.R.layout.*simple*_*list*_*item*_*1*, tests));}@Overrideprotected void onListItemClick(ListView list, View view, int position,long id) {super .onListItemClick(list, view, position, id);String testName = tests[position];try {Class clazz = Class.*forName*("com.badlogic.androidgames." + testName);Intent intent = new Intent(this , clazz);startActivity(intent);}catch (ClassNotFoundException e) {e.printStackTrace();}}
}

我们选择的包名是 com.badlogic.androidgames。这些就是我们将在代码中使用的所有类。我们的 AndroidBasicsStarter 类派生自 ListActivity 类——仍然没有什么特别的。field tests 是一个字符串数组,它保存了我们的 starter 应用应该显示的所有测试活动的名称。请注意,数组中的名称正是我们稍后要实现的活动类的 Java 类名。

下一段代码应该是熟悉的;我们必须为我们的每个活动实现 onCreate()方法,,该方法将在创建活动时被调用。记住,我们必须调用活动基类的 onCreate()方法。这是我们在自己的活动实现的 onCreate()方法中必须做的第一件事。如果我们不这样做,将会抛出一个异常,并且不会显示该活动。

这样一来,接下来我们要做的是调用一个名为 setListAdapter()的方法。这个方法是由派生它的 ListActivity 类提供给我们的。它让我们指定希望 ListActivity 类为我们显示的列表项。这些需要以实现 ListAdapter 接口的类实例的形式传递给该方法。我们使用方便的 ArrayAdapter 类来做到这一点。这个类的构造函数有三个参数:第一个是我们的活动,第二个我们将在下一段解释,第三个是 ListActivity 应该显示的项目数组。我们很乐意为第三个参数指定我们之前定义的测试数组,这就是我们需要做的全部工作。

那么, ArrayAdapter 构造函数的第二个参数是什么?为了解释这一点,我们不得不经历所有的 Android UI API 的东西,我们不打算在本书中使用。因此,我们不会在我们不需要的东西上浪费页面,而是给你一个简单明了的解释:列表中的每一项都通过视图显示。该参数定义了每个视图的布局以及每个视图的类型。安卓的价值。R.layout.simple_list_item_1 是 UI API 提供的预定义常量,用于快速启动和运行。它代表将显示文本的标准列表项视图。作为快速复习,视图是 Android 上的 UI 小部件,比如按钮、文本字段或滑块。在第二章中,我们在剖析 HelloWorldActivity 时引入了按钮实例形式的视图。

如果我们用 onCreate()方法开始我们的活动,我们会看到类似于图 4-2 所示的屏幕。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-2。我们的测试启动活动,看起来很花哨,但还没做多少

现在让我们在触摸列表项时发生一些事情。我们希望开始我们接触的列表项所代表的相应活动。

以编程方式启动活动

ListActivity 类有一个名为 onListItemClick()的受保护方法,当点击一个项目时将调用该方法。我们所需要做的就是在我们的 AndroidBasicsStarter 类中覆盖该方法。这正是我们在清单 4-1 中所做的。

这个方法的参数是 ListActivity 用来显示项目的 ListView、被触摸的视图(包含在这个 ListView 中)、被触摸的项目在列表中的位置,以及一个 ID,我们并不太感兴趣。我们真正关心的是立场论点。

onListItemClicked()方法从成为好公民开始,首先调用基类方法。如果我们覆盖一个活动的方法,这总是一件好事。接下来,我们根据 position 参数从 tests 数组中获取类名。这是拼图的第一部分。

前面,我们讨论了我们可以通过一个意图以编程方式启动我们在清单文件中定义的活动。Intent 类有一个很好的简单的构造函数来做这件事,它有两个参数:一个上下文实例和一个类实例。后者表示我们想要启动的活动的 Java 类。

上下文是为我们提供应用全局信息的接口。它是由 Activity 类实现的,所以我们只需将这个引用传递给 Intent 构造函数。

为了获得表示我们想要启动的活动的类实例,我们使用了一点反射,如果您使用过 Java,这可能对您来说很熟悉。反射允许我们在运行时以编程方式检查、实例化和调用类。静态方法 Class.forName()接受一个字符串,该字符串包含我们要为其创建类实例的类的完全限定名。我们稍后将实现的所有测试活动都将包含在 com.badlogic.androidgames 包中。将包名与我们从 tests 数组中获取的类名连接起来,将得到我们想要启动的 activity 类的完全限定名。我们将该名称传递给 Class.forName(),并获得一个可以传递给 Intent 构造函数的不错的类实例。

一旦构建了 Intent 实例,我们就可以通过调用 startActivity()方法来启动它。这个方法也在上下文接口中定义。因为我们的活动实现了那个接口,所以我们只调用它的那个方法的实现。就是这样!

那么我们的应用将如何表现呢?首先,将显示启动器活动。每次我们触摸列表上的一个项目,相应的活动就会启动。启动活动将暂停,并进入后台。新活动将由我们发出的意向创建,并将替换屏幕上的起始活动。当我们按下 Android 设备上的 back 按钮时,活动被破坏,starter 活动恢复,收回屏幕。

创建测试活动

当我们创建一个新的测试活动时,我们必须执行以下步骤:

  1. 在 com.badlogic.androidgames 包中创建相应的 Java 类,并实现其逻辑。
  2. 在清单文件中为 activity 添加一个条目,使用它需要的任何属性(即 android:configChanges 或 android:screenOrientation)。注意,我们不会指定一个元素,因为我们将以编程方式启动活动。
  3. 将活动的类名添加到 AndroidBasicsStarter 类的 tests 数组中。

只要我们坚持这个过程,其他一切都将由我们在 AndroidBasicsStarter 类中实现的逻辑来处理。新的活动会自动出现在列表中,只需轻轻一触就能启动。

您可能想知道的一件事是,在 touch 上开始的测试活动是否在它自己的进程和 VM 中运行。不是的。由活动组成的应用有一个叫做活动栈的东西。每次我们开始一个新的活动,它就会被推到堆栈上。当我们关闭新的活动时,最后一个推入堆栈的活动将被弹出并恢复,成为屏幕上新的活动活动。

这也有一些其他的含义。首先,应用的所有活动(堆栈上暂停的活动和活动的活动)共享同一个 VM。它们还共享同一个内存堆。这可能是福也可能是祸。如果您的活动中有静态字段,它们一启动就会在堆上获得内存。作为静态字段,它们将在活动的销毁和活动实例的后续垃圾收集中幸存。如果您不小心使用静态字段,这可能会导致一些严重的内存泄漏。在使用静态字段之前要三思。

正如已经说过几次的,我们在实际的游戏中只会有一个活动。前面的活动启动器是这个规则的一个例外,让我们的生活变得更轻松。但是不用担心;即使是一项活动,我们也有很多机会陷入困境。

注意这是我们对 Android UI 编程的最深理解。从现在开始,我们将总是在活动中使用单个视图来输出内容和接收输入。如果你想了解布局、视图组和 Android UI 库提供的所有功能,我们建议你看看格兰特·艾伦的书,《??》开始 Android 4(2011 年出版),或者 Android 开发者网站上的优秀开发者指南。

活动生命周期

在为 Android 编程时,我们首先要弄清楚的是一个活动是如何表现的。在 Android 上,这被称为活动生命周期。它描述了活动所处的状态以及这些状态之间的转换。我们先来讨论一下这背后的理论。

理论上

活动可以处于以下三种状态之一:

  • 运行:在这种状态下,占据屏幕并直接与用户交互的是顶层活动。
  • 暂停:当活动在屏幕上仍然可见,但被透明活动或对话框部分遮挡,或者设备屏幕被锁定时,会出现这种情况。Android 系统可以在任何时间点终止暂停的活动(例如,由于内存不足)。请注意,活动实例本身仍然活跃在 VM 堆中,并等待返回到运行状态。
  • Stopped :当一个活动被另一个活动完全遮挡,从而在屏幕上不再可见时,就会出现这种情况。例如,如果我们开始一个测试活动,我们的 AndroidBasicsStarter 活动将处于这种状态。当用户按下主屏幕按钮暂时转到主屏幕时,也会发生这种情况。如果内存不足,系统可以再次决定完全终止该活动并将其从内存中删除。

在暂停和停止状态下,Android 系统可以决定在任何时间点终止活动。它可以礼貌地这样做,首先通过调用它的 finished()方法通知活动,也可以不礼貌地这样做,悄悄终止活动的进程。

活动可以从暂停或停止状态返回到运行状态。再次注意,当活动从暂停或停止状态恢复时,它仍然是内存中的同一个 Java 实例,因此所有状态和成员变量都与活动暂停或停止前相同。

一个活动有一些受保护的方法,我们可以覆盖这些方法来获得关于状态变化的信息:

  • Activity.onCreate():当我们的活动第一次启动时调用这个函数。在这里,我们设置了所有的 UI 组件并连接到输入系统。这个方法在我们活动的生命周期中只被调用一次。
  • Activity.onRestart():当活动从停止状态恢复时调用这个函数。它前面是对 onStop()的调用。
  • Activity.onStart():在 onCreate()之后或者当活动从停止状态恢复时调用这个函数。在后一种情况下,它前面是对 onRestart()的调用。
  • Activity.onResume():在 onStart()之后或者当活动从暂停状态恢复时(例如,当屏幕解锁时)调用这个函数。
  • Activity.onPause():当活动进入暂停状态时调用该函数。这可能是我们收到的最后一个通知,因为 Android 系统可能会决定悄悄地杀死我们的应用。我们要用这种方法保存所有我们想坚持的状态!
  • Activity.onStop():当活动进入停止状态时调用该函数。它前面有一个对 onPause()的调用。这意味着活动在暂停之前就已停止。和 onPause()一样,这可能是我们在 Android 系统静默终止活动之前收到的最后一个通知。我们也可以在这里保存持久状态。然而,系统可能决定不调用这个方法,而只是终止活动。由于 onPause()总是在 onStop()之前和活动被静默终止之前被调用,我们宁愿将所有内容保存在 onPause()方法中。
  • Activity.onDestroy():当活动被不可恢复地销毁时,在活动生命周期结束时调用这个函数。这是我们最后一次保存任何信息,以便在下次重新创建活动时恢复。请注意,如果活动在系统调用 onPause()或 onStop()后被静默销毁,则实际上可能永远不会调用此方法。

图 4-3 说明了活动生命周期和方法调用顺序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-3。浩浩荡荡、令人困惑的活动生命周期

以下是我们应该从中吸取的三大教训:

  1. 在我们的活动进入运行状态之前,无论我们是从停止状态还是暂停状态恢复,onResume()方法总是被调用。因此,我们可以放心地忽略 onRestart()和 onStart()方法。我们不关心是从停止状态还是暂停状态恢复。对于我们的游戏,我们只需要知道我们现在实际上正在运行,onResume()方法向我们发出信号。
  2. 在 onPause()之后,可以静默地销毁该活动。我们永远不应该假设 onStop()或 onDestroy()被调用。我们还知道 onPause()总是在 onStop()之前被调用。因此,我们可以安全地忽略 onStop()和 onDestroy()方法,只重写 onPause()。在这种方法中,我们必须确保我们想要保持的所有状态,如高分和等级进步,都被写入外部存储,如 SD 卡。在 onPause()之后,所有的赌注都取消了,我们不知道我们的活动是否还有机会再次运行。
  3. 我们知道,如果系统在 onPause()或 onStop()之后决定终止活动,则可能永远不会调用 onDestroy()。然而,有时我们想知道活动是否真的会被扼杀。那么,如果 onDestroy()不会被调用,我们该怎么做呢?Activity 类有一个名为 Activity.isFinishing()的方法,我们可以随时调用它来检查我们的活动是否会被终止。我们至少可以保证 onPause()方法在 activity 被终止之前被调用。我们所需要做的就是在 onPause()方法中调用这个 isFinishing()方法,以决定在 onPause()调用之后活动是否会终止。

这让生活变得简单多了。我们只覆盖 onCreate()、onResume()和 onPause()方法。

  • 在 onCreate()中,我们设置我们的窗口和 UI 组件,向其呈现内容,并从其接收输入。
  • 在 onResume()中,我们(重新)开始我们的主循环线程(在第三章的中讨论)。
  • 在 onPause()中,我们简单地暂停我们的主循环线程,如果 Activity.isFinishing()返回 true,我们还会将我们希望保持的任何状态保存到磁盘中。

许多人纠结于活动的生命周期,但是如果我们遵循这些简单的规则,我们的游戏将能够处理暂停、恢复和清理。

在实践中

让我们编写演示活动生命周期的第一个测试示例。我们希望有某种输出来显示到目前为止发生了哪些状态变化。我们将通过两种方式做到这一点:

  1. 活动将显示的唯一 UI 组件是一个 TextView。顾名思义,它显示文本,我们已经在 starter 活动中隐式地使用它来显示每个条目。每当我们进入一个新的状态时,我们将向 TextView 追加一个字符串,它将显示到目前为止发生的所有状态变化。
  2. 我们将无法在 TextView 中显示活动的销毁事件,因为它会很快从屏幕上消失,所以我们还会将所有状态更改输出到 LogCat。我们用 Log 类来实现这一点,它提供了两个静态方法来将消息添加到 LogCat 中。

记住我们需要做什么来添加一个测试活动到我们的测试应用中。首先,我们在清单文件中以元素的形式定义它,它是元素的子元素:

<activity android:label="*Life Cycle Test*"android:name=".*LifeCycleTest*"android:configChanges="*keyboard*|*keyboardHidden*|*orientation*" />

接下来,我们将名为 LifeCycleTest 的新 Java 类添加到我们的 com.badlogic.androidgames 包中。最后,我们将类名添加到前面定义的 androidbasicstarter 类的 tests 成员中。(当然,当我们出于演示的目的编写这个类时,我们就已经有了。)

对于我们在接下来的部分中创建的任何测试活动,我们将不得不重复所有这些步骤。为简洁起见,我们不再提及这些步骤。还要注意,我们没有为 LifeCycleTest 活动指定方向。在本例中,我们可以处于横向模式或纵向模式,具体取决于设备方向。我们这样做是为了让您可以看到方向更改对生命周期的影响(由于我们如何设置 configChanges 属性,所以没有影响)。清单 4-2 显示了整个活动的代码。

清单 4-2。【LifeCycleTest.java】,展示活动生命周期

package com.badlogic.androidgames;import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;public class LifeCycleTest extends Activity {StringBuilder builder = new StringBuilder();TextView textView;private void log(String text) {Log.*d*("LifeCycleTest", text);builder.append(text);builder.append('\n');textView.setText(builder.toString());}@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);textView = new TextView(this );textView.setText(builder.toString());setContentView(textView);log("created");}@Overrideprotected void onResume() {super .onResume();log("resumed");}@Overrideprotected void onPause() {super .onPause();log("paused");if (isFinishing()) {log("finishing");}}
}

让我们快速浏览一下这段代码。这个类来源于 Activity——这并不奇怪。我们定义了两个成员:一个是 StringBuilder,它将保存我们到目前为止生成的所有消息,另一个是 TextView,我们用它直接在活动中显示这些消息。

接下来,我们定义一个小的私有 helper 方法,它将把文本记录到 LogCat,把它附加到我们的 StringBuilder,并更新 TextView 文本。对于 LogCat 输出,我们使用静态 Log.d()方法,该方法将一个标记作为第一个参数,将实际消息作为第二个参数。

在 onCreate()方法中,我们像往常一样首先调用超类方法。我们创建 TextView 并将其设置为活动的内容视图。它将填满活动的整个空间。最后,我们将创建的消息记录到 LogCat 中,并使用之前定义的 helper 方法 log()更新 TextView 文本。

接下来,我们覆盖活动的 onResume()方法。与我们覆盖的任何活动方法一样,我们首先调用超类方法。我们所做的就是再次调用 log()并将 resumed 作为参数。

被覆盖的 onPause()方法看起来很像 onResume()方法。我们首先将消息记录为“暂停”。我们还想知道在 onPause()方法调用之后活动是否会被销毁,所以我们检查 Activity.isFinishing()方法。如果它返回 true,我们也记录完成事件。当然,我们将看不到更新的 TextView 文本,因为在更改显示在屏幕上之前,活动将被销毁。因此,如前所述,我们也将所有内容输出到 LogCat。

运行应用,并稍微试验一下这个测试活动。下面是您可以执行的一系列操作:

  1. 从启动活动启动测试活动。
  2. 锁屏。
  3. 解锁屏幕。
  4. 按下主屏幕按钮(这将带你回到主屏幕)。
  5. 在主屏幕上,在旧的 Android 版本(版本 3 之前)上,按住 home 键,直到出现当前正在运行的应用。在 Android 版本 3+上,触摸运行应用按钮。选择 Android 基础入门应用以继续(这将使测试活动回到屏幕上)。
  6. 按“后退”按钮(这将带您返回到开始活动)。

如果你的系统在暂停的任何时候都没有决定静默终止活动,你会在图 4-4 中看到输出(当然,前提是你还没有按下返回按钮)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-4。运行生命周期测试活动

启动时,调用 onCreate(),然后调用 onResume()。当我们锁定屏幕时,调用 onPause()。当我们解锁屏幕时,调用 onResume()。当我们按下 home 键时,onPause()被调用。回到活动将再次调用 onResume()。当然,相同的消息显示在 LogCat 中,您可以在 Eclipse 的 LogCat 视图中观察到。图 4-5 显示了我们在执行前面的动作序列(加上按下后退按钮)时写入 LogCat 的内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-5。生命周期测试的 LogCat 输出

再次按 back 按钮调用 onPause()方法。由于它也破坏了活动,onPause()中的 if 语句也被触发,通知我们这是最后一次看到该活动。

这就是活动生命周期,为了我们的游戏编程需要而被去神秘化和简化。我们现在可以轻松地处理任何暂停和恢复事件,并保证在活动被销毁时得到通知。

输入设备处理

正如前面章节所讨论的,我们可以从 Android 上的许多不同的输入设备中获取信息。在这一部分,我们将讨论 Android 上三个最相关的输入设备以及如何使用它们:触摸屏、键盘、加速度计和指南针。

获取(多点)触摸事件

触摸屏可能是获取用户输入的最重要的方式。在 Android 版本之前,API 只支持处理单指触摸事件。多点触控是在 Android 2.0 (SDK 版本 5)中引入的。多点触摸事件报告被标记在单触式 API 上,在可用性方面有一些混合的结果。我们将首先研究处理单点触摸事件,这在所有 Android 版本上都可用。

处理单点触摸事件

当我们在第二章中处理点击按钮时,我们看到监听器接口是 Android 向我们报告事件的方式。触摸事件也不例外。触摸事件被传递给一个 OnTouchListener 接口实现,我们用一个视图注册它。OnTouchListener 接口只有一个方法:

public abstract boolean onTouch (View v, MotionEvent event)

第一个参数是触摸事件被调度到的视图。第二个参数是我们将分析以获得触摸事件的内容。

OnTouchListener 可以通过 View.setOnTouchListener()方法注册到任何视图实现中。在将 MotionEvent 分派给视图本身之前,将调用 OnTouchListener。在 onTouch()方法的实现中,我们可以通过从该方法返回 true 来通知视图我们已经处理了该事件。如果我们返回 false,视图本身将处理该事件。

MotionEvent 实例有三个与我们相关的方法:

  • MotionEvent.getX()和 MotionEvent.getY():这些方法报告触摸事件相对于视图的 x 和 y 坐标。坐标系定义为原点在视图的左上方,x 轴指向右侧,y 轴指向下方。坐标以像素为单位。请注意,这些方法返回浮点数,因此坐标具有子像素精度。
  • MotionEvent.getAction():该方法返回触摸事件的类型。它是一个整数,取值为MotionEvent.ACTION_DOWNMotionEvent.ACTION_MOVEMotionEvent.ACTION_CANCELMotionEvent.ACTION_UP中的一个。

听起来很简单,事实也确实如此。运动事件。手指触摸屏幕时发生 ACTION_DOWN 事件。当手指移动时,类型为 MotionEvent 的事件。ACTION_MOVE 被触发。请注意,您将始终获得 MotionEvent。动作 _ 移动事件,因为你不能保持手指不动来避免它们。触摸传感器将识别最轻微的变化。当手指再次抬起时,MotionEvent。报告了 ACTION_UP 事件。运动事件。ACTION_CANCEL 事件有点神秘。文档显示,当当前手势被取消时,它们将被触发。我们还从未在现实生活中见过这一事件。然而,我们仍然会处理它,并假设它是一个运动事件。当我们开始实现我们的第一个游戏时的 ACTION_UP 事件。

让我们编写一个简单的测试活动,看看这在代码中是如何工作的。该活动应该显示手指在屏幕上的当前位置以及事件类型。清单 4-3 显示了我们的成果。

清单 4-3。【SingleTouchTest.java】;测试单点触摸操作

package com.badlogic.androidgames;import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.TextView;public class SingleTouchTest extends Activity implements OnTouchListener {StringBuilder builder = new StringBuilder();TextView textView;public void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);textView = new TextView(this );textView.setText("Touch and drag (one finger only)!");textView.setOnTouchListener(this );setContentView(textView);}public boolean onTouch(View v, MotionEvent event) {builder.setLength(0);switch (event.getAction()) {case MotionEvent.*ACTION*_*DOWN*:builder.append("down, ");break ;case MotionEvent.*ACTION*_*MOVE*:builder.append("move, ");break ;case MotionEvent.*ACTION*_*CANCEL*:builder.append("cancel", ");break ;case MotionEvent.*ACTION*_*UP*:builder.append("up, ");break ;}builder.append(event.getX());builder.append(", ");builder.append(event.getY());String text = builder.toString();Log.*d*("TouchTest", text);textView.setText(text);return true ;}
}

我们让我们的活动实现 OnTouchListener 接口。我们还有两个成员:一个用于 TextView,另一个用于构造事件字符串的 StringBuilder。

onCreate()方法是不言自明的。惟一的新颖之处是对 TextView.setOnTouchListener()的调用,在这里我们向 TextView 注册了我们的活动,以便它接收 MotionEvents。

剩下的就是 onTouch()方法实现本身。我们忽略视图参数,因为我们知道它必须是 TextView。我们感兴趣的是获取触摸事件类型,将标识它的字符串追加到我们的 StringBuilder,追加触摸坐标,并更新 TextView 文本。就这样。我们还将事件记录到 LogCat 中,这样我们就可以看到事件发生的顺序,因为 TextView 只会显示我们处理的最后一个事件(每次调用 onTouch()时,我们都会清除 StringBuilder)。

onTouch()方法中一个微妙的细节是 return 语句,在这里我们返回 true。通常,我们会坚持侦听器的概念并返回 false,以便不干扰事件调度过程。如果我们在示例中这样做,我们将不会得到除了 MotionEvent 之外的任何事件。ACTION_DOWN 事件因此,我们告诉 TextView 我们刚刚消费了事件。在不同的视图实现之间,这种行为可能会有所不同。幸运的是,在本书的其余部分,我们只需要其他三个视图,这些视图将让我们愉快地消费我们想要的任何事件。

如果我们在模拟器或连接的设备上启动该应用,我们可以看到 TextView 总是显示向 onTouch()方法报告的最后一个事件类型和位置。此外,您可以在 LogCat 中看到相同的消息。

我们没有修复清单文件中活动的方向。当然,如果您旋转设备,使活动处于横向模式,坐标系也会改变。图 4-6 显示了纵向模式(左)和横向模式(右)下的活动。在这两种情况下,我们都试图触及视图的中间。注意 x 和 y 坐标是如何交换的。该图还显示了两种情况下的 x 轴和 y 轴(黄线),以及屏幕上我们粗略触摸过的点(绿圈)。在这两种情况下,原点都在 TextView 的左上角,x 轴指向右侧,y 轴指向下方。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-6。在纵向和横向模式下触摸屏幕

当然,根据方向的不同,我们的最大 x 和 y 值也会变化。前面的图片是在运行 Android 2.2 (Froyo)的 Nexus One 上拍摄的,它在人像模式下的屏幕分辨率为 480×800 像素(在风景模式下为 800×480)。由于触摸坐标是相对于视图给出的,并且视图没有填满整个屏幕,因此我们的最大 y 值将小于分辨率高度。稍后我们将看到如何启用全屏模式,以便标题栏和通知栏不会妨碍我们。

遗憾的是,旧版本 Android 和第一代设备上的触摸事件存在一些问题:

  • 触摸事件泛滥:当手指在触摸屏上按下时,司机会报告尽可能多的触摸事件——在一些设备上,每秒数百次。我们可以通过将 Thread.sleep(16)调用放入我们的 onTouch()方法中来解决这个问题,这将使分派这些事件的 UI 线程休眠 16 毫秒。这样的话,我们每秒最多可以处理 60 个事件,这对于一个反应灵敏的游戏来说已经足够了。这只是安卓 1.5 版本设备上的问题。如果你的目标不是那个 Android 版本,忽略这个建议。
  • 触屏吃**CPU:即使我们在我们的 onTouch()方法中休眠,系统也要处理驱动程序报告的内核中的事件。在老设备上,比如 Hero 或 G1,这可以使用高达 50%的 CPU,这使得我们的主循环线程的处理能力大大降低。因此,我们完美的帧速率将会大大下降,有时会到游戏无法播放的程度。在第二代设备上,这个问题要小得多,通常可以忽略。遗憾的是,在旧设备上没有解决方案。

处理多点触摸事件

警告:前方剧痛!multitouch API 已经被标记到 MotionEvent 类中,该类最初只处理单点触摸。当试图解码多点触摸事件时,这造成了一些主要的混乱。让我们试着理解它。

注意多点触控 API 显然也让开发它的 Android 工程师感到困惑。它在 SDK 版本 8 (Android 2.2)中得到了重大改进,增加了新方法、新常量,甚至重命名了常量。这些变化应该会让多点触控的使用变得更加容易。但是,它们仅从 SDK 版本 8 开始提供。为了支持所有支持多点触摸的 Android 版本(2.0 以上),我们必须使用 SDK 版本 5 的 API。

处理多点触摸事件与处理单点触摸事件非常相似。我们仍然实现了与单触事件相同的 OnTouchListener 接口。我们还获得了一个从中读取数据的 MotionEvent 实例。我们还处理之前处理过的事件类型,比如 MotionEvent。ACTION_UP,加上几个没什么大不了的新功能。

指针 id 和索引

当我们想要访问触摸事件的坐标时,处理多触摸事件和处理单触摸事件之间的区别就开始了。MotionEvent.getX()和 MotionEvent.getY()返回单个手指在屏幕上的坐标。当我们处理多点触摸事件时,我们使用这些方法的重载变体,它们接受一个指针索引。这可能看起来像这样:

event.getX(pointerIndex);
event.getY(pointerIndex);

现在,人们会期望指针索引直接对应于触摸屏幕的手指之一(例如,触摸的第一个手指的指针索引为 0,触摸的下一个手指的指针索引为 1,依此类推)。不幸的是,事实并非如此。

pointerIndex 是 MotionEvent 内部数组的索引,它保存触摸屏幕的特定手指的事件坐标。手指在屏幕上的真实标识符被称为指针标识符。指针标识符是唯一标识触摸屏幕的指针的一个实例的任意数字。有一个单独的方法叫做 motion event . getpointeridentifier(int pointer index),它基于指针索引返回指针标识符。只要单个手指接触屏幕,指针标识符将保持不变。指针索引不一定如此。重要的是要理解两者之间的区别,并理解你不能依赖于第一次触摸是索引 0,ID 0,因为在一些设备上,特别是 Xperia Play 的第一个版本,指针 ID 总是会增加到 15,然后从 0 开始,而不是重复使用 ID 的最低可用数字。

让我们从研究如何到达一个事件的指针索引开始。我们现在将忽略事件类型。

int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;

当我们第一次实现它时,您可能会有同样的想法。在我们对人性失去信心之前,让我们试着破译这里发生了什么。我们通过 MotionEvent.getAction()从 MotionEvent 获取事件类型。很好,我们以前做过。接下来,我们使用从 MotionEvent.getAction()方法获得的整数和一个名为 MotionEvent 的常量执行按位 AND 运算。动作 _ 指针 _ 标识 _ 掩码。现在好戏开始了。

该常量的值为 0xff00,因此我们基本上将所有位设为 0,但第 8 位至第 15 位除外,它们保存事件的指针索引。event.getAction()返回的整数的低 8 位保存事件类型的值,如 MotionEvent。ACTION_DOWN 及其同级。通过这种位运算,我们实际上抛弃了事件类型。这种转变现在应该更有意义了。我们通过运动事件转移。ACTION_POINTER_ID_SHIFT,其值为 8,因此我们基本上将第 8 位到第 15 位移动到第 0 位到第 7 位,从而得到事件的实际指针索引。这样,我们就可以获得事件的坐标,以及指针标识符。

请注意,我们的神奇常数被称为 XXX_POINTER_ID_XXX,而不是 XXX_POINTER_INDEX_XXX(这更有意义,因为我们实际上想要提取指针索引,而不是指针标识符)。好吧,安卓工程师一定也很困惑。在 SDK 版本 8 中,他们弃用了这些常数,并引入了名为 XXX_POINTER_INDEX_XXX 的新常数,这些常数与弃用的常数具有完全相同的值。为了让针对 SDK 第 5 版编写的遗留应用继续在较新的 Android 版本上工作,旧的常量仍然可用。

所以我们现在知道如何获得神秘的指针索引,我们可以用它来查询事件的坐标和指针标识符。

动作掩码和更多事件类型

接下来,我们必须获得纯事件类型减去附加指针索引,该指针索引编码在由 MotionEvent.getAction()返回的整数中。我们只需要屏蔽掉指针索引:

int action = event.getAction() & MotionEvent.ACTION_MASK;

好吧,那很简单。遗憾的是,只有当你知道指针索引是什么,并且它实际上编码在动作中时,你才能理解它。

剩下的就是像我们之前做的那样解码事件类型。我们已经说过有一些新的事件类型,现在让我们来看一下:

  • 运动事件。ACTION_POINTER_DOWN:在第一个手指触摸屏幕后,任何其他手指触摸屏幕都会发生此事件。第一个手指仍然产生运动事件。ACTION_DOWN 事件
  • 运动事件。ACTION_POINTER_UP:这类似于前面的操作。当一个手指从屏幕上抬起,并且不止一个手指触摸屏幕时,就会触发这个事件。屏幕上最后一个被抬起的手指将产生一个运动事件。ACTION_UP 事件这个手指不一定是触摸屏幕的第一个手指。

幸运的是,我们可以假设这两个新的事件类型与旧的 MotionEvent 相同。ACTION_UP 和 MotionEvent。动作 _ 停止事件。

最后一个区别是,单个 MotionEvent 可以包含多个事件的数据。是的,你没看错。为此,合并的事件必须具有相同的类型。实际上,这只会发生在运动事件中。ACTION_MOVE 事件,因此我们只需在处理所述事件类型时处理这一事实。为了检查单个 MotionEvent 中包含多少个事件,我们使用 MotionEvent.getPointerCount()方法,该方法告诉我们在 MotionEvent 中具有坐标的手指的数量。然后,我们可以通过 MotionEvent.getX()、MotionEvent.getY()和 MotionEvent.getPointerId()方法获取指针索引 0 到 motion event . getpointercount()–1 的指针标识符和坐标。

在实践中

让我们为这个优秀的 API 写一个例子。我们希望最多跟踪十个手指(还没有设备可以跟踪更多,所以我们在这里是安全的)。当我们在屏幕上添加更多手指时,Android 设备通常会分配连续的指针索引,但这并不总是有保证的,所以我们依赖于数组的指针索引,并将简单地显示哪个 id 分配给了触摸点。我们跟踪每个指针的坐标和触摸状态(触摸与否),并通过文本视图将这些信息输出到屏幕上。让我们称我们的测试活动为 MultiTouchTest。清单 4-4 显示了完整的代码。

清单 4-4。【MultiTouchTest.java】;测试多点触摸 API

package com.badlogic.androidgames;import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.TextView;@TargetApi (5)
public class MultiTouchTest extends Activity implements OnTouchListener {StringBuilder builder = new StringBuilder();TextView textView;float [] x = new float [10];float [] y = new float [10];boolean [] touched = new boolean [10];int [] id = new int [10];private void updateTextView() {builder.setLength(0);for (int i = 0; i < 10; i++) {builder.append(touched[i]);builder.append(", ");builder.append(id[i]);builder.append(", ");builder.append(x[i]);builder.append(", ");builder.append(y[i]);builder.append("\n");}textView.setText(builder.toString());}public void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);textView = new TextView(this );textView.setText("Touch and drag (multiple fingers supported)!");textView.setOnTouchListener(this );setContentView(textView);for (int i = 0; i < 10; i++) {id[i] = -1;}updateTextView();}public boolean onTouch(View v, MotionEvent event) {int action = event.getAction() & MotionEvent.*ACTION*_*MASK*;int pointerIndex = (event.getAction() & MotionEvent.*ACTION*_*POINTER*_*ID*_*MASK*) >> MotionEvent.*ACTION*_*POINTER*_*ID*_*SHIFT*;int pointerCount = event.getPointerCount();for (int i = 0; i < 10; i++) {if (i >= pointerCount) {touched[i] = false ;id[i] = -1;continue ;}if (event.getAction() != MotionEvent.*ACTION*_*MOVE*&& i != pointerIndex) {// if it's an up/down/cancel/out event, mask the id to see if we should process it for this touch pointcontinue ;}int pointerId = event.getPointerId(i);switch (action) {case MotionEvent.*ACTION*_*DOWN*:case MotionEvent.*ACTION*_*POINTER*_*DOWN*:touched[i] = true ;id[i] = pointerId;x[i] = (int ) event.getX(i);y[i] = (int ) event.getY(i);break ;case MotionEvent.*ACTION*_*UP*:case MotionEvent.*ACTION*_*POINTER*_*UP*:case MotionEvent.*ACTION*_*OUTSIDE*:case MotionEvent.*ACTION*_*CANCEL*:touched[i] = false ;id[i] = -1;x[i] = (int ) event.getX(i);y[i] = (int ) event.getY(i);break ;case MotionEvent.*ACTION*_*MOVE*:touched[i] = true ;id[i] = pointerId;x[i] = (int ) event.getX(i);y[i] = (int ) event.getY(i);break ;}}updateTextView();return true ;}
}

注意类定义顶部的 TargetApi 注释。这是必要的,因为我们访问的 API 不是我们在创建项目时指定的最低 SDK 的一部分(Android 1.5)。每次我们使用不属于最小 SDK 的 API 时,我们都需要将注释放在使用这些 API 的类的顶部!

我们像以前一样实现 OnTouchListener 接口。为了跟踪十个手指的坐标和触摸状态,我们添加了三个新的成员数组来保存这些信息。数组 x 和 y 保存每个指针 ID 的坐标,被触摸的数组存储具有该指针 ID 的手指是否按下。

接下来,我们自由地创建了一个小助手方法,将手指的当前状态输出到 TextView。该方法只需迭代所有十个手指状态,并通过 StringBuilder 将它们连接起来。最终文本将设置为 TextView。

onCreate()方法设置我们的活动,并将其注册为 TextView 中的 OnTouchListener。这部分我们已经背熟了。

现在是可怕的部分:onTouch()方法。

我们首先通过屏蔽 event.getAction()返回的整数来获取事件类型。接下来,我们提取指针索引,并从 MotionEvent 获取相应的指针标识符,如前所述。

onTouch()方法的核心是那个讨厌的大 switch 语句,我们已经用它的简化形式来处理单触事件。我们将所有事件分为三大类:

  • 一触 - 倒地事件发生 (MotionEvent。ACTION_DOWN 或 MotionEvent。ACTION_PONTER_DOWN):我们将指针标识符的触摸状态设置为 true,并保存指针的当前坐标。
  • 一触 - up 事件发生 (MotionEvent。ACTION_UP,MotionEvent。ACTION_POINTER_UP 或 MotionEvent。取消):我们将该指针标识符的触摸状态设置为假,并保存其最后已知的坐标。
  • 一个或多个手指 被拖过 屏幕(运动事件。ACTION_MOVE):我们检查 MotionEvent 中包含多少个事件,然后将指针索引 0 的坐标更新为 motion event . getpointercount()-1。对于每个事件,我们获取相应的指针 ID 并更新坐标。

处理完事件后,我们通过调用前面定义的 updateView()方法来更新 TextView。最后,我们返回 true,表明我们处理了触摸事件。

图 4-7 显示了触摸三星 Galaxy Nexus 手机的五个手指并稍微拖动它们所产生的活动输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-7。多点触控带来的乐趣

运行这个示例时,我们可以观察到一些情况:

  • 如果我们在 Android 版本低于 2.0 的设备或模拟器上启动它,我们会得到一个令人讨厌的异常,因为我们使用了一个在那些早期版本上不可用的 API。我们可以通过确定应用运行的 Android 版本来解决这个问题,在运行 Android 1.5 和 1.6 的设备上使用单触代码,在运行 Android 2.0 或更高版本的设备上使用多触代码。我们将在下一章回到这个话题。
  • 模拟器上没有多点触摸。如果我们创建一个运行 Android 或更高版本的仿真器,API 就在那里,但我们只有一个鼠标。即使我们有两只老鼠,也不会有什么不同。
  • 向下触摸两个手指,抬起第一个手指,然后再次向下触摸。第二个手指将在第一个手指抬起后保持其指针 ID。当第一个手指第二次按下时,它会获得一个新的指针 ID,通常为 0,但可以是任何整数。任何触摸屏幕的新手指都将获得一个新的指针 ID,它可以是当前没有被另一个活动触摸使用的任何东西。这是一条需要记住的规则。
  • 如果你在 Nexus One、Droid 或更新的低预算智能手机上尝试这一功能,当你在一个轴上交叉两个手指时,你会注意到一些奇怪的行为。这是因为这些设备的屏幕不完全支持对单个手指的跟踪。这是一个大问题,但是我们可以通过精心设计我们的 ui 来解决它。我们将在下一章中再来看这个问题。要记住的短语是:不要不要过河

这就是多点触摸处理在 Android 上的工作方式。这是一种痛苦,但是一旦你解开了所有的术语,并平静地接受了这一点,你就会对实现感到更加舒服,并像专家一样处理所有的接触点。

注意如果这让你的头爆炸了,我们很抱歉。这部分任务相当繁重。遗憾的是,该 API 的官方文档极其缺乏,大多数人只是通过简单地钻研来“学习”该 API。我们建议您尝试一下前面的代码示例,直到您完全理解其中的内容。

处理关键事件

经过最后一部分的疯狂,你应该得到一些非常简单的东西。欢迎处理关键事件。

为了捕捉关键事件,我们实现了另一个监听器接口,称为 OnKeyListener。它有一个名为 onKey()的方法,签名如下:

public boolean onKey(View view, int keyCode, KeyEvent event)

视图指定接收键事件的视图,keyCode 参数是在 key event 类中定义的常量之一,最后一个参数是键事件本身,它有一些附加信息。

什么是关键代码?(屏幕)键盘上的每个键和每个系统键都有一个唯一的编号。这些键码在 KeyEvent 类中被定义为静态公共最终整数。一种这样的密钥代码是 key code。KEYCODE_A,是 A 键的代码。这与按下某个键时文本字段中生成的字符没有任何关系。它实际上只是标识了密钥本身。

KeyEvent 类类似于 MotionEvent 类。它有两种与我们相关的方法:

  • KeyEvent.getAction():这将返回 KeyEvent。ACTION_DOWN,KeyEvent。ACTION_UP 和 KeyEvent。动作 _ 多个。出于我们的目的,我们可以忽略最后一个关键事件类型。另外两个将在按键被按下或释放时发送。
  • KeyEvent.getUnicodeChar():返回文本字段中的 Unicode 字符。假设我们按住 Shift 键并按下 A 键。这将被报告为一个键码为 KeyEvent 的事件。KEYCODE_A,但是带有一个 Unicode 字符 A,如果我们自己想做文本输入的话可以用这个方法。

要接收键盘事件,视图必须有焦点。这可以通过以下方法调用来强制实现:

View.setFocusableInTouchMode(true);
View.requestFocus();

第一种方法将保证视图可以聚焦。第二种方法要求特定视图获得焦点。

让我们实现一个简单的测试活动,看看这两者是如何结合起来的。我们希望获得关键事件,并在文本视图中显示我们收到的最后一个事件。我们将显示的信息是键事件类型,以及键代码和 Unicode 字符(如果会产生的话)。请注意,有些键本身并不产生 Unicode 字符,而是与其他字符组合产生。清单 4-5 展示了我们如何用少量的代码行实现所有这些。

清单 4-5。【KeyTest.Java】;测试关键事件 API

package com.badlogic.androidgames;import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.widget.TextView;public class KeyTest extends Activity implements OnKeyListener {StringBuilder builder = new StringBuilder();TextView textView;public void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);textView = new TextView(this );textView.setText("Press keys (if you have some)!");textView.setOnKeyListener(this );textView.setFocusableInTouchMode(true );textView.requestFocus();setContentView(textView);}public boolean onKey(View view, int keyCode, KeyEvent event) {builder.setLength(0);switch (event.getAction()) {case KeyEvent.*ACTION*_*DOWN*:builder.append("down, ");break ;case KeyEvent.*ACTION*_*UP*:builder.append("up, ");break ;}builder.append(event.getKeyCode());builder.append(", ");builder.append((char ) event.getUnicodeChar());String text = builder.toString();Log.*d*("KeyTest", text);textView.setText(text);return event.getKeyCode() != KeyEvent.*KEYCODE*_*BACK*;}
}

我们首先声明该活动实现了 OnKeyListener 接口。接下来,我们定义两个我们已经熟悉的成员:构造要显示的文本的 StringBuilder 和显示文本的 TextView。

在 onCreate()方法中,我们确保 TextView 获得焦点,这样它就可以接收按键事件。我们还通过 TextView.setOnKeyListener()方法将活动注册为 OnKeyListener。

onKey()方法也非常简单。我们处理 switch 语句中的两种事件类型,向 StringBuilder 追加一个适当的字符串。接下来,我们追加 KeyEvent 本身的键代码和 Unicode 字符,并将 StringBuffer 实例的内容输出到 LogCat 和 TextView。

最后一个 if 语句很有趣:如果按下 Back 键,我们从 onKey()方法返回 false,使 TextView 处理事件。否则,我们返回 true。为什么在这里进行区分?

如果我们在 Back 键的情况下返回 true,我们会稍微打乱活动的生命周期。该活动不会关闭,因为我们决定自己使用 Back key。当然,在有些情况下,我们实际上想要捕捉 Back 键,这样我们的活动就不会被关闭。但是,除非绝对必要,否则强烈建议不要这样做。

图 4-8 展示了按住机器人键盘上的 Shift 和 A 键时活动的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-8。同时按下 Shift 和 A 键

这里有几点需要注意:

  • 当您查看 LogCat 输出时,请注意我们可以轻松地处理并发的键事件。按住多个键不是问题。
  • 按下 D-pad 和滚动轨迹球都被报告为按键事件。
  • 与触摸事件一样,按键事件会耗尽旧版本 Android 和第一代设备上的大量 CPU 资源。然而,它们不会产生大量事件。

与上一节相比,这相当轻松,不是吗?

注意键处理 API 比我们在这里展示的要复杂一些。然而,对于我们的游戏编程项目来说,这里包含的信息已经足够了。如果你需要更复杂的东西,可以参考 Android 开发者网站上的官方文档。

读取加速度计状态

一个非常有趣的游戏输入选项是加速度计。所有 Android 设备都需要包含一个三轴加速度计。我们在第三章中略微谈到了加速度计。一般来说,我们只会轮询加速度计的状态。

那么,我们如何获得加速度计信息呢?你猜对了——通过注册一个监听器。我们需要实现的接口叫做 SensorEventListener,它有两个方法:

public void onSensorChanged(SensorEvent event);
public void onAccuracyChanged(Sensor sensor, int accuracy);

当新的加速度计事件到达时,调用第一个方法。当加速度计的精度改变时,调用第二种方法。出于我们的目的,我们可以安全地忽略第二种方法。

那么我们在哪里注册 SensorEventListener 呢?为此,我们必须做一点工作。首先,我们需要检查设备中是否安装了加速度计。现在,我们刚刚告诉你,所有的 Android 设备都必须包含一个加速度计。这仍然是事实,但将来可能会改变。因此,我们希望百分之百地确保我们可以使用该输入法。

我们需要做的第一件事是获取 SensorManager 的一个实例。那个人会告诉我们是否安装了加速度计,这也是我们注册监听器的地方。为了获得 SensorManager,我们使用了上下文接口的一个方法:

SensorManager manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);

SensorManager 是由 Android 系统提供的的系统服务。Android 由多个系统服务组成,每一个服务都为任何人提供不同的系统信息。

一旦有了 SensorManager,我们就可以检查加速度计是否可用:

boolean hasAccel = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).size() > 0;

使用这段代码,我们向 SensorManager 轮询所有安装的加速度计类型的传感器。虽然这意味着一个设备可以有多个加速度计,但实际上这只会返回一个加速度计传感器。

如果安装了加速度计,我们可以从 SensorManager 获取它,并向它注册 SensorEventListener,如下所示:

Sensor sensor = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0);
boolean success = manager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME);

参数 SensorManager。SENSOR_DELAY_GAME 指定监听器应该多久更新一次加速度计的最新状态。这是一个专门为游戏设计的特殊常量,所以我们很乐意使用它。请注意,SensorManager.registerListener()方法返回一个布尔值,表明注册过程是否成功。这意味着我们必须在事后检查布尔值,以确保我们确实能从传感器中获得任何事件。

一旦我们注册了侦听器,我们将在 sensoreventlistener . onsensorchanged()方法中接收 SensorEvents。该方法的名称意味着它只在传感器状态改变时被调用。这有点令人困惑,因为加速度计的状态不断变化。当我们注册侦听器时,我们实际上指定了希望接收传感器状态更新的频率。

那么我们如何处理 SensorEvent 呢?那相当容易。 SensorEvent 有一个名为 SensorEvent.values 的公共浮点数组成员,它保存加速度计三个轴中每个轴的当前加速度值。SensorEvent.values[0]保存 x 轴的值,SensorEvent.values[1]保存 y 轴的值,SensorEvent.values[2]保存 z 轴的值。我们在第三章中讨论了这些值的含义,所以如果你忘记了,请再次查看“输入”部分。

有了这些信息,我们可以编写一个简单的测试活动。我们要做的就是在 TextView 中输出每个加速度计轴的加速度计值。清单 4-6 展示了如何做到这一点。

清单 4-6。【AccelerometerTest.java】;测试加速度计 API

package com.badlogic.androidgames;import android.app.Activity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;public class AccelerometerTest extends Activity implements SensorEventListener {TextView textView;StringBuilder builder = new StringBuilder();@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);textView = new TextView(this );setContentView(textView);SensorManager manager = (SensorManager) getSystemService(Context.*SENSOR*_*SERVICE*);if (manager.getSensorList(Sensor.*TYPE*_*ACCELEROMETER*).size() == 0) {textView.setText("No accelerometer installed");}else {Sensor accelerometer = manager.getSensorList(Sensor.*TYPE*_*ACCELEROMETER*).get(0);if (!manager.registerListener(this , accelerometer,SensorManager.*SENSOR*_*DELAY*_*GAME*)) {textView.setText("Couldn't register sensor listener");}}}public void onSensorChanged(SensorEvent event) {builder.setLength(0);builder.append("x: ");builder.append(event.values[0]);builder.append(", y: ");builder.append(event.values[1]);builder.append(", z: ");builder.append(event.values[2]);textView.setText(builder.toString());}public void onAccuracyChanged(Sensor sensor, int accuracy) {// nothing to do here}
}

我们首先检查加速度计传感器是否可用。如果是,我们从 SensorManager 获取它,并尝试注册我们的活动,该活动实现 SensorEventListener 接口。如果这些都失败了,我们设置文本视图来显示一个正确的错误信息。

onSensorChanged()方法只是从传递给它的 SensorEvent 中读取轴值,并相应地更新 TextView 文本。

有了 onAccuracyChanged()方法,我们可以完全实现 SensorEventListener 接口。它没有真正的其他用途。

图 4-9 显示了当设备垂直于地面时,轴在纵向和横向模式下的数值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-9。当设备垂直于地面时,纵向模式(左)和横向模式(右)下的加速度计轴值

Android 加速度计处理的一个问题是加速度计值是相对于设备的默认方向的。这意味着,如果你的游戏只在横向模式下运行,默认方向为纵向模式的设备与默认方向为横向模式的设备的数值相差 90 度!例如,平板电脑就是这种情况。那么,如何应对这种情况呢?使用这个方便的代码片段,您应该已经准备好了:

int screenRotation;
public void onResume() {WindowManager windowMgr = (WindowManager)activity.getSystemService(Activity.WINDOW_SERVICE);// getOrientation() is deprecated in Android 8 but is the same as getRotation(), which is the rotation from the natural orientation of the devicescreenRotation = windowMgr.getDefaultDisplay().getOrientation();
}
static final int *ACCELEROMETER*_*AXIS*_*SWAP*[][] = {{1, -1, 0, 1}, // ROTATION_0{-1, -1, 1, 0}, // ROTATION_90{-1, 1, 0, 1}, // ROTATION_180{1, 1, 1, 0}}; // ROTATION_270
public void onSensorChanged(SensorEvent event) {final int [] as =*ACCELEROMETER*_*AXIS*_*SWAP*[screenRotation];float screenX = (float )as[0] * event.values[as[2]];float screenY = (float )as[1] * event.values[as[3]];float screenZ = event.values[2];// use screenX, screenY, and screenZ as your accelerometer values now!
}

下面是一些关于加速度计的结束语:

  • 正如您在图 4-9 右侧的截图中看到的,加速度计值有时可能会超出其指定范围。这是由于传感器中的小误差造成的,因此如果您需要这些值尽可能精确,就必须进行调整。
  • 无论您的活动方向如何,加速度计轴总是以相同的顺序报告。
  • 应用开发人员负责根据设备的自然方向旋转加速度计值。

读取指南针状态

除了加速度计之外的读数传感器,例如指南针,也非常相似。事实上,它是如此的相似,以至于你可以简单地替换 Sensor 的所有实例。在清单 4-6 中输入 _ 加速度计和传感器。输入 _ 方向并重新运行测试,将我们的加速计测试代码用作指南针测试!

现在,您将看到您的 x、y 和 z 值正在做一些非常不同的事情。如果您将设备平放,屏幕朝上并与地面平行,x 将读取指南针指向的度数,y 和 z 应该接近 0。现在将设备倾斜,看看这些数字是如何变化的。x 应该仍然是主航向(方位角),但是 y 和 z 应该分别显示设备的俯仰和滚动。因为 TYPE_ORIENTATION 的常数已被否决,所以您也可以通过调用 sensor manager . get ORIENTATION(float[]R,float[] values)来接收相同的指南针数据,其中 R 是旋转矩阵(请参见 sensor manager . getrotationmatrix()),values 保存三个返回值,这次以弧度为单位。

至此,我们已经讨论了游戏开发所需的 Android API 的所有与输入处理相关的类。

注意顾名思义,SensorManager 类也允许您访问其他传感器。这包括指南针和光传感器。如果你想有创意,你可以想出一个使用这些传感器的游戏创意。处理事件的方式与我们处理加速度计数据的方式类似。Android 开发者网站上的文档会给你更多的信息。

文件处理

Android 为我们提供了几种读写文件的方法。在这一节中,我们将了解素材、如何访问外部存储(大部分实现为 SD 卡)和共享首选项,它们的作用就像一个持久的哈希表。先说素材。

阅读素材

在第二章中,我们简单看了一下一个 Android 项目的所有文件夹。我们将 assets/和 res/ folders 标识为我们可以放置文件的地方,这些文件应该与我们的应用一起分发。当我们讨论 manifest 文件时,我们声明我们不打算使用 res/ folder,因为它意味着对我们如何构造文件集的限制。素材/目录是放置我们所有文件的地方,无论我们想要什么文件夹层次结构。

assets/ folder 中的文件通过一个名为 AssetManager 的类公开。对于我们的应用,我们可以获得对该管理器的引用,如下所示:

AssetManager assetManager = context.getAssets();

我们已经看到了上下文接口;它由 Activity 类实现。在现实生活中,我们会从活动中获取素材管理器。

一旦我们有了素材管理器,我们就可以开始疯狂地打开文件:

InputStream inputStream = assetManager.open("dir/dir2/filename.txt");

这个方法将返回一个普通的 Java InputStream,我们可以用它来读取任何类型的文件。AssetManager.open()方法的唯一参数是相对于素材目录的文件名。在前面的示例中,我们在 assets/文件夹中有两个目录,其中第二个目录(dir2/)是第一个目录(dir/)的子目录。在我们的 Eclipse 项目中,该文件将位于 assets/dir/dir2/中。

让我们编写一个简单的测试活动来检查这个功能。我们希望从名为 texts 的素材/目录的子目录中加载一个名为 myawesometext.txt 的文本文件。文本文件的内容将显示在文本视图中。清单 4-7 显示了这个令人敬畏的活动的来源。

***清单 4-7。***AssetsTest.java,演示如何读取素材文件

package com.badlogic.androidgames;import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;import android.app.Activity;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.widget.TextView;public class AssetsTest extends Activity {@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);TextView textView = new TextView(this );setContentView(textView);AssetManager assetManager = getAssets();InputStream inputStream = null ;try {inputStream = assetManager.open("texts/myawesometext.txt");String text = loadTextFile(inputStream);textView.setText(text);}catch (IOException e) {textView.setText("Couldn't load file");}finally {if (inputStream != null )try {inputStream.close();}catch (IOException e) {textView.setText("Couldn't close file");}}}public String loadTextFile(InputStream inputStream)throws IOException {ByteArrayOutputStream byteStream = new ByteArrayOutputStream();byte [] bytes = new byte [4096];int len = 0;while ((len = inputStream.read(bytes)) > 0)byteStream.write(bytes, 0, len);return new String(byteStream.toByteArray(), "UTF8");}
}

除了发现在 Java 中从 InputStream 加载简单文本相当冗长之外,我们在这里没有看到什么大的意外。我们编写了一个名为 loadTextFile()的小方法,它将从 InputStream 中挤出所有的字节,并以字符串的形式返回这些字节。我们假设文本文件编码为 UTF-8。剩下的只是捕捉和处理各种异常。图 4-10 显示了这个小活动的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-10。素材测试的文本输出

您应该从本节中删除以下内容:

  • 用 Java 从 InputStream 加载文本文件简直是一团糟!通常,我们会用 Apache IOUtils 这样的东西来做。我们会把它留给你自己去完成。
  • 我们只能读素材,不能写素材。
  • 我们可以很容易地修改 loadTextFile()方法来加载二进制数据。我们只需要返回字节数组而不是字符串。

访问外部存储器

虽然素材对于将我们所有的图像和声音与我们的应用一起传送来说是极好的,但是有时我们需要能够持久存储一些信息并在以后重新加载它。一个常见的例子就是高分。

Android 提供了许多不同的方法,比如使用应用的本地共享首选项,使用小型 SQLite 数据库等等。所有这些选项都有一个共同点:它们不能很好地处理大型二进制文件。我们为什么需要那个?虽然我们可以告诉 Android 将我们的应用安装在外部存储设备上,从而不浪费内部存储的内存,但这只能在 Android 2.2 及更高版本上运行。对于早期版本,我们所有的应用数据都将安装在内部存储中。理论上,我们只能将应用的代码包含在 APK 文件中,并在应用第一次启动时将所有素材文件从服务器下载到 SD 卡中。Android 上很多高配置的游戏都是这么做的。

还有其他一些场景,我们想要访问 SD 卡(在所有当前可用的设备上,sd 卡与术语外部存储几乎是同义词)。我们可以允许我们的用户用游戏内编辑器创建他们自己的关卡。我们需要将这些级别存储在某个地方,而 SD 卡正好适合这个目的。

所以,现在我们已经说服你不要使用 Android 提供的花哨机制来存储应用偏好,让我们看看如何在 SD 卡上读写文件。

我们要做的第一件事是请求访问外部存储器的许可。这是在 manifest 文件中用本章前面讨论的元素完成的。

接下来,我们必须检查用户的 Android 设备上是否真的有可用的外部存储设备。例如,如果你创建了一个 Android 虚拟设备(AVD ),你可以选择不让它模拟 SD 卡,这样你就不能在你的应用中写入它。无法访问 SD 卡的另一个原因可能是外部存储设备当前正被其他设备使用(例如,用户可能正在通过台式 PC 上的 USB 来浏览它)。下面是我们获取外部存储状态的方法:

String state = Environment.getExternalStorageState();

嗯,我们得到了一个字符串。环境类定义了几个常量。其中之一叫做环境。媒体安装。也是字符串。如果前面的方法返回的字符串等于这个常数,我们就拥有对外部存储的完全读/写访问权限。请注意,您必须使用 equals()方法来比较这两个字符串;引用相等并不是在所有情况下都有效。

一旦我们确定我们实际上可以访问外部存储,我们需要获得它的根目录名。如果我们想要访问一个特定的文件,我们需要指定它相对于这个目录的位置。为了获得根目录,我们使用另一个环境静态方法:

File externalDir = Environment.getExternalStorageDirectory();

从这里开始,我们可以使用标准的 Java I/O 类来读写文件。

让我们编写一个简单的例子,将文件写入 SD 卡,读取文件,在 TextView 中显示其内容,然后再次从 SD 卡中删除文件。清单 4-8 显示了它的源代码。

***清单 4-8。***externalstragetest 活动

package com.badlogic.androidgames;import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.widget.TextView;public class ExternalStorageTest extends Activity {@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);TextView textView = new TextView(this );setContentView(textView);String state = Environment.*getExternalStorageState*();if (!state.equals(Environment.*MEDIA*_*MOUNTED*)) {textView.setText("No external storage mounted");}else {File externalDir = Environment.*getExternalStorageDirectory*();File textFile = new File(externalDir.getAbsolutePath()+ File.*separator*+ "text.txt");try {writeTextFile(textFile, "This is a test. Roger");String text = readTextFile(textFile);textView.setText(text);if (!textFile.delete()) {textView.setText("Couldn't remove temporary file");}}catch (IOException e) {textView.setText("Something went wrong! " + e.getMessage());}}}private void writeTextFile(File file, String text)throws IOException {BufferedWriter writer = new BufferedWriter(new FileWriter(file));writer.write(text);writer.close();}private String readTextFile(File file)throws IOException {BufferedReader reader = new BufferedReader(new FileReader(file));StringBuilder text = new StringBuilder();String line;while ((line = reader.readLine()) != null ) {text.append(line);text.append("\n");}reader.close();return text.toString();}
}

首先,我们检查 SD 卡是否已经安装。如果不行,我们就提前退出。接下来,我们获取外部存储目录,并构造一个新的文件实例,它指向我们将在下一条语句中创建的文件。writeTextFile()方法使用标准的 Java I/O 类来施展它的魔法。如果文件还不存在,这个方法将创建它;否则,它将覆盖一个已经存在的文件。在我们成功地将测试文本转储到外部存储设备上的文件之后,我们再次读取它并将其设置为 TextView 的文本。最后一步,我们再次从外部存储器中删除该文件。所有这些都是通过适当的标准安全措施来完成的,这些措施将通过向 TextView 输出错误消息来报告是否出现了问题。图 4-11 显示了活动的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-11。罗杰!

以下是可以从这一部分吸取的经验教训:

  • 不要乱动任何不属于你的文件。如果你删除他们上一次度假的照片,你的用户会很生气。
  • 务必检查外部存储设备是否已安装。
  • 不要弄乱外部存储设备上的任何文件!

因为删除外部存储设备上的所有文件非常容易,所以在从 Google Play 安装下一个请求 SD 卡权限的应用之前,您可能会三思而行。该应用一旦安装,就可以完全控制你的文件。

共享偏好

Android 提供了一个简单的 API 来存储应用的键值对,称为 SharedPreferences。SharedPreferences API 与标准的 Java 属性 API 没有什么不同。一个活动可以有一个默认的 SharedPreferences 实例,也可以根据需要使用多个不同的 SharedPreferences 实例。下面是从活动中获取 SharedPreferences 实例的典型方法:

SharedPreferences prefs = PreferenceManager.*getDefaultSharedPreferences*(this );

或者:

SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);

第一个方法给出了一个公共的 SharedPreferences,它将被那个上下文(在我们的例子中是 Activity)共享。第二种方法做同样的事情,但是它让你选择共享偏好的隐私。选项是上下文。MODE_PRIVATE,这是默认的上下文。模式 _ 世界 _ 可读,和上下文。模式 _ 世界 _ 可写。使用上下文之外的任何东西。MODE_PRIVATE 更高级,它对于保存游戏设置之类的事情来说是不必要的。

要使用共享首选项,您首先需要获得编辑器。这是通过

Editor editor = prefs.edit()

现在我们可以插入一些值:

editor.putString("key1", "banana");
editor.putInt("key2", 5);

最后,当我们想要保存时,我们只需添加

editor.commit();

准备好回读了吗?正如人们所料:

String value1 = prefs.getString("key1", null);
int value2 = prefs.getInt("key2", 0);

在我们的例子中,值 1 是“香蕉”,值 2 是 5。SharedPreferences 的“get”调用的第二个参数是默认值。如果在偏好设置中找不到密钥,将使用这些选项。例如,如果从未设置“key1”,那么在 getString 调用后,value1 将为 null。SharedPreferences 非常简单,我们实际上不需要任何测试代码来演示。只要记住总是提交这些编辑!

音频编程

Android 提供了几个易于使用的 API 来播放音效和音乐文件——正好满足我们的游戏编程需求。我们来看看那些 API。

设置音量控制

如果你有一个 Android 设备,你会注意到当你按下音量调高和调低按钮时,你会根据你当前使用的应用来控制不同的音量设置。在通话中,您可以控制传入语音流的音量。在 YouTube 应用中,您可以控制视频音频的音量。在主屏幕上,您可以控制系统声音的音量,如铃声或收到的即时消息。

Android 有不同用途的不同音频流。当我们在游戏中回放音频时,我们使用将音效和音乐输出到一个特定流的类,这个特定流称为音乐流。在我们考虑播放音效或音乐之前,我们首先必须确保音量按钮将控制正确的音频流。为此,我们使用上下文接口的另一种方法:

context.setVolumeControlStream(AudioManager.STREAM_MUSIC);

一如既往,我们选择的上下文实现将是我们的活动。这次通话后,音量按钮将控制音乐流,我们稍后将向其中输出音效和音乐。我们只需要在活动生命周期中调用这个方法一次。Activity.onCreate()方法是实现这一点的最佳方法。

编写一个只包含一行代码的示例有点矫枉过正。因此,我们将在这一点上避免这样做。只要记住在所有输出声音的活动中使用这种方法。

播放声音效果

在第三章中,我们讨论了流媒体音乐和回放音效的区别。后者存储在内存中,通常不会超过几秒钟。Android 为我们提供了一个名为 SoundPool 的类,使得播放音效变得非常容易。

我们可以简单地实例化新的 SoundPool 实例,如下所示:

SoundPool soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);

第一个参数定义了我们可以同时播放的声音效果的最大数量。这并不意味着我们不能加载更多的音效;它只是限制了可以同时播放的音效数量。第二个参数定义 SoundPool 输出音频的音频流。我们选择的音乐流也设置了音量控制。最后一个参数目前没有使用,应该默认为 0。

要将声音效果从音频文件加载到堆内存中,我们可以使用 SoundPool.load()方法。我们将所有文件存储在 assets/ directory 中,因此需要使用重载的 SoundPool.load()方法,该方法采用 AssetFileDescriptor。我们如何获得 AssetFileDescriptor?很简单——通过我们之前合作过的素材管理器。下面是我们如何通过 SoundPool 从 assets/ directory 加载名为 explosion.ogg 的 OGG 文件:

AssetFileDescriptor descriptor = assetManager.openFd("explosion.ogg");
int explosionId = soundPool.load(descriptor, 1);

通过 AssetManager.openFd()方法可以直接获得 AssetFileDescriptor。通过 SoundPool 加载音效也同样简单。SoundPool.load()方法的第一个参数是我们的 AssetFileDescriptor,第二个参数指定声音效果的优先级。目前不使用,为了将来的兼容性,应该设置为 1。

SoundPool.load()方法返回一个整数,作为加载的声音效果的句柄。当我们想要播放声音效果时,我们指定这个句柄,以便 SoundPool 知道要播放什么效果。

播放声音效果也很容易:

soundPool.play(explosionId, 1.0f, 1.0f, 0, 0, 1);

第一个参数是我们从 SoundPool.load()方法收到的句柄。接下来的两个参数指定用于左右声道的音量。这些值应该在 0(无声)和 1(耳朵爆炸)之间的范围内。

接下来是两个我们很少用到的论点。第一个是优先级,目前没有使用,应该设置为 0。另一个参数指定声音效果循环的频率。不推荐循环音效,所以这里一般应该用 0。最后一个参数是回放速率。将其设置为高于 1 的值将允许声音效果以比录制时更快的速度回放,而将其设置为低于 1 的值将导致回放速度变慢。

当我们不再需要声音效果并希望释放一些内存时,我们可以使用 SoundPool.unload()方法:

soundPool.unload(explosionId);

我们只需为音效传递从 SoundPool.load()方法接收的句柄,它将从内存中卸载。

一般来说,我们在游戏中会有一个 SoundPool 实例,我们将根据需要使用它来加载、播放和卸载音效。当我们完成所有的音频输出并且不再需要 SoundPool 时,我们应该总是调用 SoundPool.release()方法,这将释放 SoundPool 通常使用的所有资源。发布之后,你当然不能再使用 SoundPool 了。此外,该 SoundPool 加载的所有声音效果都将消失。

让我们编写一个简单的测试活动,它将在我们每次点击屏幕时播放爆炸声音效果。我们已经知道了实现这个需要知道的一切,所以清单 4-9 应该不会有什么大的惊喜。

清单 4-9。【SoundPoolTest.java】;播放音效

package com.badlogic.androidgames;import java.io.IOException;import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.TextView;public class SoundPoolTest extends Activity implements OnTouchListener {SoundPool soundPool;int explosionId = -1;@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);TextView textView = new TextView(this );textView.setOnTouchListener(this );setContentView(textView);setVolumeControlStream(AudioManager.*STREAM*_*MUSIC*);soundPool = new SoundPool(20, AudioManager.*STREAM*_*MUSIC*, 0);try {AssetManager assetManager = getAssets();AssetFileDescriptor descriptor = assetManager.openFd("explosion.ogg");explosionId = soundPool.load(descriptor, 1);}catch (IOException e) {textView.setText("Couldn't load sound effect from asset, "+ e.getMessage());}}public boolean onTouch(View v, MotionEvent event) {if (event.getAction() == MotionEvent.*ACTION*_*UP*) {if (explosionId != -1) {soundPool.play(explosionId, 1, 1, 0, 0, 1);}}return true ;}
}

我们首先从 Activity 派生出我们的类,并让它实现 OnTouchListener 接口,这样我们以后就可以处理屏幕上的点击。我们的类有两个成员:SoundPool 和我们将要加载和播放的音效句柄。我们最初将其设置为–1,表示音效尚未加载。

在 onCreate()方法中,我们做了以前做过几次的事情:创建一个 TextView,将活动注册为 OnTouchListener,并将 TextView 设置为内容视图。

下一行设置音量控制来控制音乐流,如前所述。然后我们创建 SoundPool,并对其进行配置,使其可以同时播放 20 种效果。这对大多数游戏来说应该足够了。

最后,我们从 AssetManager 获得一个放在 assets/目录中的 explosion.ogg 文件的 AssetFileDescriptor。要加载声音,我们只需将描述符传递给 SoundPool.load()方法并存储返回的句柄。SoundPool.load()方法会在加载过程中出现问题时抛出异常,在这种情况下,我们会捕捉到异常并显示一条错误消息。

在 onTouch()方法中,我们简单地检查手指是否抬起,这表示屏幕被点击。如果是这种情况,并且爆炸声音效果被成功加载(由句柄不为–1 指示),我们简单地回放该声音效果。

当你执行这个小活动时,只需轻触屏幕就能让世界爆炸。如果您快速连续触摸屏幕,您会注意到声音效果会以重叠的方式播放多次。很难超过我们在 SoundPool 中配置的最大播放次数 20 次。但是,如果发生这种情况,当前播放的声音之一将被停止,以便为新请求的播放腾出空间。

请注意,在前面的示例中,我们没有卸载声音或释放 SoundPool。这是为了简洁。通常,当活动将要被销毁时,您会在 onPause()方法中释放 SoundPool。只要记住总是释放或卸载任何你不再需要的东西。

虽然 SoundPool 类非常容易使用,但是有几个注意事项您应该记住:

  • SoundPool.load()方法异步执行实际加载。这意味着在使用该声音效果调用 SoundPool.play()方法之前,您必须等待片刻,因为加载可能尚未完成。遗憾的是,没有办法检查音效何时加载完毕。这只有 SoundPool 的 SDK 版本 8 才有可能,我们希望支持所有 Android 版本。通常这没什么大不了的,因为在第一次播放声音效果之前,您很可能会加载其他资源。
  • 众所周知,SoundPool 在 MP3 文件和长声音文件方面存在问题,其中 long 被定义为“长于 5 到 6 秒”这两个问题都是没有记载的,所以没有严格的规则来决定你的音效会不会麻烦。一般来说,我们建议坚持使用 OGG 的音频文件,而不是 MP3,并在音频质量变差之前,尝试尽可能低的采样率和持续时间。

注意和我们讨论的任何 API 一样,SoundPool 中有更多的功能。我们简单地告诉过你,你可以循环音效。为此,您可以从 SoundPool.play()方法获得一个 ID,用于暂停或停止循环音效。如果您需要 SoundPool 的功能,请查看 Android 开发者网站上的 sound pool 文档。

流媒体音乐

小的音效适合 Android 应用从操作系统获得的有限堆内存。包含较长音乐片段的较大音频文件不适合。出于这个原因,我们需要将音乐流式传输到音频硬件,这意味着我们一次只能读取一小部分,足以将其解码为原始 PCM 数据并将其发送到音频芯片。

听起来很吓人。幸运的是,有 MediaPlayer 类,它为我们处理所有的事务。我们需要做的就是把它指向音频文件,告诉它回放。

实例化 MediaPlayer 类很简单:

MediaPlayer mediaPlayer = new MediaPlayer();

接下来,我们需要告诉 MediaPlayer 播放什么文件。这也是通过 AssetFileDescriptor 完成的:

AssetFileDescriptor descriptor = assetManager.openFd("music.ogg");
mediaPlayer.setDataSource(descriptor.getFileDescriptor(), descriptor.getStartOffset(), descriptor.getLength());

这比 SoundPool 的情况要复杂一些。MediaPlayer.setDataSource()方法不直接采用 AssetFileDescriptor。相反,它需要一个 FileDescriptor,我们通过 asset file descriptor . getfile descriptor()方法获得它。此外,我们必须指定音频文件的偏移量和长度。为什么要抵消?实际上,所有素材都存储在一个文件中。为了让 MediaPlayer 到达文件的开头,我们必须向它提供文件在包含的素材文件中的偏移量。

在开始播放音乐文件之前,我们必须再调用一个方法来准备 MediaPlayer 进行播放:

mediaPlayer.prepare();

这将实际打开该文件,并检查它是否可以被 MediaPlayer 实例读取和回放。从这里开始,我们可以自由播放音频文件,暂停,停止,设置为循环播放,并改变音量。

要开始回放,我们只需调用以下方法:

mediaPlayer.start();

请注意,只有在成功调用 MediaPlayer.prepare()方法之后,才能调用该方法(如果它抛出运行时异常,您会注意到)。

我们可以通过调用 pause()方法来暂停回放:

mediaPlayer.pause();

同样,只有当我们已经成功准备好 MediaPlayer 并开始播放时,调用此方法才有效。要恢复暂停的 MediaPlayer,我们可以再次调用 MediaPlayer.start()方法,无需任何准备。

要停止回放,我们调用下面的方法:

mediaPlayer.stop();

注意,当我们想要启动一个停止的 MediaPlayer 时,我们首先必须再次调用 MediaPlayer.prepare()方法。

我们可以使用以下方法设置 MediaPlayer 循环播放:

mediaPlayer.setLooping(true);

要调节音乐播放的音量,我们可以用这个方法:

mediaPlayer.setVolume(1, 1);

这将设置左右声道的音量。文档没有指定这两个参数必须在什么范围内。根据实验,有效范围似乎在 0 和 1 之间。

最后,我们需要一种方法来检查回放是否已经完成。我们可以用两种方法做到这一点。首先,我们可以向 MediaPlayer 注册一个 OnCompletionListener,它将在回放完成时被调用:

mediaPlayer.setOnCompletionListener(listener);

如果我们想要轮询 MediaPlayer 的状态,我们可以使用以下方法:

boolean isPlaying = mediaPlayer.isPlaying();

请注意,如果 MediaPlayer 设置为循环,前面的方法都不会指示 MediaPlayer 已经停止。

最后,如果我们完成了 MediaPlayer 实例,我们通过调用以下方法来确保它占用的所有资源都被释放:

mediaPlayer.release();

在丢弃实例之前总是这样做被认为是一种好的做法。

如果我们没有将 MediaPlayer 设置为循环播放,并且播放已经完成,我们可以通过再次调用 MediaPlayer.prepare()和 MediaPlayer.start()方法来重新启动 MediaPlayer。

这些方法中的大部分都是异步工作的,所以即使您调用了 MediaPlayer.stop(),MediaPlayer.isPlaying()方法也可能在此后的一小段时间内返回。我们通常不担心这个。在大多数游戏中,我们将 MediaPlayer 设置为循环播放,然后在需要时停止播放(例如,当我们切换到不同的屏幕播放其他音乐时)。

让我们编写一个小的测试活动,其中我们以循环模式从素材/目录中回放一个声音文件。这种声音效果将根据活动的生命周期暂停和恢复,当我们的活动暂停时,音乐也应该暂停,当活动恢复时,音乐播放应该从它停止的地方继续。清单 4-10 展示了这是如何做到的。

清单 4-10。【MediaPlayerTest.java】;播放音频流

package com.badlogic.androidgames;import java.io.IOException;import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.widget.TextView;public class MediaPlayerTest extends Activity {MediaPlayer mediaPlayer;@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);TextView textView = new TextView(this );setContentView(textView);setVolumeControlStream(AudioManager.*STREAM*_*MUSIC*);mediaPlayer = new MediaPlayer();try {AssetManager assetManager = getAssets();AssetFileDescriptor descriptor = assetManager.openFd("music.ogg");mediaPlayer.setDataSource(descriptor.getFileDescriptor(),descriptor.getStartOffset(), descriptor.getLength());mediaPlayer.prepare();mediaPlayer.setLooping(true );}catch (IOException e) {textView.setText("Couldn't load music file, " + e.getMessage());mediaPlayer = null ;}}@Overrideprotected void onResume() {super .onResume();if (mediaPlayer != null ) {mediaPlayer.start();}}protected void onPause() {super .onPause();if (mediaPlayer != null ) {mediaPlayer.pause();if (isFinishing()) {mediaPlayer.stop();mediaPlayer.release();}}}
}

我们以活动成员的形式保留对 MediaPlayer 的引用。在 onCreate()方法中,我们只是像往常一样创建一个 TextView 来输出任何错误消息。

在我们开始使用 MediaPlayer 之前,我们要确保音量控制确实能控制音乐流。设置好之后,我们实例化 MediaPlayer。我们从 AssetManager 中为位于 assets/ directory 中的一个名为 music.ogg 的文件获取 AssetFileDescriptor,并将其设置为 MediaPlayer 的数据源。剩下要做的就是准备 MediaPlayer 实例,并将其设置为循环流。为了防止出错,我们将 MediaPlayer 成员设置为 null,这样我们可以在以后确定加载是否成功。此外,我们向 TextView 输出一些错误文本。

在 onResume()方法中,我们只需启动 MediaPlayer(如果创建成功的话)。onResume()方法是实现这一点的最佳位置,因为它是在 onCreate()和 onPause()之后调用的。第一种情况,它会第一次开始播放;在第二种情况下,它将简单地恢复暂停的 MediaPlayer。

onResume()方法暂停 MediaPlayer。如果活动将被终止,我们停止媒体播放器,然后释放它的所有资源。

如果你在玩这个,确保你也测试了它对暂停和恢复活动的反应,通过锁定屏幕或者暂时切换到主屏幕。恢复播放时,MediaPlayer 将从暂停时停止的地方继续播放。

以下是一些需要记住的事情:

  • 方法 MediaPlayer.start()、MediaPlayer.pause()和 MediaPlayer.resume()只能在特定的状态下调用,就像刚才讨论的那样。当你还没有准备好媒体播放器的时候,千万不要打电话给他们。仅在准备好 MediaPlayer 之后,或者在通过调用 MediaPlayer.pause()显式暂停 MediaPlayer 之后想要恢复 MediaPlayer 时,才调用 MediaPlayer.start()。
  • MediaPlayer 实例相当重量级。将它们实例化会占用大量的资源。我们应该总是尝试只有一个音乐播放。SoundPool 类可以更好地处理声音效果。
  • 记得设置音量控制来处理音乐流,否则你的玩家将无法调整游戏的音量。

我们几乎完成了这一章,但是一个大的主题仍然摆在我们面前:2D 图形。

基本图形编程

Android 为我们提供了两个大的在屏幕上绘图的 API。一个主要用于简单的 2D 图形编程,另一个用于硬件加速的 3D 图形编程。这一章和下一章将集中讨论用 Canvas API 进行 2D 图形编程,Canvas API 是 Skia 库的一个很好的包装器,适用于中等复杂的 2D 图形。从第七章开始,我们将研究用 OpenGL 渲染 2D 和 3D 图形。在此之前,我们首先需要讨论两件事:唤醒锁和全屏。

使用唤醒锁

如果你把我们写的测试放在一边几秒钟,你的手机屏幕就会变暗。只有当你触摸屏幕或按下按钮时,屏幕才会恢复到最大亮度。为了让我们的屏幕一直保持清醒,我们可以使用唤醒锁

我们需要做的第一件事是在 manifest 文件中添加一个名为 android.permission.WAKE_LOCK 的适当的标记。这将允许我们使用 WakeLock 类。

我们可以从 PowerManager 中获得一个 WakeLock 实例,如下所示:

PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "My Lock");

像所有其他系统服务一样,我们从上下文实例中获取 PowerManager。PowerManager.newWakeLock()方法有两个参数:锁的类型和一个我们可以自由定义的标记。有几种不同的唤醒锁类型;出于我们的目的,电源管理器。完整 _ 唤醒 _ 锁定类型是正确的类型。它将确保屏幕将保持打开,CPU 将全速工作,键盘将保持启用。

要启用唤醒锁,我们必须调用它的 acquire()方法:

wakeLock.acquire();

从这一点开始,无论多长时间没有用户交互,手机都将保持唤醒状态。当我们的应用暂停或被破坏时,我们必须再次禁用或释放唤醒锁:

wakeLock.release();

通常我们在 Activity.onCreate()方法上实例化 WakeLock 实例,在 Activity.onResume()方法中调用 WakeLock.acquire(),在 Activity.onPause()方法中调用 WakeLock.release()方法。这样,我们保证我们的应用在被暂停或恢复的情况下仍然表现良好。因为只有四行代码要添加,所以我们不打算写一个完整的例子。相反,我们建议您只需将代码添加到下一节的全屏示例中,并观察效果。

全屏显示

在我们开始用 Android APIs 绘制我们的第一批图形之前,让我们先解决一些别的问题。到目前为止,我们所有的活动都显示了标题栏。通知栏也是可见的。我们想通过去掉这些来让我们的玩家更加沉浸其中。我们可以通过两个简单的调用来实现:

requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

第一个调用去掉了活动的标题栏。为了让活动全屏显示,从而消除通知栏,我们调用第二个方法。注意,我们必须在设置活动的内容视图之前调用这些方法。

清单 4-11 显示了一个非常简单的测试活动,演示了如何全屏显示。

清单 4-11。【FullScreenTest.java】;让我们的活动全屏进行

package com.badlogic.androidgames;import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;public class FullScreenTest extends SingleTouchTest {@Overridepublic void onCreate(Bundle savedInstanceState) {requestWindowFeature(Window.*FEATURE*_*NO*_*TITLE*);getWindow().setFlags(WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*,WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*);super .onCreate(savedInstanceState);}
}

这里发生了什么事?我们简单地从前面创建的 TouchTest 类派生并覆盖 onCreate()方法。在 onCreate()方法中,我们启用全屏模式,然后调用超类的 onCreate()方法(在本例中,是 TouchTest 活动),这将设置所有其余的活动。再次注意,我们必须在设置内容视图之前调用这两个方法。因此,在我们执行这两个方法之后,超类 onCreate()方法被调用。

我们还在清单文件中将活动的方向固定为纵向模式。您没有忘记在我们编写的每个测试的清单文件中添加元素,对吗?从现在开始,我们将总是把它固定为纵向模式或横向模式,因为我们不希望坐标系一直在变化。

通过从 TouchTest 派生,我们有了一个完全可用的示例,现在我们可以用它来探索我们将要绘制的坐标系。该活动将显示您触摸屏幕的坐标,就像在旧的 TouchTest 示例中一样。这次不同的是,我们是全屏的,这意味着我们触摸事件的最大坐标等于屏幕分辨率(每个维度减一,因为我们从[0,0]开始)。对于 Nexus One,在纵向模式下,坐标系将跨越坐标(0,0)到(479,799)(总共 480×800 像素)。

虽然看起来屏幕是连续重绘的,但实际上不是。请记住,在我们的 TouchTest 类中,每次处理触摸事件时,我们都会更新 TextView。这反过来会使 TextView 重绘自身。如果我们不触摸屏幕,文本视图不会自己重绘。对于一个游戏,我们需要尽可能频繁地重绘屏幕,最好是在我们的主循环线程中。我们将从简单开始,从 UI 线程中的连续呈现开始。

UI 线程中的连续呈现

到目前为止,我们所做的只是在需要时设置 TextView 的文本。实际的渲染是由 TextView 本身执行的。让我们创建自己的自定义视图,它的唯一目的是让我们在屏幕上绘制内容。我们还希望它尽可能经常地重画自己,并且我们希望在那个神秘的重画方法中有一个简单的方法来执行我们自己的绘制。

虽然这听起来可能很复杂,但实际上 Android 让我们很容易就能创建这样的东西。我们所要做的就是创建一个从 View 类派生的类,并覆盖一个名为 View.onDraw()的方法。每当 Android 系统需要我们的视图重绘自己时,它都会调用这个方法。这可能是这样的:

class RenderView extends View {public RenderView(Context context) {super (context);}protected void onDraw(Canvas canvas) {// to be implemented}
}

不完全是火箭科学,是吗?我们将一个名为 Canvas 的类的实例传递给 onDraw()方法。这将是我们在下面几节中的主要工作。它允许我们将形状和位图绘制到另一个位图或视图(或表面,我们稍后会谈到)。

我们可以像使用 TextView 一样使用这个 RenderView。我们只是将它设置为活动的内容视图,并连接我们需要的任何输入侦听器。然而,它还不是那么有用,有两个原因:它实际上并不绘制任何东西,即使它能够绘制某些东西,它也只会在需要重绘活动时才会这样做(也就是说,当它被创建或恢复时,或者当一个与它重叠的对话框变得不可见时)。怎么才能让它自己重画?

简单,像这样:

protected void onDraw(Canvas canvas) {// all drawing goes hereinvalidate();
}

onDraw()末尾对 View.invalidate()方法的调用将告诉 Android 系统一旦找到时间就重新绘制 RenderView。所有这些仍然发生在 UI 线程上,这有点像一匹懒马。然而,我们实际上用 onDraw()方法进行了连续渲染,尽管连续渲染相对较慢。我们稍后会解决这个问题;目前,它足以满足我们的需求。

那么,让我们回到神秘的画布类。这是一个非常强大的类,它封装了一个名为 Skia 的自定义低级图形库,专门用于在 CPU 上执行 2D 渲染。Canvas 类为我们提供了许多绘制各种形状、位图甚至文本的方法。

绘制方法绘制到哪里?那得看情况。画布可以呈现为位图实例;位图是由 Android 的 2D API 提供的另一个类,我们将在本章后面研究它。在这种情况下,它绘制到视图在屏幕上占据的区域。当然,这是一种疯狂的过度简化。在底层,它不会直接绘制到屏幕上,而是绘制到某种位图上,系统稍后会将该位图与活动的所有其他视图的位图结合使用,以合成最终的输出图像。然后,该图像将被移交给 GPU,GPU 将通过另一组神秘的路径将其显示在屏幕上。

我们真的不需要关心细节。从我们的角度来看,我们的视图似乎延伸到整个屏幕,所以它也可能是绘制到系统的帧缓冲区。在接下来的讨论中,我们将假设我们直接绘制到 framebuffer,系统为我们做所有漂亮的事情,如垂直回扫和双缓冲。

只要系统允许,就会调用 onDraw()方法。对我们来说,它非常类似于我们理论游戏主循环的主体。如果我们要用这个方法实现一个游戏,我们要把所有的游戏逻辑都放在这个方法中。出于各种原因,我们不会这样做,性能是其中之一。

所以让我们做一些有趣的事情。每次访问新的绘图 API 时,编写一个小测试来检查屏幕是否真的频繁重绘。有点像穷人的灯光秀。在每次调用 redraw 方法时,您需要做的就是用一种新的随机颜色填充屏幕。这样,您只需要找到允许您填充屏幕的那个 API 的方法,而不需要了解很多细节。让我们用我们自己的自定义 RenderView 实现来编写这样一个测试。

画布用特定颜色填充其呈现目标的方法称为 Canvas.drawRGB():

Canvas.drawRGB(int r, int g, int b);

r、g 和 b 参数分别代表我们将用来填充“屏幕”的颜色的一个分量。它们中的每一个都必须在 0 到 255 的范围内,所以我们实际上在这里指定了 RGB888 格式的颜色。如果你不记得关于颜色的细节,再看一下第三章的“数字编码颜色”一节,因为我们将在本章的其余部分使用这些信息。

清单 4-12 显示了我们的小灯光秀的代码。

Caution Running this code will rapidly fill the screen with a random color. If you have epilepsy or are otherwise light-sensitive in any way, don’t run it.

***清单 4-12。***RenderViewTest 活动

package com.badlogic.androidgames;import java.util.Random;import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;public class RenderViewTest extends Activity {class RenderView extends View {Random rand = new Random();public RenderView(Context context) {super (context);}protected void onDraw(Canvas canvas) {canvas.drawRGB(rand.nextInt(256), rand.nextInt(256),rand.nextInt(256));invalidate();}}@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);requestWindowFeature(Window.*FEATURE*_*NO*_*TITLE*);getWindow().setFlags(WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*,WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*);setContentView(new RenderView(this ));}
}

对于我们的第一个图形演示,这是非常简洁的。我们将 RenderView 类定义为 RenderViewTest 活动的内部类。如前所述,RenderView 类派生自 View 类,具有一个强制构造函数和一个被覆盖的 onDraw()方法。它还有一个 Random 类的实例作为成员;我们将用它来生成我们的随机颜色。

onDraw()方法非常简单。我们首先告诉画布用随机颜色填充整个视图。对于每个颜色分量,我们简单地指定一个 0 到 255 之间的随机数(Random.nextInt()是唯一的)。之后,我们告诉系统我们希望尽快再次调用 onDraw()方法。

活动的 onCreate()方法启用全屏模式,并将 RenderView 类的一个实例设置为内容视图。为了使示例简短,我们现在不考虑唤醒锁。

截取这个例子的截图有点没有意义。它所做的只是在 UI 线程上以系统允许的最快速度用随机颜色填充屏幕。这没什么值得大书特书的。让我们做一些更有趣的事情:画一些形状。

注意前面的连续渲染方法可以工作,但是我们强烈建议不要使用它!我们应该在 UI 线程上做尽可能少的工作。一分钟后,我们将使用一个单独的线程来讨论如何正确地做到这一点,稍后我们还可以实现我们的游戏逻辑。

获取屏幕分辨率(和坐标系)

在第二章中,我们讨论了很多关于帧缓冲区及其属性的内容。请记住,帧缓冲区保存了屏幕上显示的像素的颜色。我们可用的像素数是由屏幕分辨率定义的,屏幕分辨率是由屏幕的宽度和高度(以像素为单位)给出的。

现在,使用我们的自定义视图实现,我们实际上并不直接渲染到帧缓冲区。然而,由于我们的视角跨越了整个屏幕,我们可以假装它是这样的。为了知道我们可以在哪里渲染我们的游戏元素,我们需要知道 x 轴和 y 轴上有多少像素,或者屏幕的宽度和高度。

Canvas 类有两个方法为我们提供这些信息:

int width = canvas.getWidth();
int height = canvas.getHeight();

这将返回画布呈现的目标的宽度和高度(以像素为单位)。请注意,根据我们活动的方向,宽度可能小于或大于高度。例如,HTC Thunderbolt 在纵向模式下的分辨率为 480×800 像素,因此 Canvas.getWidth()方法将返回 480,Canvas.getHeight()方法将返回 800。在横向模式下,这两个值只是简单地交换:Canvas.getWidth()将返回 800,Canvas.getHeight()将返回 480。

我们需要知道的第二条信息是我们渲染的坐标系统的组织。首先,只有整数像素坐标才有意义(有个概念叫子像素,但我们会忽略)。我们还知道,在纵向模式和横向模式下,坐标系的原点(0,0)总是在显示屏的左上角。正 x 轴总是指向右侧,y 轴总是指向下方。图 4-12 显示了一个分辨率为 48×32 像素的假想屏幕,处于横向模式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-12。48×32 像素宽屏幕的坐标系

注意图 4-12 中坐标系的原点是如何与屏幕左上角的像素重合的。因此,屏幕左下角的像素不是我们预期的(48,32),而是(47,31)。通常,(width–1,height–1)总是屏幕右下角像素的位置。

图 4-12 显示了一个横向模式下的假想屏幕坐标系。到现在为止,你应该能够想象在纵向模式下坐标系是什么样子了。

Canvas 的所有绘制方法都是在这种坐标系下操作的。通常,我们可以寻址比 48×32 像素(例如 800×480)更多的像素。也就是说,让我们最后画一些像素、线条、圆形和矩形。

注意您可能已经注意到不同的设备可能有不同的屏幕分辨率。我们将在下一章研究这个问题。现在,让我们把注意力集中在最终让我们自己在屏幕上有所作为。

画简单的形状

深入到第四章,我们终于开始绘制我们的第一个像素。我们将快速浏览 Canvas 类提供给我们的一些绘图方法。

绘图像素

我们首先要解决的是如何绘制单个像素。那是用下面的方法完成的:

Canvas.drawPoint(float x, float y, Paint paint);

需要立即注意的两件事是,像素的坐标是用 floats 指定的,Canvas 不允许我们直接指定颜色,而是希望我们提供 Paint 类的一个实例。

不要被我们将坐标指定为浮点数的事实所迷惑。Canvas 有一些非常高级的功能,允许我们渲染到非整数坐标,这就是它的来源。不过,我们现在还不需要这个功能;我们将在下一章回到它。

Paint 类保存用于绘制形状、文本和位图的样式和颜色信息。对于绘制形状,我们只对两件事感兴趣:颜料的颜色和风格。既然一个像素并没有真正的风格,那我们就先集中在颜色上。下面是我们如何实例化 Paint 类并设置颜色:

Paint paint = new Paint();
paint.setARGB(alpha, red, green, blue);

实例化 Paint 类相当容易。Paint.setARGB()方法也应该很容易破译。每个参数代表颜色的一种颜色成分,范围从 0 到 255。因此,我们在这里指定了 ARGB8888 颜色。

或者,我们可以使用以下方法来设置 Paint 实例的颜色:

Paint.setColor(0xff00ff00);

我们向该方法传递一个 32 位整数。它再次编码 ARGB8888 颜色;在这种情况下,它是 alpha 设置为完全不透明的绿色。Color 类定义了一些静态常量,这些常量对一些标准颜色进行编码,比如 Color。红色,彩色。黄色,等等。如果您不想自己指定十六进制值,可以使用这些。

画线

要画一条线,我们可以使用下面的画布方法:

Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);

前两个参数指定线条起点的坐标,接下来的两个参数指定线条终点的坐标,最后一个参数指定 Paint 实例。画出的线将有一个像素厚。如果我们希望线条更粗,我们可以通过设置 Paint 实例的笔画宽度来以像素为单位指定线条的粗细:

Paint.setStrokeWidth(float widthInPixels);

绘制矩形

我们也可以用下面的画布方法画矩形:

Canvas.drawRect(float topleftX, float topleftY, float bottomRightX, float bottomRightY, Paint paint);

前两个参数指定矩形左上角的坐标,后两个参数指定矩形左下角的坐标,而 Paint 指定矩形的颜色和样式。那么我们可以有什么风格,如何设置呢?

要设置 Paint 实例的样式,我们调用以下方法:

Paint.setStyle(Style style);

Style 是具有值 Style 的枚举。填充,样式。笔画和风格。填充和描边。如果我们指定风格。填充,矩形将被填充油漆的颜色。如果我们指定风格。STROKE,将只绘制矩形的轮廓,同样使用绘画的颜色和笔画宽度。如果风格。设置 FILL_AND_STROKE,矩形将被填充,轮廓将用给定的颜色和笔画宽度绘制。

画圆

画圆可以带来更多的乐趣,可以是实心的,也可以是描边的(或者两者都画):

Canvas.drawCircle(float centerX, float centerY, float radius, Paint paint);

前两个参数指定圆心的坐标,下一个参数以像素为单位指定半径,最后一个参数也是一个 Paint 实例。与 Canvas.drawRectangle()方法一样,将使用颜料的颜色和样式来绘制圆。

混合

最后一件重要的事情是,所有这些绘制方法都将执行阿尔法混合。只需将颜色的 alpha 指定为 255 (0xff)以外的值,您的像素、线条、矩形和圆形将是半透明的。

把这一切放在一起

让我们编写一个快速测试活动来演示前面的方法。这一次,我们希望你首先分析清单 4-13 中的代码。在纵向模式下,找出 480×800 屏幕上不同形状将被绘制的位置。当进行图形编程时,最重要的是想象你发出的绘图命令将如何表现。这需要一些练习,但真的会有回报。

清单 4-13。【ShapeTest.java】;疯狂画形状

package com.badlogic.androidgames;import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;public class ShapeTest extends Activity {class RenderView extends View {Paint paint;public RenderView(Context context) {super (context);paint = new Paint();}protected void onDraw(Canvas canvas) {canvas.drawRGB(255, 255, 255);paint.setColor(Color.*RED*);canvas.drawLine(0, 0, canvas.getWidth()-1, canvas.getHeight()-1, paint);paint.setStyle(Style.*STROKE*);paint.setColor(0xff00ff00);canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 40, paint);paint.setStyle(Style.*FILL*);paint.setColor(0x770000ff);canvas.drawRect(100, 100, 200, 200, paint);invalidate();}}@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);requestWindowFeature(Window.*FEATURE*_*NO*_*TITLE*);getWindow().setFlags(WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*,WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*);setContentView(new RenderView(this ));}
}

你已经创造出那个心理图像了吗?那么我们来快速分析一下 RenderView.onDraw()方法。剩下的和上一个例子一样。

我们从用白色填充屏幕开始。接下来,我们从原点到屏幕的右下角画一条线。我们使用一种颜色设置为红色的颜料,所以这条线将是红色的。

接下来,我们稍微修改一下画图,将其样式设置为 style。笔画,其颜色为绿色,其阿尔法值为 255。使用我们刚刚修改的颜料,在屏幕的中心以 40 像素的半径绘制圆。由于绘画的风格,将只绘制圆的轮廓。

最后,我们再次修改油漆。我们把它的风格设定为风格。填充,颜色为全蓝色。请注意,我们这次将 alpha 设置为 0x77,这在十进制中等于 119。这意味着我们在下一次调用时绘制的形状大约有 50%是半透明的。

图 4-13 显示了纵向模式下 480×800 和 320×480 屏幕上测试活动的输出(黑色边框是后来添加的)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-13。480×800 屏幕(左)和 320×480 屏幕(右)上的 ShapeTest 输出

天啊,这里发生了什么事?这就是我们在不同屏幕分辨率下用绝对坐标和大小渲染得到的结果。两幅图中唯一不变的是红线,它只是从左上角画到右下角。这是以独立于屏幕分辨率的方式完成的。

该矩形位于(100,100)处。根据屏幕分辨率,到屏幕中心的距离会有所不同。矩形的大小为 100×100 像素。在大屏幕上,它比在小屏幕上占用的相对空间要少得多。

圆的位置也是独立于屏幕分辨率的,但它的半径不是。因此,它在较小的屏幕上比在较大的屏幕上占据更多的相对空间。

我们已经看到,处理不同的屏幕分辨率可能会有点问题。当我们考虑不同的物理屏幕尺寸时,情况会变得更糟。然而,我们将在下一章尝试解决这个问题。请记住,屏幕分辨率和物理尺寸很重要。

注意画布和绘画课程提供的远不止我们刚刚谈到的内容。事实上,所有标准的 Android 视图都是用这个 API 绘制的,所以你可以想象它背后有更多的东西。像往常一样,查看 Android 开发者网站获取更多信息。

使用位图

虽然用线条或圆形等基本形状制作游戏是可能的,但这并不十分性感。我们希望一个令人敬畏的艺术家为我们创建精灵和背景以及所有的爵士乐,然后我们可以从 PNG 或 JPEG 文件加载。在 Android 上做到这一点极其容易。

加载和检查位图

位图类将成为我们最好的朋友。我们通过使用 BitmapFactory singleton 从文件中加载位图。当我们以素材的形式存储图像时,让我们看看如何从素材/目录中加载图像:

InputStream inputStream = assetManager.open("bob.png");
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

Bitmap 类本身有一些我们感兴趣的方法。首先,我们想知道位图实例的宽度和高度,以像素为单位:

int width = bitmap.getWidth();
int height = bitmap.getHeight();

接下来我们可能想知道位图实例的颜色格式:

Bitmap.Config config = bitmap.getConfig();

位图。Config 是具有以下值的枚举:

  • 配置文件。阿尔法 8 号
  • 配置。ARGB_4444
  • 配置。ARGB_8888
  • Config.RGB_565

从第三章开始,你应该知道这些值是什么意思。如果没有,我们强烈建议你再读一遍第三章的“数字编码颜色”一节。

有趣的是,没有 RGB888 颜色格式。PNG 仅支持 ARGB8888、RGB888 和托盘化颜色。什么颜色格式将用于加载 RGB888 PNG?BitmapConfig。RGB_565 就是答案。对于我们通过 BitmapFactory 加载的任何 RGB888 PNG,这都会自动发生。原因是大多数 Android 设备的实际帧缓冲区都支持这种颜色格式。加载每像素位深度更高的图像会浪费内存,因为像素无论如何都需要转换为 RGB565 以进行最终渲染。

那么为什么会有配置?ARGB_8888 的配置呢?因为图像合成可以在将最终图像绘制到帧缓冲区之前在 CPU 上完成。在 alpha 组件的情况下,我们也有比 Config 多得多的位深度。ARGB_4444,这可能是一些高质量的图像处理所必需的。

ARGB8888 PNG 图像将加载到带有配置文件的位图中。ARGB_8888 配置。其他两种颜色格式很少使用。然而,我们可以告诉 BitmapFactory 尝试加载一个特定颜色格式的图像,即使它的原始格式是不同的。

InputStream inputStream = assetManager.open("bob.png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_4444;
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null , options);

我们使用重载的 BitmapFactory.decodeStream()方法以 BitmapFactory 实例的形式传递提示。图像解码器的选项类。我们可以通过 BitmapFactory 来指定位图实例所需的颜色格式。Options.inPreferredConfig 成员,如前所示。在这个假设的例子中,bob.png 文件将是一个 ARGB8888 PNG,我们希望 BitmapFactory 加载它并将其转换为 ARGB4444 位图。但是,BitmapFactory 可以忽略这个提示。

这将释放该位图实例使用的所有内存。当然,调用此方法后,您不能再使用位图进行渲染。

您也可以使用以下静态方法创建空位图:

Bitmap bitmap = Bitmap.createBitmap(int width, int height, Bitmap.Config config);

如果你想自己进行自定义图像合成,这可能会派上用场。Canvas 类也适用于位图:

Canvas canvas = new Canvas(bitmap);

然后,您可以像修改视图内容一样修改位图。

处置位图

BitmapFactory 可以帮助我们在加载图像时减少内存占用。位图会占用很多内存,这在第三章中已经讨论过了。通过使用较小的颜色格式来减少每像素的位数是有帮助的,但是如果我们继续一个接一个地加载位图,最终我们会耗尽内存。因此,我们应该通过下面的方法来处理我们不再需要的位图实例:

Bitmap.recycle();

绘制位图

一旦我们加载了位图,我们就可以通过画布来绘制它们。最简单的方法如下:

Canvas.drawBitmap(Bitmap bitmap, float topLeftX, float topLeftY, Paint paint);

第一个论点应该是显而易见的。参数 topLeftX 和 topLeftY 指定位图左上角在屏幕上的坐标。最后一个参数可以为空。我们可以用 Paint 指定一些非常高级的绘图参数,但是我们并不真的需要这些。

还有另一种方法也会派上用场:

Canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint);

这个方法超级牛逼。它允许我们通过第二个参数指定位图的一部分。Rect 类保存矩形的左上角和右下角坐标。当我们通过 src 指定位图的一部分时,我们是在位图的坐标系中进行的。如果我们指定 null,将使用完整的位图。

第三个参数定义在哪里绘制位图部分,同样以 Rect 实例的形式。这一次,角坐标是在画布目标的坐标系中给出的(无论是视图还是另一个位图)。令人惊讶的是,这两个矩形不一定要一样大。如果我们指定目标矩形比源矩形小,那么画布会自动缩放。当然,如果我们指定一个更大的目标矩形,情况也是如此。我们通常会将最后一个参数再次设置为 null。但是,请注意,这种缩放操作非常昂贵。我们应该只在绝对必要的时候使用它。

因此,您可能会想:如果我们有不同颜色格式的位图实例,在我们可以通过画布绘制它们之前,我们需要将它们转换成某种标准格式吗?答案是否定的。画布会自动为我们做这件事。当然,如果我们使用与本地帧缓冲区格式相同的颜色格式,速度会快一点。通常我们只是忽略这一点。

默认情况下,混合也是启用的,所以如果我们的图像每个像素包含一个 alpha 组件,它实际上是被解释的。

把这一切放在一起

有了所有这些信息,我们最终可以加载和渲染一些 bob。清单 4-14 显示了我们出于演示目的编写的 BitmapTest 活动的源代码。

***清单 4-14。***BitmapTest 活动

package com.badlogic.androidgames;import java.io.IOException;
import java.io.InputStream;import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;public class BitmapTest extends Activity {class RenderView extends View {Bitmap bob565;Bitmap bob4444;Rect dst = new Rect();public RenderView(Context context) {super (context);try {AssetManager assetManager = context.getAssets();InputStream inputStream = assetManager.open("bobrgb888.png");bob565 = BitmapFactory.*decodeStream*(inputStream);inputStream.close();Log.*d*("BitmapText","bobrgb888.png format: " + bob565.getConfig());inputStream = assetManager.open("bobargb8888.png");BitmapFactory.Options options = new BitmapFactory.Options();options.inPreferredConfig = Bitmap.Config.*ARGB*_*4444*;bob4444 = BitmapFactory.*decodeStream*(inputStream, null , options);inputStream.close();Log.*d*("BitmapText","bobargb8888.png format: " + bob4444.getConfig());}catch (IOException e) {// silently ignored, bad coder monkey, baaad!}finally {// we should really close our input streams here.}}protected void onDraw(Canvas canvas) {canvas.drawRGB(0, 0, 0);dst.set(50, 50, 350, 350);canvas.drawBitmap(bob565, null , dst, null );canvas.drawBitmap(bob4444, 100, 100, null );invalidate();}}@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);requestWindowFeature(Window.*FEATURE*_*NO*_*TITLE*);getWindow().setFlags(WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*,WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*);setContentView(new RenderView(this ));}
}

我们的 activity 的 onCreate()方法是旧的,所以让我们继续我们的自定义视图。它有两个位图成员,一个以 RGB565 格式存储 Bob 的图像(在第三章中介绍过),另一个以 ARGB4444 格式存储 Bob 的图像。我们还有一个 Rect 成员,在那里我们存储了用于呈现的目标矩形。

在 RenderView 类的构造函数中,我们首先将 Bob 加载到视图的 bob565 成员中。请注意,图像是从 RGB888 PNG 文件加载的,BitmapFactory 会自动将其转换为 RGB565 图像。为了证明这一点,我们还输出了位图。将位图配置到 LogCat。Bob 的 RGB888 版本具有不透明的白色背景,因此不需要执行任何混合。

接下来,我们从存储在 assets/ directory 中的 ARGB8888 PNG 文件加载 Bob。为了节省一些内存,我们还告诉 BitmapFactory 将 Bob 的图像转换为 ARGB4444 位图。工厂可能不会遵守这一要求(原因不明)。为了看它对我们是否友好,我们输出了位图。这个位图的配置文件。

onDraw()方法微不足道。我们所做的就是用黑色填充屏幕,绘制缩放到 250×250 像素的 bob565(从他的原始大小 160×183 像素),并在 bob565 的顶部绘制 bob4444,未缩放但已混合(这是由画布自动完成的)。图 4-14 展示了这两个 bob 的辉煌。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-14。上下重叠的两个 bob(分辨率为 480×800 像素)

LogCat 报告说 bob565 确实有颜色格式配置。RGB_565,bob4444 被转换为 Config。ARGB_4444。位图工厂没有让我们失望!

这里有一些你应该从这一部分学到的东西:

  • 使用尽可能少的颜色格式来节省内存。但是,这可能会降低视觉质量和渲染速度。
  • 除非绝对必要,否则不要绘制缩放的位图。如果您知道它们的缩放大小,请离线或在加载时预缩放它们。
  • 如果不再需要位图,请务必调用 Bitmap.recycle()方法。否则,你会得到一些内存泄漏或运行内存不足。

一直使用 LogCat 进行文本输出有点乏味。让我们看看如何通过画布呈现文本。

注意和其他类一样,位图有比我们在这个简短的部分所能描述的更多的东西。我们涵盖了给诺姆先生写信所需的最低限度。如果您想了解更多信息,请查看 Android 开发者网站上的文档。

渲染文本

虽然我们将在 Mr. Nom 游戏中输出的文本将由手工绘制,但了解如何通过 TrueType 字体绘制文本并没有坏处。让我们从从 assets/ directory 加载一个定制的 TrueType 字体文件开始。

加载字体

Android API 为我们提供了一个名为 Typeface 的类,它封装了一种 TrueType 字体。它提供了一个简单的静态方法来从 assets/ directory: 加载这样一个字体文件

Typeface font = Typeface.*createFromAsset*(context.getAssets(), "font.ttf");

有趣的是,如果字体文件无法加载,这个方法不会抛出任何异常。相反,会引发 RuntimeException。为什么这个方法没有显式抛出异常是一个谜。

用字体绘制文本

一旦我们有了自己的字体,我们就将它设置为 Paint 实例的字样:

paint.setTypeFace(font);

通过 Paint 实例,我们还指定了想要呈现字体的大小:

paint.setTextSize(30);

这种方法的文档也很少。它没有告诉我们文本大小是以磅还是像素给出的。我们只是假设后者。

最后,我们可以通过下面的 Canvas 方法用这种字体绘制文本:

canvas.drawText("This is a test!", 100, 100, paint);

第一个参数是要绘制的文本。接下来的两个参数是应该绘制文本的坐标。最后一个参数是熟悉的:它是 Paint 实例,指定要绘制的文本的颜色、字体和大小。通过设置绘画的颜色,您还可以设置要绘制的文本的颜色。

文本对齐和边界

现在,您可能想知道前面方法的坐标如何与文本字符串填充的矩形相关联。它们是否指定了包含文本的矩形的左上角?答案有点复杂。Paint 实例有一个名为对齐设置的属性。它可以通过画图类的这个方法来设置:

Paint.setTextAlign(Paint.Align align);

油漆。Align 枚举有三个值:Paint。对齐。向左,绘画。根据设置的对齐方式,传递给 Canvas.drawText()方法的坐标被解释为矩形的左上角、矩形的中上像素或矩形的右上角。标准对齐方式是 Paint.Align.LEFT。

有时知道特定字符串的边界(以像素为单位)也很有用。为此,Paint 类提供了以下方法:

Paint.getTextBounds(String text, int start, int end, Rect bounds);

第一个参数是我们想要得到界限的字符串。第二个和第三个参数指定应该测量的字符串中的开始字符和结束字符。end 参数是排他的。最后一个参数 bounds 是一个 Rect 实例,我们自己分配并传递给方法。该方法会将边框的宽度和高度写入 Rect.right 和 Rect.bottom 字段。为了方便起见,我们可以调用 Rect.width()和 Rect.height()来获得相同的值。

请注意,所有这些方法都只适用于单行文本。如果要渲染多行,就得自己做布局。

把这一切放在一起

说够了:让我们做更多的编码。清单 4-15 展示了文本呈现的实际效果。

***清单 4-15。***font test 活动

package com.badlogic.androidgames;import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;public class FontTest extends Activity {class RenderView extends View {Paint paint;Typeface font;Rect bounds = new Rect();public RenderView(Context context) {super (context);paint = new Paint();font = Typeface.*createFromAsset*(context.getAssets(), "font.ttf");}protected void onDraw(Canvas canvas) {canvas.drawRGB(0, 0, 0);paint.setColor(Color.*YELLOW*);paint.setTypeface(font);paint.setTextSize(28);paint.setTextAlign(Paint.Align.*CENTER*);canvas.drawText("This is a test!", canvas.getWidth() / 2, 100,paint);String text = "This is another test o_O";paint.setColor(Color.*WHITE*);paint.setTextSize(18);paint.setTextAlign(Paint.Align.*LEFT*);paint.getTextBounds(text, 0, text.length(), bounds);canvas.drawText(text, canvas.getWidth() - bounds.width(), 140,paint);invalidate();}}@Overridepublic void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);requestWindowFeature(Window.*FEATURE*_*NO*_*TITLE*);getWindow().setFlags(WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*,WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*);setContentView(new RenderView(this ));}
}

我们不会讨论活动的 onCreate()方法,因为我们以前见过它。

我们的 RenderView 实现有三个成员:Paint、Typeface 和 Rect,稍后我们将在其中存储文本字符串的边界。

在构造函数中,我们创建一个新的 Paint 实例,并从 assets/目录中的 font.ttf 文件加载一个字体。

在 onDraw()方法中,我们用黑色清除屏幕,将颜料设置为黄色,设置字体及其大小,并指定在 Canvas.drawText()调用中解释坐标时要使用的文本对齐方式。实际的绘图调用渲染字符串这是一个测试!,在 y 轴上的坐标 100 处水平居中。

对于第二个文本呈现调用,我们做了其他事情:我们希望文本与屏幕的右边缘右对齐。我们可以通过使用油漆来实现。Align.RIGHT 和 canvas . getwidth()–1 的 x 坐标。相反,我们通过使用字符串的边界来练习非常基本的文本布局。我们还改变了颜色和字体的大小。图 4-15 显示了此活动的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-15。文字趣味(480×800 像素分辨率)

Typeface 类的另一个神秘之处是,它没有明确允许我们释放它的所有资源。我们不得不依靠垃圾收集者来为我们做脏活。

注意我们在这里仅仅触及了文本渲染的表面。如果你想知道更多。。。现在你知道去哪里找了。

使用表面视图进行连续渲染

这是我们成为真正的男人和女人的部分。它涉及到线程,以及与之相关的所有痛苦。我们会活着度过的。我们保证!

动机

当我们第一次尝试连续渲染时,我们用了错误的方法。霸占 UI 线程是不可接受的;我们需要一个在单独的线程中完成所有脏活的解决方案。进入 SurfaceView。

顾名思义,SurfaceView 类是一个处理 Surface 的视图,这是 Android API 的另一个类。什么是曲面?它是一个原始缓冲区的抽象,由屏幕合成器用来渲染特定的视图。屏幕合成器是 Android 上所有渲染背后的主谋,它最终负责将所有像素推送到 GPU。在某些情况下,可以对表面进行硬件加速。不过,我们并不太关心这个事实。我们只需要知道,这是一种更直接的将事物渲染到屏幕上的方式。

我们的目标是在一个单独的线程中执行渲染,这样我们就不会占用忙于其他事情的 UI 线程。SurfaceView 类为我们提供了一种从一个线程而不是 UI 线程来渲染它的方法。

表面夹具和锁定

为了从一个不同于 UI 线程的线程渲染到 SurfaceView,我们需要获取 SurfaceHolder 类的一个实例,如下:

SurfaceHolder holder = surfaceView.getHolder();

SurfaceHolder 是表面的包装器,为我们做一些簿记工作。它为我们提供了两种方法:

Canvas SurfaceHolder.lockCanvas();
SurfaceHolder.unlockAndPost(Canvas canvas);

第一种方法锁定表面进行渲染,并返回一个我们可以使用的不错的 Canvas 实例。第二种方法再次解锁表面,并确保我们通过画布绘制的内容显示在屏幕上。我们将在渲染线程中使用这两种方法来获取画布,用它进行渲染,并最终使我们刚刚渲染的图像在屏幕上可见。我们必须传递给 SurfaceHolder.unlockAndPost()方法的画布必须是我们从 SurfaceHolder.lockCanvas()方法收到的画布。

实例化 SurfaceView 时,不会立即创建曲面。相反,它是异步创建的。每次暂停活动时都会破坏该表面,当活动恢复时会重新创建该表面。

表面创建和有效性

只要表面还没有生效,我们就不能从表面持有者那里获得画布。但是,我们可以通过下面的语句检查表面是否已经创建:

boolean isCreated = surfaceHolder.getSurface().isValid();

如果这个方法返回 true,我们就可以安全地锁定这个表面,并通过我们收到的画布绘制它。我们必须绝对确保在调用 SurfaceHolder.lockCanvas()后再次解锁 Surface,否则我们的活动可能会锁定手机!

把这一切放在一起

那么,我们如何将所有这些与单独的渲染线程以及活动生命周期集成在一起呢?解决这个问题的最好方法是查看一些实际的代码。清单 4-16 显示了一个完整的例子,它在一个单独的线程中对表面视图进行渲染。

***清单 4-16。***SurfaceViewTest 活动

package com.badlogic.androidgames;import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;public class SurfaceViewTest extends Activity {FastRenderView renderView;public void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState);requestWindowFeature(Window.*FEATURE*_*NO*_*TITLE*);getWindow().setFlags(WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*,WindowManager.LayoutParams.*FLAG*_*FULLSCREEN*);renderView = new FastRenderView(this );setContentView(renderView);}protected void onResume() {super .onResume();renderView.resume();}protected void onPause() {super .onPause();renderView.pause();}class FastRenderView extends SurfaceViewimplements Runnable {Thread renderThread = null ;SurfaceHolder holder;volatile boolean running = false ;public FastRenderView(Context context) {super (context);holder = getHolder();}public void resume() {running = true ;renderThread = new Thread(this );renderThread.start();}public void run() {while (running) {if (!holder.getSurface().isValid())continue ;Canvas canvas = holder.lockCanvas();canvas.drawRGB(255, 0, 0);holder.unlockCanvasAndPost(canvas);}}public void pause() {running = false ;while (true ) {try {renderThread.join();return ;}catch (InterruptedException e) {// retry}}}}
}

这看起来没那么吓人,对吧?我们的活动将 FastRenderView 实例作为成员。这是一个自定义的 SurfaceView 子类,将为我们处理所有的线程业务和表面锁定。对于活动来说,它看起来像一个普通的视图。

在 onCreate()方法中,我们启用全屏模式,创建 FastRenderView 实例,并将其设置为活动的内容视图。

这次我们还覆盖了 onResume()方法。在这个方法中,我们将通过调用 FastRenderView.resume()方法间接启动我们的渲染线程,该方法在内部完成所有的工作。这意味着线程将在最初创建活动时启动(因为 onCreate()后面总是跟有对 onResume()的调用)。当活动从暂停状态恢复时,它也会重新启动。

这当然意味着我们必须在某个地方停止线程;否则,我们会在每次调用 onResume()时创建一个新线程。这就是 onPause()的用武之地。它调用 FastRenderView.pause()方法,这将完全停止线程。在线程完全停止之前,该方法不会返回。

所以我们来看看这个例子的核心类:FastRenderView。它类似于我们在前几个例子中实现的 RenderView 类,因为它是从另一个视图类派生的。在这种情况下,我们直接从 SurfaceView 类派生它。它还实现了 Runnable 接口,因此我们可以将它传递给渲染线程,以便它运行渲染线程逻辑。

FastRenderView 类有三个成员。renderThread 成员只是对负责执行渲染线程逻辑的线程实例的引用。holder 成员是对 SurfaceHolder 实例的引用,该实例是从派生它的 SurfaceView 超类中获得的。最后,running 成员是一个简单的布尔标志,我们将使用它来通知渲染线程应该停止执行。volatile 修饰符有一个特殊的含义,我们一会儿会讲到。

我们在构造函数中所做的就是调用超类构造函数,并将对 SurfaceHolder 的引用存储在 Holder 成员中。

接下来是 FastRenderView.resume()方法。它负责启动渲染线程。注意,每次调用这个方法时,我们都会创建一个新线程。这与我们在讨论活动的 onResume()和 onPause()方法时所讨论的一致。我们还将运行标志设置为 true。一会儿你会看到它是如何在渲染线程中使用的。最后要做的是,我们将 FastRenderView 实例本身设置为线程的 Runnable。这将在新线程中执行 FastRenderView 的下一个方法。

FastRenderView.run()方法是我们自定义视图类的核心。它的主体在渲染线程中执行。如您所见,它仅仅由一个循环组成,一旦 running 标志被设置为 false,该循环将停止执行。当这种情况发生时,线程也将停止并死亡。在 while 循环中,我们首先检查表面是否有效。如果是,我们锁定它,渲染它,并再次解锁它,如前所述。在这个例子中,我们简单地用红色填充表面。

FastRenderView.pause()方法看起来有点奇怪。首先,我们将运行标志设置为 false。如果稍微向上看一下,就会看到 FastRenderView.run()方法中的 while 循环最终会因此而终止,从而停止渲染线程。在接下来的几行中,我们只是通过调用 Thread.join()来等待线程完全死亡。此方法将等待线程死亡,但可能会在线程实际死亡之前引发 InterruptedException。因为在从那个方法返回之前,我们必须绝对确定线程是死的,所以我们在一个无限循环中执行 join,直到它成功。

让我们回到运行标志的 volatile 修饰符。我们为什么需要它?原因很微妙:如果编译器发现 FastRenderView.pause()方法中的第一行与 while 块之间没有依赖关系,它可能会决定对该方法中的语句进行重新排序。如果它认为这样做会使代码执行得更快,它是被允许这样做的。然而,我们依赖于在该方法中指定的执行顺序。想象一下,如果在我们尝试加入线程后设置了运行标志。我们会进入一个无限循环,因为线程永远不会终止。

volatile 修饰符防止这种情况发生。引用该成员的任何语句都将按顺序执行。这让我们远离了讨厌的海森堡——一个来来去去却无法持续复制的 bug。

还有一件事你可能认为会导致这段代码爆炸。如果在调用 SurfaceHolder.getSurface()的过程中销毁了曲面,会怎么样呢?isValid()和 SurfaceHolder.lock()?嗯,我们很幸运——这种事永远不会发生。为了理解为什么,我们必须后退一步,看看 Surface 的生命周期是如何工作的。

我们知道表面是异步创建的。很可能我们的渲染线程会在表面有效之前执行。我们通过不锁定表面来防止这种情况,除非它是有效的。这涵盖了曲面创建的情况。

在有效性检查和锁定之间,渲染线程代码不会从被破坏的表面开始爆炸的原因与表面被破坏的时间点有关。从活动的 onPause()方法返回后,表面总是被销毁。因为我们通过调用 FastRenderView.pause()来等待线程在该方法中死亡,所以当表面实际上被破坏时,渲染线程将不再是活动的。很性感,不是吗?但也很混乱。

我们现在以正确的方式进行连续渲染。我们不再独占 UI 线程,而是使用单独的渲染线程。我们还让它遵守活动生命周期,这样它就不会在后台运行,在活动暂停时消耗电池。整个世界又是一个快乐的地方。当然,我们需要将 UI 线程中输入事件的处理与渲染线程同步。但是这将会变得非常容易,你会在下一章看到,当我们基于你在这一章中理解的所有信息实现我们的游戏框架时。

使用 Canvas 进行硬件加速渲染

Android 3.0 (Honeycomb)增加了一个显著的功能,即支持标准 2D 画布绘制调用的 GPU 硬件加速。此功能的价值因应用和设备而异,因为一些设备实际上在 2D 利用 CPU 时性能会更好,而其他设备将受益于 GPU。在引擎盖下,硬件加速分析绘制调用,并将其转换为 OpenGL。例如,如果我们指定应该从 0,0 到 100,100 绘制一条线,那么硬件加速将使用 OpenGL 组织一个特殊的画线调用,并将其绘制到一个硬件缓冲区,稍后将合成到屏幕上。

启用这种硬件加速非常简单,只需将以下内容添加到 AndroidManifest.xml 的标签下:

android:hardwareAccelerated="true"

请确保在各种设备上打开和关闭加速来测试您的游戏,以确定它是否适合您。将来,让它一直开着可能没什么问题,但是和任何事情一样,我们建议你自己采取测试和决定的方法。当然,有更多的配置选项可以让你为特定的应用、活动、窗口或视图设置硬件加速,但因为我们是在做游戏,所以我们只计划每种都有一个,所以通过应用全局设置它将是最有意义的。

Android 这一功能的开发者 Romain Guy 有一篇非常详细的博客文章,介绍了硬件加速的注意事项和注意事项,以及使用它获得良好性能的一些通用指南。博客条目的网址是Android-developers . blogspot . com/2011/03/Android-30-hardware-acceleration . html

最佳实践

Android(或者更确切地说是 Dalvik)有时会有一些奇怪的性能特征。在本节中,我们将向您介绍一些最重要的最佳实践,您应该遵循这些实践来使您的游戏像丝绸一样流畅。

  • 垃圾收集者是你最大的敌人。一旦它获得 CPU 时间来做它的脏工作,它将停止世界长达 600 毫秒。这是半秒钟,你的游戏将不会更新或渲染。用户会抱怨。尽可能避免创建对象,尤其是在内部循环中。
  • 对象可能被创建在一些不太明显的地方,而这些地方是你想要避免的。不要使用迭代器,因为它们会创建新的对象。不要使用任何标准的集合或地图集合类,因为它们会在每次插入时创建新的对象;而是使用 Android API 提供的 SparseArray 类。使用 StringBuffers,而不是用+运算符连接字符串。这将每次创建一个新的 StringBuffer。为了这个世界上所有美好的事物,不要使用装箱的原语!
  • 与其他虚拟机相比,Dalvik 中的方法调用具有更大的关联成本。如果可以的话,使用静态方法,因为静态方法的性能最好。静态方法通常被认为是邪恶的,就像静态变量一样,因为它们促进了糟糕的设计,所以尽量保持你的设计整洁。也许你也应该避免 getters 和 setters。直接字段访问比不使用 JIT 编译器的方法调用快三倍,使用 JIT 编译器快七倍。然而,在移除所有的 getters 和 setters 之前,请考虑您的设计。
  • 浮点运算是在没有 JIT 编译器的旧设备和 Dalvik 版本(Android 版之前的任何版本)上用软件实现的。守旧派游戏开发者会立即退回到定点数学。也不要这样做,因为整数除法也很慢。大多数时候,您可以使用浮点,新的设备支持浮点单元(fpu ),一旦 JIT 编译器开始运行,速度会加快很多。
  • 尝试将频繁访问的值塞进方法内部的局部变量中。访问局部变量比访问成员或调用 getters 更快。

当然,你需要小心许多其他的事情。当上下文需要时,我们将在本书的其余部分添加一些性能提示。如果你遵循前面的建议,你应该是安全的。别让收垃圾的赢了就行!

摘要

这一章涵盖了为 Android 写一个像样的小 2D 游戏所需要知道的一切。我们看到了用一些默认设置建立一个新的游戏项目是多么容易。我们讨论了神秘的活动生命周期以及如何与之共存。我们与触摸(更重要的是多点触摸)事件进行了斗争,处理了按键事件,并通过加速度计检查了设备的方向。我们探索了如何读写文件。在 Android 上输出音频被证明是轻而易举的事情,除了 SurfaceView 的线程问题之外,在屏幕上绘制东西也不是很难。诺姆先生现在可以成为现实了——一个可怕的、饥饿的现实!

http://www.hrbkazy.com/news/925.html

相关文章:

  • 温州网站建设推广网络营销课程培训机构
  • 外贸自建站多少钱新品推广计划与方案
  • 惠州网站建设制作厦门百度代理公司
  • 阿里云服务器登录入口seo网站推广价格
  • 网站建设工作成果怎么写广州seo关键词优化费用
  • 做网站哪个系统最好百度搜索链接
  • 怎么创建网站免费的新网站如何推广
  • 自助网站制作如何接广告赚钱
  • 汕头seo建站百度账号查询
  • 找建网站公司网站制作费用
  • 怎样建立企业网站免费可用的网站源码
  • 网站建设对企业的意义百家号关键词排名优化
  • 哪个基层司法所网站做的比较好整合营销传播策划方案
  • 松江品划做网站重庆百度
  • 网站建设报道稿信息流推广渠道有哪些
  • ui中国设计网站页面百度搜索智能精选入口
  • 济南街道办网站建设推广赚钱app哪个靠谱
  • 怎么做网站备案seo网络推广什么意思
  • 和县网站制作网络营销制度课完整版
  • 怎么在ps里做网站设计搜狗推广登录
  • 网络营销是什么课程优化推荐
  • 小程序网站建设的公司百度经验实用生活指南
  • 外贸网网站建设seo技术大师
  • 三亚房地产网站制作网络推广具体内容
  • 网站建设的公百度一下app
  • 做网站需要什么电脑游戏推广
  • 效果图参考网站简述网络营销的特点
  • 12306网站做的好垃圾广州seo招聘网
  • 建网站空间可以不买发布推广信息的网站
  • wordpress评论发邮件成都关键词seo推广电话