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

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="http://schemas.android.com/apk/res/android"

`

一个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="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"

**`

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

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

`
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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="http://schemas.android.com/apk/res/android"
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()编组渲染器

在下一章,你将添加你的第一个可玩角色到游戏中。

五、创造你的角色

到本书的这一点,你已经做了相当多的开发工作,并且已经了解了很多关于 OpenGL 和 Android 的知识——如此之多,你现在应该对 OpenGL 和你过去可能使用过的任何其他 API 工具之间的细微差别相当熟悉了。

到目前为止,您还没有编写过多的代码。但是你写的东西给你的游戏开了一个好头,给你带来了很大的视觉冲击。你已经完成了两层双速滚动背景、背景音乐、闪屏和主菜单系统的开发。就可玩的游戏而言,所有这些东西都有一个共同点:它们相当无聊。

也就是说,一个游戏玩家不会为了看一个花哨的双层背景滚动而过而买你的游戏。玩家需要一些动作来控制。这就是《第五章:创造你的角色》的全部内容。

在这一章中,你将创建你的可玩角色。本章结束时,你将拥有一个玩家可以在屏幕上移动的动画角色。本章的第一部分将向你介绍 2d 游戏开发的主要部分——精灵动画。然后,使用 OpenGL ES,您将从一个完整的精灵表中加载不同的精灵来创建动画角色的幻觉。你将学习如何在动作的关键点装载不同的精灵,让你的角色看起来像是在飞行中倾斜。

动画精灵

sprite 动画是 2d 游戏开发人员领域中历史最悠久的工具之一。回想一下你最喜欢的 2d 游戏,很有可能任何角色的动画都是用精灵动画制作的。

从技术上讲,精灵是二维游戏中的任何图形元素。因此,根据定义,你的主要角色是一个精灵。精灵本身只是静止的图像,停留在屏幕上,不会改变。精灵动画是你要用来赋予你的角色一些生命的过程,即使那个角色只是一艘宇宙飞船。

注意:不要混淆动画动作。在屏幕上移动精灵(图像、纹理、顶点或模型)与动画精灵不同;这两个概念和技能是相互排斥的。

精灵动画是使用翻书风格的效果来完成的。想想几乎所有的 2d 游戏,例如,马里奥兄弟马里奥兄弟是结合了精灵动画的 2d 平台游戏的最好例子之一。在游戏中,你通过一个侧滚的环境向左或向右移动马里奥。马里奥沿着你移动他的方向走,有时跑。在行走的过程中,他的双腿显然是活动的。

这个行走动画实际上是由一系列静止图片组成的。每张图片描绘了行走动作中的不同点。当玩家向左或向右移动角色时,不同的图像被交换出来,给人一种马里奥在行走的错觉。

在游戏星际战士中,你将采用同样的方法为你的主角制作一些动画。星际战斗机中的主要可玩角色是一艘飞船;因此,它不需要行走动画。尽管宇宙飞船确实需要一些动画。在这一章中,你将创建玩家飞行时向右倾斜和向左倾斜的动画。在后续章节中,您将创建碰撞爆炸动画。

精灵动画的伟大之处在于你在前一章中学习了实现它所需的所有技巧。也就是说,您学习了如何将纹理加载到 OpenGL 中。更重要的是,你学会了将纹理映射到一组顶点上。精灵动画的关键是纹理如何映射到你的顶点。

用于实现精灵动画的纹理在技术上不是独立的图像。每秒 60 次加载和映射一个新纹理所需的时间和能量——如果你能做到的话——将远远超过 Android 设备的能力。相反,你将使用一种叫做 sprite sheet 的东西。

sprite sheet 是一个单独的图像,其中包含执行 sprite 动画所需的所有独立图像。图 5–1 显示了主游戏船的精灵表。

images

图 5–1。 主角雪碧单

注:图 5–1 中的 sprite sheet 只显示了一部分。加载到 OpenGL 中的实际图像为 512 × 512。图像的下半部分是透明的,为了在书中更好地显示,已经被裁剪了。

你如何制作一个充满小图像的动画?这实际上比你想象的要容易。您将把图像作为一个纹理加载,但是您将只显示包含您想要显示给玩家的图像的纹理部分。当您想要动画显示图像时,您只需使用glTranslateF()移动到您想要显示的图像的下一部分。

如果这个概念还不太有意义,不要担心;在本章的下一节中,你将把它付诸行动。然而,第一步是创建一个类来处理你的游戏角色的绘制和加载。

注意:你可能想知道为什么精灵图片中的船只面朝下而不是朝上;特别是因为可玩的角色将会在屏幕的底部飞向顶部。这是因为 OpenGL 渲染了从最后一行到第一行的所有位图。因此,当 OpenGL 渲染这个 sprite 工作表时,它将出现在屏幕上,如图 Figure 5–2 所示。

images

图 5-2。 精灵工作表在屏幕上的样子

是的,你可以使用正确的方法绘制精灵,然后使用 OpenGL 将纹理旋转到正确的位置。然而,使用任何图像软件都可以很容易地反转 sprite 工作表,这样,你就为 OpenGL 省去了反转的周期和麻烦。

装载你的角色

在前一章中,您创建了一个类,它为背景图像加载纹理,然后在被调用时绘制该图像。你用来创建这个类的机制和你需要加载和绘制你的主角的机制是一样的。您将做一些小的调整,以允许使用 sprite 表,但除此之外,这段代码应该看起来很熟悉。

首先在项目包中创建一个名为SFGoodGuy的新类:

`package com.proandroidgames;

public class SFGoodGuy {

}`

SFGoodGuy()类中,剔除一个构造函数、draw()方法和loadTexture()方法。

提示:记住,您可以在 Eclipse 中使用 alt + shift + O 快捷键来暴露您可能需要的任何缺失的导入。

`package com.proandroidgames;

public class SFGoodGuy {

public SFGoodGuy() {

}
public void draw(GL10 gl) {

}
public void loadTexture(GL10 gl,int texture, Context context) {

}
}`

接下来,建立您将在课程中使用的缓冲区。同样,这看起来应该和你在上一章处理游戏背景时所做的一样。

您还可以添加代码来创建vertices[]数组。顶点将与背景中使用的顶点相同。

`package com.proandroidgames;

public class SFGoodGuy {

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,
};

public SFGoodGuy() {

}
public void draw(GL10 gl) {

}
public void loadTexture(GL10 gl,int texture, Context context) {

}
}`

现在,为纹理贴图创建数组。

创建纹理映射数组

纹理映射是SFGoodGuy()类与你加载背景时使用的不同之处。您将加载到这个类中的纹理是一个大的 sprite 表,它包含主要可玩角色的五个图像。您的目标是一次只显示这些图像中的一个。

理解如何告诉 OpenGL 你想要显示的图像的位置的关键是如何在 sprite 表上配置图像。再看一下图 5–1 中的精灵表。请注意,图像的布局是均匀的,第一行有四个图像,第二行有一个图像。在纹理的第一行只有四个图像,整个纹理有 1 个单位长,你可以推测你只需要显示整个纹理的四分之一就可以显示第一行四个图像中的一个。

这意味着不像对背景那样映射整个纹理(从(0,0)到(1,1),而只需要映射它的四分之一(从(0,0)到(0,. 25)。您将只使用 0.25 或四分之一的纹理来映射并显示船只的第一张图像。

像这样创建你的纹理数组:

`package com.proandroidgames;

public class SFGoodGuy {

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,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};

public SFGoodGuy() {

}
public void draw(GL10 gl) {

}
public void loadTexture(GL10 gl,int texture, Context context) {

}
}`

索引数组、draw()方法和构造函数都与在SFBackground类中使用的相同:

`package com.paroandroidgames;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;

public class SFGoodGuy {

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,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

public SFGoodGuy() {
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);
}

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 void loadTexture(GL10 gl,int texture, Context context) {

}
}`

在完成之前,您还需要对SFGoodGuy()类做一个修改。在背景的loadTexture()方法的类中,将glTexParameterf设置为GL_REPEAT,以便在顶点上移动纹理时能够重复纹理。这对于可玩的角色来说并不是必须的;因此,您要将该参数设置为GL_CLAMP_TO_EDGE

用下面的loadTexture()方法完成你的SFGoodGuy()类:

`…

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();

}
}`

现在你有了一个函数类,它将你的可玩角色纹理作为一个 sprite 表加载,显示 sprite 表中的第一个 sprite,并且当它被移动时不包装纹理。

将纹理加载到角色上

加载一个可玩角色的下一步是实例化一个SFGoodGuy()并加载一个纹理。保存并关闭SFGoodGuy()类;您现在不需要向它添加任何代码。

让我们给SFEngine添加几个快速变量和常量。你将需要这些在你的游戏循环。

首先,您将添加一个名为playerFlightAction的变量。这将用于跟踪玩家采取了什么行动,以便您可以在游戏循环中做出适当的反应。

`package com.proandroidgames;

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

public class SFEngine {

public static int playerFlightAction = 0;

/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;
}

}
}`

接下来,添加一个常量,指向本章最后一节的 sprite 表。

`package com.proandroidgames;

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

public class SFEngine {

public static int playerFlightAction = 0;
public static final int PLAYER_SHIP = R.drawable.good_sprite;

/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;
}

}
}`

接下来的三个常量表示玩家采取了什么行动。当玩家试图移动角色时,这些将被分配给playerFlightAction变量。

`package com.proandroidgames;

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

public class SFEngine {

public static int playerFlightAction = 0;
public static final int PLAYER_SHIP = R.drawable.good_sprite;
public static final int PLAYER_BANK_LEFT_1 = 1;
public static final int PLAYER_RELEASE = 3;
public static final int PLAYER_BANK_RIGHT_1 = 4;

/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;
}

}
}`

根据您对刚刚添加到SFEngine中的常数的观察程度,您可能会奇怪为什么PLAYER_BANK_LEFT_1的值是1,而PLAYER_RELEASE的值是3。这些值将代表你的精灵动画的阶段。在 sprite 工作表中,左岸动画有两个阶段,右岸动画有两个阶段。然而,在循环的代码中,您将能够推断出在PLAYER_BANK_LEFT_1PLAYER_RELEASE之间有一个值为2PLAYER_BANK_LEFT_2,并且这个常量不必在SFEngine中表示。当您在本节后面看到这个概念的实际应用时,它肯定会更有意义。

您需要的下一个常量将指示多少次循环迭代将等于一帧精灵动画。请记住,可玩角色和游戏背景之间的最大区别是,当角色在屏幕上移动时,您要制作角色动画。跟踪这个动画将是一件棘手的事情。游戏循环以每秒 60 帧的速度运行。如果您为循环的每次迭代运行一个新的精灵动画帧,您的动画将在玩家有机会欣赏它之前就结束了。常量PLAYER_FRAMES_BETWEEN_ANI会被设置为9,表示主游戏循环每迭代九次,就会有一帧精灵动画被绘制出来。

`package com.proandroidgames;

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

public class SFEngine {

public static int playerFlightAction = 0;
public static final int PLAYER_SHIP = R.drawable.good_sprite;
public static final int PLAYER_BANK_LEFT_1 = 1;
public static final int PLAYER_RELEASE = 3;
public static final int PLAYER_BANK_RIGHT_1 = 4;
public static final int PLAYER_FRAMES_BETWEEN_ANI = 9;

/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;
}

}
}`

最后,再添加一个常量和一个变量。这些将代表玩家的船从左向右移动的速度以及玩家的船在 x 轴上的当前位置。

`package com.proandroidgames;

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

public class SFEngine {

public static int playerFlightAction = 0;
public static final int PLAYER_SHIP = R.drawable.good_sprite;
public static final int PLAYER_BANK_LEFT_1 = 1;
public static final int PLAYER_RELEASE = 3;
public static final int PLAYER_BANK_RIGHT_1 = 4;
public static final int PLAYER_FRAMES_BETWEEN_ANI = 9;
public static final float PLAYER_BANK_SPEED = .1f;
public static float playerBankPosX = 1.75f;

/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;
}

}
}`

SFEngine现在有了帮助你实现你的可玩角色所需的所有代码。保存并关闭文件。

打开SFGameRenderer.java文件。这个文件是你的游戏循环的家。在前一章中,您创建了游戏循环,并添加了两种方法来绘制和滚动背景的不同层。现在,你要添加代码到你的循环中来绘制和移动可玩的角色。

设置游戏循环

第一步是实例化一个名为player1的新SFGoodGuy():

`public class SFGameRenderer implements Renderer{
private SFBackground background = new SFBackground();
private SFBackground background2 = new SFBackground();
private SFGoodGuy player1 = new SFGoodGuy();

private float bgScroll1;
private float bgScroll2;

}`

对象player1将会以与backgroundbackground2相同的方式被使用。你将从player1调用loadTexture()draw()方法来将你的角色加载到游戏中。

您需要创建一个变量来跟踪游戏循环经过了多少次迭代,这样您就可以知道何时翻转 sprite 动画中的帧。

`public class SFGameRenderer implements Renderer{
private SFBackground background = new SFBackground();
private SFBackground background2 = new SFBackground();
private SFGoodGuy player1 = new SFGoodGuy();
private int goodGuyBankFrames = 0;

private float bgScroll1;
private float bgScroll2;

}`

接下来,定位SFGameRenderer渲染器的onSurfaceCreated()方法。这个方法处理游戏纹理的加载。在上一章中,你在这个方法中调用了backgroundbackground2的加载方法。现在,您需要添加一个对player1loadTexture()方法的调用。

`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 SFGoodGuy player1 = new SFGoodGuy();
private int goodGuyBankFrames = 0;

@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);

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

player1.loadTexture(gl, SFEngine.PLAYER_SHIP, SFEngine.context);
}

}`

到目前为止,这段代码都很基本:创建纹理,加载纹理。现在,该是这一章的真正内容了。是时候写一个方法来控制你的玩家角色的移动了。

移动人物

本节将帮助您创建在屏幕上移动玩家角色所需的代码。为此,您将创建一个新的方法,将服务器作为您的核心游戏循环。最后,您将从这个循环中调用方法来执行移动角色的任务。在SFGameRenderer SFGameRenderer中创建一个接受GL10的新方法。

`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 void movePlayer1(GL10 gl){

}

}`

movePlayer1()方法中,您将在本章前面添加到SFEngineplayerFlightAction int 上运行一条switch语句。以防你从未使用过,switch语句将检查输入对象(playerFlightAction)并根据输入的值执行特定的代码。此switch语句的情况有PLAYER_BANK_LEFT_1PLAYER_RELEASEPLAYER_BANK_RIGHT_1default

`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 void movePlayer1(GL10 gl){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:

break;
case SFEngine.PLAYER_BANK_RIGHT_1:

break;
case SFEngine.PLAYER_RELEASE:

break;
default:

break;
}

}

}`

让我们从默认情况开始。当玩家没有对角色采取任何行动时,默认情况下将被调用。

绘制角色的默认状态

现在,顶点和屏幕一样大。因此,如果你现在把这个可玩的角色画到屏幕上,它会填满整个屏幕。你需要将游戏角色缩放大约 75 %,这样它在游戏中看起来会更好。

为此,您将使用glScalef()。将比例乘以 0 . 25 会使船缩小到原来的四分之一。这有一个非常重要的后果,你需要了解。

在上一章中,您简要地发现了缩放或平移顶点需要在模型矩阵模式下工作。您在任何矩阵模式下进行的任何操作都会影响该矩阵模式下的所有项目。因此,当您将玩家船的顶点缩放 0.25 倍时,您也缩放了它所占据的 x 轴和 y 轴。换句话说,当比例默认为 0(全屏)时,x 轴和 y 轴从 0 开始,到 1 结束,而当比例乘以. 25 时,x 轴和 y 轴将从 0 到 4 运行。

这对你很重要,因为当你试图跟踪玩家的位置时,你需要意识到背景可能会从 0 滚动到 1,但玩家可以从 0 滚动到 4。

加载模型矩阵视图,并在 x 和 y 轴上将播放器缩放 0.25。

`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 void movePlayer1(GL10 gl){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:

break;
case SFEngine.PLAYER_BANK_RIGHT_1:

break;
case SFEngine.PLAYER_RELEASE:

break;
default:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);

break;
}

}

}`

接下来,通过变量playerBankPosX中的值平移 x 轴上的模型矩阵。变量playerBankPosX将保存玩家在 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 void movePlayer1(GL10 gl){

default:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);

break;
}

}

}`

当播放器静止时,不需要采取其他动作,所以加载纹理矩阵,并确保它在默认位置,这是 sprite 表中的第一个 sprite。请记住,纹理矩阵模式将是您用来移动精灵片纹理的位置以翻转动画的模式。如果玩家没有移动角色,应该没有动画——因此,纹理矩阵应该默认为第一个位置。

`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 void movePlayer1(GL10 gl){

default:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
}

}

}`

switch语句中,您编写的下一个案例是针对PLAYER_RELEASE的。当玩家移动角色后释放控制时会调用PLAYER_RELEASE动作。当你还没有为游戏的实际控制编码时,玩家将触摸一个控制来告诉角色移动。当玩家释放这个控制键,从而告诉角色停止移动时,就会调用PLAYER_RELEASE动作。

编码播放器 _ 释放动作

现在,PLAYER_RELEASE的情况将执行与default情况相同的动作。也就是说,角色将停留在它在屏幕上留下的地方,无论 sprite 表中显示的是什么纹理,它都将返回到表中的第一个纹理。将default中的整个代码块复制粘贴到PLAYER_RELEASE的案例中。

`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 void movePlayer1(GL10 gl){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:

break;
case SFEngine.PLAYER_BANK_RIGHT_1:

break;
case SFEngine.PLAYER_RELEASE:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

}

}

}`

在完成PLAYER_RELEASE案例之前,您需要再添加一行代码。在本章早些时候,您已经了解到不能以与游戏循环相同的速率(每秒 60 帧)翻转精灵动画,因为精灵动画中只有两帧,在玩家意识到它发生之前就会结束。因此,您需要一个变量来保存游戏循环次数。通过了解游戏循环次数,您可以将该次数与PLAYER_FRAMES_BETWEEN_ANI常量进行比较,以确定何时需要翻转精灵动画帧。你在本章前面创建的goodGuyBankFrames变量将用于跟踪已经执行的游戏循环次数。

PLAYER_RELEASE的例子中,添加下面几行代码,每次执行一个循环,就将goodGuyBankFrames加 1。

`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 void movePlayer1(GL10 gl){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:

break;
case SFEngine.PLAYER_BANK_RIGHT_1:

break;
case SFEngine.PLAYER_RELEASE:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
goodGuyBankFrames += 1;

break;


}

}

}`

在你的movePlayer1()方法的四种情况中,PLAYER_RELEASEdefault是最容易的。现在,您需要编写当调用PLAYER_BANK_LEFT_1动作时会发生什么。

当玩家使用控件将角色船向左倾斜时,就会调用PLAYER_BANK_LEFT_1动作。这意味着你不仅需要沿着 x 轴向左移动角色,还需要使用 sprite sheet 上的两个 sprite 来设置角色的动画,这两个 sprite 表示左边的一排。

向左移动字符

就 OpenGL 而言,沿 x 轴移动角色和改变 sprite 页位置的操作使用了两种不同的矩阵模式。您将需要使用模型矩阵模式来沿着 x 轴移动角色,并且您将需要使用纹理矩阵模式来移动精灵表纹理-创建银行动画。让我们首先处理模型矩阵模式操作。

第一步是加载模型矩阵模式,并将 x 轴和 y 轴的比例设置为 0.25。

`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 void movePlayer1(GL10 gl){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);

**break;


}

}**

}`

接下来,您将使用glTranslatef()沿着 x 轴移动顶点。您从当前 x 轴位置减去PLAYER_BANK_SPEED,该位置存储在playerBankPosX中。(你在做减法以得到你需要移动到的位置,因为你试图沿着 x 轴向左移动字符。如果你想向右移动,你会增加。)然后,使用glTranslatef()将顶点移动到playerBankPosX中的位置。

`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 void movePlayer1(GL10 gl){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);

break;

}

}

}`

现在,您正在沿着 x 轴移动角色,您需要翻转到动画的下一帧。

加载正确的精灵

再一次看一下图 5–1 中的精灵表。请注意,与向左倾斜运动相对应的两个动画帧是第一行上的第四帧和第二行上的第一帧(请记住,如果纸张向后看,它会反转,因此看起来向右倾斜的帧在渲染时会向左倾斜)。

加载纹理矩阵模式,并平移纹理以在第一行显示第四个图像。因为纹理是用百分比来表示的,所以你需要做一点数学计算。再说一次,一行只有四张图片,数学很简单。

sprite 工作表的 x 轴从 0 到 1。如果除以 4,工作表中的每个精灵占据 x 轴的 0.25。因此,要将 sprite 工作表移动到该行的第四个 sprite,需要将其平移 0.75。(第一个子画面占用 x 值 0 到. 24,第二个子画面占用. 25 到. 49,第三个子画面占用. 50 到. 74,第四个子画面占用. 75 到 1。)

`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 void movePlayer1(GL10 gl){
**switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.75f,0.0f, 0.0f);

break;**


}

}

}`

绘制船之前的最后一步是增加goodGuyBankFrames,这样你就可以开始跟踪何时翻到脚本表中的第二帧。

`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 void movePlayer1(GL10 gl){
**switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.75f,0.0f, 0.0f);
goodGuyBankFrames += 1;

break;**

}

}

}`

这段代码有一个主要问题。玩家现在可以沿着 x 轴向左移动角色,船的精灵会变成左岸动画的第一个精灵。问题是,由于代码是现在写的,精灵会向左移动到无穷远处。您需要将移动字符的代码块包装在一个if. . . else语句中,该语句测试字符是否到达 x 轴上的 0。如果角色在 0 位置,表示他们在屏幕的左边缘,停止移动角色并将动画返回到默认的精灵。

