安卓游戏秘籍-一-

144 阅读1小时+

安卓游戏秘籍(一)

原文:Android Game Recipes

协议:CC BY-NC-SA 4.0

零、简介

欢迎来到安卓游戏食谱。这本书是专门为帮助你解决在为 Android 平台创建游戏的过程中可能遇到的许多常见问题而写的。Android 游戏开发可能是一个有趣、愉快和有益的过程;但是它也不是没有陷阱。在开发过程中似乎总会出现一些难以解决的问题。我希望这本书能为你提供这些解决方案。

我为 Android 开发了多个游戏,在开发过程中遇到了很多问题。我的经历,以及我找到的解决方法,被汇编成 17 章,每一章都由主要的主题分开。概述如下是本书的章节和对每一章内容的快速总结。

第一章:入门。这一章涵盖了你需要充分利用这本书的技巧和软件。第一章还包括对 Android 游戏和 OpenGL ES 版本 1 和 2 / 3 的快速介绍。

第二章:加载图像。在不同的情况下,可能需要使用或不使用 OpenGL ES 来加载图像。如果你正在创建一个闪屏,你可能不想使用 OpenGL。本章中的食谱可以帮助你在不使用 OpenGL 的情况下创建一个闪屏。

第三章:闪屏。在这里你可以找到创建闪屏时常见问题的解决方案。这些问题可能包括加载屏幕图像、多个图像之间的转换以及在闪屏后加载游戏。

第四章:菜单画面。在本章中,您将学习常见菜单屏幕问题的解决方案,例如创建按钮、载入选项、锁定屏幕旋转和检测屏幕分辨率。

第五章:读取玩家输入。本章中的配方解决了游戏中与读取玩家输入相关的问题,例如触摸屏输入、多点触摸和手势。

第六章:装载一个斜板。能够加载一个 spritesheet 对于创建一个游戏是必不可少的。本章包含加载 spritesheet 图像、动画显示多个 spritesheet 图像以及存储 sprite sheet 的解决方案。

第七章:滚动背景。现实主义的关键,第七章帮助你解决与在屏幕上滚动背景图像相关的问题,例如将图像加载到屏幕上和改变滚动速度。

第八章:滚动多个背景。在这一章中,你将会看到如何滚动多个背景图像来呈现前景、中间背景和距离的方法。

第九章:将背景与角色运动同步。在这一章中,你会找到改变与角色运动相关的背景运动的方向和速度的方法。

第十章:使用瓷砖建造关卡。您将学习如何从图形块中创建侧滚和平台游戏的关卡。使用可重复的瓷砖是一种屡试不爽的创建游戏关卡的方法。

第十一章:移动一个角色。这涵盖了当试图激活一个可玩角色时可能出现的问题,从行走、奔跑、跳跃到战斗。

第十二章:移动一个敌人。像《??》第十一章一样,这一章也讨论了在屏幕上移动角色。然而,这一章更侧重于创建基于人工智能(不可玩)的角色时遇到的具体问题,例如在预定的路径上移动。

第十三章:移动一个有障碍物的角色。大多数游戏都没有光滑的表面来玩。也就是说,许多游戏关卡包含玩家需要导航的障碍和斜坡。在这一章中,你会遇到如何让你的可玩角色通过这些障碍的方法。

第十四章:发射武器。在这一章中,你将学习如何射击或投掷武器。当制作包括动画和轨迹计算的射弹动画时,有一些特定的问题需要解决。

第十五章:碰撞检测。这是游戏开发中的一个关键话题,涵盖了复杂的碰撞检测问题。你会发现如何检测屏幕上(游戏中)物体之间的互动并做出反应的方法。

第十六章:记分。玩家跟踪他们在游戏中的进程的一种方式是通过分数。《??》第十六章中的解决方案可以帮助你计算玩家的分数并将分数写到屏幕上。

第十七章:守时。一些游戏是基于时间的,或者包含基于时间的关卡和挑战。第十七章涵盖了如何实现和跟踪游戏内动作调度时间到期的解决方案。

一、入门指南

欢迎来到安卓游戏食谱。这本书很像一本烹饪书。它旨在解决在你为 Android 平台开发游戏时可能出现的特定的、常见的问题。解决方案是以一种经过充分测试、深思熟虑的方法提供的,这种方法易于遵循并且易于适应多种情况。

假设你知道鸡汤的原理,但是你不确定如何把一些鸡肉和蔬菜做成汤。查阅一本标准的厨房食谱会给你一步一步的制作这道汤的食谱。同样的,你将能够使用 Android 游戏配方来找出如何在游戏中编写特定场景的代码——从创建闪屏到在消灭敌人时使用碰撞检测。

在你进入食谱之前,重要的是建立适当的框架来充分利用它们。在这一章中,我们将讨论你需要什么样的技巧和工具来从这本书中获得最大的收益。

你将需要什么

游戏编程作为一门学科,很复杂,可能需要数年才能掌握。但是游戏编程的基本概念其实学起来比较简单,在很多情况下都是可以重用的。你在游戏和代码上投入的时间将最终决定你和你的游戏有多成功。每个人在编写代码时都会遇到这样一个问题,不管你绞尽脑汁想了多久,或者在谷歌上搜索了多少次,你都无法得到一个精确的解决方案。这本书旨在为你解决这些问题。

技能和经验

这本书不是针对新手或者没有游戏开发经验的人。通过阅读这本书,你不会学到如何从头开始开发一个完整的游戏。这并不是说你需要成为一个专业的游戏开发者才能使用这本书。相反,通过阅读这本书,你很可能是一个休闲游戏开发者;你可能尝试过开发一两个游戏(甚至可能是为 Android 开发的),但在将你的一些开发知识转换到 Android 平台时遇到了问题。

这本书致力于帮助你解决特定的问题或场景。所以你至少要有游戏开发的工作知识,至少要有 Android 专用开发的基础知识。从“从头开始”初级读本的角度来看,这两个主题都不会涉及。

由于 Android 是用 Java 开发的,所以你也应该具备良好的 Java 开发知识。不会有关于 Java 如何工作的教程,在某些场景中可能暗示你知道 Java 结构背后的含义。

然而,你可能在另一个平台上有一些游戏开发经验——比如 Windows——甚至可能有一些商业级别的 Java 经验,但从未使用过 OpenGL ES。大多数时候,为 Android 开发游戏需要使用 OpenGL ES。出于这个原因,本章的第二部分致力于向您介绍 OpenGL ES,并解释为什么它对 Android 很重要。如果你已经有了 OpenGL ES 的经验,请随意跳过这一章的“OpenGL ES 概览”

简而言之,如果你对游戏开发和 Android 有热情,但在开发中遇到了一些问题,这本书就是为你准备的。无论你已经开始开发一款游戏并遇到了问题,还是你正处于开发的初级阶段,不知道下一步该做什么, Android 游戏开发食谱将指导你克服最常见的障碍和问题。

软件版本

此时,您可能已经准备好为您的 Android 游戏场景寻找解决方案了。那么你需要什么工具来开始你的旅程呢?

这本书面向 Android 4.1 和 4.2 果冻豆。如果你不是在果冻豆工作,建议你在http://developer.android.com/sdk/升级你的 SDK。然而,这些例子应该也适用于 Android 4.0 冰淇淋三明治。如果您需要帮助,有许多资源可以帮助您下载和安装 SDK(以及您可能需要的相应 Java 组件);然而,这本书不会涵盖安装 SDK。

您还将使用开普勒版本的 Eclipse。Eclipse 的一大特点是它将支持多个版本的 Android SDKs。因此,如果需要的话,您可以在软糖豆、冰淇淋三明治甚至姜饼中快速测试您的代码。虽然您几乎可以使用任何 Java IDE 或文本编辑器来编写 Android 代码,但我更喜欢 Eclipse,因为它具有这样的特性,以及与编译和调试 Android 代码的许多更繁琐的手动操作紧密集成的精心制作的插件。毕竟 Eclipse 是 Android 的创造者 Google 推荐的 Android 官方开发 IDE。

如果你还没有 Eclipse Kepler ,并且想尝试一下,它可以从http://eclipse.org免费下载。

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

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

在下一节中,我们将探索在 Android 平台上创建游戏最常用的工具之一,OpenGL ES。

OpenGL ES 一览

OpenGL ES,或称为 OpenGL for Embedded Systems,是一个开源图形 API,与 Android SDK 打包在一起。虽然对使用核心 Android 调用处理图形的支持有限,但如果不使用 OpenGL ES 来创建一个完整的游戏将是极其困难的——如果不是不可能的话。核心的 Android 图形调用缓慢而笨拙,除了少数例外,不应该用于游戏。这就是 OpenGL ES 的用武之地。

