Android4-游戏开发入门指南-一-

115 阅读1小时+

Android4 游戏开发入门指南(一)

原文:Beginning Android 4 Games Development

协议:CC BY-NC-SA 4.0

零、前言

欢迎来到实用 Android 4 游戏开发。这本书带你一步一步了解两种不同手机游戏的演变;从概念到代码。你将学习如何从一个根本的想法构思一个游戏,并完成编码引擎的复杂任务,将你的想法变成一个可玩的游戏。

我决定写这本书来教授为 Android 4 平台创建你自己的 2D 和 3D 游戏所需的技能。Android 4 将基于 Android 的手机和平板电脑的操作系统统一在一个通用的 SDK 下。这意味着你开发的游戏可以在最新的平板电脑和手机上玩,也可以在最好的硬件上玩。同样的游戏现在可以在任何一种设备上玩;你只需要迈出第一步,创造一个引人注目的游戏。

当第一个完全支持 OpenGL ES 2D 和 3D 的 Android SDK 发布时,我立即发现自己在寻找方法来创建引人注目和有趣的游戏。那时我意识到,创建这些游戏所需的技能虽然不难掌握,但绝对不容易靠自己发现。事实上,除非你以前有过 OpenGL 的经验,特别是 OpenGL ES,否则很难直接投入到 Android 游戏开发中。

我决定利用我在 Android 上开发休闲游戏时学到的知识,将这些知识分解成一套核心的基本技能,随着游戏开发的进展,这些技能很容易掌握和扩展。这些基本技能可能不会让你在完成这本书后马上创造出下一个红色阵营:世界末日,但它们会给你必要的知识,让你理解这些游戏是如何制作的,并可能通过正确的奉献和实践来创造它们。

毫无疑问,你的第一个 Android 游戏已经在你的脑海中设计好了。你确切地知道你想要它看起来是什么样子,也确切地知道你想要它玩起来是什么样子。你不知道的是,如何将这个想法从你的头脑中取出,放到你的手机或平板电脑上。虽然有一个游戏的想法是很好的,但让游戏从想法阶段进入“可在移动设备上玩”的阶段才是棘手的部分。

当你阅读这本书时,我给你的建议是保持你的想法简单。不要试图把一个好游戏过度复杂化,因为你可以。我的意思是,一些最“令人上瘾”的游戏不一定是最复杂的。它们往往是容易上手和玩起来却很难放下的游戏。当你开始构思你想做的游戏时,请记住这一点。在这本书里,你将制作一个简单的引擎来驱动一个滚动射击游戏。滚动射击是一种简单的游戏类型,可以包含非常困难和具有挑战性的游戏。长期以来,它一直被认为是最令人上瘾的街机风格游戏之一,因为它提供了快速的动作和几乎无限的游戏量。很容易一次又一次地回到滚动射击游戏,重新体验游戏。这就是为什么我选择这种风格的游戏来开始你。最后,如果你尝试制作你想作为游戏玩家玩的游戏,那么你的经历将会是有益的。我希望你享受你的旅程,进入 Android 游戏开发的精彩世界。

一、欢迎来到安卓游戏

我在 2008 年初开始在测试平台上开发 Android。当时,还没有发布新操作系统的手机,我们开发者真的感觉好像我们正处于一个激动人心的开端。Android 抓住了早期开源开发的所有能量和激情。为该平台开发很容易让人想起凌晨两点坐在空荡荡的学生休息室里,喝着 Jolt 可乐,等待 VAX 时间运行我们的最新代码。这是一个激动人心的平台,我很高兴我在那里看到了它。

随着 Android 开始成长,谷歌发布了更多更新来巩固最终的架构,有一件事变得很明显:Android 基于 Java,包括许多众所周知的 Java 包,对于休闲游戏开发者来说是一个容易的过渡。Java 开发人员已经掌握的大部分知识都可以在这个新平台上重复利用。大量的 Java 游戏开发者可以利用这些知识相当顺利地迁移到 Android 平台上。

那么一个 Java 开发者如何开始在 Android 上开发游戏,需要哪些工具呢?本章旨在回答这些问题以及更多问题。在这里,你将学习如何把你的游戏故事分成可以完全作为你游戏的一部分实现的大块。在以后的章节中,我们将探讨执行这些任务所需的一些基本工具

这一章非常重要,因为它给了你一些其他游戏书籍所没有的东西——对游戏起源的真正关注。虽然知道如何编写能让游戏栩栩如生的代码是非常重要的,但是如果你没有一个能让游戏栩栩如生的代码,伟大的代码也没有用。知道如何以一种清晰明了的方式将你游戏的想法从你的头脑中清除出去,这将决定一款好游戏和一款玩家爱不释手的游戏的区别。

编程安卓游戏

在 Android 上开发游戏有其利弊,你应该在开始之前了解这一点。第一,Android 游戏是用 Java 开发的,但是 Android 并不是一个完整的 Java 实现。Android 软件开发工具包(SDK)中包含了许多您可能已经用于 OpenGL 和其他图形修饰的包。“许多”并不意味着“全部”,一些对游戏开发者,尤其是 3d 游戏开发者非常有用的软件包并不包括在内。并不是每个你以前用来制作游戏的包都可以在 Android 中使用。

随着新 Android SDK 的每个版本,越来越多的包变得可用,旧的包可能会被弃用。你需要知道你必须使用哪些包,我们将在本章中讨论这些。

另一个优点是 Android 的熟悉度,缺点是它的动力不足。Android 可能在编程的熟悉性和易用性方面有所贡献,但在速度和功能方面有所欠缺。大多数视频游戏,就像那些为个人电脑或游戏机编写的游戏一样,都是用 C 语言甚至汇编语言等低级语言开发的。这使得开发人员能够最大程度地控制处理器如何执行代码以及代码运行的环境。处理器运行非常低级的代码,你越接近处理器的母语,你需要跳过的解释器就越少。Android 虽然提供了一些有限的底层编码能力,但它通过自己的执行系统解释和线程化 Java 代码。这使得开发者对游戏运行环境的控制更少。

这本书不会带你经历游戏开发的低级方法。为什么呢?因为 Java,尤其是为一般 Android 开发而呈现的 Java,广为人知,易于使用,并且可以创建一些非常有趣、有益的游戏。

本质上,如果你已经是一个经验丰富的 Java 开发人员,你会发现你的技能在应用到 Android 上时并没有在翻译中丢失。如果您还不是经验丰富的 Java 开发人员,不要害怕。Java 是一种很好的开始学习的语言。出于这个原因,我选择坚持使用 Android 的原生 Java 开发环境来编写我们的游戏。

我们已经讨论了在 Android 上开发游戏的利弊。然而,对于独立和休闲游戏开发者来说,在 Android 平台上创作和发布游戏的最大好处之一就是你可以自由发布你的游戏。虽然一些在线应用商店对可以卖什么和卖多少有非常严格的规定,但 Android Market 没有。任何人都可以自由列出和出售他们想要的任何东西。这为开发人员提供了更大的创作自由。

在第二章中,你将创建你的第一个基于 Android 的游戏,尽管非常简单。然而,首先重要的是看看幕后是什么激发了任何有价值的游戏,即故事

从一个好故事开始

每一个游戏,从最简单的街机游戏到最复杂的角色扮演游戏(RPG),都是从一个故事开始的。这个故事不需要超过一句话,就像这样:想象一下,如果我们有一艘巨大的宇宙飞船可以拍摄东西。

然而,故事可以像一本书那么长,描述游戏环境中的每一片土地、人和动物。它甚至可以描述每一件武器、挑战和成就。

**注:**故事概述了一个游戏的动作、目的、流程。你能放入的细节越多,你开发代码的工作就越容易。

看看图 1–1 中的游戏,它告诉你什么?这是来自星际战斗机的截图;你将通过本书开始的章节开发的游戏。这个游戏背后也有一个故事。

images

**图 1–1。**星际战士截屏

我们中的大多数人从来没有机会去阅读那些帮助创造了我们最喜欢的游戏的故事,因为这些故事只对创造游戏的人来说是重要的。假设开发者和创造者做好他们的工作,玩家会在玩游戏时吸收故事。

在小型的独立开发团队中,除了首席开发人员之外,任何人都不会阅读这些故事。在较大的游戏开发公司中,在故事最终落入首席开发人员手中之前,它可能会被许多设计师、作家和工程师传递和处理。

每个人都有不同的方式来编写和处理他们想要制作的游戏的故事。处理一个游戏的故事没有对错之分,只是说在你开始写任何代码之前它需要存在。下一节将解释为什么这个故事如此重要。

为什么故事很重要

诚然,在电子游戏的早期,故事可能没有像现在这样重要。推销一款不需要深入了解其目的就能提供快速享受的游戏要容易得多。

现在肯定不是这样了。无论是玩《愤怒的小鸟》还是《??》和《魔兽世界》,人们都希望游戏有一个明确的目的。这种期望甚至可能是下意识的,但你的游戏需要吸引玩家,让他们想继续玩下去。这个钩子是故事的驱动目的。

游戏背后的故事很重要,有几个不同的原因。让我们来看看,在你开始为你的游戏写任何代码之前,为什么你要花时间去开发你的故事。

游戏背后的故事之所以重要的第一个原因是,在你开始编码之前,它给了你一个从头到尾充分认识你的游戏的机会。无论你以什么为生,无论你是全职游戏开发人员还是仅仅作为业余爱好,你的时间都是有价值的。

对于全职游戏开发者来说,你花在编码和开发游戏上的每一个小时都有一个绝对的价值。如果你在业余时间开发独立的游戏,你的时间可以用你本来可以做的事情来衡量:钓鱼,和别人在一起,等等。不管你怎么看,你的时间都有明确具体的价值,你花在游戏编码上的时间越多,花费就越多。

如果在你开始写代码之前,你的游戏还没有完全实现,你将不可避免地遇到问题,迫使你回去调整或者完全重写已经完成的代码。这将耗费你的时间、金钱或理智。

**注:**一个想法要完全实现,必须是完整的。这个想法的每个方面都经过了深思熟虑。

作为一名游戏开发者,你最不希望的事情就是被迫回去修改已经完成甚至可能已经测试过的代码。理想情况下,你的代码应该具有足够的可扩展性,这样你就可以毫不费力地操作它——特别是如果你想在游戏中添加关卡或 bosses 的话。然而,你可能不得不重新编码一些相对次要的东西,比如一个角色或环境的名字,或者你可能不得不改变一些更激烈的东西。例如,也许你意识到你从来没有给你的主角完成游戏所需的武器,因为当你开始建造它时,你不知道它将如何结束。

为你的游戏设计一个完整的故事情节会给你一个线性地图,让你在编写代码时有所遵循。像这样描绘出你的游戏和它的细节将会使你避免许多问题,这些问题可能会使你重新编码你游戏中已经完成的部分。这就引出了下一个原因,为什么在开始编码之前你应该有一个故事。

你的游戏所基于的故事也将作为你写代码的参考材料。无论你是需要回顾一个角色或一群恶棍的名字的正确拼写,还是需要回忆一条城市街道的布局,你都可以从你的。

如果多人一起玩这个游戏,能够参考故事的细节是非常关键的。这个故事可能有你没有写的部分。如果您正在编写引用其中一个部分的代码,那么完全实现的故事文档对您来说是一份无价的参考资料。

让一个故事发展到这种规模和重要性意味着许多人可以参考相同的来源,他们都将得到相同的需要做什么的图片。如果你有多人在一个协作的环境中一起工作,每个人都朝着同一个方向前进是很关键的。如果每个人都开始编写他们认为游戏应该是什么样的代码,每个人都会编写不同的代码。一个写得好的故事,一个可以被每个游戏开发者参考的故事,将有助于保持团队朝着同一个目标前进。

但是,你如何把这个故事从你的脑海中抹去,并准备好供你自己或他人参考呢?这个问题将在下一节回答。

书写你的故事

写出你的故事没有窍门。只要你觉得有必要,你可以是复杂的,也可以是简单的。任何东西,从你电脑旁边便笺簿上的几个简短句子到格式良好的 Microsoft Word 文档中的几页,都足够了。重点是不要试图把这个故事出版成书;相反,你只需要把故事从你的头脑中拿出来,变成一种清晰的格式,可以参考,希望不要改变。

故事在你脑海中停留的时间越长,你就有越多的时间来改变细节。当你改变故事中的任何细节时,你就冒着重写代码的风险(我们已经讨论过这样做的危险)。因此,即使你是一个人,随意发展的机器,你认为没有必要只为你写一个故事,再想想。写下这个故事可以确保你不会忘记或不小心改变任何细节。

毫无疑问,一旦你学会了这本书里的技巧,你心里就会有一个想开发的游戏。然而,你可能从来没有真正考虑过这个游戏的故事是什么。想一想这个故事。

提示:如果你已经有了主意,现在就花点时间写下你的游戏草稿。当你完成后,把它和后面的模拟故事进行比较。

让我们看一个可以用来开发游戏的故事的简单例子。

约翰·布莱克从当地的扣押处偷了一辆速度稍快但性能强劲的汽车。坏人很快就追上了他。现在,他必须带着钱逃出维利安斯堡,躲避警察,并击退偷钱的贺刚。这伙人的车更快,但对约翰来说幸运的是,他可以一边开枪一边开车。希望安全屋的灯还亮着。

在这个简短的故事中,即使没有多少细节,你仍然有足够的时间让一个业余开发人员开始开发一个相当简单的游戏。你能从这一段中得到什么?

从这个小故事中想到的第一个概念是一个自上而下的街机风格的驾驶游戏;想当初间谍猎人。司机或汽车可能有枪向敌人的车辆开火。当玩家到达城镇的边缘,或者可能是一个安全的房子或某种车库时,游戏可能会结束。

这个简短的故事甚至有足够的细节让游戏玩起来更有乐趣。主角有名字,约翰·布莱克。有两组敌人需要避开:警察和黑帮。这个环境由 Villiansburg 的街道组成,大多数敌人的车辆行驶速度比主角的快。这里绝对有足够的好素材来制作一个快速、休闲的游戏。

你大脑中的隐喻之轮应该已经在为这个游戏出主意了。在这短短的一段中,描述了相当多的好的街机风格的动作。如果你能像这样用一小段话描述你想要制作的游戏,而不是作为一个普通的开发者,你就已经在制作一个相当有趣的游戏的路上了。

一小段话可能足以构成一个相当有说服力的休闲游戏,想象一下一个更长的故事能提供什么。你现在能在你的故事中加入的细节越多,你的工作就越容易,你的游戏就越好。

花一些额外的时间在你的故事上,让细节恰到好处。当然,像本节中这样的一小段就足够了,但是更多的细节肯定会对你的编码有所帮助。读完这个故事后,你应该问自己以下几个问题:

  • 约翰偷了什么车,开什么车?
  • 他为什么偷钱?
  • 他有什么样的武器?
  • 车上有什么武器吗?
  • vvillainburg 是城市环境还是乡村环境?
  • 最后有没有 boss 战?
  • 得分是如何累计的,如果有的话?

如果我们回过头来回答其中的一些问题,故事可能会是这样的。

约翰·布莱克被诬陷为莫须有的罪行,他抓住机会报复陷害他的那帮人。他截获了 800 万美元,这笔钱正准备汇给坏男孩的头头“大老板”。他知道自己无法徒步逃脱,所以他从当地拘留所偷了一辆速度稍快但结实的黑色轿车。

这辆车什么都有:双架机枪、浮油和迷你导弹。

坏人很快就追上了他。现在,他必须带着钱走出拥挤的维利安斯堡的街道。街道两旁是破旧的用木板封起来的建筑。约翰开得越快,活着出来的机会就越大。他所要做的就是避开警察,击退偷钱的贺刚。

这伙人的车可能更快,但对约翰来说幸运的是,他可以一边开枪一边开车。当“大老板”开着他重新服役的美国陆军坦克在城镇边缘追上他时,他将需要这些技能。

如果约翰能打败“大老板”,他将保留这笔钱,但如果他在途中被击中,“大老板”的追随者将带走他们能逃脱的一切,直到约翰一无所有。约翰最好小心点,因为“大老板”的党羽会倾尽所有向他发起攻击:跑车、摩托车、机关枪,甚至直升机。

希望安全屋的灯还亮着。

现在,让我们再来看看这个故事。我们现在有更多的东西要继续,很明显,更详细的故事会让游戏变得更有趣。任何编码这个游戏的人现在都能够辨别出下面的游戏细节。

  • 主角的车是一辆黑色轿车。
  • 车上有两挺机枪、导弹和浮油作为武器。
  • 环境是拥挤的城市街道,街道两旁是破旧的用木板封起来的建筑。
  • 玩家将从$8,000,000 (8,000,000 点)开始。
  • 如果敌人抓住或击中玩家,玩家将会损失金钱(点数)。
  • 敌人的交通工具将是跑车、摩托车和直升机。
  • 城市的尽头是一场 boss 对战坦克的战斗。
  • 当游戏没有钱(点数)时,游戏结束。

正如您所看到的,需要做什么的画面要清晰得多。这场比赛不会有混乱。这就是为什么把尽可能多的细节放入你的游戏所基于的故事中是很重要的。你肯定会从你开始编码前投入的所有时间中受益。

既然我们已经解决了你可能想要在 Android 平台上开发游戏的一些原因,并回顾了让你的游戏变得重要背后的哲学,那么让我们看看我将采取的方法以及成为一名成功的 Android 游戏开发者需要哪些工具。这些将作为其余章节中所有项目的基础。

你将要走的路

在这本书里,你将学习二维和三维开发。如果你从这本书的开头开始,并通过基本的例子来工作,一边做一边构建 2d 游戏的例子,那么关于 3D 图形的章节应该更容易理解。相反,如果你试图直接跳到 3d 开发的章节,并且你不是一个经验丰富的 OpenGL 开发者,你可能很难理解正在发生的事情。

如同任何课程、班级或学习路径一样,从头到尾遵循这本书对你会有最好的帮助。然而,如果你发现一些早期的例子对你的经验水平来说太基础了,请随意在章节之间移动。

收集您的 Android 开发工具

在这一点上,你可能渴望投入到开发你的 Android 游戏中。那么你需要什么工具来开始你的旅程呢?

首先,您需要一个良好的、功能全面的集成开发环境(IDE)。我用 Eclipse Indigo 编写了我所有的 Android 代码(这是免费下载的)。本书中的所有例子都将使用 Eclipse 呈现。虽然您可以使用几乎任何 Java IDE 或文本编辑器来编写 Android 代码,但我更喜欢 Eclipse,因为它有精心制作的插件,这些插件紧密集成了许多编译和调试 Android 代码的更繁琐的手动操作。