`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 void movePlayer1(GL10 gl){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (SFEngine.playerBankPosX > 0){

SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.75f,0.0f, 0.0f);
goodGuyBankFrames += 1;
}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}

break;

}

}

}`

现在,通过调用draw()方法来绘制字符,并将矩阵弹出堆栈。过程中的这一步应该与两个背景层相同。事实上,这一步在游戏中几乎所有的 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{

private void movePlayer1(GL10 gl){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (SFEngine.playerBankPosX > 0){
SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.75f,0.0f, 0.0f);
goodGuyBankFrames += 1;
}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

}

}

}`

现在你有一个例子,如果玩家向左移动角色,顶点沿着 x 轴向左移动,直到它们到达零。此外,纹理从默认(自上而下的视图)精灵开始,当玩家向左移动时,精灵将更改为左侧银行动画的第一帧。

加载第二帧动画

如果玩家向左移动足够远,你需要将动画翻转到左岸动画的第二帧。查看 Figure 5–1 中的 sprite 表,左岸动画的第二帧是第二行的第一帧。使用glTranslatef()很容易导航到这个页面。问题是,你怎么知道什么时候翻转雪碧?

在本章的前面,您在SFEngine中创建了一个名为PLAYER_FRAMES_BETWEEN_ANI的常量,并将其设置为9。该常量表示您希望每九帧游戏动画(即游戏循环)翻转一次玩家的角色动画。您还创建了一个名为goodGuyBankFrames的变量,每当玩家的角色被绘制时,该变量就会增加 1。

你需要比较goodGuyBankFramesPLAYER_FRAMES_BETWEEN_ANI的当前值。如果goodGuyBankFrames少,画第一帧动画。如果goodGuyBankFrames更大,画第二帧动画。下面是你的if . . . then声明应该是什么样子。

`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 void movePlayer1(GL10 gl){
**switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (goodGuyBankFrames <SFEngine.PLAYER_FRAMES_BETWEEN_ANI&&
SFEngine.playerBankPosX > 0){
SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.75f,0.0f, 0.0f);
goodGuyBankFrames += 1;
}else if (goodGuyBankFrames > = SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX > 0){
SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;

}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

}

}**

}`

if . . . else if条件下,你测试goodGuyBankFrames的值是否大于PLAYER_FRAMES_BETWEEN_ANI,这表示你应该翻到左倾斜动画的下一帧。让我们来编写翻转动画的代码块。

在图 5-1 中,左侧堤岸动画的第二帧在第二行第一个位置。这意味着该 sprite 的左上角位于 x 轴上的 0 位置(最左边),然后是 y 轴上的 1/4 处(. 25)。简单地使用glTranslatef()方法将纹理移动到这个位置。

注意:在你移动纹理之前,你需要加载纹理矩阵模式。

`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 void movePlayer1(GL10 gl){
**switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (goodGuyBankFrames <SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&SFEngine.playerBankPosX > 0){
SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.75f,0.0f, 0.0f);
goodGuyBankFrames += 1;
}else if (goodGuyBankFrames >= SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&SFEngine.playerBankPosX > 0){
SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.25f, 0.0f);
}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

}

}**

}`

您的将角色向左移动并实现两帧精灵动画的switch语句已经完成。

向右移动字符

在完成movePlayer1()方法之前,您需要完成的最后一个 case 语句是针对PLAYER_BANK_RIGHT_1的。当玩家想要将角色移动到屏幕的右侧,x 轴的正方向时,就会调用这种情况。

案例的布局看起来是一样的,但是你需要从 sprite 表中加载不同的帧。首先,布置你的模型矩阵,缩放角色顶点,并像在PLAYER_BANK_LEFT_1案例中一样设置if . . . else if语句。

这个if . . . else if语句与PLAYER_BANK_LEFT_1情况下的语句有一个不同之处。在PLAYER_BANK_LEFT_1的例子中,您测试了顶点在 x 轴上的当前位置是否大于 0,这表明角色没有离开屏幕的左侧。对于PLAYER_BANK_RIGHT_1的情况,你需要测试角色是否到达了屏幕最右边。

默认情况下,x 轴从 0 开始,到 1 结束。然而,为了使游戏角色在屏幕上看起来更小,你已经将 x 轴缩放到 0.25。这意味着 x 轴现在从 0 到 4。你需要测试可玩的角色没有向右滚动超过 4 个单位。正确吗?

不,不完全是。

OpenGL 追踪顶点的左上角。因此,如果您在遇到 4 时测试该情况,该字符将已经离开屏幕。你需要考虑角色顶点的宽度。角色顶点的宽度为 1 个单位。测试角色没有超过 x 轴值 3 将使它保持在玩家可以看到的屏幕上。

`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 void movePlayer1(GL10 gl){
**switch (SFEngine.playerFlightAction){

case SFEngine.PLAYER_BANK_RIGHT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (goodGuyBankFrames < SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&
SFEngine.playerBankPosX < 3){

}else if (goodGuyBankFrames >=
SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX < 3){

}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;


}

}**

}`

PLAYER_BANK_RIGHT_1案例中的初始代码块与PLAYER_BANK_LEFT_1中的几乎相同。您正在调整模型矩阵,测试角色在 x 轴上的位置,并测试已经运行的游戏循环帧数,以判断需要显示哪一帧精灵动画。

现在,您可以在适当的位置显示右岸动画的第一帧和第二帧。

加载右岸动画

玩家向右倾斜时应显示的第一帧动画在第一行第二个位置(参见 Figure 5–1 中的 sprite sheet)。因此,您需要将纹理矩阵在 x 轴上平移 0.25,在 y 轴上平移 0,以显示此帧。

`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 void movePlayer1(GL10 gl){
**switch (SFEngine.playerFlightAction){

case SFEngine.PLAYER_BANK_RIGHT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (goodGuyBankFrames <SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&
SFEngine.playerBankPosX < 3){
SFEngine.playerBankPosX += SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f,0.0f, 0.0f);
goodGuyBankFrames += 1;
}else if (goodGuyBankFrames >= SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&SFEngine.playerBankPosX < 3){

}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;


}**

}

}`

注意是这个代码块将PLAYER_BANK_SPEED的值加到玩家的当前位置,而不是从中减去。这是在 x 轴上向右移动顶点的关键,而不是向左。

重复这段代码,您需要在 x 轴上将纹理转换为. 50,以显示右侧银行的 sprite 动画的第二帧。

`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 void movePlayer1(GL10 gl){
**switch (SFEngine.playerFlightAction){

case SFEngine.PLAYER_BANK_RIGHT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (goodGuyBankFrames < SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&
SFEngine.playerBankPosX < 3){
SFEngine.playerBankPosX += SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f,0.0f, 0.0f);
goodGuyBankFrames += 1;
}else if (goodGuyBankFrames >= SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&SFEngine.playerBankPosX < 3){
SFEngine.playerBankPosX += SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f,0.0f, 0.0f);
}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

}

}**

}`

你的movePlayer1()方法现在完成了。当正确的动作被应用时,你的可玩角色将成功地向左和向右移动。您现在所要做的就是从游戏循环中调用movePlayer1()方法,并创建一个允许玩家实际移动角色的进程。

`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) {
try {
Thread.sleep(SFEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
} 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);

movePlayer1(gl);

//All other game drawing will be called here

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

}

}`

保存并关闭SFGameRenderer

在本章的下一节,你将学习如何在 Android 设备的屏幕上收听TouchEvent。然后,您将使用那个TouchEvent来设置玩家动作,从而将屏幕上的角色向左或向右移动。

使用触摸事件移动您的角色

您已经创建了必要的方法和调用来在屏幕上移动您的可玩角色。然而,到目前为止,玩家没有办法与游戏互动,并告诉游戏循环进行移动角色的调用。

在本节中,您将编写一个简单的触摸监听器,它将检测玩家是否触摸了屏幕的左侧或右侧。玩家将通过触摸屏幕的那一侧来向左或向右移动角色。监听器将进入托管您的游戏循环的活动,在本例中为SFGame.java

打开SFGame.java,为onTouchEvent()方法添加一个覆盖。

`package com.proandroidgames;

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

public class SFGame extends Activity {

**@Override
public boolean onTouchEvent(MotionEvent event) {

return false;
}**
}`

onTouchEvent()是一个标准的 Android 事件监听器,它将监听活动中发生的任何触摸事件。因为您的游戏是从SFGame活动运行的,所以这是您必须监听的触摸事件的活动。

提示:不要把游戏的活跃度和游戏的循环混淆。游戏循环就是SFGameRenderer;发射它的ActivitySFGame

只有当设备的屏幕被触摸、滑动、拖动或释放时,监听器才会触发。对于这个游戏,你只关心触摸或释放,以及它发生在屏幕的哪一侧。为了帮助你确定这一点,Android 向onTouchEvent()监听器发送一个MotionEvent视图;它将提供您所需要的一切,以确定哪种触摸事件触发了监听器,以及触摸发生在屏幕上的什么位置。

解析运动事件

onTouchEvent()监听器中,您首先关心的是获取触摸的 x 和 y 坐标,这样您就可以确定触摸是发生在设备屏幕的左侧还是右侧。传递给onTouchEvent()监听器的MotionEventgetX()getY()方法,可以用来确定触摸事件的 x 和 y 坐标。

注意:你在onTouchEvent()监听器中处理的 x 和 y 坐标是屏幕坐标,不是 OpenGL 坐标。

`package com.proandroidgames;

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

public class SFGame extends Activity {

**@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();

return false;
}**

}`

接下来,您将在屏幕上设置一个可玩区域。也就是说,您不希望对屏幕上任何地方的触摸事件作出反应,所以您将在屏幕底部设置一个您将作出反应的区域。屏幕上的可触摸区域很低,因此玩家可以在手持设备时用拇指触摸。

由于可玩的角色大约占据了设备屏幕的下四分之一,你将把那个区域设置为你将做出反应的区域。

`package com.proandroidgames;

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

public class SFGame extends Activity {

**@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
int height = SFEngine.display.getHeight() / 4;
int playableArea = SFEngine.display.getHeight() - height;

return false;
}**

}`

现在,您已经有了触摸事件的位置和想要对触摸事件做出反应的区域。使用一个简单的if语句来决定你是否应该对此事件做出反应。

`package com.proandroidgames;

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

public class SFGame extends Activity {

**@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
int height = SFEngine.display.getHeight() / 4;
int playableArea = SFEngine.display.getHeight() - height;
if (y > playableArea){

}
return false;
}**

}`

MotionEvent有一个非常有用的方法叫做getAction(),它返回你在屏幕上检测到的动作类型。在这个游戏中,你关心的是ACTION_UPACTION_DOWN的动作。这些动作表示玩家的手指最初接触屏幕(ACTION_DOWN)然后又离开屏幕(ACTION_UP)的时刻。

陷印动作 _ 向上和动作 _ 向下

建立一个简单的switch语句来执行ACTION_UPACTION_DOWN动作。一定要省去default案例,因为你只想对这两个具体案例做出反应。

`package com.proandroidgames;

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

public class SFGame extends Activity {

**@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
int height = SFEngine.display.getHeight() / 4;
int playableArea = SFEngine.display.getHeight() - height;
if (y > playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:

break;
case MotionEvent.ACTION_UP:

break;
}
}
return false;
}**

}`

在本章的前面,您编写了在屏幕上移动角色的代码。这段代码对您创建的三个动作常量做出反应:PLAYER_BANK_LEFT_1PLAYER_BANK_RIGHT_1PLAYER_RELEASE。这些动作将在onTechEvent()中的适当情况下设置。

让我们从PLAYER_RELEASE开始。这种情况将在玩家将手指抬离屏幕时设置,从而触发一个ACTION_UP事件。

`package com.proandroidgames;

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

public class SFGame extends Activity {

**@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
int height = SFEngine.display.getHeight() / 4;
int playableArea = SFEngine.display.getHeight() - height;
if (y > playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:

break;
case MotionEvent.ACTION_UP:
SFEngine.playerFlightAction =
SFEngine.PLAYER_RELEASE;
break;
}
}
return false;
}**

}`

最后,设置PLAYER_BANK_LEFT_1PLAYER_BANK_RIGHT_1动作。为此,您仍然需要确定玩家是触摸了屏幕的左侧还是右侧。这可以通过比较MotionEventgetX()值和 x 轴的中点很容易地确定。如果getX()小于中点,则动作在左边;如果getX()值大于中点,则事件发生在右侧。

`package com.proandroidgames;

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

public class SFGame extends Activity {

@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
int height = SFEngine.display.getHeight() / 4;
int playableArea = SFEngine.display.getHeight() - height;
if (y > playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x < SFEngine.display.getWidth() / 2){
SFEngine.playerFlightAction =
SFEngine.PLAYER_BANK_LEFT_1;
}else{
SFEngine.playerFlightAction = SFEngine.PLAYER_BANK_RIGHT_1;
}
break;
case MotionEvent.ACTION_UP:

SFEngine.playerFlightAction = SFEngine.PLAYER_RELEASE;
break;
}
}
return false;
}

}`

保存并关闭您的SFGame.java类。您已经完成了这个游戏的用户界面(UI)。玩家现在可以触摸屏幕的右侧或左侧来向左或向右移动角色。

在本章的最后一节,我们将重温游戏线程和每秒帧数的计算。

调整 FPS 延迟

在前一章中,您创建了一个延迟来减慢游戏循环,并强制它以每秒 60 帧(FPS)的速度运行。这个速度是开发者的游戏运行起来最希望的速度。然而,你可能已经开始意识到这个速度并不总是可以达到的。

你在游戏循环中执行的功能越多,循环完成的时间就越长,游戏运行的速度就越慢。这意味着你创造的延迟需要调整或完全关闭,这取决于游戏运行的速度。

只是为了比较,在当前状态下运行游戏,有两个背景和一个可玩的角色,我在 Windows 模拟器上实现了大约每秒 10 帧,在 Droid X 上大约每秒 35 帧,在摩托罗拉 Xoom 上大约每秒 43 帧。

其中一个问题是,你在不分青红皂白地延迟线程。您需要调整游戏循环的线程延迟,以考虑运行循环所需的时间。以下代码将确定循环运行所需的时间,然后从延迟中减去该时间。如果循环运行的时间比延迟的时间长,则延迟被关闭。

`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 SFGoodGuy player1 = new SFGoodGuy();

private int goodGuyBankFrames = 0;
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

private float bgScroll1;
private float bgScroll2;

@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime <SFEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(SFEngine.GAME_THREAD_FPS_SLEEP -
loopRunTime);
}
} 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);

movePlayer1(gl);

//All other game drawing will be called here

gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA);
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));

}
…`

编译并运行你的游戏。尝试在屏幕上移动角色,观察动画的变化。

总结

在这一章中,你在星际战士游戏中又前进了一大步。现在,您可以将以下技能添加到您的技能列表中:

  • 创建一个可玩的角色。
  • 使用精灵表中的纹理制作角色动画。
  • 检测设备屏幕上的触摸输入。
  • 基于玩家的触摸事件移动角色并制作动画。
  • 调整了 FPS 速率,让游戏尽可能快地运行。

六、添加敌人

作为一名 Android 游戏开发者,你的技能范围越来越广。仅在前一章中,您就添加了您的第一个可玩角色,处理了精灵动画,并创建了一个基本的侦听器来允许玩家控制角色;对于一个基本的 2-D 射击游戏,你的游戏真的正在成形。

在这一章中,你将创建一个类来帮助你管理你的纹理。你还将创建一个敌人职业,用于在星际战士中创建三种不同类型的敌人。在下一章,你将为这些敌人创建一个基本的人工智能系统。

中局看家

记住,这本书的目的是帮助你从头到尾完成一个游戏的创作过程。游戏创作并不总是一个线性的过程。有时候,你需要回头重新评估你所做的事情,以优化你的游戏工作方式。

前两章主要教你如何加载和处理精灵和精灵表。但是,使用当前的代码,您将为每个角色加载一个单独的 sprite 表。这是学习如何使用 sprite sheet 的最简单的方法,但绝不是使用sprite sheet 的最好方法。事实上,为每个角色创建一个单独的 sprite 工作表几乎违背了 sprite 工作表的目的——也就是说,你应该将所有角色的所有图像加载到一个 sprite 工作表中。

提示:当然,如果你有太多的精灵而不适合一张图片,你仍然可以使用多个精灵表。但这应该不是这个游戏中有限的角色数量的问题。

通过将游戏中所有角色的所有图像加载到一个 sprite 表中,您将大大减少游戏消耗的内存量以及 OpenGL 渲染游戏所需的处理量。

也就是说,现在是时候对你的游戏代码进行一些小的整理了,让它使用一个通用的精灵表

创建纹理类

您将使用loadTexture()方法创建一个通用纹理类。loadTexture()方法将执行与SFGoodGuy()类中的loadTexture()方法相同的功能。不同之处在于,这个公共类将返回一个 int 数组,您可以将该数组传递给所有实例化的字符。

第一步是打开SFGoodGuy()类并移除loadTexture()方法(以及支持它的任何变量)。完成后,修改后的SFGoodGuy()类应该是这样的:

`package com.proandroidgames;

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

import javax.microedition.khronos.opengles.GL10;

public class SFGoodGuy {

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

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,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

public SFGoodGuy() {
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);
}

public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[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);
}

}`

注意:当您完成这些更改时,根据您使用的 IDE,您将开始从代码的其他区域得到一些错误。现在不用担心他们;您将在本章的后面处理这些错误。

接下来,让我们创建一个新的公共类来加载你的纹理到 OpenGL 并返回一个 int 数组。在主包中创建一个名为SFTextures()的新类。

`package com.proandroidgames;

public class SFTextures {

}`

现在,为接受一个GL10实例的SFTextures()创建一个构造函数。该实例将用于初始化纹理。您还需要一个纹理变量来初始化两个元素的 int 数组。

`package com.proandroidgames;

import javax.microedition.khronos.opengles.GL10;

public class SFTextures {

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

public SFTextures(GL10 gl){

}**

}`

您需要让 OpenGL 为您正在加载的纹理生成一些名称。以前,这是在使用glGenTextures()方法的SFGoodGuy()类的loadTexture()方法中完成的。但是,因为您计划多次调用这个通用的纹理类,所以每次调用 load 方法时,OpenGL 都会为纹理分配新的名称,这将使跟踪纹理变得很困难,如果不是不可能的话。

为了避免给同一个纹理分配多个名称,您将把glGenTextures()方法调用移动到SFTextures()构造函数:

`package com.proandroidgames;

import javax.microedition.khronos.opengles.GL10;

public class SFTextures {

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

gl.glGenTextures(1, textures, 0);

public SFTextures(GL10 gl){

}**

}`

您需要为SFTextures()创建一个loadTexture()方法。在SFGoodGuy()SFBackground()类中,loadTexture()方法是一个简单的方法,没有返回。为了让你更好地控制纹理的访问,特别是当你开始加载多个 sprite 的时候,创建SFTextures()loadTexture()方法来返回一个 int 数组。

`package com.proandroidgames;

import javax.microedition.khronos.opengles.GL10;

public class SFTextures {

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

gl.glGenTextures(1, textures, 0);

public SFTextures(GL10 gl){

}

public int[] loadTexture(GL10 gl,int texture, Context context,int textureNumber)
{

}**

}`

注意添加了textureNumber参数。虽然现在这个值是 1,但是在下一章中,当你开始使用这个类来加载多个 sprite 工作表时,它将被用来指示哪个工作表正在被加载。

除此之外,loadTexture()方法的核心看起来与其在SFGoodGuy()类中的对应部分完全相同。唯一的变化——除了对glGenTextures()的调用被移除——是textureNumber参数现在被用作glBindTextures()调用中指向的数组,并且loadTextures()现在在结束时返回纹理的 int 数组。

`package com.proandroidgames;

**import java.io.IOException;
import java.io.InputStream;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;**

public class SFTextures {

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

gl.glGenTextures(1, textures, 0);

public SFTextures(GL10 gl){

}

public int[] loadTexture(GL10 gl,int texture, Context context,int textureNumber)
{
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.glBindTexture(GL10.GL_TEXTURE_2D, textures[textureNumber - 1]);

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_CLAMP_TO_EDGE);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);

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

bitmap.recycle();