自平台诞生之初,OpenGL ES 就以这样或那样的形式包含在 Android 中。在 Android 的早期版本中,OpenGL ES 的实现是 OpenGL ES 1 的受限版本。随着 Android 的发展,Android 版本的成熟,更多功能丰富的 OpenGL ES 实现被添加进来。有了 Android 版 Jelly Bean,开发者就可以接入 OpenGL ES 2 进行游戏开发。

那么 OpenGL ES 到底为你做了什么,又是怎么做到的呢?我们来看看。

OpenGL ES 如何与 Android 协同工作

Open GL ES 与图形硬件的通信方式比核心 Android 调用要直接得多。这意味着您将数据直接发送到负责处理数据的硬件。核心 Android 调用在到达图形硬件之前必须通过核心 Android 进程、线程和解释器。为 Android 平台编写的游戏只能通过直接与 GPU(图形处理单元)通信来实现可接受的速度和可玩性。

当前版本的 Android 能够使用 OpenGL ES 1 或 OpenGL ES 2 / 3 调用。这两个版本有很大的区别,你用哪个版本将决定谁能运行你的游戏,谁不能。

注意本书中包含 OpenGL ES 代码的所有示例都在 OpenGL ES 版本 1 和 OpenGL ES 版本 2 / 3 中给出。

OpenGL ES 以两种不同的方式促进了游戏和图形硬件之间的交互。运行你的游戏的 Android 设备中使用的 GPU 类型将决定你使用哪个版本的 OpenGL ES,因此 OpenGL 将如何与硬件交互。市场上有两种主要的图形硬件,因为它们非常不同,所以需要两个不同版本的 OpenGL ES 来与之交互。

两种不同类型的硬件是具有固定功能管道的硬件和具有着色器的硬件。接下来的几节快速回顾 OpenGL ES 和固定功能管道,以及 OpenGL ES 和着色器。请记住,OpenGL ES 版本 1 运行在固定函数管道上,而 OpenGL ES 2 / 3 运行在着色器上。

固定功能流水线

较老的设备将具有采用固定功能流水线的硬件。在这些较旧的 GPU 中,有特定的专用硬件来执行功能。诸如转换之类的功能是由 GPU 的专用部分来执行的,而作为开发人员,您对此几乎无法控制。这意味着你只需将你的顶点交给 GPU,告诉它转换顶点,就这样。

例如,当您有一组表示立方体的顶点时,您想要将该立方体从一个位置移动到另一个位置。这可以通过将顶点放入固定功能流水线,然后告诉硬件对这些顶点执行变换来实现。然后,硬件会为您进行矩阵运算,并确定最终立方体的位置。

在下面的代码中,您将看到在固定函数管道中所做工作的一个非常简化的版本。顶点myVertices被发送到流水线中。然后使用glTranslatef()将顶点转换到新的位置。接下来的矩阵数学会在 GPU 中为你完成。

private float myVertices[] = {
0.0f, 0.0f, 0.0f,
   1.0f, 0.0f, 0.0f,
   1.0f, 1.0f, 0.0f,
   0.0f, 1.0f, 0.0f,
};

//Other OpenGL and game stuff//

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

这样做的好处是,在使用专用硬件的情况下,可以非常快速地执行该功能。硬件可以以非常快的速度执行功能,而专用硬件(或功能集非常有限的硬件)可以更快地执行功能。

这种固定功能流水线方法的缺点是硬件不能像软件那样改变或重新配置。这限制了硬件向前发展的有用性。此外,专用硬件一次只能对一个队列项执行功能。这意味着,如果队列中有大量项目等待处理,管道通常会变慢。

另一方面,较新的设备具有使用着色器的 GPU。着色器仍然是一种专门的硬件,但它比其固定功能的前身灵活得多。OpenGL ES 通过使用一种称为 GLSL 或 OpenGL 着色语言的编程语言来执行任何数量的可编程任务,从而与着色器一起工作。

着色器

着色器是一种用着色器语言编写的软件程序,它执行过去由固定功能硬件处理的所有功能。OpenGL ES 2 / 3 使用两种不同类型的着色器:顶点着色器和片段着色器。

顶点着色器

顶点着色器对顶点执行功能,例如变换顶点的颜色、位置和纹理。着色器将在传递给它的每个顶点上运行。这意味着,如果你有一个由 256 个顶点组成的形状,顶点着色器将在每个顶点上运行。

顶点可大可小。然而,在所有情况下,顶点将由许多像素组成。顶点着色器将以相同的方式处理单个顶点中的所有像素。单个顶点内的所有像素被视为单个实体。当顶点着色器完成时,它将顶点向下游传递到光栅化器,然后传递到片段着色器。

下面是一个基本的顶点着色器:

private final String vertexShaderCode =
        "uniform mat4 uMVPMatrix;" +
        "attribute vec4 vPosition;" +
        "attribute vec2 TexCoordIn;" +
        "varying vec2 TexCoordOut;" +
        "void main() {" +
        "  gl_Position = uMVPMatrix * vPosition;" +
        "  TexCoordOut = TexCoordIn;" +
        "}";

片段着色器

顶点着色器处理整个顶点的数据,而片段着色器(有时称为像素着色器)处理每个像素。片段着色器将对光照、阴影、雾、颜色和其他会影响顶点中单个像素的事物进行计算。渐变和光照的处理是在像素级别上执行的,因为它们可以跨顶点不同地应用。

下面是一个基本的片段着色器:

    private final String fragmentShaderCode =
        "precision mediump float;" +
        "uniform vec4 vColor;" +
        "uniform sampler2D TexCoordIn;" +
        "varying vec2 TexCoordOut;" +
        "void main() {" +
        "  gl_FragColor = texture2D(TexCoordIn, TexCoordOut);" +
        "}";

注意还有其他类型的着色器,包括镶嵌着色器和几何着色器。这些可以是可选的,并在硬件中处理。你对他们的运作几乎一无所知。

大多数 Android 设备现在可以处理 OpenGL ES 1 和 OpenGL ES 2 调用的组合。一些开发人员,如果他们对编程着色器感到不舒服,将继续为视口和其他动态使用固定功能管道调用。要知道,随着 OpenGL 的发展,与 OpenGL ES 的固定函数管道调用的兼容性正在被淘汰。在不久的将来,你将被迫在 OpenGL ES 中只使用着色器。因此,如果你正处于 OpenGL ES 职业生涯的早期,我建议你尽最大努力使用着色器。

游戏如何工作

在开发游戏或游戏循环时,代码需要在特定的时间以特定的顺序执行。了解这个执行流对于理解应该如何设置代码是至关重要的。

以下部分将概述一个基本的游戏流程或游戏循环。

一个基本的游戏循环

每个视频游戏的核心是游戏引擎,游戏引擎的一部分是游戏循环。顾名思义,游戏引擎就是为游戏提供动力的代码。每一款游戏,无论是哪种类型的游戏——无论是 RPG、第一人称射击游戏、平台游戏,甚至是 RTS——都需要一个全功能的游戏引擎来运行。

游戏引擎通常在它自己的线程上运行,给它尽可能多的资源。游戏需要运行的所有任务,从图形到声音,都在游戏引擎中处理。

注意任何一款游戏的引擎都是为了通用而设计的。这使得它可以在多种情况下使用和重用,可能用于不同的游戏。

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

在你的游戏开发中,你可能使用了第三方游戏引擎。安卓有很多免费和收费的。如果你想建立自己的游戏引擎,这本书会对你有很大的帮助。

第三方游戏引擎中的许多进程变得模糊不清,您可能无法访问调试功能,或者您可能无法修改引擎中的代码。当你遇到问题时,你通常不得不求助于开发引擎的公司,最初的开发者可能需要花时间来修复它——如果他们真的能修复它的话。如果你正在考虑使用第三方游戏引擎,这可能是一个主要的缺点。

构建自己的游戏引擎的体验是无可替代的。这本书假设你正在这样做。本书剩余部分将要解决的许多问题都假设你正试图在 Android 上编写一个游戏引擎,并且遇到了一些常见的问题。

那么游戏引擎到底是做什么的呢?游戏引擎处理游戏执行的所有繁重工作,从播放音效和背景音乐到在屏幕上渲染图形。以下是一个典型的游戏引擎将执行的功能的部分列表。

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

游戏引擎的核心是游戏循环。虽然引擎可以处理任何事情,从设置一次性顶点缓冲区和检索图像,游戏循环服务于游戏的实际代码执行。

所有游戏都在一个代码循环中执行。这个循环执行得越快,游戏运行得就越好,对玩家的反应就越快,屏幕上的动作就越流畅。在游戏循环中执行在屏幕上绘图、移动游戏对象、计算分数、检测碰撞以及验证或无效项目所需的所有代码。