如果您还没有 Eclipse,并且想尝试一下,可以从 Eclipse.org 网站(http://eclipse.org)免费下载,如图 1–2 所示:

images

图 1–2。Eclipse.org??

这本书不会深入 Eclipse 的下载或安装。有许多资源,包括 Eclipse 自己的站点和 Android 开发人员论坛上的资源,可以在您需要帮助时帮助您设置环境。

提示:如果您从未安装过 Eclipse 或类似的 IDE,请仔细遵循安装说明。你最不希望的就是一个错误安装的 IDE 阻碍了你编写优秀游戏的能力。

你还需要最新的 Android SDK。与所有 Android SDKs 一样,最新版本可以在 Android 开发者网站([developer.android.com/sdk/index.html](http://developer.android.com/sdk/index.html))找到,如图图 1–3 所示:

images

图 1–3。 安卓开发者网站

与 IDE 一样,如果您需要帮助,许多资源可以帮助您下载和安装 SDK(以及您可能需要的相应 Java 组件)。

最后,你至少应该对开发有一个基本的了解,特别是 Java。虽然我尽力解释了在为这本书创建代码时使用的许多概念和实践,但我无法解释更基本的开发概念。简而言之,如果您是新手,我的解释应该足以让您理解本书中的代码,但是更高级的 Java 开发人员将能够很容易地采用我的示例并对其进行扩展。

安装 OpenGL ES

可以说,您将使用的最重要的项目之一是 OpenGL ES,这是由 Silicon Graphics 在 1992 年开发的用于计算机辅助设计(CAD)的图形库。从那时起,它就由 Khronos Group 管理,可以在大多数平台上找到。对于任何想创作游戏的人来说,这是一个非常强大和无价的工具。

**注:**值得一提的是,Android 提供并支持的 OpenGL 版本实际上是 OpenGL ES(嵌入式系统的 OpenGL)。OpenGL ES 不像标准 OpenGL 那样功能齐全。但是,它仍然是在 Android 上进行开发的优秀工具。在本书中,为了便于讨论,我将把 OpenGL ES 函数和库称为 OpenGL;请注意,我们实际上使用的是 OpenGL ES

当大多数人想到 OpenGL 时,首先想到的是 3d 图形。的确,OpenGL 非常擅长渲染 3d 图形,可以用来制作一些令人信服的 3d 游戏。然而 OpenGL 也非常擅长渲染二维图形。事实上,OpenGL 可以比原生 Android 调用更快地渲染和操作二维图形。对于大多数应用开发者来说,原生 Android 调用已经足够好了,但是对于需要尽可能多的优化的游戏来说,OpenGL 是最好的选择。

对于那些可能没有太多 OpenGL 经验的人来说,不用担心。在处理繁重的 OpenGL 图形渲染的章节中,我会尽我所能完整地解释你需要做的每一个调用。所以,如果下面的 OpenGL 代码对你来说看起来像外语,也不用担心;这本书的结尾会说得通:

gl.glViewport(0, 0, width, height); gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity(); GLU.gluOrtho2D(gl, 0.0f, 0.0f, (float)width,(float)height);

OpenGL 是你在本书中使用和学习的完美工具,因为它是一个跨平台的开发库。也就是说,您可以在许多环境和学科中使用 OpenGL 和您在这里学到的 OpenGL 知识。从 iPad 和 iPhone 到 Microsoft Windows 和 Linux,许多相同的 OpenGL 调用可以跨所有这些系统使用。

在本书中,使用 OpenGL 制作二维游戏图形还有一个额外的好处。

对于所有的意图和目的来说,OpenGL 并不关心你使用的是二维还是三维图形。许多相同的呼叫用于两者。唯一的区别是 OpenGL 在绘制到屏幕上时如何渲染多边形。也就是说,使用 OpenGL,你从二维图形到三维图形的转换会更加平滑和容易。请记住,这本书并不打算成为 OpenGL ES 的完整桌面参考,也不会向您展示复杂的矩阵数学和其他优化技巧,否则您会在专业游戏中使用。事实是,作为一个休闲游戏开发者,为 matrix math 之类的东西提供的 OpenGL 方法虽然有一些开销,但对于学习本书的课程来说已经足够好了。

在本书中,您将使用 OpenGL ES 1.0。Android 用户可以使用三个版本的 OpenGL:OpenGL ES 1.0、1.1 和 2.0。为什么要用 1.0 版本?首先,互联网上已经有很多关于 OpenGL ES 1.0 的参考资料。因此,如果你遇到困难,或者想扩大知识面,你会有很多地方可以求助。第二,屡试不爽。作为最古老的 OpenGL ES 平台,它将适用于大多数设备,并将经过广泛的测试。最后,它非常容易掌握和学习。同样,在你已经知道 1.0 之后,再去学 1.1 甚至 2.0 会容易得多。

选择安卓版本

为 Android 开发的吸引力之一是它在许多不同的设备上广泛使用,如手机、平板电脑和 MP3 播放器。你开发的游戏有机会运行一打不同的手机,桌子,甚至电子阅读器。从不同的无线运营商到不同的制造商,你的游戏可以获得的硬件暴露是相当不同的。

不幸的是,这种无处不在的情况也可能成为你需要跨越的一个艰难的障碍。在任何时候,都可能有多达 12 个不同版本的 Android 在几十种不同的硬件上运行。最新的平板电脑和手机将运行版本 2.3.3、3.0、3.1 或 4.0,这是在最强大的设备上运行的最新版本。因此,这些将是我们在本书中要针对的版本。

**注意:**如果您没有 Android 设备进行测试,您可以使用 PC 模拟器。然而,我强烈建议您尝试使用实际的 Android 手机或平板电脑来测试您的代码。在我的测试中,我注意到在模拟器上运行我的代码与在我的手机或平板电脑上运行时有一些细微的差异。

最重要的是,在创作游戏的过程中享受乐趣。游戏毕竟是好玩的,做游戏你也要乐在其中!

总结

在这一章中,我们讨论了你应该期望从本书中得到什么。你学到了故事对于游戏创作的重要性,以及如何坚持故事可以帮助你创作出更好的代码。您还了解了在 Android 平台上创建游戏的过程、Android 的版本以及 Android 的开发环境。最后,您发现了在 Android 平台上创建游戏的关键——OpenGL ES,并且我们讨论了一些关于 Android 版本发布的相关细节。

二、星际战士:一个二维射击游戏

在你阅读这本书的过程中,你将创建的游戏是星际战士星际战士是一款二维、自上而下、滚动的射击游戏。尽管动作相当有限,故事却出人意料地详细。在这一章中,你会先睹为快这个游戏以及它背后的故事。您还将了解游戏引擎的不同部分以及游戏引擎的功能。

讲述星际战士的故事

星际战士的故事如下:在我们阅读本书的过程中,我们会定期引用它:

约翰·斯塔克上尉是一名头发斑白的银河战争老兵。他从行星联盟卷入的每一场战斗中杀出一条血路。现在,在他返回地球的途中,准备从多年的服务退休到马萨诸塞州西部的一个安静的小农场,他发现自己陷入了一个突然的敌人入侵部队的中间。

斯塔克上尉准备战斗。但这不是普通的科达克入侵舰队;有些事情不一样了。

斯塔克启动了他的 AF-718 的推进器,并设置他的枪自动开火。幸运的是,AF-718 轻便灵活。只要他能避开敌人的炮火和偶尔的碰撞,自动加农炮应该能很快解决体型较小的科达克战士。

不幸的是,AF-718 拥有敏捷和自动射击能力,但它缺少护盾。斯塔克上尉最好避开敌人的飞船。如果他受到任何伤害,三击之后,他就出局了。没有好的护盾,AF-718 无法承受太多的直接爆能攻击。至于来自敌人的直接碰撞,不幸的是,对斯塔克上尉来说是“一劳永逸”的。

当 Starke 船长在一波又一波的敌舰中航行时,他可能会幸运地发现一些其他被摧毁的 AF-718 的残骸——最后一批被入侵部队突袭的伤员。只要他不在途中被摧毁,斯塔克上尉可能会找到这些零件的用处。

AF-718 有一个非常有用的功能,可以在战斗中帮助斯塔克上尉。AF-718 的最新版本,专门为最后的 Centelum Prime Rebellion 制造,配备了自我修复模式。如果斯塔克上尉遇到麻烦,他失去了护盾,或者发现他需要更多的火力,他所需要做的就是驾驶他的飞船到一些 AF-718 部件那里,这些部件在战场上四处漂流。他应该能够获得任何东西,从更强的护盾,可以使他的飞船承受的伤害增加一倍或三倍,到更强的火炮,速度更快,需要更少的打击来摧毁敌人。

斯塔克机长和他的 AF-718 并不是唯一有锦囊妙计的人。Kordark 入侵舰队由三艘不同的船组成:

  • Kordark Scout(黑暗侦察兵)
  • 科尔达克截击机
  • 拉腊什战舰

Kordark 侦察兵是入侵舰队中数量最多的船只。它们速度很快——就像斯塔克机长的 AF-718 一样快。侦察兵以快速但可预测的方式飞行。这应该使他们更容易识别,甚至更容易预测。对斯塔克来说是件好事,在将侦察兵的所有能量转移到他们的推力引擎上时,科达克给了他们非常弱的护盾。AF-718 的一次好的爆炸应该足以干掉一架科达克侦察机。他们在船的前部安装了一门爆炸加农炮,可以缓慢地发射单轮炮弹。一些快速射击和快速导航应该能让 AF-718 脱离危险,并给斯塔克上尉足够的筹码去摧毁一架侦察机。

另一方面,科达克截击机是非常直接和蓄意的敌人。他们将缓慢但直接飞向斯塔克机长的 AF-718。截击机是无人驾驶的,被用作计算机制导的攻城锤。他们被设定为一旦锁定敌人的位置就干掉所有的敌人。

拦截者被建造来穿透庞大的行星联盟战斗巡洋舰的坚固外壳。因此它们的护盾非常坚固。AF-718 最好的武器可以轻易的直接命中四次来阻止它。在这种情况下,斯塔克上尉最好的进攻就是防守。Kordark 拦截器很早就锁定了它的目标,一旦锁定,它的程序就不会中断它的路径。如果斯塔克上尉在一个安全区域,他应该可以在快速截击机接触之前离开。如果幸运的话,他可以用他的大炮摧毁一两个,但是这需要一定的技巧。

Starke 船长将要面对的最后一种敌人是 Larash 战舰。

拉腊什战舰的出现使得这支入侵舰队不同于以往任何一个斯塔克上尉。拉腊什战船和科达克截击机一样坚固,但是它们也有面向前方的火炮,就像侦察兵一样。他们可以随机机动,应该会给斯塔克上尉最大的挑战。对他来说幸运的是,这些战舰相对较少,让他有时间在两次露面之间进行休整。

AF-718 的电脑会追踪入侵部队中有多少艘船。当斯塔克上尉消灭了所有潜在的敌人后,它会通知他。这些统计数据将被发送到地球上的前方指挥部,让他们知道他对入侵的排名。

帮助斯塔克上尉消除尽可能多的入侵力波,并活着到达地球。

这就是你将被称为代号星际战士的故事。你能从这个故事中得出什么游戏细节?让我们把它们列出来,就像我们在第一章中为示例故事所做的那样:

  • 主角约翰·斯塔克船长将驾驶 AF-718 宇宙飞船。
  • 玩家不需要操作任何开火装置,因为这艘船有自动开火的功能。
  • 玩家可以通过获得更多的盾牌和枪来增强力量。
  • 如果玩家被敌人的大炮击中三次而没有修复,游戏将结束。
  • 如果游戏被敌方飞船直接击中,游戏将会结束。
  • 有三种不同类型的敌舰:
    • 侦察兵以可预测的模式快速移动并发射一门加农炮。
    • 截击机没有加农炮,但是可以承受玩家的四次直接爆能攻击。一旦他们锁定了玩家的位置,他们就不能改变他们的路线
    • 战舰有加农炮,可以承受四次直接爆能攻击。它们以随机的模式移动
  • 游戏会追踪每一波的敌人数量。玩家每消灭一个,计数器就减一,直到这波结束。
  • 分数将被上传到一个中心区域。

这听起来将会是一个非常有趣、令人兴奋和详细的游戏。最棒的是,创建这个游戏所需的代码不会那么复杂,或者至少不会像你想象的那么复杂。

在下一节中,你将了解到星际战士的游戏引擎。你将会学到游戏引擎的不同部分是什么,以及引擎作为一个整体为你的游戏做了什么。最后,你将开始剔除一些基本的引擎功能,并开始构建你的游戏。

是什么造就了一款游戏?

既然你已经知道星际战士将会是什么,我们可以开始看看构建游戏所需要的不同部分。许多部分都必须以一种非常紧密和有凝聚力的方式组合在一起,才能创造出一个可玩的、令人愉快的 Android 游戏。

当你想到一个游戏为了提供一个真正令人愉快的体验所必须做的一切时,你会开始欣赏它所花费的时间和努力,即使是最简单的游戏。一个典型的游戏会做以下事情:

  • 画一个背景。
  • 根据需要移动背景。
  • 绘制任意数量的字符。
  • 抽取武器、子弹和类似物品。
  • 独立移动角色。
  • 播放音效和背景音乐。
  • 解释输入设备的命令。
  • 跟踪人物和背景,以确保没有人移动到他们不应该移动的地方。
  • 绘制任何预定义的动画。
  • 确保当物体移动时(比如球弹跳),它们以可信的方式运动。
  • 跟踪玩家的分数。
  • 跟踪和管理网络或多个球员。
  • 建立一个菜单系统,让玩家选择玩或退出游戏。

这可能不是一个全面的列表,但它是大多数游戏所做的所有事情的一个相当好的列表。一个游戏是如何完成列表中的所有事情的?

出于本书的目的,我们可以将游戏中的所有代码分为两类:游戏引擎和特定于游戏的代码。前面列表中的所有内容都在这两类代码中的一类或两类中处理。知道哪个在哪里处理对于理解本书中的技能是至关重要的。让我们从游戏引擎开始研究这两类代码。

了解游戏引擎

每个视频游戏的核心都是游戏引擎。顾名思义,游戏引擎就是为游戏提供动力的代码。每一款游戏,不管是什么类型的——RPG,第一人称射击游戏(FPS),平台游戏,甚至是即时战略游戏(RTS)——都需要一个引擎来运行。

**注意:**任何游戏的引擎都是通用的,允许它在多种情况下使用,并且可能用于多种不同的游戏。这与游戏专用代码是直接对立的,顾名思义,游戏专用代码是特定于一个游戏且只针对一个游戏的代码。

一个非常流行的游戏引擎是虚幻引擎。虚幻引擎最初是由 Epic 在 1998 年左右为其 FPS 开发的,名为虚幻,已经在数百款游戏中使用。虚幻引擎很容易适应各种游戏类型,而不仅仅是第一人称射击游戏。这种通用结构和灵活性使得虚幻引擎不仅受到专业人士的欢迎,也受到临时开发人员的欢迎。

一般来说,游戏引擎处理游戏代码的所有繁重工作。这可能意味着从播放声音到在屏幕上呈现图形的任何事情。这里是一个典型的游戏引擎将执行的功能的简短列表。

  • 图形渲染
  • 动画
  • 声音
  • 冲突检出
  • 人工智能
  • 物理学(非碰撞)
  • 线程和内存管理
  • 建立关系网
  • 命令解释程序

为什么你需要一个游戏引擎来完成所有这些工作?简而言之,对于一个高效运行的游戏来说,它不能依赖主机系统的 OS 来完成这种繁重的工作。是的,大多数操作系统都有内置功能来处理列表中的每一项。然而,操作系统的渲染、声音和内存管理系统是为了运行操作系统和适应任何数量的不可预测的使用而构建的,而不是专门针对任何一个。如果你正在编写商业应用,这很好,但是如果你正在编写游戏,这就不那么好了。游戏需要更强大的东西。

为了让游戏流畅快速地运行,代码需要绕过标准操作系统产生的开销,直接在特定进程所需的硬件上运行。也就是说,游戏应该直接与图形硬件通信以执行图形功能,直接与声卡通信以播放效果,等等。如果您使用大多数操作系统都提供的标准内存、图形和声音系统,您的游戏就可以与系统上运行的所有其他操作系统功能和应用线程化。您的内部消息也可能与其他系统消息一起排队。这将使游戏看起来起伏不定,运行非常缓慢。

由于这个原因,游戏引擎几乎总是用低级语言编写。正如我们前面提到的,低级语言为系统硬件提供了更直接的途径。游戏引擎需要能够从特定于游戏的代码中获取代码和命令,并将它们直接传递给硬件。这使得游戏能够快速运行,并具有它需要的所有控制,能够提供有益的体验。

图 2–1 显示了游戏引擎、设备硬件和游戏特定代码之间关系的简化版本。

images

图 2–1。 游戏引擎、游戏专用代码和设备硬件之间的关系

一个游戏引擎不会专门为游戏做任何事情。也就是说,一个游戏引擎不会把一只小猫画到屏幕上。游戏引擎会在屏幕上绘制一些东西,因为它处理图形渲染,但它不会绘制任何特定的东西。游戏特定代码的工作是给引擎一只小猫来画,引擎的工作是画任何传递给它的东西。

因此,你永远不会在游戏引擎中看到以下功能:

DrawFunnyKitten();

相反,你会有一个更像这样的函数:

DrawCharacter(funnyKitten);

诚然,您在本书中创建的最终图形渲染函数将需要更多的参数,而不仅仅是需要渲染的图像的名称,但是您应该能够理解我的观点;引擎非常通用,游戏专用代码则不然。

现在你已经对引擎的功能有了一个很好的概述,让我们将它与特定于游戏的代码进行对比,这样你就会对游戏的组成有一个全面的了解。

了解游戏专用代码

让我们来看看特定于游戏的代码的作用。正如我们前面所讨论的,特定于游戏的代码是由一个游戏且仅由一个游戏运行的代码,不像游戏引擎,它可以在多个游戏之间共享和改编。

**注:**当创建小型休闲游戏时——比如本书中的游戏——游戏引擎和特定于游戏的代码可能会与引擎紧密耦合,以至于有时很难区分两者。理解两者概念上的区别还是很重要的。

特定于游戏的代码由所有在你的游戏中制造角色的代码组成(A-718,侦察兵,截击机,等等。),而游戏引擎只是画一个角色。特定于游戏的代码知道主角发射了炮弹而不是导弹,而游戏引擎绘制了一个项目。游戏特有的代码是这样的代码:如果主角击中了一个侦察兵,它就会被摧毁,但如果他击中了一个电源,它就不会被摧毁;游戏引擎将只测试屏幕上两个物体的碰撞。

例如,在简化的存根代码中,A-718 和一架侦察机的碰撞可能如下所示:

GameCharacter goodGuy; GameCharacter scout; GameCharacter arrayOfScouts[] = new GameCharacter[1]; arrayOfScouts[0] = scout; /**Move characters***/ Move(goodGuy); Move(arrayOfScouts); /***Test for collisions***/ If (TestForCollision(goodGuy,arrayOfScouts)) { Destroy(goodGuy); }

虽然这只是游戏程序的一部分可能看起来像什么的简化版本,但它表明我们创建了 A-718 和 Scout,在屏幕上移动它们,并测试它们是否碰撞。如果字符发生冲突,goodGuy将被销毁。

在这个例子中,goodGuyarrayOfScoutsDestroy()函数都是特定于游戏的代码。Move()TestForCollision()功能是游戏引擎的一部分。从这个简短的示例中,很容易看出您可以将goodGuyarrayOfScouts替换为几乎任何其他游戏中的任何角色,并且Move()TestForCollision()功能仍然有效。这说明了goodGuyarrayOfScout对象是特定于游戏的,而不是引擎的一部分,并且引擎函数Move()TestForCollision()适用于任何游戏。

在一个更大的项目中,比如一个有数十或数百人参与的游戏,引擎将首先被开发,然后特定于游戏的代码将被创建以与该引擎一起工作。对于像本书中那样的小型休闲游戏,游戏引擎和游戏专用代码可以同时开发。这将为您提供一个独特的机会,让您在创建两个代码块时看到它们之间的关系。

随着阅读本书的深入,你会发现小游戏的游戏引擎的一些功能几乎与游戏特有的代码无法区分。在小游戏中,只要游戏按照你想要的方式运行,你可能不会过分担心引擎和游戏特定代码之间的界限。然而,我强烈建议你尽可能地保持两者之间的界限清晰,以帮助提高你自己代码的可重用性,并帮助保持你的开发技能。换句话说,尽量避免懒惰的代码和懒惰的编码实践。

在第一章中,你会看到一个几乎构成任何游戏的物品列表。让我们再来看一下这个列表,确定这些项目中哪些是在游戏引擎中处理的,哪些是在游戏特定的代码中处理的;参见表 2-1 。

images

如表 2-1 所示,即使是最小的游戏也包含很多棋子。游戏的所有元素都由游戏引擎以某种方式处理;有些元件是发动机独有的。这应该让你更好地理解游戏引擎的重要性,以及引擎和游戏特定代码之间的区别。

现在你知道游戏引擎一般做什么了,那我们的游戏引擎星际战士会做什么呢?

探索星际战斗机的引擎

星际战士的游戏引擎将与你可能使用的普通游戏引擎略有不同。请记住,Android 是建立在 Linux 内核上的,开发是使用稍加修改的 Java 版本完成的。这意味着 Android 实际上足够快,可以轻松运行一些休闲游戏。我们将在星际战斗机中利用这一点,并保持我们的编码工作。

我们不打算在本书中构建一个真正的、低级的游戏引擎,仅仅因为它对于我们正在构建的游戏来说是不必要的。让我们面对它;你花在编写游戏上的时间越多,你享受玩游戏的时间就越少。Android 有我们可以利用的系统,虽然它们可能不是运行高端游戏的最佳选择,但它们易于学习,非常适合我们将要制作的游戏类型。

星际战士的游戏引擎将利用 Android SDK(及其相关的 Java 包)来完成以下任务:

  • 演讲者图形
  • 播放声音和效果
  • 解释命令
  • 检测碰撞
  • 对付敌人人工智能

在阅读了本章前面的讨论之后,你可能会注意到我们的游戏引擎缺少了一些功能,比如非碰撞物理、动画和网络/社交媒体。这是因为我们正在构建的游戏不需要利用这些特性,所以我们不需要构建它们。

为了保持这本书的流畅和逻辑性,我们将同时构建引擎和游戏特定的代码。例如,你将学习在创建背景和角色时创建图形渲染器。这将在每一章的结尾给你完整的引擎和游戏代码。

创建星际战斗机项目

作为启动和运行的第一项任务,在本节中,您将快速创建将用于 Star Fighter 游戏的项目。我们将在整本书中使用这个项目。

首先打开 Eclipse,点击菜单按钮打开新建 Android 项目向导;参见图–2。

images

图 2–2。 启动新 Android 项目向导

打开向导后,您将能够创建项目。如果你有创建 Android 项目的经验,这对你来说应该是轻而易举的。

**提示:**如果您正在使用 NetBeans 或任何其他 Java IDE 来创建您的 Android 应用,这个简短的教程不会对您有所帮助。如果您需要帮助,可以利用许多资源在这些 ide 中创建项目。

图 2–3 展示了创建项目时应该选择的选项。项目名称为planetfighter。因为游戏的所有代码都将在同一个项目中被创建,所以将这个项目命名为星球战士是有意义的。这也将导致所有的代码被放到一个planetfighter包中。

**提示:**如果您以前从未创建过 Android(或 Java)项目或包,那么您应该了解一些命名约定。当给你的包命名时,把它想象成一个 URL,只是反过来写。因此,它应该以名称开始,如comnet,并以您的实体名称结束。在这种情况下,我使用com.proandroidgames

images

图 2–3。 新建 Android 项目向导及其选中的选项

现在,您可以选择“在工作区创建新项目”选项。这将确保您的项目是在标准的 Eclipse 工作区中创建的,您应该在安装 Eclipse 时为自己设置这个工作区。默认情况下,选中“使用默认位置”复选框。除非您想为项目更改工作区的位置,否则应该保持原样。

下一步是选择最新版本的 Android SDK,然后单击 Finish 按钮。图 2–4 展示了完成的项目。我们将在下一章开始修改这个项目。

images

图 2–4。 项目设置正确。

总结

在这一章中,你了解了星际战士背后的故事。你不仅探索了普通游戏引擎的不同部分,还探索了那些将包含在星际战士游戏引擎中的部分。最后,您创建了保存游戏代码的项目。

在接下来的五章中,你将把组成星际战士游戏的代码放在一起。作为一名休闲游戏开发者,你将开始积累你的技能,并且你将了解更多关于 Android 平台的知识。

三、按下开始:制作菜单

在这一章中,你将开始开发星际战士 2D 街机射击游戏。您将在您的引擎中创建第一行代码,并开发用户将在您的游戏中看到的前两个屏幕:游戏启动屏幕和带有两个游戏选项的游戏菜单。通过这一章,你会学到在 Android 平台上开发游戏的几个基本技巧。

你会学到的

  • 显示图形
  • 创建活动和意图
  • 创建 Android 服务
  • 启动和停止 Android 线程
  • 播放音乐文件

除了启动画面和游戏菜单,您将创建一些背景音乐在菜单后面播放。

有很多内容需要介绍,所以让我们从玩家在游戏中看到的第一个屏幕开始,即闪屏。

构建启动画面

闪屏是用户将要看到的游戏的第一部分。把闪屏想象成游戏的片头字幕。它应该显示游戏的名称,一些游戏图像,可能还有一些关于游戏制作者的信息。星际战斗机的闪屏如图图 3–1 所示。

images

**图 3–1。**星际战斗机闪屏

对于由多人在多个开发商店开发的游戏,您可能会在游戏开始前看到不止一个闪屏。这种情况并不少见,因为每个开发店、发行商和制作商都有自己的闪屏,希望在游戏开始前发布。然而,对于我们的游戏,我们将创建一个闪屏,因为你将是唯一的开发者。

如果你玩任何典型的游戏,你会看到启动画面通常会自动转换到游戏的主菜单。在星际战士中,你将创建一个闪屏,淡入淡出主菜单。因此,要创建闪屏,您还需要创建保存主菜单的活动,这样您就可以正确地设置闪屏的淡入淡出效果,而不会出现任何错误。

创建活动

首先,打开您在前一章中创建的 Star Fighter 项目。如果您还没有创建星际战斗机项目,请现在返回并在继续之前创建;本章的剩余部分假设你正在星际战士项目中工作。

您的 Star Fighter 项目在当前状态下应该包含一个活动— StarfighterActivityStarfighterActivity由 Android 自动创建,是项目的自动入口点。如果你现在运行你的项目,StarfighterActivity将会启动。然而,这一章实际上需要两个活动:一个用于闪屏,一个用于游戏的主菜单。Android 已经为闪屏提供了一个活动,所以在下一节中,您将为主菜单创建一个新的活动。

即使主菜单的活动现在是空的,它也能让你完全实现闪屏的渐变,这个任务你一会儿就能完成。

创建新的类

要创建一个新的活动,首先在主包中创建一个新的 Java 类。如果您使用了与前一章中描述的相同的包名,那么您的主包就是com.proandroidgames。右键单击包名,选择新建images类,弹出图 3–2 所示的新建 Java 类窗口。

images

图 3–2。 新 Java 类创建窗口

保持大多数默认选项不变。此时您需要做的就是提供一个类名。您的类的名称应该是SFMainMenu。单击“完成”按钮创建该类。

现在,您创建的新类是一个简单的 Java 类,代码如下。

`package com.proandroidgames;

public class SFMainMenu {

}`

但是,该课程还不是一项活动。为此,您需要向该类添加一些代码。一旦这个类成为一个活动,您就可以开始创建闪屏及其效果。

将课堂转化为活动

导入Activity包,并扩展您的SFMainMenu类,将这个 Java 类变成一个 Android 活动。您的类代码现在应该如下所示:

`package com.proandroidgames;

import android.app.Activity;

public class SFMainMenu extends Activity {

}`

现在,让我们将此活动与星际战斗机项目相关联,以便创建闪屏。打开AndroidManifest.xml文件,将SFMainMenu活动与您的项目关联起来,如图图 3–3 所示。

images

**图 3–3。**androidmanifest . XML

滚动到AndroidManifest应用标签的底部,找到标有应用节点的区域。清单的这个区域列出了与您的项目关联的所有应用节点。现在,列出的唯一应用节点应该是.StarfighterActivity。因为您想要添加一个新的活动,单击添加按钮,并从图 3–4 中的画面中选择活动。

images

图 3–4。 创建新的活动元素

这将创建一个空的Activity元素。您在AndroidManifest的 GUI 中看到的空元素是在AndroidManifest.xml文件中的一个 XML 元素的表示。单击选项卡底部的AndroidManifest.xml视图,您应该会看到以下 XML 代码片段:

<activity></activity>

很明显,这个空元素对你没什么好处。您需要以某种方式告诉AndroidManifest这个活动元素代表了SFMainMenuActivity。这当然可以手动完成。然而,让我们来看看如何以自动化的方式来做这件事。

一旦您创建了新的Activity元素,您需要将这个新元素与您之前创建的实际的SFMainMenu活动相关联。单击AndroidManifest的应用节点部分中的Activity元素以突出显示它。在AndroidManifest的应用节点部分的右边是一个现在被标记为活动属性的部分,如图 Figure 3–5 所示。

**注意:**如果您点击.StarfighterActivity,该部分将被标记为.StarfighterActivity的属性。

images

图 3–5。 活动属性

单击 Name 属性旁边的 Browse 按钮,打开一个浏览工具,显示项目中所有可用的Activity类。你的浏览工具选项应该看起来像图 3–6。

请注意,SFMainMenu活动列在“匹配项目——框中。选择SFMainMenu活动,然后单击确定。

**提示:**如果您在“匹配项目”框中没有看到SFMainMenu活动,请尝试返回 Eclipse 中的 SFMainMenu 选项卡。如果选项卡标签在SFMainMenu名称前有一个星号,则文件尚未保存。保存文件,然后重新打开名称属性浏览器。

如果你仍然没有看到SFMainMenu,确认你的SFMainMenu类正在扩展Activity。如果您正在您的类中扩展Activity并且您仍然没有选择SFMainMenuActivity的选项,您可以通过填充所需的元素属性来手动编辑AndroidManifest(这些将在本章后面提供)。

images

图 3–6。 姓名属性选择器

在您选择了SFMainMenu作为这个活动的名称属性之后,为。StarfighterActivitySFMainMenu活动肖像,如图图 3–7 所示。

images

图 3–7。 将“屏幕方向”设置为纵向

.StarfighterActivity(你的闪屏)和SFMainMenu(游戏的主菜单)设置屏幕方向会将屏幕方向锁定为纵向。鉴于这款游戏的风格,您希望玩家只能在纵向模式下使用游戏。因此,即使玩家试图将设备旋转到横向模式,游戏的屏幕也将保持纵向。

新的SFMainMenu活动的 XML 代码应该如下所示:

<activity android:name="SFMainMenu" android:screenOrientation="portrait"></activity>

主菜单活动现在与 Star Fighter 项目相关联,您可以创建闪屏。请记住,主菜单的所有代码将在本章的下一节中添加;您只需要现在创建的活动来正确设置您的淡入淡出效果。

**注意:**Android 应用崩溃和失败的最常见原因之一是AndroidManifest文件中的不正确设置,该文件很容易成为项目中最重要的文件之一。

让我们快速回顾一下我们现在所处的位置以及原因。你正在为星际战士创建的闪屏将淡出主菜单。您已经创建了保存主菜单的活动,现在是时候创建闪屏和淡入淡出效果了。

创建您的闪屏图像

现在,您需要将用于闪屏图像的图形导入到项目中。Android 能够处理大多数常见的图像格式。但是,这个游戏你要坚持两个:.png.9.png。对于所有的精灵和其他游戏图像,你将使用标准的.png图像,对于闪屏和主菜单,你将使用.9.png文件。

.9.png图像也称为九片图像。九补丁图像是一种特殊的格式,允许 Android 根据需要拉伸图像,因为它在图像的左侧和顶部包含一个 1 像素的黑色边框。

**注意:**你在游戏中包含的大多数图像将会是而不是九补丁图像,因为你想要自己控制大多数图像的操作。但是,对于闪屏和主菜单,使用九补丁是完全合适的。

九补丁和其他图像大小调整过程的区别在于,你可以通过操纵黑边来控制 Android 如何拉伸图像。Figure 3–8 以九补丁格式展示了我们的闪屏图像。

images

图 3–8。 九补丁闪屏

如果你仔细观察图 3–8 中图片的左侧,你会注意到黑色圆点的细线。这条黑线是九色图像与其他图像格式的区别。

**注意:**我在这个例子中使用的九片图像是为了在所有方向上自由伸展。如果您的图像中有不想拉伸的部分,请不要在这些区域绘制边框。draw9patch工具可以帮助你想象你的图像将如何伸展,取决于你如何绘制你的边界。

不幸的是,为 Android 开发的应用可以在许多不同的设备上运行,从小型手机到大型平板电脑。因此,您的项目必须能够适应所有不同的屏幕尺寸。如果你使用九补丁图像作为你的闪屏,Android 可以调整图像的大小(在一些 XML 的帮助下),以适应任何尺寸的屏幕。

**提示:**如果你从未使用过九补丁图形,Android SDK 包括一个可以帮助你的工具。在 SDK 的\tools文件夹中,你会找到draw9patch工具。启动这个工具,你将能够导入任何图像,绘制你的九补丁边界,并用.9.png扩展名保存图像。

导入图像

现在您已经准备好了您的九补丁映像,将它从您保存它的地方拖到您的 Eclipse 项目的\res\drawable-hdpi文件夹中,如图图 3–9 所示。

你可能已经注意到有三个文件夹可供选择:drawable-hdpidrawable-ldpidrawable-mdpi。这些文件夹包含三种不同类型的 Android 设备的绘图或图像:高密度(hdpi)、中密度(mdpi)和低密度(ldpi)。

如果 Android 提供了一种机制来包含不同屏幕尺寸的不同图像,我们为什么要使用九补丁图形来缩放图像以适应任何屏幕?简而言之,这两种情况实际上是相互排斥的。是的,九补丁允许缩放图像以适应设备的屏幕,但这与设备的屏幕像素密度没有什么关系。您正在使用的图像(如果您使用此项目中的图像)是高密度图像,并且将如此显示。然而,即使图像很大,它们仍然没有 10.1 英寸摩托罗拉 Xoom 屏幕大。因此,九贴格式允许其适当拉伸。

images

图 3–9。 拖动图像到 drawable-hdpi 文件夹

当您想要使用不同的布局和图像密度来利用更大的屏幕区域,或者相反,为较小的屏幕区域做出让步时,高、中、低密度文件夹分离的真正好处就发挥出来了。如果您想要创建一个有四个按钮的菜单屏幕,每个按钮在平板电脑屏幕上堆叠在另一个按钮的顶部,但在较小的设备上并排成对分组,这些文件夹将帮助您轻松实现这一目标。

出于我们当前项目的目的,将您的闪屏九补丁图像放到drawable-hdpi文件夹中。你不能在这个游戏中使用这些文件夹。然而,你可以自己尝试,在不同的设备上创造不同的体验。

使用 R.java 文件

当你把图片放入文件夹后,Android 会为它创建一个资源指针。这个指针放在R.java文件中,是自动生成的,不应该是手动编辑的。它位于您的包名下的gen文件夹中。如果你在添加你的图像后打开R.java文件,它应该有类似如下的代码:

`package com.proandroidgames;

public final class R { ... public static final class drawable { public static final int starfighter=0x7f020002; } ... }`

R.java文件将管理您的项目使用的所有图像、id、布局和其他资源。因为这个文件现在包含一个指向您的图像的指针,所以您可以使用下面的代码行在项目中的任何地方引用这个图像:

R.drawable.starfighter

**注意:**小心不要以任何方式删除或手动修改R.java文件。例如,starfighter图像指针的十六进制(hex)值在您的系统上可能与本节中的示例代码不同。您的文件将在您的机器上工作,因为它是在您的 IDE 中生成的。如果您要修改十六进制值以匹配示例中的值,您的文件将不再按预期工作。

现在您的项目中已经有了一个想要显示为闪屏的图像,您需要告诉 Android 将这个图像显示到屏幕上。有许多方法可以实现这一点。但是,因为您想要对图像应用淡入淡出效果,所以您将使用一个布局。

布局是一个 XML 文件,用来告诉 Android 如何在屏幕上定位资源。让我们为闪屏创建布局。

创建布局文件

您将使用一个简单的布局文件,在玩家第一次加载您的游戏时,在屏幕上显示初始屏幕图像starfighter。你的闪屏应该直截了当——一个有趣的空间背景和游戏名称的图像。

现在你必须把这个图像显示在屏幕上,这样玩家才能欣赏它。首先右击res\layout文件夹,选择新建 images 其他。在新建向导中,选择AndroidimagesAndroid XML 文件,如图图 3–10 所示。

images

图 3–10。??【Android XML 文件选项】??

将新的 xml 文件命名为splashscreen.xml,并完成向导。该过程将在布局文件夹中放置一个新的 XML 布局文件,并在R.java文件中创建一个指向该文件的指针。

此时,您可以通过两种方式攻击布局。您可以使用 GUI 设计器或直接编辑 XML 文件。我们将直接编辑 XML 文件,这样您就可以更好地理解布局中包含的内容及其原因。

编辑 XML 文件

双击res\layout文件夹中的splashscreen.xml文件,实际打开 GUI 设计器。然而,如果您在 Eclipse 中查看设计器窗口的底部,您会注意到两个子选项卡。一个选项卡,即当前选项卡,被标记为图形布局。第二个选项卡标记为 splashscreen.xml,该选项卡是 xml 文件的文本编辑器。单击 splashscreen.xml 选项卡进入文本编辑器。

您的 XML 文件应该如下所示:

<?xml version="1.0" encoding="utf-8"?>

这是一个空的 XML 文件。让我们给这个文件添加一个布局。

在一般的 Android 开发中,可以使用一些不同类型的布局。因为你正在开发一个游戏,你真的不需要担心 75%的布局,因为你在创建星际战士的过程中不会碰到它们。然而,有两个可以用于这个闪屏:LinearLayoutFrameLayout。你将为星际战士使用FrameLayout,因为它非常擅长将元素居中并固定到边框上。

LinearLayout用于在屏幕上显示多个项目,并将它们一个接一个地垂直或水平放置。将LinearLayout想象成一个单列或单行的表格。它可以用来以一种有组织的线性方式将任意数量的项目放置到屏幕上,包括其他布局。

FrameLayout用来装一件物品。一个项目可以被重力设置,使其居中,填充整个空间,或靠着任何边界。FrameLayout布局看起来几乎是故意显示一个由单个图像组成的闪屏。

使用框架布局

您将使用FrameLayout来显示闪屏图像一个将您标识为开发人员的文本框。我知道我刚刚解释过FrameLayout是为展示一件物品而建造的,事实也的确如此。然而,如果你告诉一个FrameLayout显示两个项目,它会显示它们相互重叠,比其他类型的布局需要的代码少得多。

返回到您的splashscreen.xml文件,如下创建FrameLayout:

` <FrameLayout xmlns:android="schemas.android.com/apk/res/and…"

`

一个FrameLayout布局只需要你现在担心的两个属性:layout_widthlayout_height。这两个属性将告诉 Android 如何让布局适合您创建的活动。

在这种情况下,您将把layout_widthlayout_height属性设置为match_parentmatch_parent常量告诉 Android 视图的宽度和高度应该与该视图的父视图(在本例中是活动本身)的宽度和高度相匹配。

**提示:**如果你以前在 Android 上开发过,你可能会记得一个叫fill_parent的常数。Fill_parent被替换为match_parent,但是这两个常量的作用是一样的。

如下所示设置FrameLayout属性:

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> </FrameLayout>

你现在有了一个功能正常的FrameLayout,但是你没有任何东西可以让它工作。让我们添加图像和文本。

添加图像和文本

在你的FrameLayout中创建一个ImageView,并给它一个 ID "splashScreenImage"

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> **<ImageView android:id="@+id/splashScreenImage"** **>** **</ImageView>** </FrameLayout>

您已经创建了将保存您的闪屏图像的ImageView。现在,您必须将src属性设置为指向您想要显示的图像,在本例中,是在res\drawable-hdpi文件夹中的starfighter图像。您还需要设置layout_widthlayout_height属性,就像您对FrameLayout所做的那样。

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/splashScreenImage" **android:src="@drawable/starfighter"** **android:layout_width="match_parent"** **android:layout_height="match_parent">** </ImageView> </FrameLayout>

请注意,src属性指向了“@drawable/starfighter”;这告诉 Android 显示来自drawable文件夹的starfighter图像。现在来看一些不太明显的东西。如果你回想一下我们关于九补丁图像的讨论,我提到过我们需要一些代码来利用九补丁的缩放能力。将layout_width和/或layout_height设置为match_parent将利用九补丁格式以您指定的方式校正图像的比例。

现在在你的布局中创建一个TextView。这个文本视图将被用来显示你想在你的闪屏上显示的任何演职员表或文本。

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/splashScreenImage" android:src="@drawable/starfighter" android:layout_width="match_parent" android:layout_height="match_parent"> </ImageView> **<TextView** **android:text="game by: j.f.dimarzio - graphics by: ben eagel"** **android:id="@+id/creditsText"** **</TextView>** </FrameLayout>

创建这个视图并不需要什么巫术,它应该看起来相当简单。再次,你需要告诉安卓TextViewlayout_widthlayout_height。然而,如果我们将属性设置为match_parent文件,就像我们在ImageViewFrameLayout上所做的那样,你的文本会以一种非常不理想的方式覆盖图像。

相反,您要将layout_widthlayout_height设置为wrap_content,如下所示。wrap_content常量将让 Android 知道您希望TextView的大小由其中文本的大小决定。因此,您添加的文本越多,TextView就会越大。

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/splashScreenImage" android:src="@drawable/starfighter" android:layout_width="match_parent" android:layout_height="match_parent"> </ImageView> <TextView android:text="game by: j.f.dimarzio graphics by: ben eagel" android:id="@+id/creditsText" **android:layout_height="wrap_content"** **android:layout_width="wrap_content">** </TextView> </FrameLayout>

最后,您希望显示演职员表的文本不要太分散注意力,所以您将设置TextView的重力,将文本拉到FrameView的底部中心,如此处所示。

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/splashScreenImage" android:src="@drawable/starfighter" android:layout_width="match_parent" android:layout_height="match_parent"> </ImageView> <TextView android:text="game by: j.f.dimarzio graphics by: ben eagel" android:id="@+id/creditsText" **android:layout_gravity="center_horizontal|bottom"** android:layout_height="wrap_content" android:layout_width="wrap_content"> </TextView> </FrameLayout>

您已经成功创建了将显示初始屏幕的布局。现在,你只需要告诉StarfighterActivity使用这个布局。

连接星际战斗机和布局

StarfighterActivity与布局连接起来非常容易,只需要一行代码。

保存splashscreen.xml文件。保存该文件将在R.java文件中创建另一个条目,以便您可以在其他代码中引用该布局。

打开项目源代码根目录下的StarfighterActivity.java文件。该文件是在您创建项目时自动创建的。

**提示:**如果您没有名为StarfighterActivity.java的文件,请检查您是否按照上一章中的说明创建了一个项目。如果您将项目命名为除了starfighter之外的任何名称,您的StarfighterActivity将会有一个不同的名称。

当您打开StarfighterActivity.java文件时,您将看到一些自动生成的代码,显示一个名为main的预制布局。

`package com.proandroidgames;

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

public class StarfighterActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }`

setContentView()从显示main布局改为显示您刚刚创建的starfighter布局。完成的活动应该是这样的。

`package com.proandroidgames;

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

public class StarfighterActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

/display the splash screen image from a layout/ setContentView(R.layout.splashscreen); } }`

通过单击菜单栏上带有白色箭头的绿色圆圈,编译并运行您的代码。您也可以按 Ctrl + F11 或从菜单中单击Run image Run

如果您以前从未编译或调试过 Android 应用,您可能会看到一个屏幕,询问您是要将应用作为 JUnit 测试还是 Android 应用来运行。您会希望将您的应用作为 Android 应用运行。然后,您可以选择在哪个版本的仿真器或任何附加的 Android 调试模式设备上运行您的应用。

**注意:**如果您选择在 Android 模拟器中运行代码,而不是在实际的 Android 手机上,您可能会遇到一些意想不到的结果。请记住,模拟器就是模拟器,它并不是你的游戏在设备上的真实表现。这并不是说你不应该使用模拟器;只是要小心,直到你看到你的工作在一个实际的设备上。

启动你的游戏,你应该会看到如图 Figure 3–11 所示的启动画面。这是一个巨大的成就,也是创建游戏入口的第一个障碍。然而,眼下,屏幕真的没什么用。其实它除了展示真的什么都不做。你需要创建淡入淡出效果,这将导致从你的闪屏到你的主菜单。

images

图 3–11。 星际战斗机闪屏

退出StarfighterActivity,回到你的代码。是时候创建淡入和淡出效果了。

创建渐变效果

您将使用动画来创建淡入闪屏的效果,然后从闪屏淡出到主菜单。Android 有一些内置的动画效果,非常容易使用,也非常容易实现。

为什么要用动画淡入淡出?简单的答案是,这是一个让你的游戏看起来更好的简单方法。如果你只有一个静态的屏幕,从你的闪屏切换到你的主菜单,你仍然可以完成同样的目标,但是通过淡入和淡出你的屏幕,你给你的游戏一个额外的专业外观。

res\layout文件夹中再创建两个布局文件:一个名为fadein.xml,另一个名为fadeout.xml。顾名思义,fadein.xml文件将控制将闪屏淡入设备的动画。fadeout.xml文件将控制动画将启动画面淡出到主菜单。

您将要创建的动画类型称为 alpha。“alpha”是指图像的 Alpha 值,或其透明度。alpha 值为 1 表示不透明,alpha 值为 0 表示透明。因此,要使图像看起来像是淡入,您需要创建一个动画,在设定的时间内将图像的 alpha 值从 0 调整到 1。相反,如果你想淡出一个图像,你需要一个动画来调整你的图像的 alpha 值从 1 到 0。因此,您将创建两个不同的 alpha 动画来控制闪屏的淡入和淡出。

在您的res\layout文件夹中创建了fadein.xmlfadeout.xml文件后,双击fadein.xml文件在编辑器中打开它。除了下面一行之外,该文件应该为空;如果不是,删除文件的内容将除了这一行:

<?xml version="1.0" encoding="utf-8"?>

现在,这样创建一个阿尔法动画:

<?xml version="1.0" encoding="utf-8"?> **<alpha xmlns:android=”http://schemas.android.com/apk/res/android” />**

您需要为该动画定义四个属性来完成它:要使用的插值器类型、开始和结束 alpha 值以及动画的总持续时间。

首先,我们来定义插值器。插值器告诉动画如何前进。也就是动画刚好可以正常运行;它可以从慢开始,然后逐渐加速;它可以开始得很快,然后变得更慢;或者它可以重复。对于淡入效果,我们将缓慢地开始动画,然后让它在一秒钟内建立起来。

使用accelerate_interpolator告诉动画,你想开始缓慢,然后随着时间的推移加速。下面的代码说明了如何在fadein.xml中实现accelerate_interpolator:

<?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.android.com/apk/res/android" **android:interpolator="@android:anim/accelerate_interpolator" />**

你的淡入动画将开始缓慢,并逐渐加速,直到淡入完成。但是会持续多久呢?

使用android:duration属性告诉 alpha 动画要运行多长时间。android:duration属性的值以毫秒为单位。你将通过设置android:duration为 1000 来告诉动画运行 1 秒钟。

<?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator" **android:duration="1000" />**

创建淡入动画的最后一步是设置动画的开始和结束 alpha 值的属性。在这种情况下,你从完全透明到完全不透明。然而,这并不意味着这些是你唯一的选择。您可以选择在两者之间的任意值开始和结束。如果你愿意,你可以让一个动画从 25%的不透明渐变到 100%的不透明。

设置android:fromAlphaandroid:toAlpha属性来指示您想要开始和结束的 alpha 值。

<?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator" android:duration="1000" **android:fromAlpha="0.0" android:toAlpha="1.0"/>**

注意:fromAlphatoAlpha的值是浮点数而不是整数。这很重要,因为 alpha 值的范围只有 0 到 1。

这里,您已经将fromAlpha属性设置为 0.0。这表明动画从视图完全透明开始。toAlpha属性被设置为 1.0,表示动画将在视图完全不透明的情况下结束。这个动画将为你提供一个平滑的淡入效果。

现在是时候创建淡出了。

思考淡出和淡入的关系。淡出应该像淡入一样工作,只是方向相反。这意味着动画应该使用一个插值器,它开始时很快,然后变慢,直到结束。动画也应该从一个完全不透明的对象开始,然后过渡到一个完全透明的对象。

保存fadein.xml文件,打开fadeout.xml。这里也一样,在fadeout.xml中你应该只有一行代码:

<?xml version="1.0" encoding="utf-8"?>

您需要为fadeout.xml设置android:interpolatorandroid:durationandroid:fromAlphaandroid:toAlpha

您在淡入动画中使用了accelerate_interpolator以较慢的淡入速率开始,然后逐渐移动到较大的速率。因此,要反转淡出动画,您将使用decelerate_interpolatordecelerate_interpolator将以更快的速度开始动画,然后慢慢降低速度,直到动画结束。

同样,您将为淡出设置 1 秒(1000 毫秒)的动画持续时间。

<?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.android.com/apk/res/android" **android:interpolator="@android:anim/decelerate_interpolator" android:duration="1000" />**

设置属性android:fromAlphaandroid:toAlpha来完成动画。因为你是从一个纯色图像淡出到没有,你将设置android:fromAlpha为完全不透明和android:toAlpha为完全透明。这将开始动画在一个坚实的形象,并淡出到一个透明。

<?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/decelerate_interpolator" android:duration="1000" **android:fromAlpha="1.0"** **android:toAlpha="0.0"/>**

您现在可以保存完成的fadeout.xml文件。

此时,您有了一个布局和两个动画来控制和定义您的闪屏。现在,你需要一些方法来告诉他们三个互动,并创建一个动画启动画面。

为了理解你将如何创建和运行动画,你需要理解线程如何与你的游戏相关联地工作。

玩转游戏

作为游戏开发者,你需要克服的最大障碍之一是你的游戏如何在任何给定的平台上运行。在其最基本的根元素,一个 Android 游戏仍然只是一个基本的 Android 活动。为 Android 编写的其他“应用”也是作为一个活动来编写的。您的活动与任何其他活动之间的唯一区别是,您的活动将包含一个游戏,而其他活动可能是商业、地图或社交媒体工具。

这种架构的问题在于,因为所有的 Android 活动都是一样的,所以它们都被同等对待。这意味着你编写的每一个 Android 活动都将在系统的主执行线程中运行。这对游戏不利。

在系统的主执行线程中运行您的游戏意味着您的游戏必须与该线程中运行的所有其他活动竞争资源。这将导致一个断断续续或缓慢的游戏在最好的情况下和一个游戏,暂停或冻结设备在最坏的情况下。

但是不要害怕,有一种方法可以绕过这个单线程的噩梦。你必须能够产生任意数量的线程,并在其中运行任何你想运行的东西。理想情况下,您会希望您的游戏在一个线程中运行,该线程独立于设备上运行的所有其他内容,以确保您的游戏尽可能平稳地运行,并能够访问它所需的资源。

在这一章的剩余部分,你实际上将为游戏的执行产生两个独立的线程。本节讨论的第一个线程将用于游戏运行,第二个线程(您将在本章稍后创建)将用于运行您想要在游戏后播放的任何背景音乐。

为什么是两个独立的线程?除了动画和游戏逻辑之外,在设备上可以做的最耗费处理器资源的事情之一就是播放媒体,比如音乐。您将确保游戏和音乐平稳并行运行,互不干扰。通过在一个独立于游戏的线程中运行音乐,如果你发现设备资源不足,你也可以在不干扰游戏的情况下关掉音乐。

现在你明白了为什么你需要为你的游戏生成不同的线程,让我们为主游戏和闪屏创建一个。这个游戏线程将把你创建的闪屏、淡入淡出动画和主菜单联系在一起。

创建游戏线程

再次打开StarfighterActivity.java。提醒一下,您的文件当前应该能够启动闪屏,并且应该包含以下代码。

`package com.proandroidgames;

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

public class StarfighterActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

/display the splash screen image from a layout/ setContentView(R.layout.splashscreen); } }`

由于StarfighterActivity是默认启动的活动,也是启动闪屏的活动,所以它是生成游戏线程的最佳位置。你现在创建的线程将会是游戏最终运行的线程。

实例化一个新的Thread(),并覆盖run()方法以产生一个新的线程。在run()方法中,调用主菜单在新线程中运行游戏。这是您在这里要做的事情的基本路线图。

**注意:**随着你构建游戏的进展,这个线程中的代码将被修改,甚至被移动以适应更复杂的过程。

下面的代码显示了在StarfighterActivity代码中的何处生成新线程。

`package com.proandroidgames;

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

public class StarfighterActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

/display the splash screen image/ setContentView(R.layout.splashscreen);

*/ Start a new game thread */ new Thread() { @Override public void run() {

} }** } }`

但是这段代码有一个问题。正如所写的那样,代码将在闪屏显示的几毫秒内产生新的游戏线程。这几乎没有足够的时间来呈现闪屏。因此,您需要延迟游戏线程的生成,直到闪屏有足够的时间显示。

答案是用延时的Handler()。Android 有处理程序可以管理线程和活动。Handler()postDelay()方法有两个参数:要延迟的线程和要延迟的时间。

您将创建一个新的常量来保存您希望延迟线程的时间。这个常量GAME_THREAD_DELAY,将会是你游戏引擎的第一行代码。将它放在那里将允许您从单个位置调整线程上的延迟,而无需在代码中搜寻它。

在你的游戏包中创建一个名为SFEngine.java的新类文件。这是一个空的类文件,它将最终保存你的游戏引擎的大部分。将以下常量添加到类中:

`package com.proandroidgames;

public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; }`

您正在将GAME_THREAD_DELAY设置为 4 秒;在主菜单淡入之前,这应该是闪屏显示的一段时间。

保存SFEngine.java,重新打开StarfighterActivity。让我们用一个Handler()postDelay()来包装新的游戏线程,如下所示。

**提示:**同样密切关注需要导入的包;如果您试图调用尚未导入的包中的方法,您的代码将会出错。您还可以使用 Ctrl + Shift + O 快捷键来自动导入您可能已经错过的任何引用的包。

`package com.proandroidgames;

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

public class StarfighterActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

/display the splash screen image/ setContentView(R.layout.splashscreen);

**/start up the splash screen and main menu in a time delayed thread/ new Handler().postDelayed(new Thread() { @Override public void run() {

} }, SFEngine.GAME_THREAD_DELAY);**

} }`

现在,您已经创建了新线程,并设置了一个时间延迟来暂停线程的生成 4 秒钟。最后,是时候告诉线程做什么了。

设定新的目标

在新线程中,您将启动主菜单活动,终止闪屏活动,并设置淡入淡出动画。要开始一个新的活动,您必须创建一个Intent()方法。

Intent()想象成你在告诉 Android 执行的一个操作。在这种情况下,您告诉 Android 启动您的主菜单活动。下面的代码向您展示了如何创建一个新的Intent()方法来启动主菜单。

`package com.proandroidgames;

import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.os.Handler;

public class StarfighterActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); /display the splash screen image/ setContentView(R.layout.splashscreen); /start up the splash screen and main menu in a time delayed thread/ new Handler().postDelayed(new Thread() { @Override public void run() { Intent mainMenu = new Intent(StarfighterActivity.this, SFMainMenu.class); StarfighterActivity.this.startActivity(mainMenu); } }, SFEngine.GAME_THREAD_DELAY);

} }`

在继续之前,让我们讨论一下这段代码的作用。第一行在StarfighterActivity的上下文中创建了名为mainMenu的新Intent(),活动为SFMainMenu。第二行使用StarfighterActivity上下文来启动mainMenu活动。请记住,所有这些都发生在闪屏之外的一个单独的线程中。

杀戮活动

现在主菜单已经启动,您想要终止闪屏活动。代码将导航到主菜单,所以为什么要取消闪屏呢?就当是做家务吧。通过取消闪屏,您可以确保游戏不会在无意中使用设备上的后退按钮返回。如果玩家能够导航回闪屏,理论上他们可以产生任意数量的并发游戏线程,堵塞他们的设备。因此,为了安全起见,您将杀死如下所示的闪屏。

`package com.proandroidgames;

import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.os.Handler;

public class StarfighterActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

/display the splash screen image/ setContentView(R.layout.splashscreen);

/start up the splash screen and main menu in a time delayed thread/ new Handler().postDelayed(new Thread() { @Override public void run() { Intent mainMenu= new Intent(StarfighterActivity.this, SFMainMenu.class); StarfighterActivity.this.startActivity(mainMenu); StarfighterActivity.this.finish();

} }, SFEngine.GAME_THREAD_DELAY);

} }`

最后,您的新线程需要将闪屏淡入主菜单的动画。您将使用overridePendingTransition()方法告诉 Android 您想要使用您创建的两个渐变动画作为从一个活动到另一个活动的过渡。

`package com.proandroidgames;

import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.os.Handler;

public class StarfighterActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); /display the splash screen image/ setContentView(R.layout.splashscreen); /start up the splash screen and main menu in a time delayed thread/ new Handler().postDelayed(new Thread() { @Override public void run() { Intent mainMenu= new Intent(StarfighterActivity.this, SFMainMenu.class); StarfighterActivity.this.startActivity(mainMenu); StarfighterActivity.this.finish(); overridePendingTransition(R.layout.fadein,R.layout.fadeout); } }, SFEngine.GAME_THREAD_DELAY);

} }`

在运行闪屏之前,你需要做最后一件事。在布局目录中,您应该会看到一个名为main.xml的自动生成文件。让我们告诉SFMainMenu活动使用这个布局。因为布局是空的,所以活动不会显示任何内容,但是当您进入本章的下一节时,它会对您有所帮助。

打开SFMainMenu.java,并确保它具有以下代码,该代码应该与您开始修改它之前在StarfighterActivity中的代码相同:

`package com.proandroidgames;

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

public class SFMainMenu extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }`

保存SFMainMenu.java

这就是创建闪屏所需的全部代码。您应该编译并运行这段代码,看看它是如何工作的。当你这样做的时候,你的闪屏应该出现在屏幕上,然后在 4 秒钟后变成黑屏。

你的下一个任务是用游戏的主菜单替换默认的“Hello World”屏幕。在本章的下一节,你将为游戏创建主菜单。然后,在最后一部分,您将使用您创建线程的经验来为游戏音乐生成另一个线程。

创建主菜单

在本节中,您将创建游戏的主菜单。主菜单将由一个背景图像和两个按钮组成。一键启动游戏;另一个将退出游戏。

添加按钮图像

使用与前面相同的拖放过程,将按钮的图像添加到您的res\drawable-hdpi文件夹中。在为本书创建的项目中,有两个图像用于开始按钮,两个图像用于退出按钮。每个按钮的一个图像将是其静止状态,另一个图像将表示按下状态。图 3–12 和 3–13 分别显示了开始和退出按钮静止状态的两幅图像。

**注意:**注意按钮图像左边和上边的黑色边框。这些按钮图像是九个补丁。

images

**图 3–12。**开始按钮的静止状态,starfighterstartbtn

images

**图 3–13。**退出按钮的静止状态,starfighterexitbtn

图 3–14 和 3–15 分别代表启动和退出按钮的按下状态。

images

图 3–14。 开始按钮的按下状态,starfighterstartbndown

images

**图 3–15。**退出按钮的按下状态,星际战斗机

**注意:**本节中列出的代码将假设您已经命名了与上面图片标题中的名称相对应的图像。如果您用不同的名称命名图像,请确保根据需要调整代码示例。

对于主菜单的背景图像,为了简单起见,我们将使用与初始屏幕相同的图像。当然,你可以随意改变这一点,使用任何你想用在主菜单上的图片。然而,出于本书的目的,您也将使用主菜单后面的闪屏图像。

打开位于布局文件夹中的main.xml。这个文件应该是在您创建项目时自动创建的。

**注意:**如果你发现你没有一个main.xml文件,现在使用本章上一节创建splashscreen.xml的相同说明创建一个。在继续本部分之前,确保您有一个main.xml文件并且是空的。

同样,除了下面一行代码,您的main.xml应该是空的。如果不是,请清除其中的所有文本,但以下文本除外:

<?xml version="1.0" encoding="utf-8"?>

您将使用一个RelativeLayout布局来保存背景图像和按钮。使用RelativeLayout可以控制视图在布局中的精确位置。

如下创建RelativeLayout:

` **<RelativeLayout xmlns:android="schemas.android.com/apk/res/and…" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"

**`

这里,您已经创建了一个RelativeLayout布局,其中layout_widthlayout_height属性被设置为match_parent

接下来,添加将保存背景图像的ImageView。这段代码与您在上一节中为闪屏编写的代码非常接近,所以我将不再赘述的解释。如果您需要复习这些视图的功能,请参考上一节。

` <RelativeLayout xmlns:android="schemas.android.com/apk/res/and…" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"

`

接下来,你必须把按钮放在屏幕上,但在这之前,你必须施一点魔法。

设置布局

右键单击res\drawable-hdpi文件夹,添加两个新的 XML 文件:startselector.xmlexitselector.xml。这些文件将保存一个选择器,告诉你的按钮图像根据按钮的状态而改变。当玩家按下按钮时,这将允许你改变按钮的图像。

将以下代码添加到startselector.xml:

<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@drawable/starfighterstartbtndown" /> <item android:drawable="@drawable/starfighterstartbtn" /> </selector>

请注意,选择器有两个项目属性,一个表示按钮被按下时的状态(android:state_pressed= " true"),另一个表示按钮处于正常静止状态(除了图像之外没有其他指定)。按下状态的属性有一个设置为starfighterstartbtndown图像的图像,静止状态图像是starfighterstartbtn图像。

将一个ImageButtonsrc属性设置为这个选择器将会在玩家按下按钮时改变按钮的图像。

按如下方式设置exitselector.xml代码,为退出按钮实现相同的结果:

<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@drawable/starfighterexitbtndown" /> <item android:drawable="@drawable/starfighterexitbtn" /> </selector>

通过创建选择器来改变按钮图像,你可以将ImageButton添加到main.xml的布局中。

因为您希望按钮与屏幕底部对齐,所以您将在保存按钮的RelativeLayout上将alignParentBottom属性设置为true。然后,将高度设置为wrap_content,宽度设置为match_parent,将使布局仅与其中的按钮一样高,与屏幕一样宽。

开始按钮将与屏幕的左边缘对齐,退出按钮将与屏幕的右边缘对齐。这将把按钮放在屏幕的下角。

` <RelativeLayout xmlns:android="schemas.android.com/apk/res/and…" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"

**

** `

注意,start 和 exit 按钮的src属性被设置为 Start 和 Exit 选择器,您创建它们是为了更改按钮的图像。

如果你现在运行你的游戏,你应该会看到你的闪屏淡入主菜单。主菜单应该类似于图 3–16。请注意按钮和按钮图像的位置。试着按一个按钮,看看图像是否改变。

**注意:**你可能注意到你的图像按钮有一个灰色的背景,而不是图 3–16 中的透明背景。在本章后面的SFMainMenu.java代码中,你将设置ImageButton背景为透明,这样做将去除灰色。

images

图 3–16。 主菜单

连接按钮

在主菜单上剩下唯一要做的事情就是连接按钮,这样它们就能真正执行一个功能。退出按钮将被设置为退出游戏并杀死所有线程。开始按钮将开始游戏的第一关。因为您还没有创建游戏的第一关,所以您只需按下开始按钮。

打开SFEngine.java游戏引擎代码。您需要再创建几个将在主菜单中使用的常量和一个将完成退出清理工作的函数。现在,引擎应该是这样的:

`package com.proandroidgames;

public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; }`

你需要添加两个常量:一个用于设置开始和退出按钮的透明度,一个用于设置按钮的触觉反馈。

**注意:**触觉反馈是当你触摸按钮时,某些设备能够给出的触觉反应。

将以下常量添加到SFEngine:

`package com.proandroidgames;

public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; public static final int MENU_BUTTON_ALPHA = 0; public static final boolean HAPTIC_BUTTON_FEEDBACK = true; }`

接下来,创建一个返回布尔值的新方法。当退出按钮被按下以在游戏可以干净地退出之前执行任何游戏中需要的内务处理时,这个方法将被调用。

`package com.proandroidgames;

import android.view.View;

public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; public static final int MENU_BUTTON_ALPHA = 0; public static final boolean HAPTIC_BUTTON_FEEDBACK = true;

/Kill game and exit/ **public boolean onExit(View v) { try { return true; }catch(Exception e){ return false; }

}** }`

现在,这个方法没有内务处理来执行,所以它只是返回 true,让游戏继续它的退出例程。

保存游戏引擎,打开SFMainMenu.java文件。

在主菜单代码中,你要做的第一件事是设置图像按钮的背景透明度,并设置触觉反馈。

`package com.proandroidgames;

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

public class SFMainMenu extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);

/ Set menu button options */ ImageButton start = (ImageButton)findViewById(R.id.btnStart); ImageButton exit = (ImageButton)findViewById(R.id.btnExit);

start.getBackground().setAlpha(SFEngine.MENU_BUTTON_ALPHA); start.setHapticFeedbackEnabled(SFEngine.HAPTIC_BUTTON_FEEDBACK);

exit.getBackground().setAlpha(SFEngine.MENU_BUTTON_ALPHA); exit.setHapticFeedbackEnabled(SFEngine.HAPTIC_BUTTON_FEEDBACK);** } }`

在这里,您在内存中创建了两个以上的ImageButton。然后,使用findViewById()方法,将内存中的按钮设置为主菜单上的实际按钮。最后,设置背景透明度和每个按钮的触觉反馈。

添加 onClickListeners

接下来,您需要为按钮建立两个onClickListener:一个用于开始按钮,一个用于退出按钮。当玩家按下(或点击)相应的按钮时,将执行onClickListener()方法。当任一按钮被按下时,您想要执行的任何代码都需要从该按钮的onClickListener()中调用。

现在,onClickListener()对于开始按钮没有任何作用。你只要把它掐灭,为游戏开始的下一章做准备。退出按钮的onClickListener()将调用游戏引擎中的onExit()函数,如果函数返回 true,将退出游戏。

`package com.proandroidgames;

import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton;

public class SFMainMenu extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);

final SFEngine engine = new SFEngine();

/** Set menu button options */ ImageButton start = (ImageButton)findViewById(R.id.btnStart); ImageButton exit = (ImageButton)findViewById(R.id.btnExit);

start.getBackground().setAlpha(SFEngine.MENU_BUTTON_ALPHA); start.setHapticFeedbackEnabled(SFEngine.HAPTIC_BUTTON_FEEDBACK);

exit.getBackground().setAlpha(SFEngine.MENU_BUTTON_ALPHA); exit.setHapticFeedbackEnabled(SFEngine.HAPTIC_BUTTON_FEEDBACK);

start.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { / Start Game!!!! */ }

});

exit.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { boolean clean = false; clean = engine.onExit(v); if (clean) { int pid= android.os.Process.myPid(); android.os.Process.killProcess(pid); } } });** }

}`

保存SFMainMenu.java,并运行您的代码。你现在应该可以点击退出按钮来关闭游戏。按钮也应该有透明的背景,启动画面应该平滑地淡入主菜单。

创建一个非常专业的闪屏和主菜单的最后一步是添加一些背景音乐。

添加音乐

在本节中,您将学习如何从游戏中生成第二个线程。该线程将用于运行将在主菜单后面播放的背景音乐。您将生成一个线程,创建一个播放音乐的服务,然后在引擎的内务处理功能中终止音乐和线程。

**注意:**如果你之前从未使用过音乐文件和安卓系统,请谨慎对待你的文件大小。如果您的媒体文件太大,您可能会消耗活动的所有可用内存并使其崩溃。我尽量把背景音乐这样的东西放在一个 10 到 15 秒的小循环中,这样可以重复播放。

您需要做的第一件事是添加一个res\raw文件夹。所有的音乐文件都存储在raw文件夹中,但遗憾的是,这个文件夹不是在你创建项目时为你创建的。右击res文件夹,选择新建 Images 文件夹。将文件夹命名为raw,如图图 3–17 所示。

images

图 3–17。 创建原始文件夹

下一步是将您的媒体文件复制到res\raw文件夹中。

**注:**通过 Matt McFarland 在[www.mattmcfarland.com](http://www.mattmcfarland.com)签署的知识共享许可协议,使用本代码发布的音乐是免版税的音乐。我从他的歌曲中抽取了 15 秒钟的样本,在这本书的部分游戏中循环播放。

如果您正在使用这个项目中的文件,主菜单的音乐是warfieldedit.ogg。再说一次,你可以随意使用任何你想用在主菜单上的音乐;请注意尺寸。

接下来,让我们向将在音乐服务中使用的引擎添加一些常量。打开SFEngine.java,添加以下常量:

`package com.proandroidgames;

import android.content.Context; import android.view.View;

public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; public static final int MENU_BUTTON_ALPHA = 0; public static final boolean HAPTIC_BUTTON_FEEDBACK = true; public static final int SPLASH_SCREEN_MUSIC = R.raw.warfieldedit; public static final int R_VOLUME = 100; public static final int L_VOLUME = 100; public static final boolean LOOP_BACKGROUND_MUSIC = true; public static Context context;

/Kill game and exit/ public boolean onExit(View v) { try { return true; }catch(Exception e){ return false; }

}

}`

SPLASH_SCREEN_MUSIC是一个常量指针,指向您将要播放的实际音乐文件,在本例中为warfieldedit.oggR_VOLUMEL_VOLUME变量将设置音乐的初始音量,LOOP_BACKGROUND_MUSIC是一个布尔值,告诉服务是否循环。最后,context变量将保存音乐正在播放的线程的当前上下文,以便我们可以在游戏的内务处理过程中杀死它。所有这些新的常量和变量都将从服务中调用。

现在,让我们创建一个播放这个音乐文件的服务。然后,您可以在主菜单的一个线程中启动该服务。

创建音乐服务

在游戏包中添加一个名为SFMusic.java的新类文件。您应该有一个空白类,如下所示:

`package com.proandroidgames;

public class SFMusic {

}`

您需要做的第一件事是让这个类扩展Service:

`package com.proandroidgames;

import android.app.Service;

public class SFMusic extends Service{

}`

此时,Eclipse 可能会向您抛出一个错误,因为您还没有实现扩展Service所需的所有方法。暂时忽略这个错误。将以下方法添加到服务中:

`package com.proandroidgames;

import android.app.Service; import android.content.Intent; import android.os.IBinder;

public class SFMusic extends Service{

**@Override public IBinder onBind(Intent arg0) { return null; }

@Override public void onCreate() { super.onCreate(); }

public int onStartCommand(Intent intent, int flags, int startId) { return 1; } public void onStart(Intent intent, int startId) {

} public void onStop() {

}

public IBinder onUnBind(Intent arg0) { // TODO Auto-generated method stub return null; } public void onPause() {

}

@Override public void onDestroy() {

}

@Override public void onLowMemory() {

}**

}`

有了服务代码,让我们创建两个变量。第一个是名为isRunning的布尔值。这将用于查询服务以确定它是否正在运行。有时,你需要知道服务是否在运行,如果它还在运行,你可以关掉音乐,如果它已经停止,你可以重启它。

**注意:**最初isRunning布尔将被设置为false。当服务实际启动时,您将把它设置为true

您需要创建的第二个变量是MediaPlayer,它将实际播放您的音乐。

`package com.proandroidgames;

import android.app.Service; import android.media.MediaPlayer; import android.content.Intent; import android.os.IBinder;

public class SFMusic extends Service{ public static boolean isRunning = false; MediaPlayer player; @Override public IBinder onBind(Intent arg0) { return null; }

@Override public void onCreate() { super.onCreate(); }

public int onStartCommand(Intent intent, int flags, int startId) { return 1; } public void onStart(Intent intent, int startId) {

} public void onStop() {

}

public IBinder onUnBind(Intent arg0) { // TODO Auto-generated method stub return null; } public void onPause() {

}

@Override public void onDestroy() {

}

@Override public void onLowMemory() {

}

}`

接下来,您需要在服务中创建一个为MediaPlayer设置选项的方法。这些是我们在引擎中为其创建常量的选项:音量、循环和媒体文件。这个方法将接受您创建的常量,并将它们直接传递给MediaPlayer。您将从onCreate()方法中调用这个方法,这样,一旦创建了服务,就会设置MediaPlayer选项。

`package com.proandroidgames;

import android.app.Service; import android.media.MediaPlayer; import android.content.Intent; import android.os.IBinder; import android.content.Context;

public class SFMusic extends Service{ public static boolean isRunning = false; MediaPlayer player; @Override public IBinder onBind(Intent arg0) { return null; }

@Override public void onCreate() { super.onCreate(); setMusicOptions(this,SFEngine.LOOP_BACKGROUND_MUSIC,SFEngine.R_VOLUME,SFEngine.L_VOLUME, SFEngine.SPLASH_SCREEN_MUSIC); } public void setMusicOptions(Context context, boolean isLooped, int rVolume, int lVolume, int soundFile){ player = MediaPlayer.create(context, soundFile); player.setLooping(isLooped); player.setVolume(rVolume,lVolume); } public int onStartCommand(Intent intent, int flags, int startId) { return 1; } public void onStart(Intent intent, int startId) {

} public void onStop() {

}

public IBinder onUnBind(Intent arg0) { // TODO Auto-generated method stub return null; } public void onPause() {

}

@Override public void onDestroy() {

}

@Override public void onLowMemory() {

}

}`

您需要添加到服务中的最后一段代码指出了媒体播放开始和停止的所有位置。这段代码应该非常容易理解,但是有点分散。逻辑地思考一下;你要用任何处理开始或创建的方法来开始音乐,用任何处理停止的方法来停止音乐。确保相应地设置isRunning布尔值,以便您可以正确地查询服务是否正在运行。

`package com.proandroidgames;

import android.app.Service; import android.content.Context; import android.content.Intent; import android.media.MediaPlayer; import android.os.IBinder;

public class SFMusic extends Service{ public static boolean isRunning = false; MediaPlayer player;

@Override public IBinder onBind(Intent arg0) { return null; }

@Override public void onCreate() { super.onCreate();

setMusicOptions(this,SFEngine.LOOP_BACKGROUND_MUSIC,SFEngine.R_VOLUME,SFEngine.L_VOLUME, SFEngine.SPLASH_SCREEN_MUSIC); } public void setMusicOptions(Context context, boolean isLooped, int rVolume, int lVolume, int soundFile){ player = MediaPlayer.create(context, soundFile); player.setLooping(isLooped); player.setVolume(rVolume,lVolume); } **public int onStartCommand(Intent intent, int flags, int startId) { try { player.start(); isRunning = true; }catch(Exception e){ isRunning = false; player.stop(); }

return 1;** } public void onStart(Intent intent, int startId) {

} public IBinder onUnBind(Intent arg0) { // TODO Auto-generated method stub return null; } public void onStop() { isRunning = false; } public void onPause() { } @Override public void onDestroy() { player.stop(); player.release(); } @Override public void onLowMemory() { player.stop(); }

}`

服务的代码现在已经编写好了。但是,在使用它之前,您需要将该服务与您的 Android 项目相关联。之前,您使用了AndroidManifest将一个新的活动与项目关联起来。您可以按照相同的步骤将新的SFMusic服务与项目关联起来。

打开AndroidManifest.xml,点击编辑器窗口底部附近的应用选项卡。打开“应用”选项卡后,滚动到窗口底部的“应用节点”部分。单击“添加”按钮添加新节点,并从列表中选择“服务”。

在“应用节点”窗口中单击新的服务节点,并导航到编辑器窗口右侧的服务属性。现在,您应该能够单击 Name 属性右侧的 Browse 按钮了。在浏览器中找到您的SFMusic服务,并完成操作。

现在,您可以在游戏中使用音乐服务了。

播放您的音乐

打开SFEngine.java,添加一个名为musicThread的新公共Thread()。您将在SFMainMenu中初始化这个线程。

`package com.proandroidgames;

import android.content.Context; import android.content.Intent; import android.view.View;

public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; public static final int MENU_BUTTON_ALPHA = 0; public static final boolean HAPTIC_BUTTON_FEEDBACK = true; public static final int SPLASH_SCREEN_MUSIC = R.raw.warfieldedit; public static final int R_VOLUME = 100; public static final int L_VOLUME = 100; public static final boolean LOOP_BACKGROUND_MUSIC = true; public static Context context; public static Thread musicThread;

/Kill game and exit/ public boolean onExit(View v) { try { return true; }catch(Exception e){ return false; }

}

}`

现在,打开SFMainMenu.java,创建一个新的Thread()分配给musicthread来运行你的音乐服务。

`package com.proandroidgames;

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

public class SFMainMenu extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);

/ Fire up background music / SFEngine.musicThread = new Thread(){ public void run(){ Intent bgmusic = new Intent(getApplicationContext(), SFMusic.class); startService(bgmusic); SFEngine.context = getApplicationContext(); } }; SFEngine.musicThread.start();*

final SFEngine engine = new SFEngine();

/** Set menu button options */ ImageButton start = (ImageButton)findViewById(R.id.btnStart); ImageButton exit = (ImageButton)findViewById(R.id.btnExit);

start.getBackground().setAlpha(SFEngine.MENU_BUTTON_ALPHA); start.setHapticFeedbackEnabled(SFEngine.HAPTIC_BUTTON_FEEDBACK);

exit.getBackground().setAlpha(SFEngine.MENU_BUTTON_ALPHA); exit.setHapticFeedbackEnabled(SFEngine.HAPTIC_BUTTON_FEEDBACK);

start.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { /** Start Game!!!! */ }

});

exit.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { boolean clean = false; clean = engine.onExit(v); if (clean) { int pid= android.os.Process.myPid(); android.os.Process.killProcess(pid); } } ); }

}`

最后,你需要在整理房间的时候关掉背景音乐服务。返回到SFEngine,添加以下代码来终止服务和线程:

`package com.proandroidgames;

import android.content.Context; import android.content.Intent; import android.view.View;

public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; public static final int MENU_BUTTON_ALPHA = 0; public static final boolean HAPTIC_BUTTON_FEEDBACK = true; public static final int SPLASH_SCREEN_MUSIC = R.raw.warfieldedit; public static final int R_VOLUME = 100; public static final int L_VOLUME = 100; public static final boolean LOOP_BACKGROUND_MUSIC = true; public static Context context; public static Thread musicThread;

/Kill game and exit/ public boolean onExit(View v) { try { Intent bgmusic = new Intent(context, SFMusic.class); context.stopService(bgmusic); musicThread.stop(); return true; }catch(Exception e){ return false; }

}

}`

编译并运行你的游戏。你现在应该有一个工作的闪屏,背景音乐干净地退出。在下一章,你将开始建造游戏的第一关,从它的背景开始。

总结

在这一章中,你为你的游戏设定了第一个代码。你创建了一个闪屏,淡入然后淡出游戏的主菜单。您还创建了游戏的主菜单,带有开始和退出选项。最后,您使用媒体播放器和原始音乐文件为游戏添加了一些背景音乐。

在下一章,你将为你的游戏创建一个两层的滚动背景。

四、绘制环境

在这一章中,你将学习如何渲染游戏的背景。背景为游戏设定了基调和环境。对于星际战士来说,这个环境将会是一个由恒星、行星、宇宙飞船和残骸组成的背景。您将使用 OpenGL 将背景设置到游戏中,并渲染到屏幕上。

鉴于单一背景已经相当令人印象深刻,那么两个背景一定是两倍。嗯,不完全是这样——但是两个不同速度的背景给你的游戏带来了视觉深度,会非常有趣。你将在游戏中添加第二层背景,滚动速度将比第一层更快。

在本章的后面,你将从游戏设置中休息一下,让你的游戏以每秒 60 帧的速度运行。虽然许多设备可能无法以每秒 60 帧的速度运行完整的游戏,但这是大多数游戏开发者的目标。

无论你的游戏有多好,如果玩家不能使用它,它就没有任何意义。因此,在这一章中,你还将修改你的主菜单,使之能够在玩家选择开始选项时启动游戏。

到这本书的这一点,你应该有一个工作的闪屏,淡入游戏的主菜单和一些循环的背景音乐。这是一大成就;然而这一章的代码会更复杂。同样,你可以随意跳过这一章,但是要意识到大多数的例子都是累积的,因为它们都建立在前面的例子中。

最后,在这一章中,你将被介绍到大量的 OpenGL。我确实意识到大多数普通的 Android 开发人员可能没有接触过太多的 OpenGL。在你阅读本章的过程中,我会尽可能多地给出 OpenGL 的背景和说明。

说了这么多,让我们直接开始画游戏的背景。

渲染背景

在前一章中,你使用了 Android 的ImageView来显示一个位图作为游戏的闪屏。对于闪屏和主菜单来说,这是一个可以接受的解决方案。但是在这个过程中有太多的开销并且没有足够的灵活性来将其用于游戏的图形。如果你想办法用这个过程来显示你的游戏图形,游戏会运行得很慢,如果它能加载的话。

要快速将这个游戏的背景绘制到屏幕上,你需要一个既轻便又灵活的工具。幸运的是,Android 已经实现了这样一个工具:OpenGL ES。OpenGL ES 是嵌入式系统的 OpenGL 标准(为了便于讨论,我在本书中只将其称为 OpenGL)。从第一个 SDK 版本开始,它就以各种形式出现在 Android 上。OpenGL 提供了一种有用的、灵活的、相当成熟的处理游戏图形的方法。

一开始,OpenGL 在 Android 上的实现有很多问题,而且不像其他系统那样功能丰富。然而,随着更多 Android 版本的出现,OpenGL 的实现变得更加坚实。这并不是说现在还没有——在这一章中,你至少会学到一个重要的 OpenGL bug。

你将为星际战士创建一个相当复杂的双层、重复、滚动的背景。具体来说,您将看到一个滚动(并重复)的更大的背景图像,它与以更快速度移动的第二个滚动图像部分重叠。这将使背景看起来复杂,具有三维效果。图 4–1 显示了背景完成后的样子。

images

图 4–1。 完成后的背景

首先,你需要一个新的活动来运行你的游戏。当玩家点击你在前一章的主菜单中创建的开始按钮时,这个活动就会启动。

创造创造创造

游戏活动是当你开始你的实际游戏时将被启动的 Android 活动,至少是玩家将实际玩的游戏的一部分(相对于闪屏或主菜单)。虽然启动画面和菜单看起来像是游戏的一部分,但就本章的目的而言,你是根据功能将它们分开的。

到目前为止,您已经创建了游戏的几个关键特性,但是您还没有编写任何支持游戏运行的代码。这种情况现在将会改变。您将创建运行星际战士游戏的活动。

在主包中创建一个名为SFGame.java的新类。创建类后,在 Eclipse 中打开它。应该是这样的:

`package com.proandroidgames;

public class SFGame {

}`

注意:请记住,如果您没有按顺序阅读本书,您在这里看到的代码可能与您的不同,因为您可能用不同的包或类名创建了您的基类。

修改您的SFGame类来扩展Activity,并包含任何未实现的方法。

`package com.proandroidgames;

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

public class SFGame extends Activity {

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(); }

}`

**提示:**此时,您应该按照上一章中的指示,使用AndroidManifestSFGameActivityStarFighter项目关联起来。

按原样保存该文件。它现在做不了什么。事实上,它仅仅是一个活动的外壳,如果您现在运行它,您将幸运地得到一个空白屏幕,但是您很可能会收到一个漂亮的语法错误。

您需要构建一个视图,让SFGame活动能够显示。视图将调用将游戏显示到屏幕上。SFGame活动是视图到达屏幕的管道。

让我们来谈谈接下来会发生什么。

创建游戏视图

在《??》第三章中,你使用了一个预制的 Android 视图ImageView来显示游戏的启动画面和主菜单。这是一种可接受的显示静态图形的方法。然而,你在这里创造了一个极限推动游戏。一个像ImageView一样有如此多开销和如此有限功能集的视图不会给你创造一个游戏所需要的灵活性。因此,你需要在别处寻找你的图形渲染工具。

Android 自带的 OpenGL 工具正好适合这项工作。您将使用 OpenGL 来显示和操作游戏图形。它为您提供了快速显示 2D 和 3D 图形所需的能力和灵活性,非常适合您正在编写的游戏。

如果您过去做过任何 Android 开发,您可能会使用画布绘制到屏幕上。OpenGL 有自己的画布类型,您需要使用它在屏幕上显示 OpenGL 图形。GLSurfaceView将允许你在屏幕上显示游戏图形。

至此,您已经创建了SFGame活动,但是您现在需要一些东西来显示它。让我们创建一个名为SFGameView的新类:

`package com.proandroidgames;

public class SFGameView{

}`

现在,修改这个类来扩展GLSurfaceView

`package com.proandroidgames;

import android.opengl.GLSurfaceView;

public class SFGameView extends GLSurfaceView {

}`

创建了扩展GLSurfaceView的类后,您可以在您的SFGame活动中添加对它的引用。在前一章中,您将StarFighter活动中的setContentView()的值设置为一个布局。到目前为止,SFGame活动的setContentView()值还没有设置,或者设置为默认的main布局。但是,您可以将这个值设置为GLSurfaceView。将刚刚创建的SFGamesetContentView()设置为SFGameView将允许您开始使用和显示 OpenGL。

打开SFGame活动,并创建您刚刚创建的SFGameViewGLSurfaceView的实例。

`package com.proandroidgames;

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

public class SFGame extends Activity {

private SFGameView gameView;

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(); } }`

现在,实例化SFGameView,并将setContentView()设置为新的实例。

`package com.proandroidgames;

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

public class SFGame extends Activity { private SFGameView gameView;

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); gameView = new SFGameView(this); setContentView(gameView); } }`

这是足够使用SFGameView显示游戏的代码。然而,你需要提前考虑,当玩家使用它的时候,你的游戏会发生什么。如果你现在投入一些额外的时间,你可以非常简单地避免一些非常痛苦的头痛。

使用 onResume()和 onPause()

可能发生的最常见的事情之一是玩家可以通过给予另一个Activity焦点来中断游戏。这可能是有意的——如果玩家开始另一项活动并给予关注——也可能是无意的——如果玩家在游戏过程中接到电话。如果处理不当,这两种情况都会对你的游戏造成严重破坏。令人惊讶的是,这两种情况很容易编码。

Android 提供了几个处理程序来应对你的活动可能被打断的情况。如果你的活动失去了另一个活动的焦点,不管是有意还是无意,Android 都会向你的活动发送一个暂停事件。当你的活动再次成为活动时,Android 会给它发送一个恢复事件。

Activity类可以实现onPause()onResume()来处理这些情况。在您的SFGame活动中简单地覆盖这些,如下所示:

`package com.proandroidgames;

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

public class SFGame extends Activity {

private SFGameView gameView;

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); gameView = new SFGameView(this); setContentView(gameView); } @Override protected void onResume() {

}

@Override protected void onPause() {

}

}`

现在,您可以添加一些代码,根据需要暂停和恢复您的游戏活动。

`package com.proandroidgames;

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

public class SFGame extends Activity { private SFGameView gameView;

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); gameView = new SFGameView(this); setContentView(gameView); } @Override protected void onResume() { super.onResume(); gameView.onResume(); }

@Override protected void onPause() { super.onPause(); gameView.onPause(); }

}`

注:onResume()onPause()功能指的是活动执行本身的暂停,而不是游戏的暂停。暂停游戏是分开处理的。

再次保存您的SFGame类。您现在有了一个显示GLSurfaceView的活动。您需要通过SFGame活动为SFGameView创建一些要显示的内容。你需要创建的是一个GLSurfaceView渲染器。

创建渲染器

您创建的GLSurfaceView``SFGameView只是一个显示 OpenGL 的视图。GLSurfaceView需要渲染器的帮助来完成繁重的工作。理论上,你可以将渲染器整合到GLSurfaceView中。然而,我更喜欢代码的清晰分离,以区分不同的功能;这使得故障排除变得更加容易。

在您的StarFighter包中创建一个名为SFGameRenderer的新类。

`package com.proandroidgames;

public class SFGameRenderer{

}`

现在你需要实现GLSurfaceView的渲染器。

`package com.proandroidgames;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{

} Be sure to add in the unimplemented methods: package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub

}

@Override public void onSurfaceChanged(GL10 gl, int width, int height) {

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) {

} }`

这些方法的功能应该是不言自明的。当渲染器在屏幕上绘制一帧时,调用onDrawFrame()方法。当视图的大小已经改变时,调用onSurfaceChanged()方法,即使是在最初改变的时候。最后,在创建GLSurface时,调用onSurfaceCreated()方法。

让我们按照它们被调用的顺序开始编码。首先出场的是onSurfaceCreated()

创建您的 OpenGL 表面

onSurfaceCreated()中,你将初始化你的 OpenGL 并加载你的纹理。

提示:用 OpenGL 的说法,纹理也可以是图像,就像你的背景一样。你将在本章的后面得到这个,但是从技术上来说,你将使用这个游戏的背景图像作为纹理应用到两个平面三角形上并显示出来。

第一步是启用 OpenGL 的二维纹理映射功能

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { }

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glEnable(GL10.GL_TEXTURE_2D); } }`

注意,onSurfaceCreated()将 OpenGL ( GL10 gl)的一个实例作为参数。当渲染器被调用时,这个实例将由GLSurfaceView传递给方法。您不必担心为此流程创建 GL10 的实例;它会自动为您完成。

接下来,你想让 OpenGL 测试你的表面中所有物体的深度。这需要一些解释。即使你正在创建一个 2-D 游戏,你也需要用 3-D 术语来思考。

想象一下,OpenGL 环境是一个舞台。你想在游戏中画的一切都是这个舞台上的演员。现在,想象你正在拍摄演员在舞台上走动的场景。最终的电影是正在发生的事情的二维表现。如果一个演员走到另一个演员的前面,那么这个演员在电影中是看不到的。然而,如果你正在看这些演员在剧院的现场表演,根据你坐的位置,你仍然可以看到后面的演员。

这和 OpenGL 的工作思路是一样的。即使你正在制作一个二维游戏,OpenGL 也将把所有的东西当作三维空间中的三维物体来对待。事实上,在 OpenGL 中开发 2d 和 3d 的唯一区别是你如何告诉 OpenGL 渲染最终的场景。因此,你需要注意你的物体在三维空间中的位置,以确保它们在二维游戏中正确的渲染。接下来通过启用 OpenGL 深度测试,你给了 OpenGL 一种方法来文本化你的纹理并决定它们应该如何被渲染。

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { }

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

} }`

您将添加到该方法中的最后两行代码与混合有关。你现在不必太担心这个,因为你真的不会注意到这段代码的效果,直到本章的后面。你要在游戏中绘制的所有图像都要有透明的区域。这两行代码将设置 OpenGL 的混合功能来创建透明度。

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { }

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE); } }`

加载游戏纹理

onSurfaceCreated()方法中你应该做的下一件事是加载你的纹理。然而,这将是一个有点复杂的过程,您将在下一节中处理它。现在,在代码中添加一个注释,表明您将回到这里,然后让我们继续讨论onSurfaceChanged()

**注意:**你在游戏中添加的所有纹理都将被添加到onSurfaceCreated()方法中。

`public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { }

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