return textures;

}**

}`

你的普通纹理类已经完成,可以使用了。保存SFTextures()SFGoodGuy(),暂时关闭。同样,您现在应该会看到来自SFGameSFGameRenderer()类的错误。暂时忽略这些错误;当你阅读这一章时,你将会注意到它们。

在下一节中,你将创建一个类来装载你的敌舰,并让他们为与玩家的战斗做好准备。

创造敌人阶级

不管你玩过什么游戏,它们都有一个共同点:永远不会只有一个敌人。在一个游戏中只有一个敌人会导致一个非常快速和非常无聊的游戏。

星际战士中,你将为玩家创造 30 个敌人在屏幕上战斗。我们在第二章中勾勒出了星际战士所依据的故事。根据这个故事,提到了三种不同类型的敌人。在本章的这一节,你将创建这三种类型的敌人所基于的职业,以及 30 个实例化的敌人。

添加新的精灵工作表

您需要添加到项目中的第一件事是一个新的 sprite 表。在前一章中,你已经了解了 sprite sheets 对于 2d 游戏的重要性和用途。现在,您已经在代码中为所有角色精灵提供了一个通用的精灵表,您可以将它添加到您的项目中。图 6–1 展示了常见的精灵表。

images

图 6–1。 普通雪碧单

只需移除 drawable 文件夹中的good_guy sprite 工作表,然后添加这个。

注意:注意玩家的角色在这个精灵表上的位置和他们在上一个精灵表上的位置一样。因此,您将不必为玩家角色更改任何纹理定位。

接下来,您需要编辑SFEngine类来添加您将在本章中使用的常量和变量。这次有不少。你将需要 17 个常数来帮助你独自控制敌人 AI。其中一些您可能要到下一章才会用到,但是现在添加它们是个好主意:

**public static int CHARACTER_SHEET = R.drawable.character_sprite; public static int TOTAL_INTERCEPTORS = 10; public static int TOTAL_SCOUTS = 15; public static int TOTAL_WARSHIPS = 5; public static float INTERCEPTOR_SPEED = SCROLL_BACKGROUND_1 * 4f; public static float SCOUT_SPEED = SCROLL_BACKGROUND_1 * 6f; public static float WARSHIP_SPEED = SCROLL_BACKGROUND_2 * 4f; public static final int TYPE_INTERCEPTOR = 1; public static final int TYPE_SCOUT = 2; public static final int TYPE_WARSHIP = 3;** **public static final int ATTACK_RANDOM = 0; public static final int ATTACK_RIGHT = 1; public static final int ATTACK_LEFT = 2; public static final float BEZIER_X_1 = 0f; public static final float BEZIER_X_2 = 1f; public static final float BEZIER_X_3 = 2.5f; public static final float BEZIER_X_4 = 3f; public static final float BEZIER_Y_1 = 0f; public static final float BEZIER_Y_2 = 2.4f; public static final float BEZIER_Y_3 = 1.5f; public static final float BEZIER_Y_4 = 2.6f;**

因为你添加到屏幕上的敌人将作为一个职业开始,就像背景和可玩的角色一样,所以给你的主包添加一个名为SFEnemy()的新职业。这个职业将被用来把你的敌人带入游戏。

提示:尽管你总共会有 30 个三种不同类型的敌人,但它们都是从同一个SFEnemy()类中实例化出来的。

创建 SFEnemy 类

在本节中,您将创建一个类,用于在星际战士游戏中产生所有三种类型的敌人。向您的项目添加一个名为SFEnemy()的新类:

`package com.proandroidgames;

public class SFEnemy {

}`

当你开始创建 AI 逻辑时,你的敌人需要一些属性来帮助你。您将需要一些属性来设置或获取敌人当前的 x 和 y 位置、t 因子(用于以曲线飞行敌人)以及到达目标的 x 和 y 增量。

`package com.proandroidgames;

public class SFEnemy {
public float posY = 0f; //the x position of the enemy
public float posX = 0f; //the y position of the enemy
public float posT = 0f; //the t used in calculating a Bezier curve
public float incrementXToTarget = 0f; //the x increment to reach a potential
target
public float incrementYToTarget = 0f; //the y increment to reach a potential
target

}`

你还需要一些属性,让你设置或获取敌人攻击的方向,敌人是否被消灭,以及这个实例代表什么类型的敌人。

`package com.proandroidgames;

public class SFEnemy {
**public float posY = 0f; //the x position of the enemy
public float posX = 0f; //the y position of the enemy
public float posT = 0f; //the t used in calculating a Bezier curve
public float posXToTarget = 0f; //the x increment to reach a potential target
public float posYToTarget = 0f; //the y increment to reach a potential target

public int attackDirection = 0; //the attack direction of the ship
public boolean isDestroyed = false; //has this ship been destroyed?
public int enemyType = 0; //what type of enemy is this?**

}`

接下来你的敌人职业需要的三个属性是一个指示器,让你知道它是否锁定了一个目标(这对你的 AI 逻辑至关重要)和两个坐标,代表锁定目标的位置。

`package com.proandroidgames;

public class SFEnemy {
public float posY = 0f; //the x position of the enemy
public float posX = 0f; //the y position of the enemy
public float posT = 0f; //the t used in calculating a Bezier curve
public float posXToTarget = 0f; //the x increment to reach a potential target
public float posYToTarget = 0f; //the y increment to reach a potential target
public int attackDirection = 0; //the attack direction of the ship
public boolean isDestroyed = false; //has this ship been destroyed?
public int enemyType = 0; //what type of enemy is this
public boolean isLockedOn = false; //had the enemy locked on to a target?
public float lockOnPosX = 0f; //x position of the target
public float lockOnPosY = 0f; //y position of the target

}`

接下来,给你的SFEnemy()类一个接受两个 int 参数的构造函数。第一个参数将用来表示应该创造的敌人类型:TYPE_INTERCEPTORTYPE_SCOUTTYPE_WARSHIP。第二个参数将用于指示特定敌人将从屏幕上的哪个方向进攻:ATTACK_RANDOMATTACK_RIGHTATTACK_LEFT

`package com.proandroidgames;

public class SFEnemy {

**…

public SFEnemy(int type, int direction) {

}**

}`

SFEnemy()的构造函数中,需要根据传入构造函数的 int 类型来设置敌方类型。你也将设定方向。看到这些参数将让你在游戏循环中根据敌人的类型和运动方向做出决定。

`package com.proandroidgames;

public class SFEnemy {

**…
public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;

}**

}`

星际战士(在第二章中)的故事描述了三种不同敌人的攻击特点。侦察兵以快速但可预测的模式飞行,截击机锁定并直接飞向玩家的角色,战舰以随机模式机动。每艘船都需要从屏幕上的一个特定点出发。

通常在滚动射击游戏中,敌人从屏幕外 y 轴上的一点开始,然后向下滚动到玩家。因此,在构造函数中你要做的下一件事是为敌人建立一个 y 轴起点。

Android 的随机数生成器是选择起点的好方法。Android 随机数生成器将生成一个介于 0 和 1 之间的数字。然而,你的敌人的 y 轴是从 0 到 4。将随机数生成器生成的数字乘以 4,结果将是屏幕上一个有效的 y 轴位置。在有效的 y 位置上加 4,然后将起点推出屏幕。

`package com.proandroidgames;

public class SFEnemy {

**…

private Random randomPos = new Random();
public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;

}**

}`

它负责 y 轴。现在,你需要建立一个 x 轴位置。看看您在SFEngine中创建的常量。三个代表 x 轴上敌人可能攻击的位置:ATTACK_LEFTATTACK_RANDOMATTACK_RIGHT。左侧的 x 轴值为 0。右边的 x 轴值是 3(从 4 中减去 1 个单位以说明精灵的大小)。

可以使用一个case语句,根据传递给构造函数的攻击方向来分配 x 轴的起点。

`package com.proandroidgames;

public class SFEnemy {

**…

public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;
switch(attackDirection){
case SFEngine.ATTACK_LEFT:
posX = 0;
break;
case SFEngine.ATTACK_RANDOM:
posX = randomPos.nextFloat() * 3;
break;
case SFEngine.ATTACK_RIGHT:
posX = 3;
break;
}

}**

}`

您需要建立的最后一个变量是posT。不要担心posT现在做什么;在本章的后面你会发现。将posT设置为SFEngine.SCOUT_SPEED的值。

`package com.proandroidgames;

public class SFEnemy {


public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;
switch(attackDirection){
case SFEngine.ATTACK_LEFT:
posX = 0;
break;
case SFEngine.ATTACK_RANDOM:
posX = randomPos.nextFloat() * 3;
break;
case SFEngine.ATTACK_RIGHT:
posX = 3;

**break;
}
posT = SFEngine.SCOUT_SPEED;

}**

}`

你可以创造的两种敌人类型,截击机和战舰,将会以对角线的直线行进。生成这些攻击路径的代码将在游戏循环中处理,因为沿直线引导物体相对容易。另一方面,侦察型敌人会以一种被称为贝塞尔曲线的模式移动。在下一节中,您将创建帮助敌人以曲线飞行的方法。

贝塞尔曲线

虽然你可能不知道它的名字,但你很可能以前见过贝塞尔曲线。图 6–2 展示了贝塞尔曲线的样子。

images

图 6–2。 一条二次贝塞尔曲线

为了让侦察兵以二次贝塞尔曲线从屏幕的顶部到底部飞行,您需要两个方法:一个是获取贝塞尔曲线上的下一个 x 轴值,另一个是获取贝塞尔曲线上的下一个 y 轴值。每次你调用这些方法,你会得到 x 和 y 轴上的下一个点,特定的敌人需要移动到这个点。

幸运的是,在贝塞尔曲线上绘制点相当简单。要构造一条二次贝塞尔曲线,你需要四个笛卡尔点:一个起点,一个终点,以及曲线环绕的两个点。这些点在星际战士游戏中永远不会改变。每个侦察兵都会沿着同一条曲线,从左边或右边。因此,在SFEngine中创建了八个常数来表示每个轴上的四个二次贝塞尔曲线点。

绘制点的关键值是 t 因子。t 因子告诉公式您在曲线上的位置,从而允许公式计算该单个位置的 x 或 y 坐标。因为你的船将以一个预先定义的速度移动,你将使用这个值作为 t 的种子值。

提示:如果你不理解本节公式背后的数学,有许多关于贝塞尔曲线的伟大资源,包括下面的维基百科页面:【http://en.wikipedia.org/wiki/Bézier_curve】??。

在您的SFEnemy()类中创建两个方法:一个获取下一个 x 轴值,另一个获取下一个 y 轴值。

`package com.proandroidgames;

public class SFEnemy {

**…
public SFEnemy(int type, int direction) {


}

public float getNextScoutX(){

}
public float getNextScoutY(){

}**

}`

下面是在 y 轴上的二次贝塞尔曲线上寻找一个点的公式(用x代替y来寻找 x 轴上的值):

(y<sub>1</sub>*(t<sup>3</sup>)) + (y<sub>2</sub> * 3 * (t<sup>2</sup>) * (1-t)) + (y<sub>3</sub> * 3 * t * (1-t)<sup>2</sup>) + (y<sub>4</sub>* (1-t)<sup>3</sup>)

在您的getNextScoutY()方法中使用这个公式和正确的变量。

`package com.proandroidgames;

public class SFEnemy {

**…

public SFEnemy(int type, int direction) {


}

public float getNextScoutX(){

}

public float getNextScoutY(){**
**return (float)((SFEngine.BEZIER_Y_1(posTposT*posT)) +
(SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));

}**

}`

对 x 轴使用相同的公式,有一个小的变化。如果敌人从屏幕的左边攻击,而不是右边,你需要颠倒公式。

`package com.proandroidgames;

public class SFEnemy {

**…
public SFEnemy(int type, int direction) {


}

public float getNextScoutX(){
if (attackDirection == SFEngine.ATTACK_LEFT){
return (float)((SFEngine.BEZIER_X_4(posTposTposT)) +
(SFEngine.BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_2 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT))));
}else{
return (float)((SFEngine.BEZIER_X_1
(posTposTposT)) +
(SFEngine.BEZIER_X_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_4 * ((1-posT) * (1-posT) * (1-posT))));
}

}

public float getNextScoutY(){
return (float)((SFEngine.BEZIER_Y_1(posTposT*posT)) +
(SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}**

}`

注意,在计算 x 轴的右侧时,值为 x 1 、x 2 、x 3 和 x4—从左侧开始,点的使用顺序相反:x 4 、x 3 、x 2 和 x 1

考虑到使用新的通用 sprite 表所做的更改,SFEnemy类的其余部分看起来应该和SFGoodGuy类一样。

`package com.proandroidgames;

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

import javax.microedition.khronos.opengles.GL10;

public class SFEnemy {

public float posY = 0f;
public float posX = 0f;
public float posT = 0f;
public float incrementXToTarget = 0f;
public float incrementYToTarget = 0f;
public int attackDirection = 0;
public boolean isDestroyed = false;

public int enemyType = 0;

public boolean isLockedOn = false;
public float lockOnPosX = 0f;
public float lockOnPosY = 0f;

private Random randomPos = new Random();

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

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,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};**

public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;
switch(attackDirection){
case SFEngine.ATTACK_LEFT:
posX = 0;
break;
case SFEngine.ATTACK_RANDOM:
posX = randomPos.nextFloat() * 3;
break;
case SFEngine.ATTACK_RIGHT:
posX = 3;
break;
}
posT = SFEngine.SCOUT_SPEED;

**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);**
}

public float getNextScoutX(){
if (attackDirection == SFEngine.ATTACK_LEFT){
return (float)((SFEngine.BEZIER_X_4(posTposTposT)) +
(SFEngine.BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_2 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT))));
}else{
return (float)((SFEngine.BEZIER_X_1
(posTposTposT)) +
(SFEngine.BEZIER_X_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_4 * ((1-posT) * (1-posT) * (1-posT))));
}

}

public float getNextScoutY(){
return (float)((SFEngine.BEZIER_Y_1(posTposT*posT)) +
(SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}

**public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[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);
}**

}`

你现在有了一个工人阶级,你可以在游戏中培养所有的敌人。保存SFEnemy()类。在下一章,你将开始为你的敌人创造人工智能。

总结

在这一章中,你的技能又向前迈进了一大步。为你的游戏创造敌人已经做了很多工作,还有更多工作要做。以下列表描述了您在本章中学到的内容,您将在第七章中对您所学的内容进行扩展:

  • 创建一个通用的纹理类来保存一个大的 sprite 表。
  • 创建一个数组来容纳游戏中的所有敌人,以便于处理。
  • 创建SFEnemy()类来繁殖三个不同的敌人。
  • 创建一个用贝塞尔曲线移动敌人的方法。

七、添加基础敌方人工智能

敌人的人工智能(AI)将定义敌人如何攻击玩家,以及玩家赢得游戏的难易程度。通过拦截玩家对touchListener的监听调用,可以很容易地创建一个预测玩家一举一动的人工智能。然而,这不会给玩家带来有趣的体验,你的游戏也不会很有成就感。你在上一章创造的敌人需要某种攻击计划来吸引玩家并创造出令人满意的游戏体验。

在这一章中,你将为三种不同的敌人类型添加三种不同的人工智能,这三种类型在第二章第一节中讨论过,在第六章第三节中创建:截击机、侦察兵和战舰。从表面上看,鉴于你在上一章学到的东西,这个任务似乎很容易,但事实是创造敌人比创造可玩角色更难。为什么?可玩角色不用思考;这就是玩家所做的。另一方面,敌人至少需要一个基本的人工智能来引导他们完成游戏。

让敌人为人工智能做好准备

在你能处理 AI 之前,你需要首先初始化敌人和他们的纹理。所以,要开始,打开并编辑游戏循环,SFGameRenderer()。你需要添加一个数组来容纳游戏中的所有敌人。要确定敌人的数量,将TOTAL_INTECEPTORSTOTAL_SCOUTSTOTAL_WARSHIPS的值相加(减 1 表示零基数组)。

`package com.proandroidgames;

import java.util.Random;

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 SFGoodGuy player1 = new SFGoodGuy();

private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];

private int goodGuyBankFrames = 0;
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

private float bgScroll1;
private float bgScroll2;

}`

接下来,创建一个新的SFTextures类实例和一个新的 int 数组来保存公共的 sprite 表。现在,spriteSheets[]数组将包含一个元素。在下一章,你将改变这个数组,使它能容纳更多的内容。

提示:你可以更进一步,修改spriteSheets[]数组和SFBackground()来保存背景和精灵的纹理。这样做很容易,并且会让你获得更多的优化。

`public class SFGameRenderer implements Renderer{

private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];
private SFTextures textureLoader;
private int[] spriteSheets = new int[1];

private int goodGuyBankFrames = 0;
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

private float bgScroll1;
private float bgScroll2;

}`

现在,你有一个数组来容纳你的敌人,但是没有敌人可以放进去。你需要三个私有方法来填充你的数组:截击机、侦察兵和战舰各一个。

`;

public class SFGameRenderer implements Renderer{

private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];
private SFTextures textureLoader;
private int[] spriteSheets = new int[2];

**private void initializeInterceptors(){

}

private void initializeScouts(){

}

private void initializeWarships(){

}**

}`

创造每个敌人的逻辑

使用一个简单的for循环实例化一个相应类型的新敌人,并将其添加到数组中。例如,在initializeInterceptors()方法中,创建一个for循环,该循环计数到TOTAL_INTERCEPTORS的值。这个循环将实例化一个新的TYPE_INTERCEPTOR类型的敌人,并将其添加到数组中。

`public class SFGameRenderer implements Renderer{


private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];
private SFTextures textureLoader;
private int[] spriteSheets = new int[2];

;

**private void initializeInterceptors(){

for (int x = 0; x<SFEngine.TOTAL_INTERCEPTORS -1 ; x++){**
**SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_INTERCEPTOR,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;
}

}

private void initializeScouts(){

}

private void initializeWarships(){

}**

}`

在战舰上使用同样的循环逻辑。

`public class SFGameRenderer implements Renderer{

private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];
private SFTextures textureLoader;
private int[] spriteSheets = new int[2];

**private void initializeInterceptors(){

for (int x = 0; x<SFEngine.TOTAL_INTERCEPTORS -1 ; x++){
SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_INTERCEPTOR,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;
}

}

private void initializeScouts(){

}

private void initializeWarships(){

for (int x = SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS -1;
x<SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS -1;
x++){
SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_WARSHIP,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;**
**}

}**

}`

截击机和战舰都从任意方向攻击。然而,侦察兵会从右边或者左边进攻。因此,在实例化 scouts 的循环中,将负载分成两半,从右边实例化一半,从左边实例化一半。

`package com.proandroidgames;

public class SFGameRenderer implements Renderer{


private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];
private SFTextures textureLoader;
private int[] spriteSheets = new int[2];

**private void initializeInterceptors(){

for (int x = 0; x<SFEngine.TOTAL_INTERCEPTORS -1 ; x++){
SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_INTERCEPTOR,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;
}

}

private void initializeScouts(){

for (int x = SFEngine.TOTAL_INTERCEPTORS -1;
x<SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS -1; x++){
SFEnemy interceptor;
if (x>=(SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS) / 2
){
interceptor = new SFEnemy(SFEngine.TYPE_SCOUT,
SFEngine.ATTACK_RIGHT);
}else{
interceptor = new SFEnemy(SFEngine.TYPE_SCOUT,
SFEngine.ATTACK_LEFT);
}
enemies[x] = interceptor;
}

}**
**private void initializeWarships(){

for (int x = SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS -1;
x<SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS -1;
x++){
SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_WARSHIP,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;
}

}**

}`

初始化敌人

你有你的方法来初始化你的敌人。所有其他游戏循环初始化都发生在SFGameRendereronSurfaceCreated()方法中。因此,按理说,您刚刚创建的新初始化方法也将从这里调用。

`public class SFGameRenderer implements Renderer{

private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];
private SFTextures textureLoader;
private int[] spriteSheets = new int[2];

**private void initializeInterceptors(){

for (int x = 0; x<SFEngine.TOTAL_INTERCEPTORS -1 ; x++){
SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_INTERCEPTOR,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;
}

}

private void initializeScouts(){

for (int x = SFEngine.TOTAL_INTERCEPTORS -1;
x<SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS -1; x++){
SFEnemy interceptor;
if (x>=(SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS) / 2
){**
**interceptor = new SFEnemy(SFEngine.TYPE_SCOUT,
SFEngine.ATTACK_RIGHT);
}else{
interceptor = new SFEnemy(SFEngine.TYPE_SCOUT,
SFEngine.ATTACK_LEFT);
}
enemies[x] = interceptor;
}

}

private void initializeWarships(){

for (int x = SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS -1;
x<SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS -1;
x++){
SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_WARSHIP,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;
}

}**

**@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initializeInterceptors();
initializeScouts();
initializeWarships();

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

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

}`

加载精灵表

随着enemies[]数组的初始化,您可以关注 sprite 表。回想一下,您创建了一个通用纹理方法,该方法将返回在 int 数组中指定的所有纹理的 OpenGL 指定名称。这个 OpenGL 名称的 int 数组将保存在spriteSheets[]数组中。

实例化你的textureLoader()方法。在textureLoader()被实例化后,调用loadTexture()方法,将它传递给CHARACTER_SHEET,并将返回值赋给spriteSheets[]数组。

`public class SFGameRenderer implements Renderer{

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initializeInterceptors();
initializeScouts();
initializeWarships();
textureLoader = new SFTextures(gl);
spriteSheets = textureLoader.loadTexture(gl, SFEngine.CHARACTER_SHEET,
SFEngine.context, 1);

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

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

}`

敌人及其纹理的初始化已经完成。3+39.′′′

+是时候转向人工智能逻辑了。让我们从拦截器开始。

回顾人工智能

截击机 AI 的描述听起来很复杂,但实际上,它是三个敌人中最简单的。拦截器将开始沿着 y 轴直线运动。在 y 轴的某个点上,它会锁定玩家的飞船,并直接飞向这些坐标,试图撞击玩家的飞船。

实现这一点的方法是从 y 轴位置减去一个预定义的量INTERCEPTOR_SPEED,慢慢地将拦截器向屏幕下方移动。因为截击机可能在屏幕可见边缘上方的任意点,所以你必须等到它可见后才能锁定敌人。一旦拦截器到达这一点,你就可以把玩家飞船的 x 和 y 坐标传给它。最后,您将使用一个简单的斜率公式将拦截器移向这些坐标。

创建 moveEnemy()方法

添加一些敌人 AI 的第一步是创建一个moveEnemy()方法,它将为你的敌人保存所有的 AI 逻辑。就像movePlayer1()方法一样,moveEnemy()方法将被游戏循环调用来更新敌舰的位置。

`package com.proandroidgames;