一个游戏循环就是一组在连续循环中执行的代码。该循环在游戏开始时开始,并且在游戏停止之前不会停止执行(有一些例外)。让我们来看看一个游戏循环在每一次迭代中都应该做的事情。典型的游戏循环可以执行以下操作:

  • 解释输入设备的命令
  • 跟踪人物和/或背景,以确保没有人移动到他们不应该移动到的地方
  • 测试对象之间的碰撞
  • 根据需要移动背景
  • 绘制背景
  • 画任意数量的固定物品
  • 计算任何移动物体的物理性质
  • 移动任何重新放置的武器/子弹/物品
  • 拔出武器/子弹/物品
  • 独立移动角色
  • 画人物
  • 播放音效
  • 剥离连续背景音乐的线程
  • 追踪玩家的分数
  • 跟踪和管理联网或多个玩家

这不是一个全面的列表,但它是一个相当好的列表,列出了游戏循环中所有要做的事情。

精炼和优化你所有的游戏代码是非常重要的。游戏循环中的代码越优化,它执行所有调用的速度就越快,从而给你最好的游戏体验。在下一节中,我们将了解 Android 作为一个平台是如何处理游戏引擎和游戏循环的。

安卓和游戏引擎

Android 打包了一个功能强大、功能全面的图形 API,称为 OpenGL ES。但是 OpenGL ES 对于游戏开发来说是绝对必要的吗?与其费尽周折去学习一个相当低级的 API,比如 OpenGL ES,你能不能只写一个有核心 Android API 调用的游戏?

简而言之,要让游戏高效运行,它不能依赖核心 Android API 调用来完成这种繁重的工作。是的,大多数 Android 都有核心调用,可以处理列表中的每一项。然而,Android 的渲染、声音和存储系统是为一般任务而构建的,并适应任何数量的不可预测的用途,而不是专门针对任何一个。不可预测性意味着一件事:开销。运行游戏所需的核心 Android API 调用伴随着大量无关代码。如果你正在编写商业应用,这是可以接受的,但是如果你正在编写游戏,就不可以了。开销增加了代码的速度,游戏需要更强大的功能。

为了让游戏流畅快速地运行,代码需要绕过核心 Android API 调用中固有的开销;也就是说,一个游戏应该直接与图形硬件通信以执行图形功能,直接与声卡通信以播放声音效果,等等。如果你使用通过核心 Android API 提供的标准内存、图形和声音系统,你的游戏可以与系统上运行的所有其他 Android 应用线程化。这将使游戏看起来起伏不定,运行非常缓慢。

正因如此,游戏引擎和游戏循环几乎都是用低级语言或特定 API 编写的,比如 OpenGL ES。正如我们将在第二章中提到的,低级语言为系统硬件提供了一条更直接的途径。游戏引擎需要能够从引擎获取代码和命令,并将它们直接传递给硬件。这使得游戏能够快速运行,并具有它需要的所有控制,能够提供有益的体验。

摘要

在这一章中,我们讨论了你需要什么工具来充分利用这本书。Android 版本的 Jelly Bean、Eclipse Kepler 和一些基本的 Java 和/或游戏开发经验将在本书的剩余部分帮助你。我们还讲述了 OpenGL ES 版本 1 和 2 / 3 之间的区别,以及固定管道和着色器之间的区别。

在接下来的几章中,我们将开始研究一个典型游戏引擎中的一些问题。更具体地说,我们将看看加载图像的不同方式可能出现的问题。有许多不同的图像格式和一些不同的方法来加载这些图像并显示在屏幕上。如果你尝试过,你可能会遇到一些意想不到的结果。

二、加载图像

不言而喻,如果你计划开发一个游戏,休闲或其他,你需要使用图像。从背景和字符到菜单和文本的一切都是由图像组成的。Android 可以使用不同的方法将这些图像显示在屏幕上。本章将帮助您解决在 Android 中检索、存储和提供图像时遇到的任何问题。

在 Android 中有两种截然不同的方式来提供图像,每种方式在游戏开发中都有自己的位置。在 Android 中提供图像的第一种方法是使用核心 Android 方法——或者那些不涉及直接使用 OpenGL ES 的方法。这些核心方法需要很少甚至不需要代码就可以使用,但是它们很慢,而且肯定不够灵活,不能用于游戏中主要的面向动作的部分。

在 Android 中提供图像的第二种方法是使用 OpenGL ES。OpenGL ES 快速、灵活,非常适合在游戏中使用;然而,它比核心的 Android 方法需要更多的代码。我们将在这一章中探讨这两个问题。

那么什么时候你会使用一种方法而不是另一种方法呢?

使用 Android 核心方法加载的图像非常适合闪屏、标题屏甚至菜单。给定 Android 活动的架构,使用核心 Android 方法创建包含游戏菜单系统的活动是非常容易的。菜单可以包括在启动游戏线程之前更容易完成的项目,如检查分数、访问在线商店或查看预装的关卡信息。然后,当玩家选择进入游戏时,该菜单可用于启动主游戏线程。一旦进入主游戏线程,OpenGL ES 就可以接管处理更多图形密集型游戏的任务。本章中的解决方案将帮助您解决在 OpenGL ES 中加载图像和使用 Android 核心方法时遇到的许多常见问题。

2.1 使用核心 Android 方法加载图像

问题

在游戏中,有时你可能不需要使用 OpenGL ES 来显示图像;例如,标题和菜单屏幕。然而,在你决定使用 Android 核心方法或 OpenGL ES 之后,你如何在你的项目中存储图像以便 Android 可以访问它们呢?

解决办法

在 Android 中使用之前,图像文件存储在res文件夹中。res文件夹——或资源文件夹——是存储您的 Android 项目的所有资源的地方。名为drawable*res文件夹下有一组子目录。你所有的图片都应该放在一个drawable文件夹里。然后使用 Android ImageView 节点将这些图像显示到屏幕上。这是一个完美的解决方案,适用于游戏启动画面或游戏中任何在游戏开始前显示图像的部分。

它是如何工作的

这个解决方案的一个好处是,它可以在没有任何手工编码的情况下完成。一些拖放动作将立即为您设置好这个解决方案。由于这个解决方案有两个部分(存储和显示图像),让我们分别看一下每个部分。

在 Android 中存储图像

问题的第一部分是你在 Android 中存储图片的位置。您在 Android 项目中使用的所有资源文件都保存在名为res的项目目录中。如果您打开您的项目,并在项目浏览器下展开文件系统,您将看到一个名为res的根级文件夹;这是存储所有应用内资源的地方,比如字符串和图像。

注意如果你使用的是 Eclipse (本书写作时的最新版本是 Juno),那么你会在包浏览器中看到res文件夹。但是,如果您使用的是不同的 IDE,或者根本没有 IDE,那么请找到文件浏览等效项以查看res文件夹。

如果您使用的是 IDE,打开res文件夹,您应该会找到一些子文件夹。其中一些子文件夹应该以单词drawable-开头。你的应用中所有用于存储图片的子文件夹都将以这个单词开头。你还会注意到每个文件夹名称的末尾有一个符号,从-ldpi-xhdpi。这是什么意思?

Android 支持许多不同的屏幕尺寸和像素密度。因为你可能想为不同的屏幕尺寸或像素密度提供不同分辨率的图像,Android 为这些图像提供了不同的子文件夹。文件夹名称中的符号表示从小(drawable-small)到超大(drawable-xlarge)的屏幕尺寸,表示从低密度(drawable-ldpi)到超高密度(drawable-xhdpi)的像素密度。

提示如果你不在乎用来显示图像的屏幕的像素密度,那么你可以把你所有的文件放在默认的drawable文件夹中。如果默认情况下您的 IDE 没有创建这个文件夹,您可以随意添加它。当你没有指定要使用的像素密度时,Android 会在这里查看。

我们将在这个例子中使用的图像是我们的虚拟游戏超级强盗的启动画面,如图 2-1 所示。

9781430257646_Fig02-01.jpg

图 2-1 。超级土匪家伙闪屏图像

只需将这张图片从你的工作文件夹中拖放到正确的drawable dpi 文件夹,如图图 2-2 所示。在这种情况下,我使用了drawable-xhdpi在平板电脑上进行测试。

9781430257646_Fig02-02.jpg

图 2-2 。将图像拖动到 res/drawable-xhdpi 文件夹中

这就是将图像导入 Android 的全部内容。

注意Android 中使用的所有图像文件名必须以小写字母开头。

加载和显示图像

图像现在可以使用了。为了在屏幕上显示这个图像,您需要创建一个 ImageView 。

注意同样,如果您使用的是 Eclipse,那么应该已经为您创建了一个通用布局。如果您没有使用 Eclipse,请按照 IDE 的说明创建一个主屏幕布局

展开布局根文件夹,打开activity_main_menu.xml文件。打开布局,展开图像&媒体调色板,定位 ImageView,如图图 2-3 所示。

9781430257646_Fig02-03.jpg

图 2-3 。定位的 ImageView