//TODO Add texture loading for background image } }`

onSurfacedChanged()方法将处理显示图像所需的所有设置。每次调整屏幕大小时,方向都会改变,并且在初次启动时,会调用此方法。

你需要设置glViewport()并调用渲染例程来完成onSurfacedChanged()

glViewport()方法有四个参数。前两个参数是屏幕左下角的 x 和 y 坐标。通常,这些值将是(0,0),因为屏幕的左下角将是 x 轴和 y 轴相交的地方——各自的 0 坐标。glViewport()方法的下两个参数是视窗的宽度和高度。除非你希望你的游戏比设备的屏幕小,否则这些应该被设置为设备的宽度和高度。

`public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { } @Override public void onSurfaceChanged(GL10 gl, int width, int height) {

gl.glViewport(0, 0, width,height); }

...

}`

注意,调用表面,在本例中是SFGameView,向onSurfacedChanged()方法发送widthheight参数。您可以将glViewport()的宽度和高度设置为SFGameView发送的相应的widthheight

注:SFGameView发来的widthheight将代表设备的宽度和高度减去屏幕顶部的通知栏。

如果glViewport()代表拍摄场景的镜头,那么glOrthof()就是图像处理器。设置好视口后,你现在要做的就是使用glOrth0f()渲染表面。