import java.util.Random;

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

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

}**

}`

方法会在一次调用中更新你所有的敌人。在一次召唤中解决所有的敌人是更新大量不可玩角色的最好方法。这样做可以节省宝贵的处理器周期。

创建敌人[]数组循环

您想要在moveEnemy()方法中创建一个for循环,它将能够遍历enemies[]数组中的每个活着的敌人。通过将你进程的核心限制在那些没有被消灭的敌人身上,你在游戏中注意了两件事。首先,你要确保你没有画出不应该出现在屏幕上的敌人。第二,你要确保你不会在没有任何移动要处理的敌人身上浪费周期。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

}
}**
}

}`

注意:不要太担心到底是什么树立了敌人的isDestroyed旗帜。我们将在下一章关于碰撞检测的章节中解决这个问题。你也会把这个逻辑应用到玩家的角色上。

现在,在你的更新方法中有一个循环,它为游戏中的每个敌人运行一次,并跳过那些已经被消灭的敌人。

使用人工智能逻辑移动每个敌人

你必须为三种不同的敌人运行这个循环,每一种都有自己的 AI。enemy类有一个enemyType属性,是在实例化敌人时设置的。因此,你需要在enemyType上设置一个开关,这样你就会知道哪个 AI 会为正在更新的敌人运行。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

break;
case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

在下一部分中,你将创建拦截者 AI,这个逻辑将拦截者的敌人推向玩家。

创造拦截者人工智能

让我们现在创建拦截者人工智能。在这个 AI 中,你要测试的第一件事是拦截器是否已经离开了屏幕。回想一下,所有的敌人都将从屏幕的顶部移动到底部。除非它们被玩家摧毁,否则它们最终会到达屏幕底部。

当你设计这样的游戏时,你有一个选择。当一个敌人到达屏幕的底部时,你可以杀死它,让它退出循环,或者你可以重置它再次运行。对于星际战士,你要把敌人重置到屏幕上方的随机位置,让它继续攻击玩家,直到被消灭。

测试敌人的 y 轴位置是否小于 0——低于屏幕底部边缘——如果是,将其 x 和 y 位置重置为随机位置。此外,你会想要清除任何锁定位置和锁定标志,以防敌人先前锁定玩家。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }

break;
case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;**
**}

}
}

}**

}`

在下一节中,您将向逻辑中添加 OpenGL 代码。

调整顶点

人工智能的下一步是一些标准的 OpenGL 工作。您需要加载模型矩阵模式并调整顶点。这段代码对你来说应该很熟悉,因为它已经在上一章中介绍过了。简而言之,你调整了顶点的大小,使敌人的飞船和玩家的大小差不多,而不是整个屏幕的大小。

`package com.proandroidgames;

import java.util.Random;

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

import android.opengl.GLSurfaceView.Renderer;

public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);**
    **gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);

break;
case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

此时,拦截器的 AI 将被分成两个不同的子句。第一个描述了拦截器锁定玩家位置之前发生的事情,第二个描述了拦截器锁定玩家位置之后发生的事情。

锁定玩家的位置

在拦截器锁定玩家的位置之前,它只会沿着屏幕直线向下移动。选取 y 轴上的任意位置;这将是拦截器锁定玩家位置的点。对于星际战斗机来说,拦截器锁定到玩家位置时的 y 轴位置是 3,意味着拦截器会从 y 轴上的任意位置开始,沿直线向下移动,直到到达 3。

`;

public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:**
**if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);
    if (enemies[x].posY >= 3){

}else{

}

break;
case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

在拦截器到达锁定位置之前,它将沿着屏幕直线移动。这是通过从拦截器的当前 y 轴位置减去INTERCEPTOR_SPEED值来实现的。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){**
**case SFEngine.TYPE_INTERCEPTOR:

if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);
    if (enemies[x].posY >= 3){
    enemies[x].posY -=
    SFEngine.INTERCEPTOR_SPEED;
    }else{

}

break;
case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

现在,你可以编程拦截器的人工智能逻辑的第二部分。

实现斜率公式

首先,将拦截器设置为锁定,并获取玩家的当前位置。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){**
    **if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);
    if (enemies[x].posY >= 3){
    enemies[x].posY -=
    SFEngine.INTERCEPTOR_SPEED;
    }else{
    if (!enemies[x].isLockedOn){
    enemies[x].lockOnPosX =
    SFEngine.playerBankPosX;
    enemies[x].isLockedOn = true;

}

}

break;
case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

接下来,您将使用一个简单的斜率公式来确定拦截器到达玩家所需移动的增量。斜率可通过以下公式确定:

(x<sub>1</sub> - x<sub>2</sub>) / (y<sub>1</sub> - y<sub>2</sub>)

让事情变得更有趣一点,您希望拦截器在锁定目标后加速。因此,将斜率中的 y 2 替换为INTERCEPTOR_SPEED。在使用这个公式之前,你需要对它做一个修改。

这个公式可以让你一次就把全部位置直接给玩家。然而,你想以稳定的增量向玩家移动。所以你需要用 y 1 除以 y 2 而不是减去 y 2 。这将为您提供一个增量值,您可以不断地将它添加到自身中,以推动拦截器前进。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);
    if (enemies[x].posY >= 3){
    enemies[x].posY -=
    SFEngine.INTERCEPTOR_SPEED;
    }else{
    if (!enemies[x].isLockedOn){
    enemies[x].lockOnPosX =
    SFEngine.playerBankPosX;
    enemies[x].isLockedOn = true;
    enemies[x].incrementXToTarget =
    (float) ((enemies[x].lockOnPosX - enemies[x].posX )/ (enemies[x].posY /
    (SFEngine.INTERCEPTOR_SPEED * 4)));
    }

}

break;**
**case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

通过设置拦截器的 x 和 y 位置来完成逻辑。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);
    if (enemies[x].posY >= 3){
    enemies[x].posY
    -= SFEngine.INTERCEPTOR_SPEED;
    }else{
    if (!enemies[x].isLockedOn){
    enemies[x].lockOnPosX =
    SFEngine.playerBankPosX;
    enemies[x].isLockedOn = true;**
    **enemies[x].incrementXToTarget =
    (float) ((enemies[x].lockOnPosX - enemies[x].posX )/ (enemies[x].posY /
    (SFEngine.INTERCEPTOR_SPEED * 4)));
    }
    enemies[x].posY -=
    (SFEngine.INTERCEPTOR_SPEED * 4);
    enemies[x].posX +=
    enemies[x].incrementXToTarget;

}

break;
case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

最后,您可以用您对拦截器所做的位置更改来更新 OpenGL。您需要根据拦截器新的 x 和 y 轴位置来移动顶点。然后,您需要将纹理矩阵推出堆栈,并将纹理设置为公共 sprite 表上的拦截器 sprite。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;**
        **enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);
    if (enemies[x].posY >= 3){
    enemies[x].posY -=
    SFEngine.INTERCEPTOR_SPEED;
    }else{
    if (!enemies[x].isLockedOn){
    enemies[x].lockOnPosX =
    SFEngine.playerBankPosX;
    enemies[x].isLockedOn = true;
    enemies[x].incrementXToTarget =
    (float) ((enemies[x].lockOnPosX - enemies[x].posX )/ (enemies[x].posY /
    (SFEngine.INTERCEPTOR_SPEED * 4)));
    }
    enemies[x].posY -=
    (SFEngine.INTERCEPTOR_SPEED * 4);
    enemies[x].posX +=
    enemies[x].incrementXToTarget;
    gl.glTranslatef(enemies[x].posX,
    enemies[x].posY, 0f);
    gl.glMatrixMode(GL10.GL_TEXTURE);
    gl.glLoadIdentity();
    gl.glTranslatef(0.25f, .25f , 0.0f);
    }

break;
case SFEngine.TYPE_SCOUT:

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

画出拦截者,你就准备好对付侦察兵 AI 了。

`public class SFGameRenderer implements Renderer{

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);
    if (enemies[x].posY >= 3){
    enemies[x].posY -=
    SFEngine.INTERCEPTOR_SPEED;
    }else{
    if (!enemies[x].isLockedOn){
    enemies[x].lockOnPosX =
    SFEngine.playerBankPosX;
    enemies[x].isLockedOn = true;
    enemies[x].incrementXToTarget =
    (float) ((enemies[x].lockOnPosX - enemies[x].posX )/ (enemies[x].posY /
    (SFEngine.INTERCEPTOR_SPEED * 4)));
    }
    enemies[x].posY -=
    (SFEngine.INTERCEPTOR_SPEED * 4);
    enemies[x].posX +=
    enemies[x].incrementXToTarget;
    gl.glTranslatef(enemies[x].posX,
    enemies[x].posY, 0f);
    gl.glMatrixMode(GL10.GL_TEXTURE);
    gl.glLoadIdentity();
    gl.glTranslatef(0.25f, .25f , 0.0f);
    }
    enemies[x].draw(gl, spriteSheets);
    gl.glPopMatrix();
    gl.glLoadIdentity();

break;
case SFEngine.TYPE_SCOUT:**

**break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

}`

在下一节中,您将创建侦察敌人类型的 AI 逻辑。

创造侦察兵 AI

现在你已经用一点人工智能工作弄脏了你的手,剩下的两个敌人应该是相当容易的。侦察兵和截击机之间唯一的主要区别是侦察兵会按照预先定义的模式在屏幕上移动。

首先,测试以确定侦察兵是否离开屏幕底部;如果是,重置它。在拦截器的相同逻辑中,您将 x 和 y 轴位置都设置为随机值。但是,侦察兵只会从画面的最左边或者最右边攻击。因此,根据其攻击方向将 x 轴位置设置为 0 或 3。

`public class SFGameRenderer implements Renderer{

...

**private void moveEnemy(GL10 gl){

for (int x = 0; x < sfengine.TOTAL_INTERCEPTORS + sfengine.TOTAL_SCOUTS

  • sfengine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

...

break;
case SFEngine.TYPE_SCOUT:
if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].isLockedOn = false;
        enemies[x].posT = SFEngine.SCOUT_SPEED;**
        **enemies[x].lockOnPosX =
        enemies[x].getNextScoutX();
        enemies[x].lockOnPosY =
        enemies[x].getNextScoutY();
        if(enemies[x].attackDirection ==
        SFEngine.ATTACK_LEFT){
        enemies[x].posX = 0;
        }else{
        enemies[x].posX = 3f;
        }
        }
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glPushMatrix();
        gl.glScalef(.25f, .25f, 1f);

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

...

}`

就像你对截击机所做的一样,你要慢慢地移动侦察机直到它到达锁定点。

设置一个随机点来移动侦察员

因为如果所有的敌人都在屏幕上的同一点改变方向,这个动作对玩家来说会显得过于机械,所以你应该把侦察兵的锁定点设置得比截击机的锁定点低一点;否则,代码是相同的。

`public class SFGameRenderer implements Renderer{

...

**private void moveEnemy(GL10 gl){

for (int x = 0; x < sfengine.TOTAL_INTERCEPTORS + sfengine.TOTAL_SCOUTS

  • sfengine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();**

**switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

...

break;
case SFEngine.TYPE_SCOUT:
if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].isLockedOn = false;
        enemies[x].posT = SFEngine.SCOUT_SPEED;
        enemies[x].lockOnPosX =
        enemies[x].getNextScoutX();
        enemies[x].lockOnPosY =
        enemies[x].getNextScoutY();
        if(enemies[x].attackDirection ==
        SFEngine.ATTACK_LEFT){
        enemies[x].posX = 0;
        }else{
        enemies[x].posX = 3f;
        }
        }
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glPushMatrix();
        gl.glScalef(.25f, .25f, 1f);
        if (enemies[x].posY >= 2.75f){
        enemies[x].posY -= SFEngine.SCOUT_SPEED;
        }else{

}

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

...

}
In the next section, you will learn how to move the scout along a Bezier curve.`

沿着贝塞尔曲线移动

幸运的是,您已经创建了在贝塞尔曲线中自动为您提供下一个 x 和 y 坐标的方法。现在您所要做的就是调用getNextScoutX()getNextScoutY()方法,开始沿着曲线路径移动侦察器。在调用这些方法后,将posT增加SCOUT_SPEED的值;否则,下次调用它们时,您将获得相同的值。

`public class SFGameRenderer implements Renderer{

...

**private void moveEnemy(GL10 gl){
for (int x = 0; x < sfengine.TOTAL_INTERCEPTORS + sfengine.TOTAL_SCOUTS

  • sfengine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){
    Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

...

break;
case SFEngine.TYPE_SCOUT:
if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].isLockedOn = false;
        enemies[x].posT = SFEngine.SCOUT_SPEED;
        enemies[x].lockOnPosX =
        enemies[x].getNextScoutX();
        enemies[x].lockOnPosY =
        enemies[x].getNextScoutY();
        if(enemies[x].attackDirection ==
        SFEngine.ATTACK_LEFT){
        enemies[x].posX = 0;
        }else{
        enemies[x].posX = 3f;
        }
        }
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glPushMatrix();
        gl.glScalef(.25f, .25f, 1f);
        if (enemies[x].posY >= 2.75f){
        enemies[x].posY -= SFEngine.SCOUT_SPEED;
        }else{
        enemies[x].posX =
        enemies[x].getNextScoutX();
        enemies[x].posY =
        enemies[x].getNextScoutY();**
        **enemies[x].posT += SFEngine.SCOUT_SPEED;
        }

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

...

}`

信不信由你,这就是侦察兵 AI 的全部。完成这个敌人的 AI,执行你的 OpenGL 程序来转换顶点,并为侦察兵设置正确的精灵。

`public class SFGameRenderer implements Renderer{

...

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

...

break;
case SFEngine.TYPE_SCOUT:
if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].isLockedOn = false;
        enemies[x].posT = SFEngine.SCOUT_SPEED;
        enemies[x].lockOnPosX =
        enemies[x].getNextScoutX();
        enemies[x].lockOnPosY =
        enemies[x].getNextScoutY();
        if(enemies[x].attackDirection ==
        SFEngine.ATTACK_LEFT){
        enemies[x].posX = 0;
        }else{**
        **enemies[x].posX = 3f;
        }
        }
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glPushMatrix();
        gl.glScalef(.25f, .25f, 1f);
        if (enemies[x].posY >= 2.75f){
        enemies[x].posY -= SFEngine.SCOUT_SPEED;
        }else{
        enemies[x].posX =
        enemies[x].getNextScoutX();
        enemies[x].posY =
        enemies[x].getNextScoutY();
        enemies[x].posT += SFEngine.SCOUT_SPEED;
        }
        gl.glTranslatef(enemies[x].posX, enemies[x].posY, 0f);
        gl.glMatrixMode(GL10.GL_TEXTURE);
        gl.glLoadIdentity();
        gl.glTranslatef(0.75f, .25f , 0.0f);
        enemies[x].draw(gl, spriteSheets);
        gl.glPopMatrix();
        gl.glLoadIdentity();

break;
case SFEngine.TYPE_WARSHIP

break;

}

}
}

}**

...

}`

你需要添加到你的moveEnemy()方法中的最后一点 AI 是战舰。

创造人工智能战舰

星球大战 er 的故事中,战舰朝着玩家随机的方向移动。你将通过在 x 轴上选择一个随机位置,并使用与拦截器相同的逻辑,将战舰移向随机点,而不是直接移动到玩家的位置,来实现这一点。

将战舰移动到一个随机的位置会使游戏变得不可预测,并使敌人更难对抗。然而,战舰的 AI 将与拦截者的 AI 几乎相同,除了你需要用 0 到 3 之间的随机数替换玩家锁定的 x 位置。

`public class SFGameRenderer implements Renderer{

...

**private void moveEnemy(GL10 gl){

for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){

Random randomPos = new Random();

switch (enemies[x].enemyType){
case SFEngine.TYPE_INTERCEPTOR:

...

break;
...

break;
case SFEngine.TYPE_WARSHIP
if (enemies[x].posY < 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);

if (enemies[x].posY >= 3){
enemies[x].posY -=
SFEngine.WARSHIP_SPEED;
}else{
if (!enemies[x].isLockedOn){
enemies[x].lockOnPosX =
randomPos.nextFloat() * 3;
enemies[x].isLockedOn = true;
enemies[x].incrementXToTarget =
(float) ((enemies[x].lockOnPosX - enemies[x].posX )/ (enemies[x].posY /
(SFEngine.WARSHIP_SPEED * 4)));
}
enemies[x].posY -=
(SFEngine.WARSHIP_SPEED * 2);
enemies[x].posX +=
enemies[x].incrementXToTarget;**
**}
gl.glTranslatef(enemies[x].posX,
enemies[x].posY, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f, .25f , 0.0f);

enemies[x].draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;

}

}
}

}**

...

}`

在调用movePlayer1()之后,通过从onDrawFrame()方法调用新的moveEnemy()方法来完成游戏循环。

总结

在这一章中,你学到了很多关于为你的敌人创造三种不同的,基本的人工智能结构。你也是

  • 创建了加载多个纹理的调用
  • 创造方法来移动你的敌人
  • 在移动你的敌人之前测试它的状况
  • 创造逻辑让你的敌人沿着路径移动

在下一章,你将通过开发一些武器和实现碰撞检测来完成你的游戏。

八、保护你自己!

你的游戏开发技能正在飞速发展。虽然星际战士不会赢得任何游戏奖项,但它是你磨练新开发的游戏制作技能的完美沙盒。

现在,你发现自己已经到了这本书的最后一章,这一章将直接涉及一个基于 2-D 精灵的游戏的开发。在这一章中,你将为你的角色添加一件武器,并创建一些基本的 2-D 碰撞检测。您将加载一个包含一些武器图像的 sprite 表,为子弹编写一些 AI 逻辑以遵循一条路径,并创建一些碰撞检测以确保您知道您的武器何时击中目标。

如果这将是一个完整的游戏,你将向公众发布,你会想增加一些分数跟踪,多个级别,可能的项目,如电源和可升级的武器。然而,这个小型 2d 项目的真正目的是给你一个适当的知识基础和足够的使用技巧的经验,这样关于 3d 游戏开发的章节(第十章–第十二章)对你来说就有意义了。在这一章的结尾,你将有机会回顾到目前为止你已经处理过的一些关键文件。这将确保你在进入三维开发的下一阶段之前一切就绪。

创建武器精灵表

你的玩家在游戏中坚持不了多久,就无法抵御上一章中敌人的攻击。因此,你将为你的玩家配备标准的太空战斗武器——爆能枪。

你首先需要为你的武器创建一个精灵表,就像你为第六章的敌人和第五章的可玩角色创建精灵表一样。

注:理论上,武器可以和玩家以及敌舰放在同一个精灵表上。然而,看看如何在 OpenGL 中处理两个纹理是一个很好的练习。

将 sprite 表(可以从本书的 Apress.com 页面下载,连同该项目的代码)添加到你的 Star Fighter 项目中。图 8–1 中的精灵表包括多种武器和角色爆炸。

images

图 8–1。武器精灵表

一旦将 sprite 表添加到项目中,打开SFEngine.java文件并向其中添加以下常量:

SFEngine public static final int WEAPONS_SHEET = R.drawable.destruction; **public static final int PLAYER_SHIELDS = 5; public static final int SCOUT_SHIELDS = 3; public static final int INTERCEPTOR_SHIELDS = 1; public static final int WARSHIP_SHIELDS = 5; public static final float PLAYER_BULLET_SPEED = .125f;** SFMusic

WEAPONS_SHEET常量将保存指向新 sprite 工作表的指针。SHOUT_SHIELDSINTERCEPTOR_SHIELDSWARSHIP_SHIELDS常量将指示各自的敌人在被摧毁前可以承受多少击,而PLAYER_BULLET_SPEED常量将保持爆能枪离开可玩角色并在屏幕上移动的速度。

创建武器类别

当你创建了可玩的角色和敌人时,你创建了一个类作为他们的基础。你要对武器进行同样的处理。为你的武器创建一个名为SFWeapon的新职业。

`package com.proandroidgames;

public class SFWeapon {

}`

你需要知道关于你的武器的三件事来把它们画到屏幕上:精灵顶点的 x 和 y 位置,以及精灵当前是否在屏幕上。x 和 y 位置将帮助你把精灵放在屏幕上的正确点上,它们也将帮助你确定碰撞。

就像你可能会看到多个敌人一样,屏幕上会同时出现不止一个爆能枪。因此,你要把武器放在一个数组里。当你遍历数组时,你需要知道你正在看的镜头是否在屏幕上,或者它是否可以自由发射。

将下列公共属性添加到您的类中:

`package com.proandroidgames;

public class SFWeapon {

public float posY = 0f;
public float posX = 0f;
public boolean shotFired = false;

}`

创建顶点,索引和纹理数组,就像你在敌人和可玩角色类中所做的一样。这些数组和构造函数一起,将用于设置 OpenGL 将你的武器绘制到屏幕上所需的数据。

`public class SFWeapon {

public float posY = 0f;
public float posX = 0f;
public boolean shotFired = false;

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

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,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

public SFWeapon() {

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);
}**

}`

创建武器类的最后一步是创建一个onDraw()方法。学习了前面的章节,您应该非常熟悉onDraw()方法。请注意,武器精灵表将是你在前一章为游戏循环创建的spriteSheet数组中的第二个 OpenGL 指针。因此,适当地修改onDraw()方法,以便在调用该方法时从正确的 sprite 表中提取。

`public class SFWeapon {

**public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[1]);

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);
}**
}`

随着你的武器类的创建,你现在可以进入游戏循环并添加武器了。在下一节中,您将为您的可玩角色创建自动射击过程,并让它从您创建的武器类中发射武器。

给你的武器一个轨迹

现在你已经创建了你的武器类,你已经准备好实例化它并创建一个方法来允许可玩角色发射它。回想一下,在星际战士的故事中,可玩角色的武器是自动开火的。因此,你创建的发射武器的方法必须没有玩家的参与。

创建武器阵列

就像你对敌舰所做的那样,你将创建一个数组来保存你的玩家可能发射的所有可能的射击。打开SFGameRenderer,在你的游戏循环类中创建一个新的SFWeapon()数组。

`package com.proandroidgames;