现在,将图像从调色板拖到工作区的布局中。在工作区的顶部(再次参考图 2-3 ,您会看到一排菜单图标。选择状态菜单图标将允许您将屏幕布局的方向从纵向更改为横向。我见过在任何方向玩的游戏;然而,对于这个例子,超级强盗家伙是在风景中玩的。因此,在未来的屏幕截图中,方向的变化将是显而易见的。将 ImageView 添加到布局中后,展开 ImageView 属性并选择 Src 属性。点击 Src 属性旁边的省略号将显示可提取资源的列表。

选择正确的图像,如图 2-4 所示。

9781430257646_Fig02-04.jpg

图 2-4 。使用 ImageView 属性选择正确的图像

编译并运行您的项目。结果应如图 2-5 所示。

9781430257646_Fig02-05.jpg

图 2-5 。显示启动画面

在这件事结束之前,有一件事你可能想处理一下。注意在图 2-5 中,图像上方有一个动作栏菜单。这是在一些 ide 中默认添加的(在 Android 3.0 和更高版本中),具体取决于创建项目时选择的 Android 主题。去掉这个动作栏很容易。

返回到项目浏览器,在res文件夹中,您应该能够找到名为values的文件夹。这个文件夹里有一个名为styles.xml的文件。将下面一行添加到styles.xml文件中,在您的应用正在使用的样式的样式标签之间。

<item name="android:windowActionBar">false</item>

2.2 使用 OpenGL ES 加载图像

在这个食谱中,我提出了两个问题和两个解决方案。你首先要纠正 ImageView 的图像调用 才能在游戏中正常运行。然后,您将看到如何确保 OpenGL ES 在使用 Android 设备时显示正确的图像。

问题 1

ImageView 图像调用 在游戏中使用太慢。

解决方案 1

使用 OpenGL ES 将您的图像写入屏幕。您必须创建一个 OpenGL ES 渲染器、一个GLSurfaceView,以及一组顶点和纹理。尽管这种解决方案听起来工作量很大,但是您只需要将大部分工作做一次,然后就可以在整个项目中重用相同的类。

也就是说,渲染器和GLSurfaceView只需要为你的游戏创建一次。它们被一遍又一遍地重复使用。解决方案中唯一需要为每个要显示的图像重新创建的部分是定义图像的顶点和纹理。

它是如何工作的

我们将把这个解决方案分成三个部分:创建顶点和纹理,创建渲染器,最后创建GLSurfaceView。让我们从创建顶点和纹理开始。

创建顶点和纹理

这是过程中最复杂的部分,也是需要最多代码的部分。但如果慢慢来,应该没问题。此外,鉴于创建顶点和纹理是以某种形式在整个游戏中重复的一部分,您将获得大量代码练习。你用得越多,就会越容易。

就 OpenGL ES 而言,所有的图像都是纹理。纹理应该映射到形状上。您将创建一个原始的正方形来映射您的图像(或纹理)并通过渲染器和GLSurfaceView将其显示到屏幕上。

为此,您需要创建一个新的类SBGSplash,这涉及到以下步骤,稍后将对所有这些步骤进行描述:

  1. 创建一些缓冲区。
  2. 创建构造函数。
  3. 创建loadTexture()方法。
  4. 创造draw()方法 。

SBGSplash类的构造函数将设置你需要与 OpenGL ES 交互的所有变量(见清单 2-1 )。您需要一个数组来保存纹理的映射坐标,一个数组来保存顶点的坐标,一个数组来保存顶点的索引。最后,创建一个引用纹理的资源标识符数组。

清单 2-1SBGSplash (OpenGL 是 1)

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

private float[]vertices = {
0f, 1f, 0f,
0f, 0f, 0f,
1f, 0f, 0f,
1f, 1f, 0f,
};
private float[] texture = {
1f, 0f,
1f, 1f,
0f, 1f,
0f, 0f,
};
private byte[] indices = {
0,1,2,
0,2,3,
};
public SBGSplash() {
//empty constructor
}
}

数组保存了你正在加载的每一个纹理的标识符。您将它硬编码为 1,因为您将只加载一个图像,但是我们将它留得足够灵活,以便您在将来重用,而无需太多的重写。

vertices数组列出了一系列的点。这里的每一行代表一个正方形的一个角的 x、y 和 z 值。这个正方形是图像将被纹理化以显示的原始形状。在这种情况下,您正在制作一个屏幕大小的正方形,确保图像覆盖整个屏幕。

texture数组表示图像(或纹理)的角与你创建的正方形的角对齐的位置。同样,在这种情况下,你希望纹理覆盖整个正方形,从而覆盖整个背景。

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

如果你使用的是 OpenGL ES 3,你需要在这里添加你的着色器代码,如清单 2-2 所示。

清单 2-2SBGSplash (OpenGL 是 2/3)

public class SBGSplash {
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
"  gl_Position = uMVPMatrix * vPosition;" +
"  TexCoordOut = TexCoordIn;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float scroll;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x + scroll,TexCoordOut.y));"+
"}";
private int[] textures = new int[1];

private float[]vertices = {
0f, 1f, 0f,
0f, 0f, 0f,
1f, 0f, 0f,
1f, 1f, 0f,
};
private float[] texture = {
1f, 0f,
1f, 1f,
0f, 1f,
0f, 0f,
};
private byte[] indices = {
0,1,2,
0,2,3,
};
public SBGSplash() {
//empty constructor
}
}

是时候创建类构造函数中也使用的缓冲区了。因为缓冲区和前面代码清单中的变量一样,在类的多个方法中使用,所以我们将在类的主体中设置它们。

创建缓冲区

现在,创建一些我们可以用来保存这些数组的缓冲区(见清单 2-3 )。这些缓冲区将被加载到 OpenGL ES 1 中。

清单 2-3 。缓冲区(OpenGL ES 1)

importjava.nio.ByteBuffer;
importjava.nio.FloatBuffer;

public class SBGSplash {

private FloatBuffervertexBuffer;
private FloatBuffertextureBuffer;
private ByteBufferindexBuffer;

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

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

OpenGL ES 2 和 3 需要一些额外的缓冲变量,如清单 2-4 所示。

清单 2-4 。缓冲区和变量(OpenGL ES 2/3)

public class SBGSplash {
private final FloatBuffer vertexBuffer;
private final ShortBuffer indexBuffer;
private final FloatBuffer textureBuffer;
private final int mProgram;
private int mPositionHandle;
private int mMVPMatrixHandle;

static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
"  gl_Position = uMVPMatrix * vPosition;" +
"  TexCoordOut = TexCoordIn;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float scroll;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x + scroll,TexCoordOut.y));"+
"}";
private int[] textures = new int[1];

private float[]vertices = {
0f, 1f, 0f,
0f, 0f, 0f,
1f, 0f, 0f,
1f, 1f, 0f,
};
private float[] texture = {
1f, 0f,
1f, 1f,
0f, 1f,
0f, 0f,
};
private byte[] indices = {
0,1,2,
 0,2,3,
};
public SBGSplash() {
//empty constructor
}
}

缓冲区填充在类的构造函数中。现在,构造函数是空的。下一节描述完成构造函数需要什么代码。

创建构造函数

现在在SBGSplash构造函数中用适当的数组填充适当的缓冲区,如清单 2-5 和清单 2-6 所示。

清单 2-5 。构造函数(OpenGL ES 1)

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

public class SBGSplash {

...

public SBGSplash() {

ByteBufferbyteBuf = 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.order(ByteOrder.nativeOrder());
indexBuffer.put(indices);
indexBuffer.position(0);
}
}

清单 2-6 。构造器(OpenGL ES 2/3)

public class SBGSplash {

...

public SBGSplash() {

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.order(ByteOrder.nativeOrder());
indexBuffer.put(indices);
indexBuffer.position(0);

int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
GLES20.glShaderSource(vertexShader, vertexShaderCode);
GLES20.glCompileShader(vertexShader);

int fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
GLES20.glShaderSource(fragmentShader, fragmentShaderCode);
GLES20.glCompileShader(fragmentShader);

mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
}
}

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

OpenGL ES 1 代码和 OpenGL ES 2/3 代码之间的唯一区别是 OpenGL ES 2/3 要求着色器附加到程序上。三行代码编译每个着色器并将其附加到程序中。

创建loadTexture()方法

接下来,您需要创建loadTexture()方法(参见清单 2-7 和清单 2-8 )。loadTexture()方法将接收一个图像标识符,然后将图像加载到流中。该流将作为纹理加载到 OpenGL ES 中。在绘制过程中,你将把这个纹理映射到顶点上。

清单 2-7loadTexture() (OpenGL 是 1)

public class SBGSplash {

...

public SBGSplash() {
...

}

public void loadTexture(GL10 gl,int texture, Context context) {
InputStreamimagestream = context.getResources().openRawResource(texture);
      Bitmap bitmap = null;
android.graphics.Matrix flip = new android.graphics.Matrix();
flip.postScale(-1f, -1f);
try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){
//handle your exception here
}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);

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

bitmap.recycle();
   }

}