渲染表面

要访问glOrthof(),你需要将 OpenGL 置于投影矩阵模式。OpenGL 有不同的矩阵模式,允许您访问引擎的不同部分。在这本书里,你会接触到大部分,如果不是全部的话。这是你第一次合作。投影矩阵模式允许您访问场景的渲染方式。

要进入投影矩阵模式,您需要将glMatrixMode()设置为GL_PROJECTION

`public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { }

...

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

//TODO Add texture loading for background image } }`

现在 OpenGL 处于投影矩阵模式,您需要加载当前身份。把身份想象成 OpenGL 的默认状态。

`public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { }

...

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

//TODO Add texture loading for background image } }`

加载身份后,您可以设置glOrthof(),它将为您的场景设置一个正交的二维渲染。这个调用有六个参数,每个参数定义一个裁剪平面。

剪裁平面向渲染器指示停止渲染的位置。换句话说,任何落在裁剪平面之外的图像都不会被glOrthof()拾取。六个剪裁平面是左、右、下、上、近和远。这些代表 x、y 和 z 轴上的点。

`public class SFGameRenderer implements Renderer{ @Override public void onDrawFrame(GL10 gl) { } @Override public void onSurfaceChanged(GL10 gl, int width, int height) {

gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity(); gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

//TODO Add texture loading for background image } }`