import java.util.Random;

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 SFGoodGuy player1 = new SFGoodGuy();
private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];
private SFTextures textureLoader;
private int[] spriteSheets = new int[2];
private SFWeapon[] playerFire = new SFWeapon[4];

...

}`

在上一章中,您创建了一个通用纹理类。因为这个类当前在你的游戏循环中被实例化,它将包含两个纹理。现在是时候向这个数组添加第二个 sprite 工作表了。

添加第二个子画面

第二个子画面是保存武器的画面。

`public class SFGameRenderer implements Renderer{

...

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initializeInterceptors();
initializeScouts();
initializeWarships();
initializePlayerWeapons();
textureLoader = new SFTextures(gl);
spriteSheets = textureLoader.loadTexture(gl, SFEngine.CHARACTER_SHEET,
SFEngine.context, 1);
spriteSheets = textureLoader.loadTexture(gl, SFEngine.WEAPONS_SHEET, SFEngine.context,
2);

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

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

}`

注意:小心将新的工作表标记为 2 号,否则您将替换之前的工作表。

在前一章中,您编写了一个初始化方法来实例化敌人类的新副本,并将其添加到敌人的数组中。当你为你的角色创造武器时,你将遵循同样的过程。

初始化武器

首先创建一个名为initializePlayerWeapons()的初始化类,如下所示:

`public class SFGameRenderer implements Renderer{

...

**private void initializePlayerWeapons(){

}**

...

}`

initializePlayerWeapons()方法中,您需要遍历您创建的playerFire[]数组,并向其中添加一个新的SFWeapon()类实例。

`public class SFGameRenderer implements Renderer{

...

**private void initializePlayerWeapons(){
for(int x = 0; x < 4; x++){
SFWeapon weapon = new SFWeapon();
playerFire[x] = weapon;
}

}**

...

}`

通过设置要发射的第一个镜头的初始属性来结束初始化方法。由于武器是自动点火的,你可以将第一枪设定为已经发射。镜头的 x 轴位置将等于玩家角色的当前 x 轴位置。

至于 y 轴的位置,设置为 1.25。这会将镜头的 y 轴设置为略高于玩家的船,让它看起来像是从正面的爆能炮中出来的。如果你把 y 轴设置的更低,这个镜头将会被绘制在船的上方,看起来就像是从船的顶部的某个地方来的。

`public class SFGameRenderer implements Renderer{

...

private void initializePlayerWeapons(){
for(int x = 0; x < 4; x++){
SFWeapon weapon = new SFWeapon();
playerFire[x] = weapon;
}
playerFire[0].shotFired = true;
playerFire[0].posX = SFEngine.playerBankPosX;
playerFire[0].posY = 1.25f;
}

...

}`

武器射击的数组被创建、实例化和填充。在前面的章节中,你创建了私有方法,可以从游戏循环中调用这些方法来移动玩家和敌人。现在,您需要创建一个私有方法来在屏幕上移动武器镜头。

移动武器射击

每次射击的轨迹将是一条直线,它将从射击时玩家的 x 位置移动到屏幕的顶部。创建一个名为firePlayerWeapon()的方法,用于在发射时沿直线移动每个镜头。

`public class SFGameRenderer implements Renderer{

...

**private void initializePlayerWeapons(){
for(int x = 0; x < 4; x++){
SFWeapon weapon = new SFWeapon();
playerFire[x] = weapon;
}
playerFire[0].shotFired = true;
playerFire[0].posX = SFEngine.playerBankPosX;
playerFire[0].posY = 1.25f;
}

...

private void firePlayerWeapon(GL10 gl){

}**

...

}`

firePlayerWeapon()方法中,创建一个循环,该循环只有在镜头被触发时才会运行。这将使你免于在不需要绘制的镜头上循环。

`public class SFGameRenderer implements Renderer{

...

**private void firePlayerWeapon(GL10 gl){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){

}
}

}**

...

}`

在这个方法中,您要做的第一件事是创建一个名为nextShot的 int。可玩角色的自动射击功能会连续射击每个镜头。因此,一个镜头不应该发射,直到前一个镜头已经从角色移动了可接受的距离。int 跟踪下一个要发射的镜头,所以你可以在合适的时候设置一些初始属性。

`public class SFGameRenderer implements Renderer{

...

**private void firePlayerWeapon(GL10 gl){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){
int nextShot = 0;

}
}
}**

...

}`

检测屏幕边缘

您需要一种方法来确定一个镜头是否击中了可视屏幕的边缘,这样当玩家看不到它时,大炮爆炸就不会被绘制出来,从而浪费了宝贵的资源。设置一个if语句来测试当前镜头是否离开了屏幕。如果快照已经延伸到屏幕之外,将其shotFired属性设置为false以防止它被不必要的绘制。

`public class SFGameRenderer implements Renderer{

...

**private void firePlayerWeapon(GL10 gl){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){
int nextShot = 0;
if (playerFire[x].posY > 4.25){
playerFire[x].shotFired = false;
}else{

}

}
}
}**

...

}`

假设镜头还没有延伸到屏幕之外,它必须仍然在玩家的视野中,并且必须被处理。因为镜头以直线轨迹飞行,所以你要移动镜头所要做的就是继续将PLAYER_BULLET_SPEED添加到镜头的当前 y 位置。然后,您可以调用在屏幕上绘制字符时一直在处理的所有 OpenGL 操作。

提示:如果以下代码中的任何 OpenGL 操作看起来不熟悉,请查看第四章和第五章。

`public class SFGameRenderer implements Renderer{

...

**private void firePlayerWeapon(GL10 gl){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){
int nextShot = 0;
if (playerFire[x].posY > 4.25){
playerFire[x].shotFired = false;
}else{

playerFire[x].posY += SFEngine.PLAYER_BULLET_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 0f);
gl.glTranslatef(playerFire[x].posX, playerFire[x].posY, 0f);

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

playerFire[x].draw(gl,spriteSheets);**
**gl.glPopMatrix();
gl.glLoadIdentity();

}
}
}
}**

...

}`

在这个方法中,你需要注意最后一件事。一旦当前镜头沿 y 轴移动到距离角色超过 1 个单位,就该发射下一个镜头了。因此,您需要测试当前镜头是否距离角色超过 1 个 y 轴单位,如果是,则设置要发射的下一个镜头的属性。

请记住,镜头是连续发射的,所以当最后一个镜头发射时,第一个镜头应该离开屏幕并被禁用。当最后一次发射超过发射阈值时,可以再次发射第一次发射。

`public class SFGameRenderer implements Renderer{

...

private void firePlayerWeapon(GL10 gl){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){
int nextShot = 0;
if (playerFire[x].posY > 4.25){
playerFire[x].shotFired = false;
}else{
if (playerFire[x].posY> 2){
if (x == 3){
nextShot = 0;
}else{
nextShot = x + 1;
}
if (playerFire[nextShot].shotFired ==
false){
playerFire[nextShot].shotFired =
true;
playerFire[nextShot].posX =
SFEngine.playerBankPosX;
playerFire[nextShot].posY =
1.25f;
}
}
playerFire[x].posY +=
SFEngine.PLAYER_BULLET_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();

**gl.glScalef(.25f, .25f, 0f);
gl.glTranslatef(playerFire[x].posX, playerFire[x].posY, 0f);

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

playerFire[x].draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();

}
}
}
}**

...

}`

调用 firePlayerWeapons()方法

当你在处理可玩的角色和敌人时,你从主游戏循环中调用方法来移动他们。对玩家的武器遵循这个过程的问题是,你不知道玩家的角色当前是否有效并准备好开火。为了解决这个问题,您将从movePlayer1()方法中调用firePlayerWeapons()方法,而不是从主游戏循环中调用它。这样做可以确保你只在有资格移动的时候才移动屏幕上的武器。

`public class SFGameRenderer implements Renderer{

...

private void movePlayer1(GL10 gl){
if(!player1.isDestroyed){
switch (SFEngine.playerFlightAction){

...

}
firePlayerWeapon(gl);
}
}

...

}`

你的玩家现在可以向敌人开火。然而,这些武器不能完成任何事情。如果你现在编译并玩这个游戏,你会看到镜头简单地飞过任何敌人,并一直持续到它们到达屏幕的边缘。同样,敌人会继续下降,对任何枪声都视而不见。

为了使你的镜头有效,你需要创建一些碰撞检测。在下一节中,您将创建一个简单的 2-D 碰撞检测方法,用于确定是否应该消灭敌人。

实施碰撞检测

碰撞检测确定屏幕上的两个物体是否接触,并且对于任何视频游戏都是必不可少的。在星际战士中,你会使用基本的碰撞检测来消灭敌人。在其他游戏中,碰撞检测用于阻止玩家穿墙,允许玩家捡起新物品,甚至确定敌人是否能从模糊的视野中看到玩家。

在本节中,您将创建一个方法来跟踪屏幕上每个敌人的位置,以及每一次射击,以确定是否有任何射击击中了任何敌人。在像星际战士这样的 2d 游戏中,这个过程变得更容易,因为你只需要在两个轴上测试(在 2d 游戏中你不需要处理 z 轴)。

施加碰撞伤害

当检测到碰撞时,你必须对被击中的敌人造成伤害。每个敌人在被消灭前都可以受到一定的伤害。为了跟踪这种损坏,在您的SFEnemy()类中创建一个名为applyDamage()的新方法。这个方法只是在每次特定的敌人被击中时增加一个 int 值。当 int 值达到该敌人的预定义限制时,isDestroyed旗将被翻转,敌人将不再被吸引到屏幕上。

`package com.proandroidgames;

...

import javax.microedition.khronos.opengles.GL10;

public class SFEnemy {

public float posY = 0f;
public float posX = 0f;
public float posT = 0f;
public float incrementXToTarget = 0f;
public float incrementYToTarget = 0f;
public int attackDirection = 0;
**public boolean isDestroyed = false;
private int damage = 0;

...

public void applyDamage(){
damage++;
switch(enemyType){**
case SFEngine.TYPE_INTERCEPTOR:
if (damage == SFEngine.INTERCEPTOR_SHIELDS){
isDestroyed = true;
}
break;
case SFEngine.TYPE_SCOUT:
if (damage == SFEngine.SCOUT_SHIELDS){
isDestroyed = true;
}
break;
case SFEngine.TYPE_WARSHIP:
if (damage == SFEngine.WARSHIP_SHIELDS){
isDestroyed = true;
}
break;
}
}

...

}`

每当你的碰撞检测方法确定与敌船发生碰撞时,你所要做的就是调用敌人的appyDamage()方法,剩下的就交给它了。一旦敌人身上的isDestroyed旗帜被设定为true,该敌人将不再被moveEnemy()方法处理或被绘制到屏幕上。保存并关闭SFEnemy.java

创建 detectCollisions()方法

碰撞损伤计算完成后,通过在你的SFGameRenderer.java文件中创建一个名为detectCollisions()的方法继续编辑你的渲染器。

`public class SFGameRenderer implements Renderer{
**private void initializePlayerWeapons(){
for(int x = 0; x < 4; x++){
sfweapon weapon = new sfweapon();
playerFire[x] = weapon;
}
playerFire[0].shotFired = true;
playerFire[0].posX = sfengine.playerBankPosX;
playerFire[0].posY = 1.25f;
}

...

private void detectCollisions(){

}**

...

}`

detectCollisions()方法中,设置两个循环,一个循环遍历每一次射击,另一个循环遍历每一个还没有被消灭的敌人。请记住,因为敌人是在屏幕上边缘以外的随机位置开始的,所以他们可以在玩家视野之外有效(isDestroyed == false)。这意味着你还需要测试敌人是否在玩家的视野之内,以及它是否已经被摧毁。

`public class SFGameRenderer implements Renderer{

...

**private void detectCollisions(){
for (int y = 0; y < 3; y ++){
if (playerFire[y].shotFired){
for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1; x++ ){
if(!enemies[x].isDestroyed && enemies[x].posY <
4.25 ){

}
}
}
}

}**

...

}`

现在该方法的棘手部分来了。你知道每一个敌人和每一发子弹的两条信息:x 和 y 位置。你也知道敌人和射击的顶点的尺寸;在这种情况下,它们都是 1 × 1 的单位。

检测特定的碰撞

创建一个if语句,根据一个镜头和一个敌人的 x 和 y 位置以及它们各自的尺寸来确定它们是否发生碰撞。

`public class SFGameRenderer implements Renderer{

...

private void detectCollisions(){
for (int y = 0; y < 3; y ++){
if (playerFire[y].shotFired){
for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1; x++ ){

**if(!enemies[x].isDestroyed && enemies[x].posY <
4.25 ){
if ((playerFire[y].posY >=
enemies[x].posY - 1
&& playerFire[y].posY <= enemies[x].posY )
&& (playerFire[y].posX <= enemies[x].posX + 1
&& playerFire[y].posX >= enemies[x].posX - 1)){

}
}
}
}
}
}**

...

...

}`

如果敌人和被测试的子弹都通过了这个if声明,那么他们就相撞了。当和敌人和一发子弹相撞时,你需要对敌人调用applyDamage()方法,要么增加那个敌人的伤害,要么彻底消灭它。

删除无效镜头

一旦一个镜头击中了一个敌人,不管这个敌人是否被完全消灭,这个镜头都需要被移出屏幕,这样它就不能击中任何其他的敌人。将镜头上的shotFired标志设置为false以取消该镜头。

注意:无论是一枪命中敌人还是行进到屏幕顶端,都会有相同的结果;可以激活阵列中的下一个镜头。因此,在你的碰撞方法中,如果一个射击击中了一个敌人,在你已经使碰撞的那个无效之后激活下一个射击。

`public class SFGameRenderer implements Renderer{

...

private void detectCollisions(){
for (int y = 0; y < 3; y ++){
if (playerFire[y].shotFired){
for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1; x++ ){
if(!enemies[x].isDestroyed && enemies[x].posY <
4.25 ){

**if ((playerFire[y].posY >=
enemies[x].posY - 1
&& playerFire[y].posY <= enemies[x].posY )
&& (playerFire[y].posX <= enemies[x].posX + 1
&& playerFire[y].posX >= enemies[x].posX - 1)){
int nextShot = 0;
enemies[x].applyDamage();
playerFire[y].shotFired = false;
if (y == 3){
nextShot = 0;
}else{
nextShot = y + 1;
}
if
(playerFire[nextShot].shotFired == false){

playerFire[nextShot].shotFired = true;

playerFire[nextShot].posX = SFEngine.playerBankPosX;

playerFire[nextShot].posY = 1.25f;
}
}
}
}
}
}
}**

...

...

}`

这是一个相当简单的二维碰撞检测版本,应该可以让你很好地创建一个有趣的游戏。现在,你所要做的就是从主游戏循环中调用碰撞检测方法。

`...

public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime < SFEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(SFEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

scrollBackground1(gl);
scrollBackground2(gl);

movePlayer1(gl);
moveEnemy(gl);

detectCollisions();

gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA);
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));

}

...`

保存并编译你的游戏。你现在可以移动你的角色,并在敌人试图攻击你时干掉他们。这是直接处理 2-D 图形和星际战士游戏的最后一章。在下一节中,你将找到一些建议,在进入 3d 游戏领域之前,扩展你在这些章节中学到的知识。

扩展你所学的知识

如果你真的想扩展你的星际战士游戏,你可以在你的代码中添加几个关键元素,这将对游戏产生重大影响。

  • 扩展武器,使它们也能从侦察兵和战舰上发射。
  • 扩展碰撞检测以包括玩家受到的射击冲击以及玩家和敌人之间的碰撞。
  • 添加一个三个或四个精灵的爆炸动画序列,可以在船只被摧毁时触发。
  • 给玩家不止一次的机会。

所有这些项目都可以很容易地用你在本书中已经获得的技能来完成。

总结

在这一章中,你学习了如何制造玩家可以自动开火的武器。你还增加了一些基本的二维碰撞检测来消灭被玩家击中的敌人。

在下一章中,你将学习如何在转到 3d 游戏编程之前在 Android Marketplace 上发布你的游戏。

但是在我们继续之前,请回顾一下我在下一节中提供的 Star Fighter 密钥文件的完整源代码。我选择了修改最多或者代码最多的文件。提供了这些文件的代码清单,以便您可以将您的代码与项目中的代码进行比较。

如果您在编译或运行项目时遇到问题,这可以帮助您。考虑到从头开始创建游戏可能会很复杂,而且当您阅读章节时可能会忽略一些代码,您可能会发现您无法按照书中的方式正确运行或编译游戏。作为你将学习 2D 游戏的最后一章,这是一个停下来回顾你的代码的好地方。

查看钥匙二维码

如果你在运行星际争霸游戏时遇到问题,你应该检查的第一个文件是 SFEngine.java。这个文件包含了在整个游戏中使用的设置,并且在项目的几乎每个类中使用。你首先在第三章中创建了这个文件,并在第一部分的中继续编辑它。因此,这是最有可能的地方,你可能已经错过了一些东西。SFEngine.java 的来源显示在清单 8–1 中。

清单 8–1。SFEngine.java??

`package com.proandroidgames;

import android.content.Context;
import android.content.Intent;
import android.view.Display;
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 staticfinal 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 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;
public static final int PLAYER_BANK_LEFT_1 = 1;
public static final int PLAYER_RELEASE = 3;
public static final int PLAYER_BANK_RIGHT_1 = 4;
public static final int PLAYER_FRAMES_BETWEEN_ANI = 9;
public static final float PLAYER_BANK_SPEED = .1f;
public static int CHARACTER_SHEET = R.drawable.character_sprite;
public static int TOTAL_INTERCEPTORS = 10;
public static int TOTAL_SCOUTS = 15;
public static int TOTAL_WARSHIPS = 5;
public static float INTERCEPTOR_SPEED = SCROLL_BACKGROUND_1 * 4f;
public static float SCOUT_SPEED = SCROLL_BACKGROUND_1 * 6f;
public static float WARSHIP_SPEED = SCROLL_BACKGROUND_2 * 4f;
public static final int TYPE_INTERCEPTOR = 1;
public static final int TYPE_SCOUT = 2;
public static final int TYPE_WARSHIP = 3;
public static final int ATTACK_RANDOM = 0;
public static final int ATTACK_RIGHT = 1;
public static final int ATTACK_LEFT = 2;
public static final float BEZIER_X_1 = 0f;
public static final float BEZIER_X_2 = 1f;
public static final float BEZIER_X_3 = 2.5f;
public static final float BEZIER_X_4 = 3f;
public static final float BEZIER_Y_1 = 0f;
public static final float BEZIER_Y_2 = 2.4f;
public static final float BEZIER_Y_3 = 1.5f;
public static final float BEZIER_Y_4 = 2.6f;
public static final int WEAPONS_SHEET = R.drawable.destruction;
public static final int PLAYER_SHIELDS = 5;
public static final int SCOUT_SHIELDS = 3;
public static final int INTERCEPTOR_SHIELDS = 1;
public static final int WARSHIP_SHIELDS = 5;
public static final float PLAYER_BULLET_SPEED = .125f;
/Game Variables/

public static Context context;
public static Thread musicThread;
public static Display display;
public static int playerFlightAction = 0;
public static float playerBankPosX = 1.75f;
/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;
}
}
}`

下一个文件(清单 8–2)是用来创建你的武器的类。该文件创建较早,因此可能被忽略。当你对照你的文件检查这个文件时,注意 onDraw()方法——如果你从一个类似的文件中复制了这个文件的内容,比如 SFBackground.java,你可能错过了一些改变。

清单 8–2。SFWeapon.java??

`package com.proandroidgames;

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

import javax.microedition.khronos.opengles.GL10;

public class SFWeapon {

public float posY = 0f;
public float posX = 0f;
public boolean shotFired = false;

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

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,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

public SFWeapon() {

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);
}

public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[1]);

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);
}

}`

SFTextures 文件对代码来说也是一个相对较新的文件,因此这里也可能出现问题。此代码用于扩展更新调用纹理的现有过程。如果你没有集中注意力,就很容易忽略这个重要的部分。当您检查清单 8–3 中的代码时,请确保您的数组被正确实例化。

清单 8–3。SFTextures.java??

`package com.proandroidgames;

import java.io.IOException;
import java.io.InputStream;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;

public class SFTextures {

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

public SFTextures(GL10 gl){

gl.glGenTextures(2, textures, 0);

}
public int[] loadTexture(GL10 gl,int texture, Context context,int
textureNumber) {
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.glBindTexture(GL10.GL_TEXTURE_2D, textures[textureNumber - 1]);

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_CLAMP_TO_EDGE);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);

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

bitmap.recycle();

return textures;
}
}`

如果您的文件似乎与前面的列表中的文件匹配,并且您仍然有问题,那么是时候进入游戏循环了。SFGameRenderer.java 包含星际战斗机的主要游戏循环,也是最有可能发生问题的地方。不幸的是,作为游戏中最大的文件,它也是最难解决的文件。清单 8–4 显示了 SFGameRenderer.java.Pay 的源代码。

清单 8–4。SFGameRenderer.java??

`package com.proandroidgames;

import java.util.Random;

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 SFGoodGuy player1 = new SFGoodGuy();
private SFEnemy[] enemies = new SFEnemy[SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1];
private SFTextures textureLoader;
private int[] spriteSheets = new int[2];
private SFWeapon[] playerFire = new SFWeapon[4];