清单 2-8loadTexture() (OpenGL 是 2/3)

public class SBGSplash {

...

public SBGSplash() {
...

}

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

android.graphics.Matrix flip = new android.graphics.Matrix();
flip.postScale(-1f, -1f);

try {

bitmap = BitmapFactory.decodeStream(imagestream);
imagestream.close();
imagestream = null;

}catch(Exception e){

//handle your exception here

}

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

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);

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

bitmap.recycle();

   }

}

loadTexture()的第一部分相当简单。它接收标识符并将结果图像加载到位图流中。传递到openRawResource()中的texture是您的res文件夹中一个图像的资源 ID。您将在稍后的解答中通过该测试。然后关闭该流。此外,由于 OpenGL ES 以先入后出的字节顺序显示图像,因此默认情况下图像会上下颠倒。因此,您使用一个Matrix通过调用postScale()来翻转图像。

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

gl.glGenTextures(1, textures, 0);
...
GLES20.glGenTextures(1, textures, 0);

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

第二行将纹理绑定到 OpenGL ES 中。

gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
...
GLES20.glBindTexture(GLES20.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);
...
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

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

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

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

创建draw()方法

您需要编写的完成 SBGSplash 类的最后一段代码是将纹理绘制到顶点上的方法(清单 2-9 和 2-10 )。

清单 2-9draw() (OpenGL 是 1)

...
public class SBGSplash {
...
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 SBGSplash() {
...

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

}

清单 2-10draw() (OpenGL 是 2/3)

public class SBGSplash {
...
public void draw(GL10 gl) {
GLES20.glUseProgram(mProgram);

mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

GLES20.glEnableVertexAttribArray(mPositionHandle);

int vsTextureCoord = GLES20.glGetAttribLocation(mProgram, "TexCoordIn");

GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
GLES20.glUniform1i(fsTexture, 0);

mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

GLES20.glDisableVertexAttribArray(mPositionHandle);

}

public SBGSplash() {
...

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

}

每次你想在屏幕上绘制这个图像的时候,都会调用draw()方法,而loadTexture()方法只会在你初始化游戏的时候被调用。

这个方法的第一行将纹理绑定到你的目标。纹理被加载并准备使用。

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

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

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

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

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

SBGSplash课现在结束了。您所需要做的就是创建支持类来帮助在屏幕上显示SBGSplash。这是通过渲染完成的。

创建渲染器

创建一个新类,SBGGameRenderer

public class SBGGameRenderer{

}

现在你需要实现GLSurfaceViewRenderer

importandroid.opengl.GLSurfaceView.Renderer;

public class SBGGameRenderer implements Renderer{

}

确保添加未实现的方法。

清单 2-11SBGGameRenderer()

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

importandroid.opengl.GLSurfaceView.Renderer;

public class SBGGameRenderer 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, EGLConfigconfig) {

}
}

这些方法的功能应该是不言自明的。当创建GLSurface时,调用onSurfaceCreated()方法。当视图的大小改变时(包括初始加载),调用onSurfaceChanged()方法。最后,当Renderer在屏幕上绘制一帧时,调用onDrawFrame()方法。

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

onSurfaceCreated()

onSurfaceCreated()方法中,你将初始化你的 OpenGL ES 并加载你的纹理,如清单 2-12 所示。

清单 2-12onSurfaceCreated()

public class SBGGameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

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

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

注意,onSurfaceCreated()方法将 OpenGL ES ( GL10 gl)的一个实例作为参数。当调用Renderer时,这将由GLSurfaceView传递到方法中。只有在使用 OpenGL ES 1 时才使用它;否则,它将被忽略。您不必担心为此流程创建 GL10 的实例;它会自动为您完成。

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

想象一下,OpenGL ES 环境是一个舞台。你想在游戏中画的一切都是这个舞台上的演员。现在,想象你正在拍摄演员在舞台上走动的场景。最终的电影是舞台上发生的事情的 2D 再现。如果一个演员在另一个演员前面移动,后面的演员在影片上就看不到了。然而,如果你在电影院看这些演员的现场表演,取决于你坐在哪里,你仍然可以看到后面的演员。

这与 OpenGL ES 在幕后的工作原理是一样的。即使你正在制作一个 2D 游戏,OpenGL ES 也会把所有东西都当作 3D 空间中的 3D 物体来对待。事实上,在 2D 开发和在 OpenGL ES 中进行 3D 开发的唯一区别是你如何告诉 OpenGL ES 渲染最终的场景。因此,你需要注意你的对象在 3D 空间中的位置,以确保它们像 2D 游戏一样正确渲染。通过接下来启用 OpenGL ES 深度测试(见清单 2-13 ,你给了 OpenGL ES 一种测试你的纹理并决定它们应该如何渲染的方法。

清单 2-13 。深度测试

public class SBGBameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

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

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

}
}

您将添加到该方法的最后两行代码与混合有关。清单 2-14 中的两行粗体代码将设置 OpenGL 的混合特性来创建透明度。

清单 2-14 。混合

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

public class SBGGameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

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

@Override
public void onSurfaceCreated(GL10 gl, EGLConfigconfig) {
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()方法中调用SGBSplashloadTexture()。向loadTexture()方法传递想要加载的图像的资源标识符。在清单 2-15 中,我使用了res/drawable文件夹中名为titlescreen的图像。

清单 2-15 。onSurfaceCreated

public class SBGGameRenderer implements Renderer{
private SBGSplashsplashImage = new SBGSplash();

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

@Override
public void onSurfaceCreated(GL10 gl, EGLConfigconfig) {
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);

splashImage.loadTexture(gl, R.drawable.titlescreen, context);
}
}

注意,loadTexture()方法采用了一个上下文参数。让我们修改SBGGameRenderer的构造函数,以允许传递应用的上下文(参见清单 2-16 )。当渲染器启动时,可以将上下文传递到构造函数中,并在整个过程中使用。

清单 2-16 。修改的构造函数

public class SBGGameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

private Context context;

public SBGGameRenderer(Context appContext){
context = appContext;
}

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

@Override
public void onSurfaceCreated(GL10 gl, EGLConfigconfig) {
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);

splashImage.loadTexture(gl, R.drawable.titlescreen, context);
}
}

使用 OpenGL ES 2/3 的onSurfaceCreated()方法 在代码上稍微轻一点。

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

GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

}

在 OpenGL ES 2/3 中,背景颜色正在被清除。这实际上是一个可选步骤,因为无论如何,整个屏幕区域都应该充满游戏图形。

现在,让我们继续讨论onSurfaceChanged()方法。

onSurfacedChanged()

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

您需要设置glViewport(),然后调用渲染例程来完成onSurfacedChanged()方法。

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

清单 2-17 。glViewport

public class SBGGameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

private Context context;

public SBGGameRenderer(Context appContext){
context = appContext;
}

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

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

@Override
public void onSurfaceCreated(GL10 gl, EGLConfigconfig) {

...

}
}

调用方GLSurfaceView将向onSurfacedChanged()方法发送一个widthheight参数。您可以简单地将glViewport()的宽度和高度设置为GLSurfaceView发送的相应的widthheight。参见清单 2-18 。

清单 2-18 。宽度和高度

public class SBGGameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

private Context context;

public SBGGameRenderer(Context appContext){
context = appContext;
}

@Override
public void onDrawFrame(GL10 gl) {
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfigconfig) {

...

}
}

注意GLSurfaceView发送的widthheight将代表设备的宽度和高度减去屏幕顶部的通知栏。

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

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

要进入投影矩阵模式,需要将glMatrixMode()设置为GL_PROJECTION,如清单 2-19 所示。

清单 2-19 。glMatrixMode

public class SBGGameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

private Context context;

public SBGGameRenderer(Context appContext){
context = appContext;
}

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

}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfigconfig) {

...

}
}

现在 OpenGL ES 处于投影矩阵模式,你需要加载当前身份(见清单 2-20 )。把身份想象成 OpenGL ES 1 的默认状态。

清单 2-20 。加载身份

public class SBGGameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

private Context context;

public SBGGameRenderer(Context appContext){
context = appContext;
}

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

}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfigconfig) {

...

}
}

既然身份已经加载,你可以设置glOrthof()(见清单 2-21 )。

清单 2-21 。格洛霍夫

public class SBGGameRenderer implements Renderer{

private SBGSplashsplashImage = new SBGSplash();

private Context context;

public SBGGameRenderer(Context appContext){
context = appContext;
}

@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, EGLConfigconfig) {

...

}
}

方法将为你的场景建立一个正交的二维渲染。这个调用有六个参数,每个参数定义一个裁剪平面。

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