这就是你为游戏设置渲染和投影所要做的一切。继续并保存SFGameRenderer;您将在本章稍后回到onDrawFrame()

设置好onSurfaceCreated()onSurfaceChanged()方法后,您可以返回到添加到onSurfaceCreated()的注释。在本章的下一节,你将把你的背景图像作为一个纹理载入,并从onSurfaceCreated()调用它。

使用 OpenGL 加载图像

OpenGL 中的图像是作为纹理加载的。这就是说,一个图像,任何你想用 OpenGL 显示的图像,实际上都被当作一个应用于 3d 对象的纹理。

在这个游戏中,你创建的是二维图形,但是 OpenGL 会把它们当作三维对象。因此,您将构建正方形和三角形来映射您的图像。一旦您的图像作为纹理映射到这些平面形状上,您就可以将它们发送到渲染器中。听起来真的比实际复杂。

让我们从将一个文件复制到 Eclipse 中作为背景开始。我使用的图像称为backgroundstars.png,如图 4–2 中的所示。

images

图 4–2*。背景图像*

如果你使用的是摩托罗拉 Droid 型号的手机,OpenGL 中有一个漏洞,至少可以追溯到 Android 的 Froyo 版本。幸运的是,有一个变通办法。

我个人有一个 Droid X,亲眼见过这个 bug。如果您在 Droid 上使用 OpenGL 将图像作为纹理加载,您可能最终只会在图像应该出现的地方看到一个白框。这个错误与你的 Android 包中图片的大小和位置有关。