private int goodGuyBankFrames = 0;
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

private float bgScroll1;
private float bgScroll2;

@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
// TODO Auto-generated method stub
try {
if (loopRunTime < SFEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(SFEngine.GAME_THREAD_FPS_SLEEP -
loopRunTime);
}
} 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);

movePlayer1(gl);
moveEnemy(gl);

detectCollisions();

gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA);
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));

}
private void initializeInterceptors(){
for (int x = 0; x<SFEngine.TOTAL_INTERCEPTORS -1 ; x++){
SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_INTERCEPTOR,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;
}
}
private void initializeScouts(){
for (int x = SFEngine.TOTAL_INTERCEPTORS -1;
x<SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS -1; x++){
SFEnemy interceptor;
if (x>=(SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS) / 2
){
interceptor = new SFEnemy(SFEngine.TYPE_SCOUT,
SFEngine.ATTACK_RIGHT);
}else{
interceptor = new SFEnemy(SFEngine.TYPE_SCOUT,
SFEngine.ATTACK_LEFT);
}
enemies[x] = interceptor;
}
}
private void initializeWarships(){
for (int x = SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS -1;
x<SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS -1;
x++){
SFEnemy interceptor = new SFEnemy(SFEngine.TYPE_WARSHIP,
SFEngine.ATTACK_RANDOM);
enemies[x] = interceptor;
}
}
private void initializePlayerWeapons(){
for(int x = 0; x < 4; x++){
SFWeapon weapon = new SFWeapon();
playerFire[x] = weapon;
}
playerFire[0].shotFired = true;
playerFire[0].posX = SFEngine.playerBankPosX;
playerFire[0].posY = 1.25f;
}
private void moveEnemy(GL10 gl){
for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS + SFEngine.TOTAL_SCOUTS

  • SFEngine.TOTAL_WARSHIPS - 1; x++){
    if (!enemies[x].isDestroyed){
    Random randomPos = new Random();
    switch (enemies[x].enemyType){
    case SFEngine.TYPE_INTERCEPTOR:
    if (enemies[x].posY < 0){
    enemies[x].posY = (randomPos.nextFloat()
      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);

if (enemies[x].posY >= 3){
enemies[x].posY -=
SFEngine.INTERCEPTOR_SPEED;
}else{
if (!enemies[x].isLockedOn){
enemies[x].lockOnPosX =
SFEngine.playerBankPosX;
enemies[x].isLockedOn = true;
enemies[x].incrementXToTarget
=(float) ((enemies[x].lockOnPosX - enemies[x].posX )/ (enemies[x].posY /
(SFEngine.INTERCEPTOR_SPEED* 4)));
}
enemies[x].posY -=
(SFEngine.INTERCEPTOR_SPEED* 4);
enemies[x].posX +=
enemies[x].incrementXToTarget;

}
gl.glTranslatef(enemies[x].posX,
enemies[x].posY, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f, .25f , 0.0f);
enemies[x].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
case SFEngine.TYPE_SCOUT:
if (enemies[x].posY <= 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].isLockedOn = false;
        enemies[x].posT = SFEngine.SCOUT_SPEED;
        enemies[x].lockOnPosX =
        enemies[x].getNextScoutX();
        enemies[x].lockOnPosY =
        enemies[x].getNextScoutY();
        if(enemies[x].attackDirection ==
        SFEngine.ATTACK_LEFT){
        enemies[x].posX = 0;
        }else{
        enemies[x].posX = 3f;
        }
        }
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glPushMatrix();
        gl.glScalef(.25f, .25f, 1f);

if (enemies[x].posY >= 2.75f){
enemies[x].posY -= SFEngine.SCOUT_SPEED;

}else{
enemies[x].posX =
enemies[x].getNextScoutX();
enemies[x].posY =
enemies[x].getNextScoutY();
enemies[x].posT += SFEngine.SCOUT_SPEED;

}
gl.glTranslatef(enemies[x].posX,
enemies[x].posY, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f, .25f , 0.0f);
enemies[x].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
case SFEngine.TYPE_WARSHIP:
if (enemies[x].posY < 0){
enemies[x].posY = (randomPos.nextFloat()

      • 4;
        enemies[x].posX = randomPos.nextFloat()
  • 3;
    enemies[x].isLockedOn = false;
    enemies[x].lockOnPosX = 0;
    }
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glPushMatrix();
    gl.glScalef(.25f, .25f, 1f);

if (enemies[x].posY >= 3){
enemies[x].posY -=
SFEngine.WARSHIP_SPEED;

}else{
if (!enemies[x].isLockedOn){
enemies[x].lockOnPosX =
randomPos.nextFloat() * 3;
enemies[x].isLockedOn = true;
enemies[x].incrementXToTarget
=(float) ((enemies[x].lockOnPosX - enemies[x].posX )/ (enemies[x].posY /
(SFEngine.WARSHIP_SPEED* 4)));
}
enemies[x].posY -=
(SFEngine.WARSHIP_SPEED* 2);
enemies[x].posX +=
enemies[x].incrementXToTarget;

}
gl.glTranslatef(enemies[x].posX,
enemies[x].posY, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.75f, .25f , 0.0f);
enemies[x].draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

}

}
}

}

private void movePlayer1(GL10 gl){
if(!player1.isDestroyed){
switch (SFEngine.playerFlightAction){
case SFEngine.PLAYER_BANK_LEFT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (goodGuyBankFrames <
SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX > 0){
SFEngine.playerBankPosX -=
SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f,
0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.75f,0.0f, 0.0f);
goodGuyBankFrames += 1;
}else if (goodGuyBankFrames >=
SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX > 0){
SFEngine.playerBankPosX -=
SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f,
0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.25f, 0.0f);
}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f,
0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}
player1.draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
case SFEngine.PLAYER_BANK_RIGHT_1:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (goodGuyBankFrames <
SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX < 3){
SFEngine.playerBankPosX +=
SFEngine.PLAYER_BANK_SPEED;
gl.glTranslatef(SFEngine.playerBankPosX, 0f,
0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f,0.0f, 0.0f);
goodGuyBankFrames += 1;
}else if (goodGuyBankFrames >=
SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX < 3){
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f,0.0f, 0.0f);
SFEngine.playerBankPosX +=
SFEngine.PLAYER_BANK_SPEED;
}else{
gl.glTranslatef(SFEngine.playerBankPosX, 0f,
0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
}
player1.draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case SFEngine.PLAYER_RELEASE:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
goodGuyBankFrames = 0;
player1.draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
default:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
player1.draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
firePlayerWeapon(gl);
}

}
private void detectCollisions(){
for (int y = 0; y < 3; y ++){
if (playerFire[y].shotFired){
for (int x = 0; x < SFEngine.TOTAL_INTERCEPTORS +
SFEngine.TOTAL_SCOUTS + SFEngine.TOTAL_WARSHIPS - 1; x++ ){
if(!enemies[x].isDestroyed && enemies[x].posY <
4.25 ){
if ((playerFire[y].posY >=
enemies[x].posY - 1
&& playerFire[y].posY <=
enemies[x].posY )
&& (playerFire[y].posX
<= enemies[x].posX + 1
&& playerFire[y].posX >=
enemies[x].posX - 1)){
int nextShot = 0;
enemies[x].applyDamage();
playerFire[y].shotFired = false;
if (y == 3){
nextShot = 0;
}else{
nextShot = y + 1;
}
if
(playerFire[nextShot].shotFired == false){

playerFire[nextShot].shotFired = true;

playerFire[nextShot].posX = SFEngine.playerBankPosX;

playerFire[nextShot].posY = 1.25f;
}
}
}
}
}
}
}
private void firePlayerWeapon(GL10 gl){
for(int x = 0; x < 4; x++){
if (playerFire[x].shotFired){
int nextShot = 0;
if (playerFire[x].posY > 4.25){
playerFire[x].shotFired = false;
}else{
if (playerFire[x].posY> 2){
if (x == 3){
nextShot = 0;
}else{
nextShot = x + 1;
}
if (playerFire[nextShot].shotFired ==
false){
playerFire[nextShot].shotFired =
true;
playerFire[nextShot].posX =
SFEngine.playerBankPosX;
playerFire[nextShot].posY =
1.25f;
}

}
playerFire[x].posY +=
SFEngine.PLAYER_BULLET_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 0f);
gl.glTranslatef(playerFire[x].posX,
playerFire[x].posY, 0f);

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

playerFire[x].draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();

}
}
}
}
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
initializeInterceptors();
initializeScouts();
initializeWarships();
initializePlayerWeapons();
textureLoader = new SFTextures(gl);
spriteSheets = textureLoader.loadTexture(gl, SFEngine.CHARACTER_SHEET,
SFEngine.context, 1);
spriteSheets = textureLoader.loadTexture(gl, SFEngine.WEAPONS_SHEET,
SFEngine.context, 2);

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

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

}`

最后,清单 8–5 和 8–6 显示了这个项目中的最后两个关键文件。SFGoodGuy.java 和 SFBadGuy.java 包含玩家和敌人的代码。虽然在编写这个游戏的早期,你不应该注意到这些文件中的任何问题,但仔细检查你的工作不会有什么坏处。

查看 SFEnemy.java 时,检查贝塞尔曲线的公式。

清单 8–5。SFGoodGuy.java??

`package com.proandroidgames;

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

import javax.microedition.khronos.opengles.GL10;

public class SFGoodGuy {
public boolean isDestroyed = false;
private int damage = 0;

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

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,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

public void applyDamage(){
damage++;
if (damage == SFEngine.PLAYER_SHIELDS){
isDestroyed = true;
}

}
public SFGoodGuy() {
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);
}

public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[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);
}

}`

清单 8–6。SFEnemy.java??

`package com.proandroidgames;

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

import javax.microedition.khronos.opengles.GL10;

public class SFEnemy {

public float posY = 0f;
public float posX = 0f;
public float posT = 0f;
public float incrementXToTarget = 0f;
public float incrementYToTarget = 0f;
public int attackDirection = 0;
public boolean isDestroyed = false;
private int damage = 0;

public int enemyType = 0;

public boolean isLockedOn = false;
public float lockOnPosX = 0f;
public float lockOnPosY = 0f;

private Random randomPos = new Random();

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

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,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};
public void applyDamage(){
damage++;
switch(enemyType){
case SFEngine.TYPE_INTERCEPTOR:
if (damage == SFEngine.INTERCEPTOR_SHIELDS){
isDestroyed = true;
}
break;
case SFEngine.TYPE_SCOUT:
if (damage == SFEngine.SCOUT_SHIELDS){
isDestroyed = true;
}
break;
case SFEngine.TYPE_WARSHIP:
if (damage == SFEngine.WARSHIP_SHIELDS){
isDestroyed = true;
}
break;
}
}

public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;
switch(attackDirection){
case SFEngine.ATTACK_LEFT:
posX = 0;
break;
case SFEngine.ATTACK_RANDOM:
posX = randomPos.nextFloat() * 3;
break;
case SFEngine.ATTACK_RIGHT:
posX = 3;
break;
}
posT = SFEngine.SCOUT_SPEED;

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);
}
public float getNextScoutX(){
if (attackDirection == SFEngine.ATTACK_LEFT){
return (float)((SFEngine.BEZIER_X_4(posTposTposT)) +
(SFEngine.BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_2 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT))));
}else{
return (float)((SFEngine.BEZIER_X_1
(posTposTposT)) +
(SFEngine.BEZIER_X_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_4 * ((1-posT) * (1-posT) * (1-posT))));
}

}
public float getNextScoutY(){
return (float)((SFEngine.BEZIER_Y_1(posTposT*posT)) +
(SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}

public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[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);
}

}`

九、发布您的游戏

到目前为止,你可能已经有了一个相当有趣的 2d 游戏。像大多数休闲游戏开发者一样,你可能想与世界其他地方分享你的创作。让你的游戏进入大众手中和设备上的方法是将它发布到 Android Marketplace。本章将概述在 Android Marketplace 上发布游戏的流程。

在您可以发布您的杰作之前,您必须做一些事情来准备您的代码被编译以供发布。这一章将带你完成准备游戏发布的步骤。您必须准备好您的AndroidManifest文件,并签署和调整您的代码。

注意:网上有很多资源,包括 Android 开发者论坛,可以获得关于实际上传到市场的指导。这一章不包括上传过程,只包括可能被忽略的准备步骤。

这是您最后一次使用目前为止创建的二维代码的机会。在本书的剩余部分,你将学习创建 3d 游戏的技巧。然而,无论你想发布什么样的游戏或应用,这里概述的步骤都适用。

准备您的货单

准备要发布的代码的第一步是确保您的AndroidManifest文件是有序的。您的AndroidManifest必须具备三条关键信息才能发布。这些关键信息是

  • versionCode
  • versionName
  • android:icon

在 XML 视图中打开您的AndroidManfest文件。您必须在货单中包含的信息以粗体显示如下:

`
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.proandroidgames"
android:versionCode="1"
android:versionName="1.0">

<application android:label="@string/app_name" android:icon="@drawable/sficon">








`

如果您的AndroidManifest文件没有此信息,您必须在继续之前添加它。市场主要使用versionCodeversionName来跟踪你上传的游戏版本。如果你要发布游戏的升级版本,这是很有帮助的。

上述代码中的另一个关键元素是图标的规范。你的游戏必须有一个图标显示在 Android 用户界面上。图标不需要精心制作;它甚至可以是普通的 Android 图标,但你确实需要一个。

然而,这些信息应该已经在您的清单中了,特别是如果您使用 Eclipse 来创建您的项目。下一步是签名、发布编译和调整您的代码。

准备签署、校准和发布

所有发布到 Android Marketplace 的应用都必须经过代码签名。这允许市场识别你,除非你的游戏被签名,否则它不会被接受。如果您没有来自证书颁发机构(CA)的证书,您可以自行签名。Android Marketplace 将接受自签名应用。

签署代码后,您需要对齐它。对齐代码只是确保它设置在 4 位边界。4 位边界最适合在移动设备上下载。

幸运的是,如果您使用 Eclipse 作为您的 Android IDE,一个简单的向导将立刻处理这两项任务。打开你的项目,进入File images Export,如图图 9–1 所示。这将打开导出向导。

images

图 9–1。 打开导出向导

在向导打开的情况下,从 Android 目的地选择导出 Android 应用选项,如 Figure 9–2 所示。

images

图 9–2。?? 选择导出安卓应用的目的地

提示:或者,您可以通过在 Eclipse 中右键单击项目并选择 Export 来直接进入这一步。

做出选择后,单击“下一步”按钮。Eclipse 现在将测试您的项目的AndroidManifest文件,以确保它满足前面讨论的需求——被签署和发布。在图 9–3 所示的项目检查屏幕中,点击浏览按钮。

images

图 9–3。 项目检查窗口

现在,您可以让向导检查您的项目清单中的错误,这些错误会阻止对项目进行签名。

检查机器人清单的准备情况

当您单击 Browse 按钮时,将会打开一个较小的窗口,其中列出了所有已加载的项目。从该列表中选择您的项目,如 Figure 9–4 所示。

images

图 9–4。 选择星际战斗机项目

“导出向导”现在将检查您的代码,以确保它已准备好进行签名。假设您已经满足了要求,包括有一个图标、一个版本代码和一个版本名称,那么您应该会看到消息“没有发现错误”,如 Figure 9–5 所示。

images

图 9–5。?? 检查成功

检查完成后,单击“下一步”按钮开始签名过程。

创建密钥库

向导的下一个屏幕是密钥库选择,如 Figure 9–6 所示。如果您已经创建了一个现有的证书密钥库(可能是从您之前上传的应用或您购买的证书中创建的),请选择“使用现有的密钥库选项”来导入它。

但是,如果是自签名,则应该选择“创建新的密钥库”选项。选择此选项将引导您完成创建新密钥库的过程。

images

图 9–6。??【密钥库选择】窗口

选择密钥库文件的有效位置,并输入密码。

注意:您应该为您的密钥库选择一个既安全又有备份的位置。每次更新游戏或应用时,都必须使用相同的密钥库。因此,如果您丢失了密钥库,您将无法再向此游戏上传更新。

点击 Next 按钮进入密钥创建窗口,如 Figure 9–7 所示。在这里,您必须输入在市场上识别您的所有信息。

images

图 9–7。??【钥匙创造】窗口

输入密钥库所需的信息后,单击“下一步”按钮。Eclipse 现在将为您生成一个 keystore,它将在流程的下一步中用于对您的应用进行签名。在向导的下一个也是最后一个屏幕上,即“目的地和密钥/证书检查”窗口(参见图 9–8,您将选择您的.apk文件进行签名。

在真正创建之前选择.apk文件可能看起来有点混乱,但还是跟着做吧。点击 Browse 按钮,您应该会看到starfighter.apk

images

图 9–8。 向导的最终屏幕

单击“完成”按钮编译并签署您的最终游戏。在此过程中,代码将被调整为 4 位边界,使移动设备更容易下载。

你现在已经准备好将你的作品上传到 Android Marketplace 了——这让你的朋友和同事羡慕不已。如果你的游戏开发爱好更多地延伸到下一代而不是复古,这本书的其余章节正是你所需要的。本书的最后四章将建立在你到目前为止所学的技能之上,并增加你在 3d 游戏环境中使用 OpenGL 的能力。

总结

在这一章中,你学习了如何准备你的代码上传到 Android Marketplace。您还使用 Eclipse Export 向导创建了一个密钥库,并根据市场需求对您的游戏进行了签名和调整。在下一章中,你将开始使用在前八章中学到的相同技能来创建一个 3d 游戏。

十、Blob Hunter:创建 3d 游戏

在这本书的前半部分,你花了很多时间来建立你的 OpenGL ES 技能,创造了星际战士。诚然,星际战士不会让玩家敲你的门来玩它。然而,这个游戏为你做了什么远比这个更重要。你在创建一个 2-D、自上而下的射击游戏时磨练出来的技能可以很容易地转化为创建一些令人惊叹的 3-D 游戏所需的技能。

在本书的剩余部分,您将构建一个 3d 环境,用于创建任意数量的引人入胜的 3d 游戏。让我们从讨论 2d 游戏和 3d 游戏的区别开始。

比较二维和三维游戏

视觉上,我们都可以区分二维游戏和三维游戏。2d 游戏看起来是平面的,很像动画片,而 3d 游戏在动态空间中具有多面物体的外观。2d 游戏无关紧要吗?当然不是。随着令人上瘾的手机游戏的出现,如愤怒的小鸟,以及令人眼花缭乱的其他 iPhone、Android 和脸书游戏,2-D 游戏市场仍然活跃并且相当不错。你可以继续扩展你的 2-D 游戏技能,创造一些令人惊奇的游戏。然而,如果你更喜欢更复杂的 3d 游戏,你需要从学习本书剩余章节中解释的内容开始。

当你创建你的 2d 游戏星际战士时,你创建了平面正方形(用平面三角形)。然后,您将一个精灵映射到该正方形的表面来创建您的角色。然而,拿起一张纸,看看它。即使它是平面的,但它在你手中仍然是立体的。您可以转动、旋转或弯曲它。拿六张纸,创建一个立方体。现在,三维形状更清晰了,但你真正改变的只是平面纸片的数量以及它们的排列方式。

这是一个非常基本的解释,说明你在星际战士中学到的技能和你开始构建一个新的 3d 游戏斑点猎人所需要的技能之间的简单过渡。你看到了吧,在没有意识到的情况下,你一直在做 3d 工作。您忽略了 z 轴的任何值,并告诉 OpenGL 在 2d 中渲染您的场景,从而展平了一切。

就 OpenGL 而言,2d 或 3d 游戏在空间上是一样的。区别在于你如何对待对象,以及你如何告诉 OpenGL 渲染它们。您需要创建更有说服力的复杂多边形,成为您的角色和环境,而不是创建有精灵映射的平面正方形。

在这一章中,你将创建一个新的 Android 项目来保存 Blob Hunter ,它将成为你学习一些重要的 3d 游戏开发技能的沙箱。您还将设置开始三维开发所需的几个文件。

创建您的三维项目

在本节中,您将开始创建将在本书的其余部分中使用的项目。创建 3-D 项目的过程将与您创建星际战士游戏项目的过程相同。

按照您在第二章中使用的相同步骤,创建一个名为blobhunter的新项目。这个项目将包含本书剩余部分的所有例子。你不会创建另一个像星际战士一样完整的项目,你将学习把你在 2d 工作的知识转换成 3d 环境的秘密。

一旦创建了新的blobhunter项目,就用一些启动文件填充它。虽然这个项目不会有星际战士的所有 flash 和菜单,但你仍然有一些启动游戏的基本文件。

在本书的前面,你学习了如何制作菜单和闪屏。事实是,无论游戏是二维还是三维的,用来创建游戏关键部分的过程都是一样的。因此,这里不再赘述。

但是,在接下来的部分中,您将向项目中添加四个基本文件来创建和显示渲染器。这就是你在这里所做的一切。你将不会有任何菜单,或任何优雅的代码杀死程序,就像你在星际战士中做的那样。

BlobhunterActivity.java

您需要在新的blobhunter项目中创建的第一个文件是BlobhunterActivity.java。在星际战斗机项目中,StarfighterActivity.java启动了闪屏,闪屏又启动了主菜单。然而,由于这里没有这些组件,BlobhunterActivity可以简单地启动gameview

提示:你将在本章看到的大部分代码对你来说应该非常熟悉。从本质上来说,它都是取自星际战斗机项目。不同的是,它已经被严格地剥离和重新命名。

`package com.proandroidgames;

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