现在让我们设置onDraw()方法。

onDrawFrame()

该方法将包含对您已经在该解决方案中使用的方法的调用,因此应该很容易理解。然而,它也将包含对SBGSplash类的draw()方法的调用。参见清单 2-22 。

清单 2-22 。onDrawFrame

public void onDrawFrame(GL10 unused) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);
}

这个解决方案的最后一步是设置GLSurfaceView并从主活动中调用它。

创建GLSurfaceView

创建一个名为SBGGameView 的新类,如清单 2-23 所示。

清单 2-23SBGGameView阶级

importandroid.content.Context;
importandroid.opengl.GLSurfaceView;

public class SBGGameView extends GLSurfaceView {

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

setRenderer(new SBGGameRenderer(context));

}
}

注意,GLSurfaceView的惟一功能是将Renderer设置为您创建的Renderer的一个实例。现在您可以将GLSurfaceView设置为活动的主要内容视图,如清单 2-24 所示。

清单 2-24 。设置GLSurfaceView

import com.jfdimarzio.superbanditguy.SBGGameView;
import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

private SBGGameViewgameView;

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

现在,您应该能够编译和运行您的活动了。图像应如图 2-6 所示。

9781430257646_Fig02-06.jpg

图 2-6 。使用 OpenGL ES 显示的闪屏

现在,让我们确保使用仿真器显示的图像在设备中以相同的方式工作。

问题 2

OpenGL ES 仅在使用 Android 设备时显示白色图像,但在使用仿真器时工作正常。

解决方案 2

确保图像分辨率是 2 的幂。

它是如何工作的

这是一个相当普遍的问题,幸运的是,这个问题很容易解决。

为了避免这个*白盒,*你必须确保你的图像分辨率是 2 的导数。飞溅的图像(图 2-6 )为 512 x 512。但是我发现 128 x 128 和 64 x 64 也可以。

编辑您的图像并以正确的分辨率重新保存它们将会很快解决这个问题。

2.3 存储不同屏幕分辨率的图像

问题

在你的游戏中,不同的屏幕分辨率有不同的图像。

解决办法

使用res文件夹中的多个drawable-文件夹存储正确分辨率的图像。

它是如何工作的

Android 作为一个平台,可以支持无数不同的设备屏幕分辨率。如果您要创建不同的图像以在不同的设备屏幕上使用,您需要将这些图像存储在正确的位置。

根据目标设备的屏幕分辨率,表 2-1 提供了一些存储图像的指南。

表 2-1 。推荐的图像存储位置

|

文件夹

|

解决

| | --- | --- | | res/drawable-ldpi | 高达 120 dpi | | res/drawable-mdpi | 从 120 到 160 dpi | | res/drawable-hdpi | 从 160 到 240 dpi | | res/drawable-xhdpi | 从 240 到 320 dpi | | res/drawable-xxhdi | 超过 320 dpi | | res/drawable-nodpi | 任何(非指定的)dpi |

三、闪屏

在这一章中,我们将清理一些当你在游戏启动画面上工作时可能出现的常见问题。闪屏,有时称为标题卡,是玩家开始游戏时看到的第一个东西。

闪屏可以由一个或多个不同的图像组成。这些图像通常在一些后台设置过程运行时显示,可以代表从游戏开发公司到发行公司或代理的任何内容。

除非你正在创建一个不需要玩家设置的游戏,否则你可以在游戏线程开始之前,使用 Android 在主活动线程中加载这些闪屏。原因很简单。大多数游戏会在游戏开始前用菜单屏幕提示玩家。菜单屏幕可以有从开始游戏、查看分数到登录网络服务的选项。如果你的游戏将包括这种菜单系统,你将希望在主活动线程中启动菜单。当玩家选择开始游戏时,你可以让菜单产生游戏线程。

本章介绍的解决方案假设您将在主活动线程而不是游戏线程中启动游戏的闪屏。此外,如第二章中所述,该闪屏和菜单屏幕示例将采用横向模式。为什么要对此进行区分呢?如果你在主游戏线程中启动闪屏,你可以使用 OpenGL ES 来显示屏幕,然后使用你的游戏代码来跟踪玩家在菜单中做什么。虽然完全可以接受,但这个解决方案有点大材小用。在主活动线程中加载和处理闪屏的解决方案更容易编码和跟踪。

3.1 创建闪屏

问题

当游戏在后台加载时,您无法显示游戏名称。

解决办法

当您在后台执行其他游戏相关功能时,使用闪屏显示游戏信息。闪屏通常是当你的主要 Android 活动被加载时显示的图像。这意味着您将在主活动线程中加载图像,并在第二个线程中启动游戏。

它是如何工作的

这个解决方案通过三个简单的步骤实现。您需要创建一个布局来显示您想要用作闪屏的图像。然后你需要在你的应用中创建第二个Activity来代表你的游戏。最后,您需要创建一个postDelayed() Handler() ,它将执行您的后台代码,然后在完成后启动您的游戏线程。

最终的结果是一个遵循这个路径的游戏流:当玩家启动你的游戏时,主活动开始,然后当你的游戏在后台做一些内务处理时,出现一个闪屏,最后,当这个内务处理完成时,活动直接启动到游戏中。

创建布局

首先创建一个显示初始屏幕图像的布局。创建该布局的说明在第二章的中解释。activity_main.xml的代码如清单 3-1 所示。关于代码含义的进一步解释,请参见第二章。

清单 3-1activity_main.xml

<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:contentDescription="@string/splash_screen_description"
android:layout_alignParentTop="true"
android:scaleType="fitXY"
android:src="@drawable/titlescreen" />

</RelativeLayout>

activity_main.xml文件中显示的图像显示在图 3-1 中。

9781430257646_Fig03-01.jpg

图 3-1 。游戏的启动画面

创建新的Activity

现在你的布局已经创建好了,你需要在你的应用中创建一个新的Activity来代表你的游戏的主要活动。一个Activity的基本代码如清单 3-2 所示。

清单 3-2 。基本Activity代码

public class SBGGame extends Activity{

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

//Place your game code here

}
}

现在你有主Activity(一个布局代表你的闪屏),你有Activity代表你游戏的主启动点。你是如何从主Activity进入游戏Activity的?

你将在主Activity中使用一个Handler()来延迟游戏Activity的推出。

创建一个postDelayed() Handler()

Handler()有一个名为postDelayed()的方法,可以用来延迟另一个Activity意图的开始。您需要执行的所有日常工作都可以在Handler()内完成。清单 3-3 到 3-6 会告诉你怎么做。

在您的主Activity中,创建一个名为 GAME_THREAD_DELAY 的常量,并将其设置为值 999000,如清单 3-3 所示。这意味着在你的游戏Activity启动之前会有 999 秒的延迟。

清单 3-3Activity一延迟

public class MainActivity extends Activity {
static int GAME_THREAD_DELAY = 999000;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
}

}

现在创建一个新的Handler()实例。在GAME_THREAD_DELAY到期后,使用postDelayed()方法延迟一个新线程的启动,如清单 3-4 所示。

清单 3-4 。使用postDelayed

public class MainActivity extends Activity {
static int GAME_THREAD_DELAY = 999000;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
}
}, GAME_THREAD_DELAY);
}
}

现在将所有的内务代码、启动游戏的代码Activity和杀死主程序的代码Activity放到新的 runnable 对象的run()方法中(见清单 3-5 )。

清单 3-5 。启动新的Activity

public class MainActivity extends Activity {
static int GAME_THREAD_DELAY = 999000;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {

Intent gameMain = new Intent(MainActivity.this, SBGGame.class);
MainActivity.this.startActivity(gameMain);

//Perform all of your housekeeping activities here

MainActivity.this.finish();
}
}, GAME_THREAD_DELAY);
}

}

最后,在所有内务操作完成后,将GAME_THREAD_DELAY从 999 秒改为 1 秒,强制其启动游戏Activity,如清单 3-6 所示。这给了你 999 秒来执行所有的游戏预加载。然后,当你完成预加载游戏,你只需设置延迟 1 秒,以强制启动游戏Activity

清单 3-6 。缩短延迟定时器

public class MainActivity extends Activity {
static int GAME_THREAD_DELAY = 999000;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
new Handler().postDelayed(new Thread() {
@Override
public void run() {
Intent gameMain = new Intent(MainActivity.this, SBGGame.class);
MainActivity.this.startActivity(gameMain);

//Perform all of your housekeeping activities here

GAME_THREAD_DELAY = 1000;

MainActivity.this.finish();

}
}, GAME_THREAD_DELAY);
}

}

3.2 在闪屏期间加载多个图像

问题

当游戏在后台加载时,您希望在闪屏中显示多个图像。

解决办法

创建第二个布局,为主页面Activity创建第二个闪屏图像。