对于 Android 的大多数正常安装,图像可以放在任何res/drawable-[density]文件夹中,并且可以是任何尺寸。在上一章中,您将一些不同尺寸的图像放入了res/drawable-hdpi文件夹中,希望显示它们没有问题。

要避免这种可怕的机器人白盒错误,请遵循以下两个步骤。首先,在你的res下创建一个名为drawable-nopi的新的drawable文件夹。旧版本的 Android 安装了这个文件夹;我只能假设机器人手机上的某些东西仍然在引用它。您想使用 OpenGL 显示的所有图像现在都应该放在这个新的res/drawable-nopi文件夹中。

其次,你必须确保你的图像是 256 ^ 256(2 的幂)的衍生物。背景图像(参见图 4–1)为 256×256 像素。不过我发现 128 128 和 64 64 也可以。希望这个错误能在未来版本的 Droid 手机或 Android 软件中得到修复。

也就是说,把你正在用作背景的图像复制到你各自的res/drawable文件夹中。您现在可以使用R.java文件来引用它。

现在,创建一个新类,SFBackground。这个新的类文件将被调用来加载图像作为纹理,并将其返回给渲染器。

`package com.proandroidgames;

public class SFBackground {

}`

您将从 SFGameRenderer 调用SFBackground.loadTexture()方法来将背景加载到 OpenGL。但是首先你需要构建构造函数。SFBackground类的构造函数将设置所有你需要与 OpenGL 交互的变量。