public class BlobhunterActivity extends Activity {
/** Called when the activity is first created. */

private BHGameView gameView;

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

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

注意,在突出显示的部分,这段代码引用了一个名为BHGameView的类。BHGameView级扩展了GLSurfaceView,与星际战斗机中的SFGameView用途相同。直到在下一节中创建了BHGameView,前面的代码才会编译。

BHGameView

创建BHGameView类的代码非常简单,应该如下所示:

`package com.proandroidgames;

import android.content.Context;
import android.opengl.GLSurfaceView;

public class BHGameView extends GLSurfaceView {
private BHGameRenderer renderer;

public BHGameView(Context context) { super(context);

renderer = new BHGameRenderer();

this.setRenderer(renderer);

}

}`

再次注意,在突出显示的部分中,您引用了另一个类。是这个项目的游戏循环,将持有大部分代码。

游戏渲染器

现在,创建一个名为BHGameRenderer的新文件类。

`package com.proandroidgames;

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

import android.opengl.GLSurfaceView.Renderer;

public class BHGameRenderer implements Renderer{

private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime < BHEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(BHEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();

loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));

}

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

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

`gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@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.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
gl.glDisable(GL10.GL_DITHER);
}

}`

同样,如果你看一下BHGameRenderer的代码,你会注意到它只是你在星际战斗机中使用的代码的精简版。这将是足够让你真正进入 3d 游戏开发的代码。

BHEngine

建立项目需要创建的最后一个文件是BHEngine。在星际战斗机项目中,你创建了SFEngine文件,保存了游戏的所有全局常量、变量和方法。在 Blob Hunte r 项目中需要创建相同的文件来保存任何游戏引擎相关的代码。

`package com.proandroidgames;
import android.content.Context;
import android.view.Display;

public class BHEngine {
/Constants that will be used in the game/
public static final int GAME_THREAD_DELAY = 4000;
public static final int GAME_THREAD_FPS_SLEEP = (1000/60);
/Game Variables/

public static Context context;
}`

就是这样。现在,您应该有足够的代码来让您的项目脱离代码。然而,代码——以及包含它的项目——并没有真正做任何事情。让我们创建一个小型的三维测试来展示这个新项目可以做什么。

创建三维对象测试

在本节中,您将使用在上一节中设置的 Blob Hunter 项目,并向其中添加一些代码以生成一个 3-D 测试。你将使用星际战士中的一个精灵图像来创建一个围绕玩家旋转的快速图像。

首先拍摄侦察员的图像,如图 10–1 所示,并将其添加到斑点猎人项目的drawable-nodpi文件夹中。

images

图 10–1。 侦察兵形象

将图像添加到项目中后,在BHEngine类中为它创建一个常量。

注意:创建这个三维测试的步骤对你来说应该是非常熟悉的,并且在前几章中还记忆犹新。因此,对于一些基本的(之前已经介绍过的)技术,就不多解释了。然而,如果有些东西没有意义,试着回到前面的章节。

创建常数

打开BHEngine.java文件,添加以下突出显示的代码行:

`package com.proandroidgames;

import android.content.Context;
import android.view.Display;

public class BHEngine {
/Constants that will be used in the game/
public static final int GAME_THREAD_DELAY = 4000;
public static final int GAME_THREAD_FPS_SLEEP = (1000/60);
public static final int BACKGROUND = R.drawable.scout;
/Game Variables/

public static Context context;
public static Display display;
}`

你现在要创建一个平面正方形,就像你为星际战士所做的一样,然后将这张侦察图像作为纹理映射到它上面。

创建 BHWalls 类

在项目中创建一个名为BHWalls的新类。BHWalls类将在以后的章节中被用来创建墙壁,但是它在这里将作为一种创建平面正方形的方法。所有BHWalls类的代码都来自你为星际战士创建的SFBackground类;什么都没有改变。

`package com.proandroidgames;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;

public class BHWalls {

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, 0f,
1f, 1.0f,
0f, 1f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

public BHWalls() {
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);
}

public void draw(GL10 gl) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glFrontFace(GL10.GL_CCW);

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 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();
}
}`

现在您已经创建了一个类来构建您的对象,您将在游戏循环中实例化它。

实例化 BHWalls 类

当您创建BHWalls的实例化时,您也将创建两个 floats。这些将被用来在三维空间中移动飞船的图像。

注意:需要明确的是,您并没有用这个代码创建一个 3d 的船。你将只拍摄上一个项目中的一幅图像,并在三维空间中旋转它——这在星际战斗机中是不可能做到的。

`package com.proandroidgames;

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

import android.opengl.GLSurfaceView.Renderer;

public class BHGameRenderer implements Renderer{
private BHWalls background = new BHWalls();
private float rotateAngle = .25f;
private float rotateIncrement = .25f;

private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime < BHEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(BHEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();

loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));`

`}

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

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

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@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.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
gl.glDisable(GL10.GL_DITHER);
}

}`

BHWalls类已经被实例化,是时候调用loadTexture()方法了。

映射图像

在本节中,您将使用loadTexture()方法,它是在星际战士游戏中引入的。回想一下,loadTexture()方法会将图像映射到BHWalls的顶点上。

`package com.proandroidgames;

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

import android.opengl.GLSurfaceView.Renderer;

public class BHGameRenderer implements Renderer{
private BHWalls background = new BHWalls();
private float rotateAngle = .25f;
private float rotateIncrement = .25f;

private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime < BHEngine.GAME_THREAD_FPS_SLEEP){ Thread.sleep(BHEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();

loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));

}

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

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

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@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.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
gl.glDisable(GL10.GL_DITHER);
background.loadTexture(gl,BHEngine.BACKGROUND, BHEngine.context);
}

}`

在这一点上,你可能想知道使用 OpenGL ES 进行 2-D 和 3-D 的最大区别在哪里,因为到目前为止,你使用的所有代码都来自 2-D Star Fighter 项目。

OpenGL 处理二维和三维的主要区别在于你如何告诉系统渲染你的世界。在星际战士游戏中,你告诉 Open GL 使用glOrthof()方法将你的世界渲染成一个扁平的 2d 环境。

glOrthof()方法丢弃了 z 轴值的含义。也就是说,当你使用glOrthof()时,所有的东西都以同样的尺寸呈现,不管它离玩家有多远。

为了在 3d 中渲染你的对象,你将使用gluPerspective(),这将在下面讨论。

使用 gluPerspective()

gluPerspective()方法将考虑对象在 z 轴上与玩家的距离,然后以相对于其位置的正确大小和视角呈现对象。

gluPerspective()方法的参数与glOrthof()略有不同。要调用gluPerspective(),你需要给它传递一个有效的GL10实例,一个视角,一个方向,一个近的和一个远的 z 轴裁剪平面。

gluPerspective(gl10, angle, aspect, nearz, farz)

传递给gluPerspective()的角度指定了您希望 OpenGL 渲染的视角;任何超出该视角的东西都不会被看到。aspect参数是一个宽度/高度的浮点数。最后,近 z 裁剪平面和远 z 裁剪平面告诉 OpenGL 在哪里停止渲染。任何比近 z 平面更近或比远 z 平面更远的物体都将从渲染中被裁剪掉。

BHGameRenderonSurfaceChanged()方法中,您将添加对gluPerspective()的调用。

`package com.proandroidgames;

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

import android.opengl.GLSurfaceView.Renderer;

public class BHGameRenderer implements Renderer{
private BHWalls background = new BHWalls();
private float rotateAngle = .25f;
private float rotateIncrement = .25f;

private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;`

`@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime < BHEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(BHEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();

loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));

}

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

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

GLU.gluPerspective(gl, 45.0f, (float) width / height, .1f, 100.f);

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@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.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
gl.glDisable(GL10.GL_DITHER);
background.loadTexture(gl,BHEngine.BACKGROUND, BHEngine.context);
}
}`

在下一节中,您将使用名为drawBackground()的方法绘制背景平面。

创建 drawBackground()方法

你需要一个新的方法来将BHWalls顶点绘制到屏幕上,并移动它们来展示 OpenGL 的 3d 渲染。现在,创建一个drawBackground()方法,它将使用glRotatef()方法在 z 轴上围绕玩家旋转侦察员的图像。

OpenGL 方法glRotatef()有四个参数。第一个指定旋转的角度。第二个、第三个和第四个参数是 x、y 和 z 轴的标志,指示要对哪个轴应用旋转角度。

以下代码显示了上下文中的drawBackground()方法:

`package com.proandroidgames;

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

import android.opengl.GLSurfaceView.Renderer;

public class BHGameRenderer implements Renderer{
private BHWalls background = new BHWalls();
private float rotateAngle = .25f;
private float rotateIncrement = .25f;

private long loopStart = 0; private long loopEnd = 0;
private long loopRunTime = 0 ;

@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime < BHEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(BHEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();

loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));

}

private void drawBackground(GL10 gl){

GLU.gluLookAt(gl, 0f, 0f, 5f, 0f, 0f, 0f, 0f, 1f, 0f);
gl.glRotatef(rotateAngle, 0.0f, 1.0f, 0.0f);
gl.glTranslatef(0.0f, 0.0f, -3f);

background.draw(gl);
rotateAngle += rotateIncrement;

}

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

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

GLU.gluPerspective(gl, 45.0f, (float) width / height, .1f, 100.f);

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@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.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
gl.glDisable(GL10.GL_DITHER);
background.loadTexture(gl,BHEngine.BACKGROUND, BHEngine.context);
}
}`

注意这个例子中有一个新的方法调用。这个gluLookAt()调用告诉“摄像机”在世界的哪个地方寻找。如果您曾经使用过 3-D 渲染软件,如 Maya 或 3-D Studio Max,您可能会熟悉这样一个概念,即在渲染场景时,摄影机充当场景的查看者。OpenGL 并没有真正把相机作为一个独立的对象。然而,gluLookAt()方法是一种指向渲染以查看世界上特定位置的方法。

gluLookAt()方法接受一个有效的GL10对象加上三组三个参数。这三组三个参数是眼睛的 x、y 和 z 值(渲染器正在看的地方);“相机”中心(渲染器位于世界中的位置)的 x、y 和 z 值;以及表示哪个轴向上的 x、y 和 z 位置。如本例所述,您正在告诉“摄像机”查看位于0x0y5z的一个点,将其自身集中在0x0y0z点上,并且向上的方向朝向1y

画龙点睛

现在,调用drawBackground()方法,编译你的游戏。你应该看到一个侦察船的图像在你的前方和后方旋转。

`package com.proandroidgames;

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

import android.opengl.GLSurfaceView.Renderer;

public class BHGameRenderer implements Renderer{
private BHWalls background = new BHWalls();
private float rotateAngle = .25f;
private float rotateIncrement = .25f;

private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime < BHEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(BHEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();

drawBackground(gl);

loopEnd = System.currentTimeMillis(); loopRunTime = ((loopEnd - loopStart));

}

private void drawBackground(GL10 gl){

GLU.gluLookAt(gl, 0f, 0f, 5f, 0f, 0f, 0f, 0f, 1f, 0f);
gl.glRotatef(rotateAngle, 0.0f, 1.0f, 0.0f);
gl.glTranslatef(0.0f, 0.0f, -3f);

background.draw(gl);
rotateAngle += rotateIncrement;

}

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

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

GLU.gluPerspective(gl, 45.0f, (float) width / height, .1f, 100.f);

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@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.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
gl.glDisable(GL10.GL_DITHER);
background.loadTexture(gl,BHEngine.BACKGROUND, BHEngine.context);
}
}`

在下一章中,你将为 Blob Hunter 创建一个 3d 环境。这将是一个基于走廊的环境,很像早期的末日雷神之锤 FPS 游戏。

总结

在这一章中,你为 3d 游戏 Blob Hunter 创建了项目。您还了解了 OpenGL ES 渲染二维和三维环境的不同之处。从二维环境创建三维环境的关键在于你告诉 OpenGL 渲染你的对象的方式。OpenGL 通过允许你使用相同的顶点和纹理,并且只改变几行代码,使得从 2d 游戏到 3d 游戏的过程变得非常容易。当你创建一个物体在三维空间旋转的快速演示时,这个过程就清楚了。

十一、创造身临其境的环境

在前一章中,你学习了如何将一些新的 2d 游戏开发技巧应用到 3d 游戏中。您了解了 OpenGL ES 如何以三维方式呈现对象,以及如何移动这些对象以获得三维效果。

在这一章中,你将为你的 3d 游戏建立一个环境。因为这是一个 3d 开发的初级读本,你将学习如何创建所有 FPS 游戏的标准-走廊。您将使用在前面章节中学到的技术来创建一个 L 形走廊供玩家导航。

最后,在本书的最后一章,你将了解到你的玩家如何通过这个走廊,并实施一些碰撞检测,以防止他们穿墙而过。

让我们从你在第十章中创建的BHWalls类开始。

使用 BHWalls 类

在前一章中,您创建了一个小型的三维测试。作为这个测试的一部分,您创建了一个BHWalls类,它创建了一个方形的墙壁形状并对其应用了纹理。这如何应用到你要创建的 3d 游戏中呢?让我们来看看一个 3d 世界来找出答案。在本节中,您将学习如何从上一章的小型BHWalls测试转移到 3d 走廊。

现在看看你的周围;你看到了什么?走到外面;向上看,向下看。

如果你在室内环顾四周,你可能会看到一些墙。你看不到墙外的房间或环境,因为墙的结构挡住了你的视线。你可能知道你在一所房子或一栋大楼里,但是你的眼睛只能看到环境中没有遮挡的部分。

如果你搬到外面,这同样适用;是的,环境大了很多,你能看到的也多了很多,但你看到的依然是有限的空间。总有一些东西会限制你的视野——可能是房子,树木,甚至是你站的地方。

现在将您的注意力转移到计算机环境。直到你放置物体到你的环境中,你的玩家将拥有一个 360 度无阻碍的视野。由您决定将视图限制在您希望他们体验特定区域。在本书前面你创作的 2d 游戏星际战士中,很容易控制玩家的世界观。您创建了场景的单一静态视图。游戏的所有动作都发生在这一个视图中。

在 3d 游戏中,控制玩家看到的东西有点困难,因为玩家可以控制他们对世界的看法。因此,你必须以这样一种方式放置物体和创造环境,即你控制玩家可以看到什么,即使他们将控制如何看到它。

在 3d 第一人称游戏的早期,对游戏环境的控制是通过走廊来实现的。回想一下一些最受欢迎的早期第一人称射击游戏,如末日雷神之锤沃尔芬斯坦城堡。他们都使用房间和走廊来引导你去你需要去的地方,但让你感觉好像你在一个更大的,自由漫游的环境中。

您已经具备了创建有效的三维走廊所需的所有技能。不管任何一条走廊有多长,它都可以由一系列的墙组成。在第十章中,你建了一堵墙,并在玩家周围移动。你可以简单地多建 5、10 或 15 堵这样的墙,把它们放在特定的位置,创造出一条长长的转弯走廊。

从多个 BHWalls 实例创建走廊

让我们来看看BHWalls到底创造了什么。

`…

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,
};

…`

这段代码是BHWalls级的一部分——你应该还记得星际战士中非常重要的一部分。这个数组表示一个可以呈现到屏幕上的正方形。虽然这个正方形理论上可以代表任何东西,但对你来说,它是一面墙。

您可以渲染多面墙,然后使用glTranslatef(),您可以将每面墙移动到位。代码看起来会像这样:

`gl.glTranslatef(0.0f, 0.0f, 0f);
gl.glRotatef( 45.0f, 0.0f,1.0f, 0.0f);
corridor.draw(gl);

gl.glTranslatef(0.0f, 0.0f, 1f);
gl.glRotatef( 45.0f, 0.0f,1.0f, 0.0f);
corridor.draw(gl);

gl.glTranslatef(-1.0f, 0.0f, 0f);
gl.glRotatef( 45.0f, 0.0f,1.0f, 0.0f);
corridor.draw(gl);

gl.glTranslatef(-1.0f, 0.0f, 1f);
gl.glRotatef( 45.0f, 0.0f,1.0f, 0.0f);
corridor.draw(gl);

gl.glTranslatef(0.0f, 0.0f, 0f);
gl.glRotatef( 0.0f, 0.0f,0.0f, 0.0f);
corridor.draw(gl);

…`

虽然这比你可以在游戏中直接使用的任何东西都要更加伪代码,但你可以看到如何通过渲染几面墙并使用 OpenGL 来平移和旋转它们来创建一个走廊;你可以拼凑出一条走廊。

然而,这种方法有其缺点。首先,这很费时间。要建造一个相当大的走廊需要很长时间才能砌好所有的墙。第二,由于要跟踪这么多独立的对象,搞砸一些事情会非常容易。你可能会弄不清哪堵墙通向哪里,把某些东西转错方向。最后,有那么多的对象需要 OpenGL 来创建、移动和渲染,你的游戏将不会尽可能的高效。

有更好的方法来创造游戏环境。您可以用一个对象一次构建整个走廊。

使用 BHCorridor 类

在本节中,您将创建一个新的类,BHCorridorBHCorridor类将负责从多个多边形创建一条走廊。然后,您将能够将该道路视为单个对象。

在下一章,也是最后一章,能够把走廊当作一个单独的物体将会非常方便,你将允许玩家在走廊中导航。这将需要四处移动对象,当您要跟踪的对象较少时,这将容易得多。

让我们构建BHCorridor类。我们将遍历整个类,因为在BHCorridor和你在上一章中使用的BHWalls类之间会有一些不同。

建造 BHCorridorClass

在本节中,您将开始构建BHCorridor类。这个类将用于一次创建一个完整的三维走廊,而不是将许多独立的墙拼凑在一起。首先,在你的 Blob Hunter 项目中创建一个名为BHCorridor的新类。

`package com.proandroidgames;

public class BHCorridor {

}`

接下来,您需要设置您的阵列。

`package com.proandroidgames;

import java.nio.FloatBuffer;

