建站哪家好要认定兴田德润,长沙官网seo技术厂家,软件网络推广方案,西城网站建设浩森宇特原文#xff1a;Beginning Android Games 协议#xff1a;CC BY-NC-SA 4.0 零、简介
大家好#xff0c;欢迎来到 Android 游戏开发的世界。你来这里是为了学习 Android 上的游戏开发#xff0c;我们希望成为让你实现自己想法的人。
我们将一起涵盖相当广泛的材质和主题:A… 原文Beginning Android Games 协议CC BY-NC-SA 4.0 零、简介
大家好欢迎来到 Android 游戏开发的世界。你来这里是为了学习 Android 上的游戏开发我们希望成为让你实现自己想法的人。
我们将一起涵盖相当广泛的材质和主题:Android 基础知识音频和图形编程一点数学和物理OpenGL ESAndroid 原生开发工具包(NDK)介绍最后出版营销从你的游戏中赚钱。基于所有这些知识我们将开发三个不同的游戏其中一个甚至是 3D 的。
如果你知道自己在做什么游戏编程会很容易。因此我们试图以这样一种方式呈现这些材质不仅给你有用的代码片段供你重用而且实际上向你展示游戏开发的全貌。理解潜在的原则是解决越来越复杂的游戏想法的关键。你不仅能够编写与本书中开发的游戏相似的游戏而且你还将具备足够的知识去上网或逛书店自己开发新的游戏领域。
这本书是给谁的
这本书首先面向游戏编程的完全初学者。你不需要任何关于主题的先验知识我们会教你所有的基本知识。然而我们需要假设你对 Java 有一点了解。如果你对这个问题感到生疏我们建议你读一读布鲁斯·埃凯尔(Prentice Hall2006 年)的《用 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?f21发帖联系 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 外围支持这使制造商可以选择支持键盘和操纵杆。至于新的 APIICS 增加了一些比如 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 有企鹅 TuxGNU 有它的。。。好吧gnuMozilla 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 的可编程 GPUWVGA 屏风多点触摸支持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 的可编程 GPU1280 × 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 应用。这包括以下步骤:
设置开发环境。在 Eclipse 中创建新项目并编写代码。在模拟器或设备上运行应用。调试和分析应用。
我们将通过研究有用的第三方工具来结束本章。让我们从设置开发环境开始。
设置开发环境
Android SDK 非常灵活可以很好地与多种开发环境集成。纯粹主义者可能会选择使用命令行工具。不过我们希望事情变得更舒适一点所以我们将使用 IDE(集成开发环境)走更简单、更可视化的路线。
以下是您需要按照给定顺序下载并安装的软件列表:
Java 开发工具包(JDK) 版本 5 或 6。我们建议用 6。在撰写本文时JDK 7 在 Android 开发方面存在问题。必须指示编译器为 Java 6 编译。Android 软件开发工具包(Android SDK)。Eclipse for Java Developers3.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 插件管理器。这里我们将选择第二条路线: 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。添加存储库 在第一个文本字段中输入存储库的名称类似“ADT 存储库”的东西就可以了。第二个文本字段指定存储库的 URL。对于 ADT 插件该字段应为https://dl-ssl.google.com/android/eclipse/。请注意对于较新的版本此 URL 可能会有所不同因此请查看 ADT 插件网站以获取最新链接。 单击 OK您将返回到安装对话框现在应该会获取存储库中可用插件的列表。选中开发工具复选框然后单击下一步按钮。 Eclipse 会计算所有必要的依赖项然后向您呈现一个新的对话框其中列出了将要安装的所有插件和依赖项。单击“下一步”按钮进行确认。 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. 注意在安装过程中会要求您确认未签名软件的安装。别担心插件只是没有经过验证的签名。同意安装以继续该过程。 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 的位置:
打开窗口偏好设置并在出现的对话框的树形视图中选择 Android。在右侧单击浏览按钮选择 Android SDK 安装的根目录。单击“确定”按钮关闭对话框。现在您将能够创建您的第一个 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 削减。ctrlc 副本。ctrlv 蛋糕。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 项目向导对话框你必须做出一些决定。请遵循以下步骤: 定义应用名称。这是 Android 上的启动器中显示的名称。我们会用“你好世界” 指定项目名称。这是您的项目在 Eclipse 中将被引用的名称。习惯上使用全部小写字母所以我们将输入“helloworld” 指定包名。这是您所有 Java 代码所在的包的名称。向导试图根据您的项目名来猜测您的包名但是您可以根据自己的需要随意修改它。在本例中我们将使用“com.helloworld”。 指定生成 SDK。选择安卓 4.1。这允许我们使用最新的 API。 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 特性。这听起来可能很糟糕但是正如你将在第五章中看到的给定一个好的应用架构你可以很容易地启用和禁用某些特定于版本的功能而没有崩溃的风险。 单击下一步。您将看到一个对话框让您定义您的应用的图标。我们将保持一切不变因此只需单击“下一步”。 在下一个对话框中询问您是否想要创建一个空白活动。接受此选择然后单击“下一步”继续。 在最后一个对话框中您可以修改向导将为您创建的空白活动的一些属性。我们将活动名称设置为“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 上的应用创建包含图标图像的文件夹。下面是我们解决这个问题的方法:
在 res/目录下创建一个名为 drawable/的文件夹。您可以在 Package Explorer 视图中直接这样做方法是右键单击 res/目录并从上下文菜单中选择 New Folder。将 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 的步骤: 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)对话框 每个 AVD 都有一个名称您可以通过它来引用它。你可以自由选择任何你想要的名字。 目标指定 AVD 应该使用的 Android 版本。对于我们简单的“hello world”项目您可以选择一个 Android 4.0.3 目标。 CPU/ABI 指定 AVD 应该模拟哪种 CPU 类型。在此选择手臂。 您可以通过皮肤设置中的选项指定 AVDsd 卡的大小以及屏幕大小。让这些字段保持原样。对于实际测试您通常会希望创建多个 avd覆盖您希望应用处理的所有 Android 版本和屏幕尺寸。 启用快照选项将在您关闭时保存模拟器的状态。下次启动时模拟器将加载保存状态的快照而不是引导。这可以在启动新的模拟器实例时节省一些时间。 硬件选项更先进。我们将在下一节中探究其中的一些。它们允许您修改仿真器设备和仿真器本身的低级属性例如仿真器的图形输出是否应该进行硬件加速。
注意除非你有几十个不同 Android 版本和屏幕尺寸的不同设备否则你应该使用仿真器对 Android 版本/屏幕尺寸组合进行额外测试。
安装高级仿真器功能
现在有一些硬件虚拟化实现支持 Android 模拟器英特尔是其中之一。如果您有英特尔 CPU您应该能够安装英特尔硬件加速执行管理器(HAXM) 它与 x86 仿真器映像结合使用将虚拟化您的 CPU并且运行速度明显快于普通的完全仿真映像。与此同时启用 GPU 加速将(理论上)提供一个合理的性能测试环境。我们对这些工具当前状态的经验是它们仍然有一点缺陷但事情看起来很有希望所以请确保关注 Google 的官方声明。同时让我们做好准备: 从英特尔下载并安装 HAXM 软件可在http://software.intel.com/en-us/articles/intel-hardware-accelerated-execution-manager/获得。 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 系统映像 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 将在后台执行以下步骤:
如果自上次编译以来有任何文件发生了更改则将项目编译为 APK 文件。为 Android 项目创建一个新的运行配置(如果尚不存在的话)。(我们稍后将了解运行配置。)通过使用合适的 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 项目的运行配置请执行以下操作:
在 Package Explorer 视图中右键单击项目并选择 Run AsRun configuration。从左侧的列表中选择“hello world”项目。在对话框的右侧您现在可以修改运行配置的名称并更改 Android、Target 和 Commons 选项卡上的其他设置。要将自动部署更改为手动部署请单击目标选项卡并选择手动。
当您再次运行应用时系统会提示您选择一个兼容的仿真器或设备来运行应用。图 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-resourcesPVRTune/PVRTrace :用在 PowerVR 芯片上(三星LG等)http://www.imgtec.com/powervr/insider/powervr-utilities.aspNVidia 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 美元一张超过 100000 次下载这对于一个非常简单的游戏来说是相当大的一笔钱。这种销售数字在 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 (Apress2011)。
投入
用户肯定会想以某种方式与我们的游戏互动。这就是输入模块的用武之地。在大多数操作系统上诸如触摸屏幕或按键之类的输入事件被分派到当前聚焦的窗口。然后窗口将进一步将事件分派给具有焦点的 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 ListKeyEvent getKeyEvents();public ListTouchEvent 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 以 44100Hz 或 44.1KHz 的采样率回放。例如当通过电话线传输语音时采样率较低(在这种情况下通常为 8KHz)。
采样率只是决定录音质量的一个因素。我们存储每个膜状态样本的方式也起作用它也受数字化的影响。让我们回忆一下膜的实际状态是什么:它是膜离中性状态的距离。因为膜是被向内推还是向外推是有区别的所以我们记录了带符号的距离。因此特定时间步的膜状态是单个负数或正数。我们可以用多种方式存储这个有符号数:作为有符号的 8 位、16 位或 32 位整数作为 32 位浮点数甚至作为 64 位浮点数。每种数据类型都有有限的精度。一个 8 位有符号整数可以存储 127 个正距离值和 128 个负距离值。32 位整数提供了更高的分辨率。当存储为浮点型时膜状态通常归一化为 1 到 1 之间的范围。最大的正值和最小的负值代表了膜离开其中性状态的最远距离。膜状态也被称为振幅。它代表撞击它的声音的响度。
使用单个麦克风我们只能录制单声道声音这将丢失所有空间信息。通过两个麦克风我们可以测量空间中不同位置的声音从而获得所谓的立体声。例如您可以将一个麦克风放在发声物体的左侧另一个放在右侧从而获得立体声。当声音通过两个扬声器同时播放时我们可以合理地再现音频的空间分量。但这也意味着当存储立体声音频时我们需要存储两倍数量的样本。
回放最终是一件简单的事情。一旦我们获得了数字形式的音频样本并且具有特定的采样速率和数据类型我们就可以将这些数据发送到音频处理单元它会将信息转换为信号供连接的扬声器使用。扬声器解释这个信号并将其转化为薄膜的振动这又会导致周围的空气分子移动并产生声波。这正是为记录所做的只是颠倒了
音频质量和压缩
哇好多理论。我们为什么关心如果您注意了现在您可以根据采样率和用于存储每个样本的数据类型来判断音频文件是否是高质量的。采样率越高数据类型精度越高音频质量就越好。然而这也意味着我们需要更多的存储空间来存放音频信号。
想象一下我们以 60 秒的长度录制相同的声音但我们录制了两次:一次是以 8KHz 的采样率、每样本 8 位另一次是以 44KHz 的采样率、16 位精度。我们需要多少内存来存储每个声音在第一种情况下每个样本需要 1 个字节。将其乘以 8,000Hz 的采样率我们需要每秒 8000 字节。对于我们完整的 60 秒录音这是 480000 字节或大约半兆字节(MB)。我们更高质量的录音需要更多的内存:每个样本 2 字节每秒 44000 字节的 2 倍。也就是每秒 88000 字节。将此乘以 60 秒我们得到 5280000 字节或 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 轴我们可以看到由于我们坐标的离散性原点与网格中左上角的像素重合位于(00)。原点像素右边的像素位于(10)原点像素下面的像素位于(01)依此类推(见图 3-21 左侧)。显示器的光栅网格是有限的因此有意义的坐标数量有限。负坐标在屏幕外。大于或等于栅格宽度或高度的坐标也在屏幕之外。请注意最大的 x 坐标是栅格的宽度减 1最大的 y 坐标是栅格的高度减 1。这是因为原点与左上角的像素重合。一个接一个的错误是图形编程中常见的挫折来源。 图 3-21。显示光栅网格和 VRAM过于简化
显示器从图形处理器接收恒定的信息流。它按照控制屏幕绘制的程序或操作系统的指定对显示器光栅中每个像素的颜色进行编码。显示器将每秒刷新其状态几十次。确切的速率称为刷新率。它用赫兹表示。液晶显示器的刷新率通常为每秒 60Hz 阴极射线管(CRT)显示器和等离子显示器通常具有更高的刷新率。
图形处理器可以访问一个称为视频随机存取存储器(VRAM)的特殊内存区域。在 VRAM 中有一个保留区域用于存储屏幕上显示的每个像素。这个区域通常被称为帧缓冲区。一幅完整的屏幕图像因此被称为一帧。对于显示器光栅网格中的每个像素在保存像素颜色的帧缓冲区中都有相应的内存地址。当我们想改变屏幕上显示的内容时我们只需改变 VRAM 内存区域中像素的颜色值。
现在是时候解释为什么显示器坐标系中的 y 轴指向下方了。内存无论是 VRAM 还是普通 RAM都是线性一维的。把它想象成一个一维数组。那么我们如何将二维像素坐标映射到一维内存地址呢图 3-21 显示了一个相当小的 3×2 像素的显示光栅网格以及它在 VRAM 中的表示。(我们假设 VRAM 仅由帧缓冲存储器组成。)由此我们可以很容易地推导出下面的公式来计算一个像素在(xy)处的内存地址:
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 表示完全强度)。黑色位于原点(000)白色位于原点(111)。 图 3-23。强大的 RGB 颜色立方体
数字编码颜色
我们如何在计算机内存中对 RGB 颜色三元组进行编码首先我们必须定义颜色组件要使用的数据类型。我们可以使用浮点数并将有效范围指定为 0.0 到 1.0 之间。这将为每个组件提供相当多的分辨率并为我们提供许多不同的颜色。遗憾的是这种方法占用了大量空间(每像素 3 乘以 4 或 8 字节这取决于我们使用的是 32 位还是 64 位浮点)。
我们可以做得更好——以失去一些颜色为代价——这完全没问题因为显示器通常只能发出有限的颜色。我们可以使用无符号整数而不是对每个组件使用浮点数。现在如果我们对每个分量使用 32 位整数我们没有得到任何东西。相反我们对每个分量使用一个无符号字节。每个分量的强度范围从 0 到 255。因此对于 1 个像素我们需要 3 个字节即 24 位。这是 2 的 24 次方(16777216)种不同的颜色。这对我们的需要来说足够了。
我们能再降低一点吗是的我们可以。我们可以将每个组件打包成一个 16 位字因此每个像素需要 2 个字节的存储空间。红色用 5 位绿色用 6 位蓝色用剩下的 5 位。绿色获得 6 位的原因是我们的眼睛可以看到更多的绿色阴影而不是红色或蓝色。所有的位加在一起构成 2 的 16 次方(65536)种我们可以编码的不同颜色。图 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 编码我们的像素。该图像的大小为 1024 × 1024 像素。这是 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 * (1 – 0.5) 0.5
blue 0.5 * 0.5 1 * (1 – 0.5) 0.75
red 0.5 * 0.5 0 * (1 – 0.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 中(xy)处的像素设置为给定的颜色。屏幕外的坐标将被忽略。这叫做削波。Graphics.drawLine()方法类似于 Graphics.drawPixel()方法。我们指定线条的起点和终点以及颜色。位于帧缓冲区栅格之外的线的任何部分都将被忽略。Graphics.drawRect()方法在 framebuffer 中绘制一个矩形。(xy)指定帧缓冲区中矩形左上角的位置。参数 width 和 height 指定 x 和 y 的像素数矩形将从(xy)开始填充。我们在 y 方向向下填充。颜色参数是用来填充矩形的颜色。Graphics.drawPixmap()方法将 Pixmap 的矩形部分绘制到 framebuffer 中。(xy)坐标指定了帧缓冲区中位图目标位置的左上角位置。参数 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 类将会做什么:
当 MySuperAwesomeGame 类被创建时它将设置窗口、我们向其呈现和从其接收事件的 UI 组件、接收窗口和输入事件的回调以及主循环线程。最后它将调用自己的 mysuperawesomegay . getstartscreen()方法该方法将返回 MySuperAwesomeStartScreen()类的一个实例。在 MySuperAwesomeStartScreen 构造函数中我们从磁盘加载一个位图并将其存储在一个成员变量中。这就完成了我们的屏幕设置控制权交还给了 MySuperAwesomeGame 类。主循环线程现在将不断调用我们刚刚创建的实例的 mysuperawesomestartscreen . update()和 mysuperawesomestartscreen . present()方法。在 mysuperawesomestartscreen . update()方法中我们每帧增加一个名为 x 的成员。这个成员持有我们想要渲染的图像的 x 坐标。当 x 坐标值大于 100 时我们将其重置为 0。在 mysuperawesomestartscreen . present()方法中我们用黑色(0x00000000 0)清除帧缓冲区并在位置(x0)呈现我们的位图。主循环线程将重复步骤 3 到 5直到用户按下设备上的后退按钮退出游戏。游戏实例将调用 mysuperawesomestarscreen . dispose()方法该方法将释放位图。
这是我们第一个(不那么)激动人心的游戏用户只会看到图像在屏幕上从左向右移动。这并不是一个令人愉快的用户体验但我们稍后会解决这个问题。请注意在 Android 上游戏可以暂停并在任何时间点恢复。然后我们的 MyAwesomeGame 实现将调用 mysuperawesomestartscreen . pause()和 mysuperawesomestartscreen . resume()方法。只要应用本身暂停主循环线程就会暂停。
还有最后一个我们必须要说的问题:帧率——独立运动。
帧速率-独立运动
让我们假设用户的设备可以以 60FPS 的速度运行上一节中的游戏。我们的 Pixmap 将在 100 帧中前进 100 个像素因为我们每帧将 MySuperAwesomeStartScreen.x 成员增加 1 个像素。在 60FPS 的帧速率下到达位置(1000)大约需要 1.66 秒。
现在让我们假设第二个用户在不同的设备上玩我们的游戏。那个设备能够以每秒 30 帧的速度运行我们的游戏。每秒我们的位图前进 30 个像素所以到达位置(1000)需要 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-filteraction 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 是一个特殊的 intentAndroid 系统使用它来启动应用的主活动。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 卡。它应该能让我们得到一个唤醒锁。
利用你刚刚获得的信息这些是一些容易实现的目标。以下是步骤:
通过打开 new Android project 向导在 Eclipse 中创建新的 Android 项目如第二章中所述。创建项目后打开 AndroidManifest.xml 文件。要让 Android 在 SD 卡上安装游戏(如果有的话),需要将 installLocation 属性添加到元素中并将其设置为 preferExternal。要固定活动的方向将 screenOrientation 属性添加到元素并指定您想要的方向(纵向或横向)。要告诉 Android 我们想要处理键盘、keyboardHidden 和 orientation 配置更改请将元素的 configChanges 属性设置为 keyboard | keyboard hidden | orientation。在元素中添加两个元素并指定名称属性 Android . permission . write _ external stage 和 android.permission.WAKE_LOCK。设置元素的 minSdkVersion 和 targetSdkVersion 属性(例如minSdkVersion 设置为 3targetSdkVersion 设置为 16)。在 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-filteraction android:name*android*.*intent*.*action*.*MAIN* /category android:name*android*.*intent*.*category*.*LAUNCHER* //intent-filter/activity/applicationuses-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。另一个最喜欢的是参考搜索功能。想知道什么调用特定的方法吗只需点击选择它然后按 CtrlShiftG 或选择搜索引用工作空间。所有的重命名或移动操作都被归类为“重构”操作所以在您因为看不到任何重命名类或文件的方法而沮丧之前请看一下重构选项。因为 Java 没有单独的头文件和实现文件所以没有“跳转到头文件/实现”的快捷方式。如果您启用了项目自动构建Java 文件的编译是自动的。启用该设置后每次进行更改时您的项目都会被增量编译。要自动完成只需按 CtrlSpace。
作为一名新的 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 ArrayAdapterString(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 活动恢复收回屏幕。
创建测试活动
当我们创建一个新的测试活动时我们必须执行以下步骤:
在 com.badlogic.androidgames 包中创建相应的 Java 类并实现其逻辑。在清单文件中为 activity 添加一个条目使用它需要的任何属性(即 android:configChanges 或 android:screenOrientation)。注意我们不会指定一个元素因为我们将以编程方式启动活动。将活动的类名添加到 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。浩浩荡荡、令人困惑的活动生命周期
以下是我们应该从中吸取的三大教训:
在我们的活动进入运行状态之前无论我们是从停止状态还是暂停状态恢复onResume()方法总是被调用。因此我们可以放心地忽略 onRestart()和 onStart()方法。我们不关心是从停止状态还是暂停状态恢复。对于我们的游戏我们只需要知道我们现在实际上正在运行onResume()方法向我们发出信号。在 onPause()之后可以静默地销毁该活动。我们永远不应该假设 onStop()或 onDestroy()被调用。我们还知道 onPause()总是在 onStop()之前被调用。因此我们可以安全地忽略 onStop()和 onDestroy()方法只重写 onPause()。在这种方法中我们必须确保我们想要保持的所有状态如高分和等级进步都被写入外部存储如 SD 卡。在 onPause()之后所有的赌注都取消了我们不知道我们的活动是否还有机会再次运行。我们知道如果系统在 onPause()或 onStop()之后决定终止活动则可能永远不会调用 onDestroy()。然而有时我们想知道活动是否真的会被扼杀。那么如果 onDestroy()不会被调用我们该怎么做呢Activity 类有一个名为 Activity.isFinishing()的方法我们可以随时调用它来检查我们的活动是否会被终止。我们至少可以保证 onPause()方法在 activity 被终止之前被调用。我们所需要做的就是在 onPause()方法中调用这个 isFinishing()方法以决定在 onPause()调用之后活动是否会终止。
这让生活变得简单多了。我们只覆盖 onCreate()、onResume()和 onPause()方法。
在 onCreate()中我们设置我们的窗口和 UI 组件向其呈现内容并从其接收输入。在 onResume()中我们(重新)开始我们的主循环线程(在第三章的中讨论)。在 onPause()中我们简单地暂停我们的主循环线程如果 Activity.isFinishing()返回 true我们还会将我们希望保持的任何状态保存到磁盘中。
许多人纠结于活动的生命周期但是如果我们遵循这些简单的规则我们的游戏将能够处理暂停、恢复和清理。
在实践中
让我们编写演示活动生命周期的第一个测试示例。我们希望有某种输出来显示到目前为止发生了哪些状态变化。我们将通过两种方式做到这一点:
活动将显示的唯一 UI 组件是一个 TextView。顾名思义它显示文本我们已经在 starter 活动中隐式地使用它来显示每个条目。每当我们进入一个新的状态时我们将向 TextView 追加一个字符串它将显示到目前为止发生的所有状态变化。我们将无法在 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。
运行应用并稍微试验一下这个测试活动。下面是您可以执行的一系列操作:
从启动活动启动测试活动。锁屏。解锁屏幕。按下主屏幕按钮(这将带你回到主屏幕)。在主屏幕上在旧的 Android 版本(版本 3 之前)上按住 home 键直到出现当前正在运行的应用。在 Android 版本 3上触摸运行应用按钮。选择 Android 基础入门应用以继续(这将使测试活动回到屏幕上)。按“后退”按钮(这将带您返回到开始活动)。
如果你的系统在暂停的任何时候都没有决定静默终止活动你会在图 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_DOWN、MotionEvent.ACTION_MOVE、MotionEvent.ACTION_CANCEL或MotionEvent.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)它基于指针索引返回指针标识符。只要单个手指接触屏幕指针标识符将保持不变。指针索引不一定如此。重要的是要理解两者之间的区别并理解你不能依赖于第一次触摸是索引 0ID 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 its 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_UPMotionEvent。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_DOWNKeyEvent。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(Couldnt 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[]Rfloat[] 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(Couldnt load file);}finally {if (inputStream ! null )try {inputStream.close();}catch (IOException e) {textView.setText(Couldnt 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(Couldnt 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(Couldnt 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(Couldnt 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 示例中一样。这次不同的是我们是全屏的这意味着我们触摸事件的最大坐标等于屏幕分辨率(每个维度减一因为我们从[00]开始)。对于 Nexus One在纵向模式下坐标系将跨越坐标(00)到(479799)(总共 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 提供的另一个类我们将在本章后面研究它。在这种情况下它绘制到视图在屏幕上占据的区域。当然这是一种疯狂的过度简化。在底层它不会直接绘制到屏幕上而是绘制到某种位图上系统稍后会将该位图与活动的所有其他视图的位图结合使用以合成最终的输出图像。然后该图像将被移交给 GPUGPU 将通过另一组神秘的路径将其显示在屏幕上。
我们真的不需要关心细节。从我们的角度来看我们的视图似乎延伸到整个屏幕所以它也可能是绘制到系统的帧缓冲区。在接下来的讨论中我们将假设我们直接绘制到 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()方法将返回 480Canvas.getHeight()方法将返回 800。在横向模式下这两个值只是简单地交换:Canvas.getWidth()将返回 800Canvas.getHeight()将返回 480。
我们需要知道的第二条信息是我们渲染的坐标系统的组织。首先只有整数像素坐标才有意义(有个概念叫子像素但我们会忽略)。我们还知道在纵向模式和横向模式下坐标系的原点(00)总是在显示屏的左上角。正 x 轴总是指向右侧y 轴总是指向下方。图 4-12 显示了一个分辨率为 48×32 像素的假想屏幕处于横向模式。 图 4-12。48×32 像素宽屏幕的坐标系
注意图 4-12 中坐标系的原点是如何与屏幕左上角的像素重合的。因此屏幕左下角的像素不是我们预期的(4832)而是(4731)。通常(width–1height–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 输出
天啊这里发生了什么事这就是我们在不同屏幕分辨率下用绝对坐标和大小渲染得到的结果。两幅图中唯一不变的是红线它只是从左上角画到右下角。这是以独立于屏幕分辨率的方式完成的。
该矩形位于(100100)处。根据屏幕分辨率到屏幕中心的距离会有所不同。矩形的大小为 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_8888Config.RGB_565
从第三章开始你应该知道这些值是什么意思。如果没有我们强烈建议你再读一遍第三章的“数字编码颜色”一节。
有趣的是没有 RGB888 颜色格式。PNG 仅支持 ARGB8888、RGB888 和托盘化颜色。什么颜色格式将用于加载 RGB888 PNGBitmapConfig。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_565bob4444 被转换为 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。例如如果我们指定应该从 00 到 100100 绘制一条线那么硬件加速将使用 OpenGL 组织一个特殊的画线调用并将其绘制到一个硬件缓冲区稍后将合成到屏幕上。
启用这种硬件加速非常简单只需将以下内容添加到 AndroidManifest.xml 的标签下:
android:hardwareAcceleratedtrue请确保在各种设备上打开和关闭加速来测试您的游戏以确定它是否适合您。将来让它一直开着可能没什么问题但是和任何事情一样我们建议你自己采取测试和决定的方法。当然有更多的配置选项可以让你为特定的应用、活动、窗口或视图设置硬件加速但因为我们是在做游戏所以我们只计划每种都有一个所以通过应用全局设置它将是最有意义的。
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 的线程问题之外在屏幕上绘制东西也不是很难。诺姆先生现在可以成为现实了——一个可怕的、饥饿的现实