它是如何工作的

这个解决方案将基于上一个问题的解决方案。在问题 3.1 中,你在主Activity中创建了一个Handler()Handler()执行了一些后台任务,完成后启动了游戏Activity

您将向该解决方案添加第二个布局,用于显示第二个图像或闪屏。您将在第二个闪屏中显示的图像如图 3-2 所示。

9781430257646_Fig03-02.jpg

图 3-2 。游戏的第二个闪屏

第一步是创建一个名为second_image的新布局来显示图像。您可以从您的第一个布局中复制 XML(清单 3-1 )以使事情变得简单(这里再次展示以供参考)。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:contentDescription="@string/splash_screen_two_description"
android:layout_alignParentTop="true"
android:scaleType="fitXY"
android:src="@drawable/credits" />

</RelativeLayout>

现在,修改你的主Activity来显示使用这个布局,如清单 3-7 中的所示。

清单 3-7 。加载新布局

public class MainActivity extends Activity{

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
SBGVars.context = this;
new Handler().postDelayed(new Thread() {
@Override
public void run() {
setContentView(R.layout.second_image);
}
       }
}

Handler() delay到期时,它将显示第二个闪屏,如图图 3-2 所示。

3.3 淡入和淡出闪屏

问题

游戏的第一个闪屏应该淡入游戏的菜单中,以获得更微妙的开场。

解决办法

使用动画和overridePendingTransition()和从一个闪屏图像渐变到另一个。

它是如何工作的

为使该解决方案正常工作,您需要从第二章中的菜单屏幕开始。

在这个解决方案中,你想要做的是创建一个动画,它将从主Activity的闪屏渐变到菜单屏。这不是一个难以完成的任务;它需要使用一种方法和几个布局文件。

首先,在res/layout文件夹中,新建两个布局文件;说出一个fadein.xml和另一个fadeout.xml的名字。第一个表示将图像淡入显示的动画布局,第二个表示将图像淡出屏幕的动画布局。

文件的代码应该如清单 3-8 中的所示。

清单 3-8fadein.xml

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

这段代码说的是,使用指定的动画插值器,在一秒钟内从完全透明(android:fromAlpha="0.0")移动到完全不透明(android:toAlpha="1.0")状态(android:duration="1000")。

fadeout.xml文件中,你将做大致相同的转换,只是不是从透明到不透明,而是从不透明到透明,如清单 3-9 所示。

清单 3-9fadeout.xml

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

现在,在解决方案 3.1 和 3.2 中解释的Handler() 中,添加一个对overridePendingTransition()的调用,传递一个指向fadein.xmlfadeout.xml的指针(参见清单 3-10 )。

清单 3-10 。使用overridePendingTransition()

public class MainActivity extends Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
new Handler().postDelayed(new Thread() {
@Override
public void run() {
Intent mainMenu = new Intent(MainActivity.this, SBGMenuScreen.class);
MainActivity.this.startActivity(mainMenu);

//Perform background tasks

GAME_THREAD_DELAY = 1000;
MainActivity.this.finish();
overridePendingTransition(R.layout.fadein,R.layout.fadeout);
}
}, GAME_THREAD_DELAY);
}
}

当你开始你的游戏时,你应该会看到第一个启动画面载入,然后是第二个平滑的渐变到菜单画面。

四、菜单屏幕

你可能已经制作了一个游戏,或者正在制作中,但是仍然需要一个合适的菜单屏幕来启动它。不要害怕。如果你在为你的游戏创建一个工作菜单屏幕时有问题,这一章应该可以帮助你。

在这一章中,你将找到创建一个双按钮菜单屏幕的解决方案,在所述菜单屏幕上连接按钮以开始和退出游戏,以及在你创建菜单时可能出现的更多问题。

第一个解决方案将为你的游戏提供一个合适的双按钮菜单屏幕。

4.1 创建一个双按钮菜单屏幕

问题

你的游戏需要一个菜单屏幕来向玩家展示选项。

解决办法

使用 Android 布局创建一个菜单,有两个按钮:一个开始游戏,一个退出游戏。

它是如何工作的

虽然你不需要使用完整的例子,但是这个解决方案可以很好地处理为解决第三章的问题而创建的闪屏。如果您确实想一起使用这些解决方案,请将第三章中的creditscreen.xml (这是从第一个闪屏淡入的布局)替换为将在该解决方案中创建的 main_menu.xml

第一步是向项目中添加一些图像。图 4-1 中的第一幅图像是菜单屏幕的背景。在这种情况下,我们将使用与游戏启动画面相同的图像,但是您可以随意使用您想要的任何图像。

9781430257646_Fig04-01.jpg

图 4-1 。菜单屏幕背景

现在你还需要两个图像,每个按钮一个。对于这个解决方案,您将创建一个启动游戏的按钮和一个退出游戏的按钮。图 4-2 和 4-3 分别代表开始按钮图像和退出按钮图像。

9781430257646_Fig04-02.jpg

图 4-2 。开始按钮图像

9781430257646_Fig04-03.jpg

图 4-3 。退出按钮图片

注意我在最终解决方案中使用的图像由透明背景上的白色文本组成。然而,为了使这些图像在本书中正确显示,背景被填充了灰色。

创建一个名为main_menu.xml的新 xml 布局。这个布局将使用ImageButton节点保存新的背景图像(在一个ImageView中)和两个按钮,如清单 4-1 所示。

清单 4-1main_menu.xml

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

<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"
android:contentDescription="@string/splash_screen_description"
android:scaleType="fitXY"
android:src="@drawable/titlescreen" />

<RelativeLayout
android:id="@+id/buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:orientation="horizontal" >
</RelativeLayout>

<ImageButton
android:id="@+id/btnExit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/buttons"
android:layout_alignParentRight="true"
android:layout_marginBottom="50dp"
android:layout_marginRight="55dp"
android:clickable="true"
android:contentDescription="@string/start_description"
android:src="@drawable/exit" />

<ImageButton
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignTop="@+id/btnExit"
android:layout_marginLeft="48dp"
android:clickable="true"
android:contentDescription="@string/exit_description"
android:src="@drawable/start" />

</RelativeLayout>

现在您已经有了菜单的布局,您需要一个Activity来显示它。在您的游戏项目中创建一个新的Activity;在这个例子中,它将被命名为SBGMenuScreenSBGMenuScreen Activity应该使用setContentView() 来显示新的main_menu布局(参见清单 4-2 )。

清单 4-2SBGMenuScreen布局

public class SBGMenuScreen extends Activity{

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

你现在有了一个由Activity显示的主菜单,但是它在你的游戏项目中处于什么位置呢?

你有两个选择。第一种选择是将SBGMenuScreen设置为你游戏的入口点。第二种是使用闪屏淡入菜单。

如果你选择第一个选项,并将SBGMenuScreen设置为游戏的主要入口,那么这将是玩家看到的第一个屏幕。在许多情况下,这可能是一个非常有效的解决方案,这个例子就讲到这里了。然而,如果你遵循了第三章中的解决方案,并想继续使用闪屏,这个解决方案的其余部分将解释如何在你的闪屏中显示菜单。

从打开MainActivity第三章。这是启动初始屏幕的地方。更改在清单 4-3 中加粗的引用,指向您创建的新SBGMenuScreenActivity

清单 4-3 。启动菜单

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int GAME_THREAD_DELAY = 4000;
setContentView(R.layout.activity_main);
new Handler().postDelayed(new Thread() {
@Override
public void run() {
Intent mainMenu = new Intent(MainActivity.this, SBGMenuScreen.class);
MainActivity.this.startActivity(mainMenu);
   MainActivity.this.finish();
overridePendingTransition(R.layout.fadein,R.layout.fadeout);
}
}, GAME_THREAD_DELAY);
}

}

无论您如何完成您的解决方案,完成的菜单屏幕应如图 4-4 所示。

9781430257646_Fig04-04.jpg

图 4-4 。菜单屏幕

4.2 焊线菜单按钮

问题

单击按钮时没有响应。

解决办法

使用OnClickListener() 对按钮点击做出反应。

它是如何工作的

你的游戏有一个菜单,就像解决方案 4.1 中的那样。然而,当玩家触摸你的按钮时,你的按钮没有反应。这个问题的解决方案比你想象的要简单。要解决这个问题,你需要做的就是创建几个OnClickListener()来监听和响应用户与你的按钮的交互。

这个解决方案使用显示菜单的Activity。如果您使用配方 4.1 中的解决方案创建了一个菜单,那么您需要打开的文件是SBGMenuScreen。 清单 4-4 提供了菜单Activity的当前代码。

清单 4-4 。sbg 菜单屏幕