public class BHCorridor {
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;

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

private float vertices[] = {

};

private float texture[] = {

};

}`

BHWalls类中,甚至在本书前面的SFBackground中,vertices[]数组将保存 12 个值,如下所示:

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, };

这些值表示正方形角的 x、y 和 z 轴坐标(更准确地说,是构成正方形的两个三角形的角)。

您将通过向数组中一次性输入构建整个走廊所需的所有坐标来构建此逻辑(而不是实例化多个墙对象并将它们粘贴在一起)。你将要建造的走廊将是 L 形的,如图图 11–1 所示。

images

图 11–1。?? 完工的走廊形状

Figure 11–1 中的图像展示了您将要创建的走廊的形状。这个图像中添加了一个任意的纹理来帮助你看清形状。请注意,走廊呈 L 形,向左弯曲,由四个主要的墙段组成。这些部分被标记为 A、B、C 和 D,我们在建造墙壁时将参考这些字母。

用 vertices[]数组构建多面墙

让我们设置vertices[]数组来创建多面墙。您可以从墙段 a 开始。这是一面平墙,正对着站在走廊尽头的玩家。

private float vertices[] = { -2.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, -2.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f,

接下来,您将为墙段 b 添加顶点。此段与右侧的 A 相连,并在 z 轴上向玩家延伸。

`private float vertices[] = {
-2.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
-2.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,

1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 5.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 5.0f,`

现在,为墙段 c 添加顶点。该墙段与墙段 B 相对,也向玩家延伸。

`private float vertices[] = {
-2.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
-2.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,

1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 5.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 5.0f,

0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 5.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 5.0f,`

最后,添加墙段 d 的顶点。这是与墙段 A 相对的墙段,从墙段 C 的末端向屏幕左侧延伸。

`private float vertices[] = {
-2.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
-2.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,

1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 5.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 5.0f,

0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 5.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 5.0f,

-2.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
-2.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
};`

就是这样。这些都是建造走廊所需的顶点。

注意:因为您使用的是 Java,所以像这样在其他.java文件中存储大型数据数组并非不可能。然后,您可以加载这些文件并从中读取数组。

随着vertices[]数组的完成,您可以创建texture[]数组。像vertices[]阵列一样,texture[]阵列需要一些小的调整,然后才能用于对走廊应用纹理。

创建纹理[]数组

在前面的章节中,您构建了一个类似如下的texture[]数组:

private float texture[] = { -1.0f, 0.0f, 1.0f, 0f, -1f, 1f, 1f, 1.0f, };

texture[]数组包含映射点,它告诉 OpenGL 纹理如何适合顶点。既然您已经创建了一个新的有四组不同顶点的vertices[]数组,那么您还需要一个包含映射点集的texture[]数组:每组顶点一个。

尽量不要纠结于如何在没有 z 轴坐标的情况下将纹理映射到走廊墙壁上。在texture[]数组中的映射点对应于纹理的角落,而不是墙的顶点。因此,因为您将把整个纹理映射到每面墙上,所以四组纹理映射点将是相同的。

`private float texture[] = {
-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

};`

现在,您的BHCorridor类应该是这样的:

`package com.proandroidgames;

import java.nio.FloatBuffer;

public class BHCorridor {
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;

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

`private float vertices[] = {
s
-2.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
-2.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,

1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 5.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 5.0f,

0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 5.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 5.0f,

-2.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
-2.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,

};

private float texture[] = {

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

};

}`

接下来,添加一个将创建缓冲区的构造函数,就像您对BHWallsSFBackground所做的那样。

`public BHCorridor() {
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);
}`

之后,添加loadTexture()方法。这里也没有什么大的变化,所以不需要进一步的解释(如果你不确定loadTexture()方法是如何工作的,回头查看第四章中的详细解释)。

`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()方法。记住,渲染器调用draw()方法来绘制走廊。在BHCorridor.draw()中有一些变化来解释vertices[]数组中的多组顶点。

创建 draw()方法

您将使用指向墙段顶点的指针的glDrawArrays()方法。看看你的vertices[]阵。该阵列在视觉上被分成四组,每组四个顶点——墙段的每个角一个顶点。你需要告诉 OpenGL 每个墙段的开始和结束位置。因此,您将把每个墙段的起点和顶点数传递到glDrawArrays()中。

墙段 A 的glDrawArrays()调用将如下所示:

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0,4);

这一行告诉 OpenGL 从数组中的位置 0 开始,读取四个顶点。按照这种逻辑,墙段 2 将从数组中的位置 4 开始,延伸到另外四个顶点,依此类推。

由于对glDrawArrays()的调用是draw()方法与BHCorridor的唯一区别,您的方法应该如下所示:

`public void draw(GL10 gl) {

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

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

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

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 4,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 8,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 12,4);

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

}`

你完成的BHCorridor类显示在清单 11–1 中。

清单 11-1。BHCorridor.java??

`package com.proandroidgames;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;`

`import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;

public class BHCorridor {
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;

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

private float vertices[] = {

-2.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
-2.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,

1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 5.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 5.0f,

0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 5.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 5.0f,

-2.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
-2.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,

};

private float texture[] = {

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f, 1f, 1.0f,

};

public BHCorridor() {
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);
}

public void draw(GL10 gl) {

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

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

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

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 4,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 8,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 12,4);

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

}

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();
}

}`

添加墙壁纹理

随着BHCorridor类的创建并能够将纹理映射到墙壁上,是时候添加一个漂亮的墙壁纹理来替换图 11–1 中显示的临时纹理了。图 11–2 展示了你将要映射到走廊墙壁上的墙壁纹理。

images

图 11–2。 墙壁纹理

首先将这张图片添加到您的drawable.nodpi文件夹中。然后,在BHEngine中添加对它的引用,如下所示:

public static final int BACK_WALL = R.drawable.walltexture256;

当这个新纹理被应用时,你完成的墙将会出现如图 Figure 11–3 所示。

images

图 11–3。 带纹理的墙壁

在下一节中,你将实例化一个BHCorridor类的副本,并用你刚刚添加到斑点猎人的新纹理将它绘制到屏幕上。

呼叫 BHCorridor

正如本书前面的星际战士项目,所有斑点猎人的渲染都在游戏渲染类中执行。在第十章中,你创建了这个类,BHGameRenderer。在本节中,您将添加一个将在游戏循环中调用的drawCorridor()方法。打开BHGameRenderer.java文件,添加一个BHCorridor的新实例,如下所示:

private BHCorridor corridor = new BHCorridor();

现在,您可以创建一个drawCorridor()方法。该方法将设置gluLookAt()(参见第十章了解如何工作的描述),并在其 x 和 y 轴上旋转走廊,如图图 11–3 所示。

`private void drawCorridor(GL10 gl){

GLU.gluLookAt(gl, 0f, 0f, 5f, 0f, 0f, 0, 0, 1, 0);
gl.glRotatef( 40.0f, 1.0f,0.0f, 0.0f);
gl.glRotatef( 20.0f, 0.0f,1.0f, 0.0f);
gl.glTranslatef(0.0f, 0.0f, -3f);

corridor.draw(gl);

}`

再次强调,所有这些代码看起来都应该非常熟悉。到这本书的这一点,应该很少有新的代码。你只是把你在创建的 2d 游戏中学到的代码和技能应用到 3d 环境中。

总结

在本章中,您学习了如何在一次调用中创建一个多多边形对象-走廊。然后,你给走廊添加了一个纹理,并用BHGamerRenderer渲染到屏幕上。这是你学习曲线上的一个尖峰,因为它教会你如何管理一些非常复杂的对象。几乎任何你能想到的 3d 环境,从广阔的城市景观到复杂的迷宫,都可以用这种技术建造。

在本书的最后一章,第十二章,你将创建穿越这个 3d 走廊所需的控件,包括碰撞检测,以确保玩家不会穿过你的墙壁。

十二、在三维环境中导航

你已经进入了学习 Android 游戏开发冒险的最后一章。在这本书里,你从零开始,创造了一个二维滚动射击游戏。从创建那个游戏中学到的技能,你能够创建一个 3d 游戏的环境。虽然这本书没有涵盖使用你已经获得的所有技能或一步一步地创建一个完整的 3-D 游戏,你会学到足够的基础知识,希望使用这种逻辑来完成游戏。在本章中,您将了解当您试图创建一个控制系统来导航三维走廊时,等待您的是什么样的不同。

当你为 2d星际战士游戏创建一个控制系统时,动作很简单。玩家只能向左或向右移动。在 Blob Hunter 中,玩家应该有在 z 平面上 360 度移动的自由。让我们来看看这会给你带来什么样的挑战。

在这一章的最后,我提供了一个 3D 项目的关键文件列表。选择这些文件是因为它们的复杂性、更改的数量或者在编译项目时容易引起问题。如果您在本章末尾运行 3D 项目时遇到问题,请对照摘要后列出的文件检查您的文件。

创建控制界面

在本节中,您将创建控制界面,即玩家与游戏交互的方式。

星际战斗机中,控制界面是简单的左右运动。然而,在 3d 游戏中,玩家希望能够向左、向右、向前、向后移动,还可能向上或向下看。尽管需要跟踪更多的控制,你为星际战斗机学习的基本概念仍然适用。

让我们借用一些星际战士的代码,并快速改编它,让玩家在走廊中前进。

目前,您的BlobhunterActivity应该如下所示:

`package com.proandroidgames;

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

public class BlobhunterActivity extends Activity {
private BHGameView gameView;

@Override
public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
gameView = new BHGameView(this);
setContentView(gameView);
BHEngine.context = this;
}
@Override
protected void onResume() {
super.onResume();
gameView.onResume();
}

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

}`

你将修改你在星际战士中创建的onTouchEvent()方法来处理向前运动。

注意:在本章中,您将只添加前进运动控制。但是,您可以轻松地调整该控件来处理向后运动。

在添加您的onTouchEvent()方法之前,您需要向BHEngine添加一些常量。

编辑 BHEngine

这里的目标是帮助你追踪玩家正在试图做什么,以及玩家在环境中的位置。为此,将以下几行添加到您的BHEngine.java文件中:

public static final int PLAYER_FORWARD = 1; public static final int PLAYER_RIGHT = 2; public static final int PLAYER_LEFT = 3;
public static final float PLAYER_ROTATE_SPEED = 1f; public static final float PLAYER_WALK_SPEED = 0.1f; public static int playerMovementAction = 0;

PLAYER_FORWARDPLAYER_RIGHTPLAYER_LEFT常量将用于跟踪玩家触摸了什么控件,指示玩家想要在环境中移动到哪里。PLAYER_ROTATE_SPEEDPLAYER_WALK_SPEED常量分别表示玩家的视角在 y 轴上旋转的速度和玩家在环境中行走的速度。最后,playerMovementAction跟踪哪个动作(PLAYER_FORWARDPLAYER_RIGHTPLAYER_LEFT)是当前动作。

现在您的常量已经就位,您可以在BlobhunterActivity.java中创建控制界面。

编辑博客互动

您需要添加到BlobhunterActivity的第一个代码是对BHEngine.display方法的调用。你需要初始化display变量,这样控制界面就可以调用它来确定玩家触摸了屏幕上的什么地方。

`...

@Override
public void onCreate(Bundle savedInstanceState) {

BHEngine.display = ((WindowManager)
getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

super.onCreate(savedInstanceState);
gameView = new BHGameView(this);
setContentView(gameView);
BHEngine.context = this;
}

...`

初始化display后,向BlobhunterActivity类添加一个onTouchEvent()方法:

`...

@Override
public boolean onTouchEvent(MotionEvent event) {
return false;
}

...`

如果你还有星际战士项目,可以直接从它的控制界面复制粘贴以下代码到 Blob Hunter 的新onTouchEvent()方法中。如果你不再有 Star Fighter 项目的代码,可以从 Apress 网站下载完整的项目。

注意:如果你要从星际战士项目中复制粘贴,一定要重命名适当的常量和变量,使之与斑点猎人项目中的相应。

`...

@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();

int height = BHEngine.display.getHeight() / 4;
int playableArea = BHEngine.display.getHeight() - height;

if (y > playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x < BHEngine.display.getWidth() / 2){
BHEngine.playerMovementAction = BHEngine.PLAYER_LEFT;
}else{
BHEngine.playerMovementAction = BHEngine.PLAYER_RIGHT;
}
break;
case MotionEvent.ACTION_UP:
BHEngine.playerMovementAction = 0;
break;
}
}

return false;
}

...`

接下来,让我们添加检测向前运动的控件。

让你的球员向前移动

现在,onTouchEvent()使用y >playableArea条件来检测玩家是否触摸了屏幕的下部。添加一个else语句来检测对屏幕上部的触摸。您将使用这个触摸屏幕的上部来确定用户想要向前移动。

`...

@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();`

`int height = BHEngine.display.getHeight() / 4;
int playableArea = BHEngine.display.getHeight() - height;
if (y > playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x < BHEngine.display.getWidth() / 2){
BHEngine.playerMovementAction = BHEngine.PLAYER_LEFT;
}else{
BHEngine.playerMovementAction = BHEngine.PLAYER_RIGHT;
}
break;
case MotionEvent.ACTION_UP:
BHEngine.playerMovementAction = 0;
break;
}
}else{
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
BHEngine.playerMovementAction = BHEngine.PLAYER_FORWARD;
break;
case MotionEvent.ACTION_UP:
BHEngine.playerMovementAction = 0;
break;
}
}

return false;
}

...`

在这段新代码中,您所做的就是检测玩家是否触摸了屏幕的上部,如果是,您就将playerMovementAction设置为PLAYER_FORWARD

请记住,当你创建一个完整的游戏时,你会想稍微调整一下,也考虑到向后触摸控制,可能还有一些向上或向下平移的控制。在下一节中,您将对BHGameRenderer类中的这些控件做出反应,并相应地在走廊中移动玩家。

穿过走廊

穿过走廊有点棘手,但通过一些练习,你可以创建一个平稳的控制系统。诚然,如果你对 OpenGL 足够熟练,能够创建自己的矩阵并执行自己的矩阵乘法,你将能够优化一个伟大的相机系统。然而,从本书开始,目标就一直是让你使用 OpenGL 的内置工具,作为手动过程的学习曲线的替代品。

打开BHGameRenderer.java,这是你的游戏循环代码存放的地方。你需要做的第一件事是添加几个变量来帮助追踪玩家的位置。

`...

public class BHGameRenderer implements Renderer{
private BHCorridor corridor = new BHCorridor();
private float corridorZPosition = -5f;

private float playerRotate = 0f;

private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

...`

corridorZPosition变量最初设置为-5。这表示玩家在走廊中的初始位置。值-5 应该将播放器设置在走廊的末端,因为走廊,正如您在BDCorridor类中设置的那样,向 z 轴上的 4 个单位延伸。因此,从-5(或向玩家/屏幕方向 5 个单位)开始播放会给人一种玩家正站在走廊入口处的感觉。

接下来,找到您在上一章中创建的drawCorridor()方法,并删除它的所有内容,除了对走廊的draw()方法的调用,如下所示:

`private void drawCorridor(GL10 gl){

corridor.draw(gl);

}`

使用switch…case语句,类似于星际战士中的语句,你将探测到玩家试图采取的动作。然而,如果向前的动作是玩家想要做的,你该如何应对呢?

星际战士项目中,你只需向左或向右移动玩家。这两种运动都是通过 x 轴上的正值或负值来完成的。然而,在一个三维环境中,在 x 轴上加减将会导致一个侧向或扫射的运动,这不是你在这里要做的。你想让玩家向前移动,让他们把头转向左边或右边。这些动作与你在星球大战中使用的动作完全不同。

要向前移动播放器,您需要向 z 轴添加值。回想一下,您正在沿着 z 轴查看走廊,走廊的 z 轴的 0 值位于远处的墙上。因此,你从-5 开始(见corridorZPosition变量)并移动到 0。

为了模拟转动玩家的头部,你需要沿着 y 轴旋转,而不是平移:你实际上并不想沿着 y 轴或 x 轴移动;而是,就像现实生活中转头一样,想绕轴旋转。

添加一条switch . . . case语句,相应地调整corridorZPositonplayerRotate值。这和星际战斗机用的工艺一样,所以就不详细讨论了。如果它看起来不熟悉,通过第五章中的星际战斗机代码进行检查。

`private void drawCorridor(GL10 gl){

switch(BHEngine.playerMovementAction){
case BHEngine.PLAYER_FORWARD:
corridorZPosition += BHEngine.PLAYER_WALK_SPEED;
break;
case BHEngine.PLAYER_LEFT:
playerRotate -= BHEngine.PLAYER_ROTATE_SPEED;
break;
case BHEngine.PLAYER_RIGHT:
playerRotate += BHEngine.PLAYER_ROTATE_SPEED;
break;
default:
break;
}

corridor.draw(gl);

}`

在下一节中,您将调整玩家在走廊中移动时的位置或视角。

调整玩家的视角

如前所述,OpenGL 不像一些 3d 系统那样有摄像机的概念。更确切地说,可以说,你是在通过欺骗的方式让环境看起来对玩家来说是特定的。

你在 Star Fighter 中用来移动场景中 2-D 模型的相同的平移和旋转也将被用来旋转和平移走廊,以便玩家相信他或她正在穿过它。

drawCorridor()方法添加一个 translate,它将沿着 z 轴移动模型,并添加一个 rotate,它将根据玩家正在看的地方旋转模型。

`private void drawCorridor(GL10 gl){

switch(BHEngine.playerMovementAction){
case BHEngine.PLAYER_FORWARD:
corridorZPosition += BHEngine.PLAYER_WALK_SPEED;
break;
case BHEngine.PLAYER_LEFT:
playerRotate -= BHEngine.PLAYER_ROTATE_SPEED;
break;
case BHEngine.PLAYER_RIGHT:
playerRotate += BHEngine.PLAYER_ROTATE_SPEED;
break;
default: break;
}

GLU.gluLookAt(gl, 0f, 0f, 0.5f, 0f, 0f, 0f, 0f, 1f, 0f);
gl.glTranslatef(-0.5f, -0.5f, corridorZPosition);
gl.glRotatef( playerRotate, 0.0f,1.0f, 0.0f);

corridor.draw(gl);

}`

编译并运行您的代码;你现在应该有一个基本的导航系统向前移动,并向左转和向右转。使用你已经学过的技能做一点工作,你可以很容易地添加一些碰撞检测来防止玩家穿墙。自己尝试这些例子:

  • 添加一个导航控件,允许玩家在走廊中倒车。这里有一个这样做的提示:即使在屏幕上创建一个触摸,当触摸时,将从当前 z 轴位置减去一个给定的整数值。
  • 创建碰撞检测系统,防止玩家穿墙而过。给你一个提示:追踪玩家当前的轴线位置,并对照走廊墙壁的已知位置进行测试。请记住,走廊墙壁不会移动。类似这样的东西可能会对你有所帮助:

if corridorZPosition <= -5f){ corridorZPosition = -5f; } if corridorZPosition >= 0f){ corridorZPosition = 0f; }

  • 创建一个导航系统,让玩家在环境中上下查看。作为对这项任务的一个提示,考虑它听起来比实际困难。只需添加一个触摸事件,该事件将在 x 轴上的新旋转中增加或减少值。这将使玩家的视野向上或向下旋转。

你拥有创建一个全功能 3d 游戏所需的技能,而且令人惊讶的是,这些技能与你创建一个全功能 2d 游戏所用的技能是一样的;你刚刚增加了更多的细节。

总结

我希望你喜欢这本介绍创建一些有趣的休闲游戏所需的基本技能的入门书,并希望你继续练习和扩展这些技能。关于 OpenGL ES 和 Android 冰激凌三明治的内容远不止这本书所涵盖的,但是你现在已经有了一个很好的知识基础,这将帮助你在 Android 游戏开发的世界中规划你的课程。

查看关键的三维代码

下面的清单包含了在 Blob Hunter 无法正常运行时仔细检查您的工作所需的所有代码。我选择了 BHEngine.java、BHCorridor.java 和 BHGameRenderer.java。这些文件要么接触最多的代码——像 BHEngine,包含复杂的概念——像 BHCorridor,要么执行最多的功能——像 BHGameRenderer。

您可以检查的第一个文件是 BHEngine.java,如清单 12–1 所示。BHEngine 是关键设置文件,它包含整个项目中使用的设置。因为这个文件在 Blob Hunter 项目中被广泛使用,所以它最有可能在编译时引起问题。

清单 12–1。BHEngine.java??

`package com.proandroidgames;

import android.content.Context;
import android.view.Display;

public class BHEngine {
/Constants that will be used in the game/
public static final int GAME_THREAD_DELAY = 4000;
public static final int GAME_THREAD_FPS_SLEEP = (1000/60);
public static final int BACK_WALL = R.drawable.walltexture256;
public static final int PLAYER_FORWARD = 1;
public static final int PLAYER_RIGHT = 2;
public static final int PLAYER_LEFT = 3;
public static final float PLAYER_ROTATE_SPEED = 1f;
public static final float PLAYER_WALK_SPEED = 0.1f;
/Game Variables/
public static int playerMovementAction = 0;
public static Context context;
public static Display display;
}`

清单 12–2 显示了 BHCorridor.java 文件。这个文件可能会给您带来问题,因为它包含了一个代码概念,这个概念不仅是抽象的,而且在本书的第一部分中也没有涉及到。[顶点]和纹理的结构?数组是整个项目功能的关键。如果数组设置不正确,项目将无法按预期运行。检查该文件时,请密切注意数组和数组定义。

清单 12–2。BHCorridor.java??

import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer;

`import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;

public class BHCorridor {

private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;

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

private float vertices[] = {
-2.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
-2.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,

1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 5.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 5.0f,

0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 5.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 5.0f,

-2.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
-2.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
};

private float texture[] = {
-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,

-1.0f, 0.0f,
1.0f, 0f,
-1f, 1f,
1f, 1.0f,`

`};

public BHCorridor() {
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);
}

public void draw(GL10 gl) {

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

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

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

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 4,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 8,4);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 12,4);

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

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();
}
}`

Blob Hunter 中的最后一个密钥文件是 BHGameRenderer.java。这个文件包含了 Blob 猎人游戏的游戏循环。就像 Star Fighter 一样,游戏循环是最有可能出现代码问题的地方,因为它拥有项目中所有文件中最多的代码。清单 12–3 提供了 BHGameRenderer.java 的源代码。

清单 12–3。BHGameRenderer.java??

`package com.proandroidgames;

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

import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLU;

public class BHGameRenderer implements Renderer{
private BHCorridor corridor = new BHCorridor();
private float corridorZPosition = -5f;
private float playerRotate = 0f;

private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;

@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
// TODO Auto-generated method stub
try {
if (loopRunTime < BHEngine.GAME_THREAD_FPS_SLEEP){
Thread.sleep(BHEngine.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();

drawCorridor(gl);

loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));

}

private void drawCorridor(GL10 gl){

if (corridorZPosition <= -5f){
corridorZPosition = -5f;
}
if (corridorZPosition >= 0f){
corridorZPosition = 0f;
}

switch(BHEngine.playerMovementAction){
case BHEngine.PLAYER_FORWARD:
corridorZPosition += BHEngine.PLAYER_WALK_SPEED;
break;
case BHEngine.PLAYER_LEFT:
playerRotate -= BHEngine.PLAYER_ROTATE_SPEED;
break;
case BHEngine.PLAYER_RIGHT:
playerRotate += BHEngine.PLAYER_ROTATE_SPEED;
break;
default:
break;
}

GLU.gluLookAt(gl, 0f, 0f, 0.5f, 0f, 0f, 0f, 0f, 1f, 0f);
gl.glTranslatef(-0.5f, -0.5f, corridorZPosition);
gl.glRotatef( playerRotate, 0.0f,1.0f, 0.0f);

corridor.draw(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();

GLU.gluPerspective(gl, 45.0f, (float) width / height, .1f, 100.f);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();`

`}

@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.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
gl.glDisable(GL10.GL_DITHER);

corridor.loadTexture(gl, BHEngine.BACK_WALL, BHEngine.context);

}

}`

第一部分:2D 游戏的策划与创作

这本书的第一部分,第一章 - 9 ,将带你经历规划和创建一个可玩的 2D 安卓游戏——星际战士的过程。这款游戏的创作将遵循一条独特的逻辑路径。首先,你将计划并撰写游戏背后的故事。接下来,您将为游戏创建背景。然后你将创建可玩和不可玩的角色。最后,您将创建武器系统和碰撞检测。在第九章的中遵循将游戏部署到移动设备所需的步骤之前,在第八章的结尾,我提供了您在第一部分中创建或修改的最重要的 2D 文件的完整代码清单。使用这些清单来比较您的代码,并确保每个游戏都能正常运行。这将为第二部分的 3D 开发阶段做好准备:“创建 3D 游戏”(第十章 - 第十二章)。

第二部分:创建 3D 游戏

在本书的第一部分,你为 Android 平台创建了你的第一个 2D 游戏。在第二部分 ( 第十章–第十二章)中,你将学习如何使用这些相同的技能来创建一个 3D 游戏。虽然你不会在第十章 - 第十二章中创建完整的 3D 游戏,但你将学习如何使用在第一章 - 第九章中学到的技能来创建引人入胜的 3D 游戏。最后,在第十二章的结尾,如同在第八章的结尾一样,您将再次看到本书这一部分的关键文件的源代码。这将帮助你检查你的工作,并加强你作为一个游戏开发者的技能。

posted @   绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示