您将需要一个数组来保存纹理的贴图坐标,一个数组来保存顶点的坐标,一个数组来保存顶点的索引。您还将创建一个指向纹理的指针数组。

注意:在这个类中,你将只加载一个纹理到类中,但是在以后的章节中,你将加载多个纹理到一个类中。因此,为了使代码尽可能通用,您将对大多数纹理加载类使用相同的结构。

`package com.proandroidgames;

public class SFBackground { private int[] textures = new int[1];

private float vertices[] = { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, }; private float texture[] = { 0.0f, 0.0f, 1.0f, 0f, 1, 1.0f, 0f, 1f, }; private byte indices[] = { 0,1,2, 0,2,3, }; public SFBackground() {

} }`

在下一节中,您将添加构建多边形来保存纹理的数组。

顶点、纹理和索引。。。我的天啊。

让我们简单讨论一下顶点、纹理和索引值代表什么。vertices[]数组列出了一系列的点。这里的每一行代表一个正方形的一个角的 x、y 和 z 值。在这种情况下,您正在制作一个与屏幕全尺寸相同的正方形。这将确保图像覆盖整个背景区域。

texture[]数组表示图像的角(即纹理)将与您创建的正方形的角对齐的位置。同样,在这种情况下,你希望纹理覆盖整个正方形,从而覆盖整个背景。数组保存了一个指针,指向你正在加载到你的形状上的每一个纹理。您正在将此硬编码为 1,因为您将只在此形状上加载一个背景图像。

最后,indices[]数组保存了正方形表面的定义。正方形的面被分成两个三角形。这个数组中的值是这些三角形按逆时针顺序排列的角。请注意,一条线(两点)重叠(0 和 2)。图 4–3 说明了这个概念。

images

图 4–3。 标注索引点

现在,创建一些缓冲区来存放这些数组。然后可以将缓冲区加载到 OpenGL 中。

`package com.proandroidgames;

import java.nio.ByteBuffer; import java.nio.FloatBuffer;

public class SFBackground {

private FloatBuffer vertexBuffer; private FloatBuffer textureBuffer; private ByteBuffer indexBuffer;

private int[] textures = new int[1];

private float vertices[] = { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, }; private float texture[] = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0, 1.0f, 0.0f, 1.of, }; private byte indices[] = { 0,1,2, 0,2,3, }; public SFBackground() {

}

}`

SFBackground类的构造函数中,您将使用适当的数组填充适当的缓冲区。

`package com.proandroidgames;

import java.nio.ByteOrder; import java.nio.ByteBuffer; import java.nio.FloatBuffer;

public class SFBackground {

private FloatBuffer vertexBuffer; private FloatBuffer textureBuffer; private ByteBuffer indexBuffer;

private int[] textures = new int[1];

private float vertices[] = { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, }; private float texture[] = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0, 1.0f, 0.0f, 1.of, }; private byte indices[] = { 0,1,2, 0,2,3, };

public SFBackground() {

ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4); byteBuf.order(ByteOrder.nativeOrder()); vertexBuffer = byteBuf.asFloatBuffer(); vertexBuffer.put(vertices); vertexBuffer.position(0);

byteBuf = ByteBuffer.allocateDirect(texture.length * 4); byteBuf.order(ByteOrder.nativeOrder()); textureBuffer = byteBuf.asFloatBuffer(); textureBuffer.put(texture); textureBuffer.position(0);

indexBuffer = ByteBuffer.allocateDirect(indices.length); indexBuffer.put(indices); indexBuffer.position(0);

}

}`

这里的代码应该是不言自明的。您正在用顶点和纹理数组的值创建一个ByteBuffer。请注意,每个数组中的值的数量都乘以 4,以在ByteBuffer中分配空间。这是因为数组中的值是浮点数,而浮点数的大小是字节的四倍。索引数组是整数,可以直接加载到indexBuffer中。

创建 loadTexture()方法

接下来,您需要创建loadTexture()方法。loadTexture()方法将接收一个图像指针,然后将图像加载到流中。然后,该流将作为纹理加载到 OpenGL 中。在绘制过程中,你将把这个纹理映射到顶点上。

`package com.proandroidgames;

import javax.microedition.khronos.opengles.GL10;

import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLUtils; import java.io.IOException; import java.io.InputStream;

public class SFBackground {

...

public SFBackground() {

...

} public void loadTexture(GL10 gl,int texture, Context context) { InputStream imagestream = context.getResources().openRawResource(texture); Bitmap bitmap = null; try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){

}finally { //Always clear and close try { imagestream.close(); imagestream = null; } catch (IOException e) { } }

gl.glGenTextures(1, textures, 0); gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle(); }

}`

loadTexture()的第一部分相当简单。它接收指针并将结果图像加载到位图流中。然后关闭该流。

然而loadTexture()的第二部分在 OpenGL 中相当沉重。第一行生成一个纹理指针,它的结构就像一个字典。

gl.glGenTextures(1, textures, 0);

第一个参数是需要生成的纹理名称的数量。当需要将纹理绑定到一组顶点时,您将通过名称从 OpenGL 中调用它们。这里,你只加载一个纹理,所以你只需要生成一个纹理名称。第二个参数是您为保存每个纹理的数量而创建的int数组。同样,现在这个数组中只有一个值。最后,最后一个参数保存指针在数组中的偏移量。因为数组是从零开始的,所以偏移量是 0。

第二行将纹理绑定到 OpenGL。

gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

如果你同时加载了两个纹理,那么这前两行各有两行:一行加载第一个图像,一行加载第二个图像。

接下来的两行处理 OpenGL 如何将纹理映射到顶点上。您希望映射快速进行,但产生清晰的像素。

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

The following two lines are important.Star Fighter is a scrolling shooter游戏,所以background应该是continuously scroll to give the illusion that the playable character is flying through space. Obviously, the image you are using for the background is finite. Therefore, to create the illusion that your player is flying through the endless vastness of space, the image must repeat ad infinitum. Luckily, OpenGL can handle this for you.

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);

在这两行中,你告诉 OpenGL 在 S 和 T 方向上不断重复你的背景纹理。现在,你的顶点是屏幕的大小,初始的背景纹理将直接映射到它的上面。到了滚动背景的时候(在本章的下一节),你实际上是在移动顶点上的纹理,而不是移动顶点。通过移动纹理,您允许 OpenGL 为您重复纹理,以覆盖移动纹理时暴露的顶点。这是 OpenGL 的一个非常方便的特性,尤其是在游戏开发中。

最后,在loadTexture()方法的最后两行,您将创建的位图输入流与第一个纹理相关联。然后位图流被回收。

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0); bitmap.recycle();

绘制你的纹理

完成SFBackground类需要编写的最后一段代码是将纹理绘制到顶点上的方法。

`package com.proandroidgames;

import javax.microedition.khronos.opengles.GL10;

import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLUtils; import java.io.IOException; import java.io.InputStream;

public class SFBackground { ...

public void draw(GL10 gl) {

gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glFrontFace(GL10.GL_CCW); gl.glEnable(GL10.GL_CULL_FACE); gl.glCullFace(GL10.GL_BACK);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisable(GL10.GL_CULL_FACE);

}

public SFBackground() {

...

} public void loadTexture(GL10 gl,int texture, Context context) { InputStream imagestream = context.getResources().openRawResource(texture); Bitmap bitmap = null; try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){

}finally {

try { imagestream.close(); imagestream = null; } catch (IOException e) { } }

gl.glGenTextures(1, textures, 0); gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle(); }

}`

每次你想要绘制背景时,都会调用draw()方法,而loadTexture()方法只会在你初始化游戏时调用。

这个方法的第一行将纹理绑定到你的目标。把它想象成把一颗子弹放进枪膛;纹理被加载并准备使用。

gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

draw()方法中接下来的三行告诉 OpenGL 启用剔除,基本上忽略任何不在正面的顶点。因为你是在二维正交视图中渲染游戏,你不希望 OpenGL 花费宝贵的处理器时间来处理玩家永远看不到的顶点。现在,你所有的顶点都是面向前方的,但是这是一个很好的代码。

gl.glFrontFace(GL10.GL_CCW); gl.glEnable(GL10.GL_CULL_FACE); gl.glCullFace(GL10.GL_BACK);

接下来的四行启用顶点和纹理状态,并将顶点和纹理缓冲区加载到 OpenGL 中。

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

最后,纹理被绘制到顶点上,所有启用的状态都被禁用。

**gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE,** **indexBuffer);** **gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);** **gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);** **gl.glDisable(GL10.GL_CULL_FACE);**

您的SFBackground类现在已经完成,可以被SFGameRenderer调用了。保存SFBackground.java文件并重新打开SFGameRenderer

调用 loadTexture()和 draw()

您需要添加对SFBackgroundloadTexture()draw()方法的适当调用。将从SFGameRendereronSurfaceCreated()方法中调用loadTexture()方法。

因为SFBackgroundloadTexture()方法将一个图像指针作为参数,所以需要给SFEngine添加一个新的常量。打开SFEngine并添加下面的常量指向你添加到 drawable 文件夹的 backgroundstars.png 文件。

`package com.proandroidgames;

import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton; package com.proandroidgames;

import android.content.Context; import android.content.Intent; import android.view.View;

public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; public static final int MENU_BUTTON_ALPHA = 0; public static final boolean HAPTIC_BUTTON_FEEDBACK = true; public static final int SPLASH_SCREEN_MUSIC = R.raw.warfieldedit; public static final int R_VOLUME = 100; public static final int L_VOLUME = 100; public static final boolean LOOP_BACKGROUND_MUSIC = true; public static Context context; public static Thread musicThread; public static final int BACKGROUND_LAYER_ONE = R.drawable.backgroundstars;

/Kill game and exit/ public boolean onExit(View v) { try { Intent bgmusic = new Intent(context, sfmusic.class); context.stopService(bgmusic); musicThread.stop(); return true; }catch(Exception e){ return false; }

}

}`

您现在将调用SFBackground类的loadTexture()方法,并向其传递该常量。这将把背景星星图像作为纹理加载到 OpenGL 中。

保存SFEngine,并返回SFGameRenderer。您现在将实例化一个新的SFBackground,并从onSurfaceCreated()调用它的loadTexture()方法。最好实例化新的SFBackground,这样就可以在整个类中访问它。在本课程中,您将多次调用SFBackground

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{

private SFBackground background = new SFBackground();

@Override public void onDrawFrame(GL10 gl) {

}

@Override public void onSurfaceChanged(GL10 gl, int width, int height) { gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) {

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context);

}

}`

在这一点上,如果你调用了SFBackgrounddraw()方法,你将得到一个静态的星域图像。然而,一个静态的背景不是你在这个游戏中想要的。星际战士的主要可玩角色在太空中与敌人战斗,为了模拟太空中的比赛,背景需要滚动。在下一节中,您将创建一个滚动背景的方法,就好像您正在星空中飞行一样。

滚动背景

与你在这一章已经完成的相比,编写滚动背景的方法将会非常容易。在**SFGameRenderer**中,创建一个名为scrollBackground1()的新方法。

您还需要一个名为bgScroll1的新 float。当你不在这个方法中时,这个浮动将记录背景滚动了多少。因为您需要值在scrollBackground1()方法之外持久化,所以在类可以访问它的地方创建它。

**注意:**你将这个方法命名为scrollBackground1(),因为在本章的后面,你将创建一个scrollBackground2()来滚动背景的第二层。

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground();

private float bgScroll1;

@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub }

private void scrollBackground1(GL10 gl){

}

@Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub

gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

} @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context);

}

}`

在这个方法中你要做的第一件事是测试以确保bgScroll1的值不会超过一个浮点数的最大可能值并抛出一个异常。bgScroll1升到那么高的几率非常小,尤其是当你看到我们将它增加多少的时候。然而,谨慎行事总是更好。

测试bgScroll1不等于浮动的最大尺寸。如果bgScroll1是浮动的最大尺寸,将其设置为零。

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground();

private float bgScroll1;

@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub }

private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; }

}