public class SBGMenuScreen extends Activity{

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

SBGMenuScreen中引用的main_menu布局包含以下代码。我给你的是main_menu布局的代码,因为这个解决方案需要从布局中调用元素。因此,万一您的菜单布局不完全匹配,您将有一个工作参考。

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

<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"
android:contentDescription="@string/splash_screen_description"
android:scaleType="fitXY"
android:src="@drawable/titlescreen" />

<RelativeLayout
android:id="@+id/buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:orientation="horizontal" >
</RelativeLayout>

<ImageButton
android:id="@+id/btnExit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/buttons"
android:layout_alignParentRight="true"
android:layout_marginBottom="50dp"
android:layout_marginRight="55dp"
android:clickable="true"
android:contentDescription="@string/start_description"
android:src="@drawable/exit" />

<ImageButton
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignTop="@+id/btnExit"
android:layout_marginLeft="48dp"
android:clickable="true"
android:contentDescription="@string/exit_description"
android:src="@drawable/start" />

</RelativeLayout>

解决方案的第一步是创建一对ImageButton 变量,并将它们设置为菜单布局中使用的图像按钮。您将用来为图像按钮设置变量的方法是findViewById()

提示因为findViewById()本身并不知道你正在查找的视图的类型,所以在分配它之前,一定要确保结果是正确的类型。

清单 4-5 。findViewByIf

public class SBGMenuScreen extends Activity{

@Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main_menu);
ImageButton start = (ImageButton)findViewById(R.id.btnStart);
        ImageButton exit = (ImageButton)findViewById(R.id.btnExit);

   }
}

所有视图都有方法setOnClickListener()。您将使用这个方法为特定的按钮分配一个新的OnClickListener()。这就是完成解决方案所需的全部内容。

清单 4-6 。方法

public class SBGMenuScreen extends Activity{

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_menu);

ImageButton start = (ImageButton)findViewById(R.id.btnStart);
ImageButton exit = (ImageButton)findViewById(R.id.btnExit);

start.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {

//TODO all of your startup code

}

});

exit.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {

//TODO all of your exit code

}
});

   }
}

每个OnClickListener()都有一个OnClick()方法。OnClick()方法中的代码将在每次按钮的OnClickListener()被触发时被执行。替换

当玩家分别按下开始或退出按钮时,TODO用您希望执行的代码进行注释。

4.3 启动一个游戏线程

问题

当玩家按下菜单上的开始游戏按钮时,游戏线程需要开始。

解决办法

从开始按钮的OnClick()方法中启动游戏Activity

它是如何工作的

这是一个相对简单的解决方案,只需在 start 按钮的OnClick()方法中添加几行代码。如果你已经有一个Activity用来开始你的游戏,在这里使用它。如果你的游戏还没有Activity,创建一个基本的Activity,如清单 4-7 所示。

清单 4-7基本活动

public class SBGGameMain extends Activity {

private SBGGameView gameView;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//The content view here represents the GLSurfaceView
//for your game
gameView = new SBGGameView(this);
setContentView(gameView);
}
@Override
protected void onResume() {
super.onResume();
gameView.onResume();
}

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

}

同样,如果你已经为你的游戏创建了一个Activity,在你的解决方案中使用它来代替这个。

启动游戏的唯一步骤是修改连接到开始按钮的OnClickListener() a 的OnClick()方法。简单地为游戏Activty创建一个新的Intent,并从OnClick()内部启动它,如清单 4-8 所示。

清单 4-8 。从onClick() 启动Activity

start.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
//Start the game
Intent game = new Intent(getApplicationContext(),SBGGameMain.class);
SBGMenuScreen.this.startActivity(game);

}

});

现在,当玩家按下开始按钮时,你的菜单将干净利落地进入游戏。

4.4 干净地退出一个游戏线程

问题

游戏在退出时需要清理所有线程和正在运行的进程。

解决办法

创建一个方法,在退出游戏前关闭打开的项目。然后,杀死游戏线程。

它是如何工作的

这是一个由两部分组成的解决方案,包括创建一个方法,可以在游戏退出之前调用该方法来完成任何内务处理,然后终止游戏线程。

在玩家关闭游戏之前,您可能需要处理一些任务,例如保存玩家数据、将统计数据更新到中央服务器,甚至是取消正在播放的任何背景音乐。为此,您需要在游戏中的某个地方创建一个可以从主菜单中调用的方法。

在清单 4-9 的中,我创建了一个名为onExit()的方法。在onExit()中,我正在删除游戏中播放的一些背景音乐。同样,您向onExit()添加执行日常工作所需的任何代码。该方法的重要部分是返回一个布尔值。结果为真意味着一切都已处理好,游戏可以退出,而结果为假则需要在游戏退出前进一步处理。

清单 4-9onExit()

public boolean onExit(View v) {
try
{
//Sample code to stop some background music
Intent bgmusic = new Intent(context, SFMusic.class);
context.stopService(bgmusic);
musicThread.stop();

return true;
}catch(Exception e){
return false;

}

}

提示onExit()方法可以在项目中的任何地方,只要它能看到你想在其中做的任何事情。

现在,为你的退出按钮的OnClickListener()修改你的OnClick()方法来调用onExit()(参见清单 4-10 )。

清单 4-10 。呼叫onExit()

exit.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
boolean clean = false;
clean = onExit(v);
if (clean)
{
}
}
});

最后,假设你的onExit()返回一个真结果,杀死当前进程并退出(见清单 4-11 )。

清单 4-11 。扼杀游戏进程

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

4.5 交换菜单按钮图像

问题

菜单按钮应该改变颜色或图像时,点击。

解决办法

将按钮图像的源指向控制图像交换的 xml 选择器。

它是如何工作的

当玩家选择菜单的按钮图像时,你可能希望通过改变菜单的按钮图像来增加游戏菜单的冲击力。通过创建一个 xml 选择器,可以很容易地实现这一点,该选择器包含指向所需图像的指针,以及显示这些图像的状态。然后,在布局文件中,将指向按钮原始图像文件的源指针替换为 xml 选择器的源指针。

对于这个解决方案,当玩家选择适当的按钮时,您将在图 4-2 和 4-3 中的图像与图 4-5 和 4-6 中的图像之间进行交换。

9781430257646_Fig04-05.jpg

图 4-5 。新的开始按钮图像

9781430257646_Fig04-06.jpg

图 4-6 。新的退出按钮图像

原始按钮图像分别被称为@drawable/start@drawable/exit。新文件一旦添加到drawable文件夹中,就会变成@drawable/newstart@drawable/newexit。你可以通过三个步骤来完成。

第一步是创建一个名为startselector.xml的新 xml 文件,并确保将其与图像一起放在drawable文件夹中。这不是存放 xml 文件的通常位置。通常,您会想到将一个 xml 文件放入layout

文件夹。但是,因为该文件将被替换为图像源,所以需要将其放在drawable文件夹中。

打开startselector.xml文件并创建如清单 4-12 所示的 xml 选择器。

清单 4-12startselector.xml

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

选择器中的两个item表示您想要换出图像的不同状态。第一个item是默认状态。这是将在空闲条件下显示的图像。第二个item只有在state_pressedtrue时才显示。因此,当按下按钮时,选择器将向其发送要显示的newstart图像。

创建名为exitselector.xml的第二个选择器 xml 文件,如清单 4-13 所示。该文件的格式应该与startselector.xml文件相同,尽管它将用于更改退出按钮图像。

清单 4-13exitselector.xml

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

这个解决方案的最后一步是更改菜单的布局文件。更改每个按钮的图像源以指向合适的选择器,而不是图像文件(见清单 4-14 )。

清单 4-14main_menu.xml

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

<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"
android:contentDescription="@string/splash_screen_description"
android:scaleType="fitXY"
android:src="@drawable/titlescreen" />

<RelativeLayout
android:id="@+id/buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:orientation="horizontal" >
</RelativeLayout>

<ImageButton
android:id="@+id/btnExit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/buttons"
android:layout_alignParentRight="true"
android:layout_marginBottom="50dp"
android:layout_marginRight="55dp"
android:clickable="true"
android:contentDescription="@string/start_description"
android:src="@drawable/exitselector" />

<ImageButton
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignTop="@+id/btnExit"
android:layout_marginLeft="48dp"
android:clickable="true"
android:contentDescription="@string/exit_description"
android:src="@drawable/startselector" />

</RelativeLayout>

4.6 锁定屏幕方向

问题

当设备在横向和纵向模式之间移动时,菜单屏幕不应改变方向。

解决办法

锁定屏幕方向,使其无法改变。

它是如何工作的

对于一个常见的问题,这是一个相当简单的解决方案。最快的方法是手动编辑项目的AndroidManifest.xml文件。清单文件包含项目活动的主要设置。将游戏的所有屏幕锁定在特定方向是个好主意。

找到主菜单Activity的活动标签,并将其锁定为横向模式,如下所示:

<activity android:name="SBGMenuScreen" android:screenOrientation="landscape"></activity>