...

}`

在本章的前面,我们讨论了 OpenGL 的两种矩阵模式:纹理和投影。你必须把 OpenGL 放在纹理矩阵模式下来滚动顶点上的纹理。

注意:记住你实际上是在顶点上移动纹理。您没有移动顶点。

因为您没有移动顶点,所以您需要确保它们在正确的位置,并且没有意外移动。为什么呢?这是学习 OpenGL 的棘手部分之一。

OpenGL 矩阵

将 OpenGL 设置为纹理矩阵模式,甚至模型视图矩阵模式(用于移动和缩放顶点)将使您在那时分别访问 OpenGL 中的所有纹理和所有顶点。这意味着当你将 OpenGL 放入纹理矩阵模式,并在 x 轴上移动一个单位的纹理时,你实际上是将 OpenGL 中的所有纹理在 x 轴上移动了一个单位。

这种情况在一个游戏中可能是有问题的,在这个游戏中,你可能有任何数量的物品在任何给定的时间以不同的速度和方向移动和缩放。然而,如果 OpenGL 同时处理所有的纹理和所有的顶点,你如何分别移动单独的项目呢?

这可能听起来很混乱,但是有一个合理的方法来解决这个问题。

所有矩阵模式都保存在一个堆栈中。这个过程是将模式推出堆栈(在这个例子中,是纹理矩阵模式)。一旦模式离开堆栈,你就移动所有的纹理,并且只重画那些你希望被特定的移动影响的纹理。然后将纹理弹出堆栈,并对要移动的下一个纹理重复该过程。

在开始使用矩阵模式之前,您必须小心地将它重置回默认状态,否则它将具有您上次设置的值。例如,假设你有纹理 A 和纹理 b,你想把纹理 A 在 x 轴上移动 1 个单位,在 y 轴上移动 1 个单位。你想把纹理 B 在 x 轴上移动 1 个单位。将纹理矩阵推出堆栈,并在 x 轴和 y 轴上各移动一个单位。然后绘制纹理 A,将矩阵弹出堆栈。那很容易。

现在,你移动到纹理 b。你将矩阵推出堆栈,并在 x 轴上移动矩阵 1 单位。然而,矩阵已经被设置为(1,1),因为你在纹理 A 上做的最后一个操作是在每个轴上将纹理矩阵移动 1 个单位。所以你无意中在 x 轴上移动了 2 个单位,在 y 轴上移动了 1 个单位。因此,在将矩阵推出堆栈后,需要将其重置为默认状态,以确保从默认单位开始。使用glLoadIdentity()调用完成矩阵复位。

您将执行的用于滚动背景的 OpenGL 操作是glTranslatef()glTranslatef()方法有三个参数,值 x、y 和 z。它将根据提供的值调整当前矩阵。你将把你滚动背景的值存储在一个常量中。将以下常量添加到SFEngine

**public static float SCROLL_BACKGROUND_1 = .002f;**

保存SFEngine并移回SFGameRenderer。滚动背景纹理的第一步是将模型矩阵模式推出堆栈并重置它,以防将来任何移动会影响模型模式。然后你将把纹理矩阵推出堆栈,并进行滚动。

scrollBackground1() 方法中添加以下几行:

`gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f); package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground();

private float bgScroll1;

@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub }

private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; } /This code just resets the scale and translate of the Model matrix mode, we are not moving it/ gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f); }

@Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub

gl.glViewport(0, 0, width,height); gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context);

}

}`

同样,这段代码在这一点上比任何东西都更像是内务处理。

变换纹理

现在,你要加载纹理矩阵模式,并执行你的滚动。您将通过bgScroll1中的值调整 y 轴。这样做的结果是背景将沿着 y 轴移动bgScroll1中的量。

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private float bgScroll1;

@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub

} private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; } gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f, bgScroll1, 0.0f); //scrolling the texture

}

@Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub

gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context);

}

}`

scrollBackground1()中你需要做的最后一件事是通过调用SFBackgrounddraw()方法来绘制背景,将矩阵弹出堆栈,并增加bgScroll1

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private float bgScroll1; @Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub

} private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f, bgScroll1, 0.0f);

background.draw(gl); gl.glPopMatrix(); bgScroll1 += SFEngine.SCROLL_BACKGROUND_1; gl.glLoadIdentity();

}

@Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub

gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context);

} }`

这个小方法是你滚动游戏背景所需要的。简单回顾一下这个方法的作用:

  • 它重置模型矩阵,以确保它没有被无意中移动。
  • 它加载纹理矩阵并沿 y 轴移动SCROLL_BACKGROUND_1中的值。
  • 它绘制背景并将矩阵弹出堆栈。

这种滚动会给你一个很好的移动星域,玩家的飞船可以飞过。现在试着运行你的游戏,看看背景是如何滚动的。如果有任何问题,这也是一个很好的时机,在您进入更复杂的代码之前进行一些调试。然而,特别是以今天的游戏标准来看,目前的背景相当简单。你需要做些什么来给它一点活力。

在下一节中,您将添加第二层的背景。这将给你的游戏背景一些深度,即使是 2-D 游戏。如果你看过任何两层侧滚游戏,像超级马里奥兄弟,你应该注意到两层滚动的速度不同。在接下来的部分中,您将为您的游戏提供这种两层、两种速度的滚动效果。

添加第二层

此时,您已经初始化了 OpenGL,将背景图像作为纹理加载,并创建了一个方法来将该纹理向下滚动到游戏的背景中。现在,是时候创建第二层背景了。这第二层将非常容易创建,尤其是与你从游戏外观中获得的好处相比。

第二层的大部分实现实际上已经完成;您只需要创建一个新的滚动函数,添加几个新的常量,并实例化您的SFBackground的一个新副本。

首先,给你的res/drawable文件夹添加一张新图片。我用过的图像叫做debris.png

注意:因为你已经完成了背景第一层的大部分工作,所以我不会像本章前面的部分那样详细。

将图像放入res/drawable文件夹后,您可以向 SFEngine 添加两个常量。第一个是指向新图像文件的指针,该文件可以传递给 SFBackground 的 loadTexture()方法,第二个是保存第二层背景的滚动值的 float。这个浮动常量是背景第二层的关键部分,因为它会使第二层滚动得比第一层更快——给你的游戏增加一些深度。

将以下常量添加到您的 SFEngine 中。

**public static float SCROLL_BACKGROUND_2 = .007f;** **public static final int BACKGROUND_LAYER_TWO = R.drawable.debris;**

请注意,SCROLL_BACKGROUND_2被设置为比SCROLL_BACKGROUND_1更高的(十进制)值。值越大,意味着 y 轴的增量越大,因此背景的第二层看起来比第一层移动得更快。如果第二层滚动得比第一层快,就会产生背景有深度的错觉。

接下来,回到你的SFGameRenderer,实例化一个名为background2SFBackground的新副本。注意,您正在重用SFBackground类。这种重用是游戏引擎代码和特定于游戏的代码之间的部分区别。因为SFBackground是通用的,可以加载和绘制作为纹理传递给它的任何图像,所以它是引擎的一部分,可以重复用于我们的任何背景层。

因为您正在实例化一个新的SFBackground副本,所以您也应该创建一个名为bgScroll2的新 float。这个浮动将跟踪背景第二层的累积滚动系数,而不是背景第一层的滚动系数,后者保存在bgScroll1浮动中。

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private float bgScroll1; private float bgScroll2; @Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub

} private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f, bgScroll1, 0.0f); background.draw(gl); gl.glPopMatrix(); bgScroll1 += SFEngine.SCROLL_BACKGROUND_1; gl.glLoadIdentity();

}

@Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub

gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context);

}

}`

加载第二个纹理

现在你已经为第二层实例化了SFBackground的副本,你可以为它加载纹理了。您将调用与背景的第一层相同的loadTexture()方法。在调用了SFGameRendereronSurfaceCreated()方法中的第一个之后,你将调用背景的第二层的loadTexture()

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer; public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private float bgScroll1; private float bgScroll2;

...

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context); background2.loadTexture(gl,SFEngine.BACKGROUND_LAYER_TWO, SFEngine.context);

}

}`

确保当你为背景的第二层调用loadTexture()方法时,你传递给它正确的图像指针。之前,您在SFEngine中创建了一个名为BACKGROUND_LAYER_TWO的新常量,它有一个指向新图像的指针;这是你应该传递给background2loadTexture()方法的指针。

你现在有一个新的背景层实例化,你正在加载一个纹理到它。接下来,您需要编写一个新的方法来控制滚动。

滚动第二层

你将在这个滚动方法中做一些与背景第一层的滚动方法稍有不同的事情。因为背景的第二层只是较小的图像,不应该控制背景的整体外观,所以您将在模型矩阵视图中调整顶点的大小,以便第二层纹理的顶点是屏幕宽度的一半。然后,您将沿着 x 轴移动顶点,因此图像看起来在屏幕右侧的一半。

在您的SFGameRenderer中创建一个名为scrollBackground2()的新方法。您还应该插入与在scrollBackground1()中相同的测试,以确保bgScroll2没有超过浮动的最大大小。

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private float bgScroll1; private float bgScroll2;

@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub

} private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,bgScroll1, 0.0f);

background.draw(gl); gl.glPopMatrix(); bgScroll1 += SFEngine.SCROLL_BACKGROUND_1; gl.glLoadIdentity();

}

private void scrollBackground2(GL10 gl){ if (bgScroll2 == Float.MAX_VALUE){ bgScroll2 = 0f; }

}

...

}`

处理矩阵

这里是scrollBackground2()的代码要稍微改变的地方。在scrollBackground1()中,您添加了一些内务代码,以确保模型矩阵没有改变,并将其重置为默认值。在scrollBackground2()中,你将对模型矩阵进行两次转换。首先,您将在 x 轴上缩放模型矩阵,使其为屏幕大小的一半。然后,您将在 x 轴上移动模型矩阵,使其位于屏幕右侧的一半。

因为您是在模型矩阵而不是纹理矩阵上执行这些操作,所以您将转换顶点而不是应用于它的纹理。也就是说,虽然在视觉上你会看到纹理收缩并移动到屏幕的一侧,但你实际上是在收缩和移动顶点,而不是纹理。

您将把glScale()方法的 x 值设置为. 5,以便在 x 轴上将顶点缩小一半。请小心理解,将轴设置为 0.5 并不意味着您要向它添加 0.5 个单位。所有的值都相乘。因此,通过将glScale()的 x 设置为. 5,您是在告诉 OpenGL 将 x 的当前值乘以. 5,从而(在您的例子中)将 x 轴缩小一半。

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private float bgScroll1; private float bgScroll2;

@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub

} private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,bgScroll1, 0.0f);`

`background.draw(gl); gl.glPopMatrix(); bgScroll1 += SFEngine.SCROLL_BACKGROUND_1; gl.glLoadIdentity();

} private void scrollBackground2(GL10 gl){ if (bgScroll2 == Float.MAX_VALUE){ bgScroll2 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.5f, 1f, 1f); gl.glTranslatef(1.5f, 0f, 0f);

} @Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub

gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context); background2.loadTexture(gl,SFEngine.BACKGROUND_LAYER_TWO, SFEngine.context);

}

}`

注意scrollBackground1()scrollBackground2()的区别。因为scrollBackground2()直接处理模型矩阵,所以你要确保你在scrollBackground1()中有代码来重置它。否则,你的星域背景将会被减半并被推到屏幕的右边。

完成 scrollBackground2()方法

scrollBackground2()方法的其余部分与scrollBackground1()相同。你需要将背景纹理沿着 y 轴移动bgScroll2中的值,然后将该值增加SCROLL_BACKGROUND_2

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private float bgScroll1; private float bgScroll2;

@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub

} private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,bgScroll1, 0.0f);

background.draw(gl); gl.glPopMatrix(); bgScroll1 += SFEngine.SCROLL_BACKGROUND_1; gl.glLoadIdentity();

} private void scrollBackground2(GL10 gl){ if (bgScroll2 == Float.MAX_VALUE){ bgScroll2 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.5f, 1f, 1f); gl.glTranslatef(1.5f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef( 0.0f,bgScroll2, 0.0f);

background2.draw(gl); gl.glPopMatrix(); bgScroll2 += SFEngine.SCROLL_BACKGROUND_2; gl.glLoadIdentity(); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub

gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context); background2.loadTexture(gl,SFEngine.BACKGROUND_LAYER_TWO, SFEngine.context);

}

}`

到目前为止,你已经在这一章中做了大量的编码工作,而且你已经有了一个相当完整的环境,玩家可以在其中体验游戏。然而,SFGameRenderer的一个非常重要的部分留给了编码;onDrawFrame()法。这种方法不仅可以控制背景的滚动(以及最终的绘制),还可以控制游戏运行的帧速率。

以每秒 60 帧的速度运行

游戏运行速度的圣杯是每秒 60 帧。您的游戏应该以每秒 60 帧或尽可能接近每秒 60 帧的速度运行,以获得流畅的游戏体验。在本章的这一节,你将编写一个快速线程暂停例程,确保你的游戏以每秒 60 帧的速度运行。

使用GLSurfaceView渲染器作为游戏(SFGameRenderer)的主要启动点的好处是它已经为你线程化了。除非你明确地设置它,否则onDrawFrame()方法会被不断地调用。您不需要担心为游戏执行手动设置任何额外的线程或者在循环中调用游戏方法。当您将SFGameRenderer设置为活动的主视图时,将执行一个线程化操作,该操作将持续调用SFGameRendereronDrawFrame()方法。

因此,您需要整理这个方法的运行方式,以便将其限制在一秒钟内运行 60 次。

您可以在onDrawFrame()中放一个快速暂停例程,让线程休眠一段特定的时间。你想让线程休眠的时间是 1 秒除以 60。您将把这个值存储在SFEngine中的一个常量中。

`public class SFEngine { /Constants that will be used in the game/ public static final int GAME_THREAD_DELAY = 4000; public static final int MENU_BUTTON_ALPHA = 0; public static final boolean HAPTIC_BUTTON_FEEDBACK = true; public static final int SPLASH_SCREEN_MUSIC = R.raw.warfieldedit; public static final int R_VOLUME = 100; public static final int L_VOLUME = 100; public static final boolean LOOP_BACKGROUND_MUSIC = true; public static final int GAME_THREAD_FPS_SLEEP = (1000/60); public static Context context; public static Thread musicThread; public static Display display; public static float SCROLL_BACKGROUND_1 = .002f; public static float SCROLL_BACKGROUND_2 = .007f; public static final int BACKGROUND_LAYER_ONE = R.drawable.backgroundstars; public static final int BACKGROUND_LAYER_TWO = R.drawable.debris;

/Kill game and exit/ public boolean onExit(View v) { try { Intent bgmusic = new Intent(context, sfmusic.class); context.stopService(bgmusic); musicThread.stop();

return true; }catch(Exception e){ return false; }`

`}

}`

**提示:**在第五章中,你将可以选择稍微修改这个公式。当你添加更多的对象时,你需要考虑 OpenGL 渲染游戏所需的时间。目前,这个公式应该没问题。

暂停游戏循环

既然常量已经创建,您可以在onDrawFrame()方法中设置暂停例程。在方法的顶部,插入Thread.sleep()

`package com.proandroidgames;

import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private float bgScroll1; private float bgScroll2;

@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub try { Thread.sleep(SFEngine.GAME_THREAD_FPS_SLEEP); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }

} private void scrollBackground1(GL10 gl){ if (bgScroll1 == Float.MAX_VALUE){ bgScroll1 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(1f, 1f, 1f); gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,bgScroll1, 0.0f);`

`background.draw(gl); gl.glPopMatrix(); bgScroll1 += SFEngine.SCROLL_BACKGROUND_1; gl.glLoadIdentity();

} private void scrollBackground2(GL10 gl){ if (bgScroll2 == Float.MAX_VALUE){ bgScroll2 = 0f; }

gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.5f, 1f, 1f); gl.glTranslatef(1.5f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef( 0.0f,bgScroll2, 0.0f);

background2.draw(gl); gl.glPopMatrix(); bgScroll2 += SFEngine.SCROLL_BACKGROUND_2; gl.glLoadIdentity(); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub

gl.glViewport(0, 0, width,height);

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);

}

@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context); background2.loadTexture(gl,SFEngine.BACKGROUND_LAYER_TWO, SFEngine.context);`

`}

}`

现在,你把放在包含Thread.sleep()try. . .catch之后,任何东西都只能每秒运行 60 次。您将使用这个带有暂停例程的onDrawFrame()作为您的游戏循环。在这里,你可以做任何你需要在游戏中调用的事情。

清除 OpenGL 缓冲区

游戏循环的第一步是清除 OpenGL 缓冲区。这将为你将要做的所有渲染和转换准备 OpenGL。

`@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub try { Thread.sleep(SFEngine.GAME_THREAD_FPS_SLEEP); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

}`

一旦缓冲区被清空,你可以调用你在本章最后一节创建的两个滚动方法。这两种方法将适当地移动和绘制背景的两个层。

`@Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub try { Thread.sleep(SFEngine.GAME_THREAD_FPS_SLEEP); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

scrollBackground1(gl); scrollBackground2(gl);

}`

最后你要调用 OpenGL 的透明度混合函数。这个 OpenGL 函数将确保你应该能够看透的一切都是透明的。如果没有这个功能,你将看不到纹理周围的顶点。

@Override public void onDrawFrame(GL10 gl) { `// TODO Auto-generated method stub try { Thread.sleep(SFEngine.GAME_THREAD_FPS_SLEEP); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

scrollBackground1(gl); scrollBackground2(gl);

//All other game drawing will be called here

gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE); }`

恭喜你!您刚刚成功地使用 OpenGL 创建了一个两层、双速滚动的背景。运行游戏前的最后一步是连接主菜单的开始按钮来调用SFGame活动。

修改主菜单

打开您在上一章中创建的SFMainMenu文件。在第三章中,你为开始按钮创建了一个onClickListener()。您将为SFGame活动的这个方法添加一个新的意图。将此活动添加到onClickListener()将在玩家点击(或触摸)主菜单上的开始按钮时开始您的游戏活动。

`start.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { /** Start Game!!!! */ Intent game = new Intent(getApplicationContext(),SFGame.class); SFMainMenu.this.startActivity(game);

} });`

您可以编译并运行您的代码。您应该会看到闪屏淡入主菜单。如果你点击主菜单上的开始按钮,你将进入你的游戏,你会看到两层背景以不同的速度随着背景音乐滚动。

点击设备上的后退菜单按钮返回主菜单,点击退出按钮退出游戏,杀死线程。

**注意:**记住,你还没有输入一些重要的内务代码。例如,如果你离开游戏的焦点,线程会继续运行(音乐也是)。在本书的后面部分,您将添加代码来解决这个问题。现在,当你测试你的游戏时,确保你通过点击退出按钮来终止线程。

总结

在这一章中,你学习了游戏开发者在游戏中添加背景的几个关键技巧。具体来说,您现在应该对以下内容有了基本的了解:

  • 创建一个GLSurface实例
  • 创建渲染器
  • 正在初始化 OpenGL
  • 从图像中加载纹理
  • 修改 OpenGL 矩阵
  • 推动和弹出矩阵
  • 使用glScale()glTranslatef()移动纹理和顶点
  • 使用Thread.sleep()编组渲染器

在下一章,你将添加你的第一个可玩角色到游戏中。