安卓游戏秘籍-全-

安卓游戏秘籍(全)

原文: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>

五、读取玩家输入

如果这是你第一次为移动设备或平板电脑编写游戏代码,你可能会很快注意到明显缺少输入选项来将玩家的意图反馈到游戏代码中。没有游戏控制器、键盘或鼠标的好处,很难为你的玩家提供复杂的输入系统。

连接游戏来检测和响应设备上的触摸事件并不像表面上看起来那么困难。

让我们来看看使用触摸屏作为游戏输入的一些更常见的问题。

5.1 检测屏幕触摸

问题

您的游戏无法检测到玩家何时触摸了屏幕。

解决办法

使用onTouchEvent()检测玩家触摸屏幕的位置和时间。

它是如何工作的

你的 Android 游戏是从一个扩展了Activity的类启动的。这个类将用于检测游戏中发生的触摸事件并做出反应。请记住,你的游戏代码和游戏循环将通过RendererGLSurfaceView中运行。但是,您仍然可以使用启动游戏的Activity来跟踪玩家在屏幕上的输入。

在您的Activity中,按如下方式覆盖onTouchEvent():

@Override
public boolean onTouchEvent(MotionEvent event) {
}

onTouchEvent()接收一个MotionEvent。当事件调用生成时,这个MotionEvent由系统自动传入。

MotionEvent包含了所有你需要的信息来帮助你判断和解读玩家的行动。从MotionEvent中,你可以获得玩家触摸的 x 和 y 坐标、触摸的压力和持续时间等信息,甚至可以确定滑动的方向。

例如,这里您只是简单地获取玩家的触摸坐标:

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

现在,您可以根据需要对 x 和 y 坐标做出反应。

5.2 检测屏幕多点触摸

问题

您的游戏无法使用onTouchEvent() 同时检测多个屏幕触摸。

解决办法

使用getPointerCount()PointerCoords帮助检索指针对象,以检测多点触摸输入。

它是如何工作的

传递给onTouchEvent()MotionEvent可以跟踪多达五个不同的同时发生的屏幕触摸。这里的概念是遍历所有使用getPointerCount()检测到的指针。在圈内,你是

将使用getPointerID()来检索每个指针所需的信息。

首先设置你的onTouchEvent()并遍历检测到的指针,如清单 5-1 所示。

清单 5-1onTouchEvent()

@Override
public boolean onTouchEvent(MotionEvent event) {

MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[event.getPointerCount()];

   For(int i = 0; i< event.getPointerCount(); i++)
   {
event.getPointerCoords(i, coords[i]);
   }
}

现在,您可以从检测到的每个指针中获得所需的所有信息。将coord[]传递到你的游戏循环中,你将获得每个触摸点的 x 和 y 坐标。您还将获得触摸点的方向、压力、大小(面积)以及长轴和短轴的长度。

5.3 将屏幕划分为触摸区域

问题

您需要确定玩家是触摸了屏幕的右侧还是左侧。

解决办法

使用屏幕的高度和宽度来确定玩家触摸了屏幕的哪一侧。

它是如何工作的

你知道如何使用onTouchEvent()来确定玩家是否以及何时触摸了屏幕,以及玩家触摸的坐标。当你试图为你的游戏创建一个输入系统时,这是非常有用的信息。你现在面临的问题是试图确定给你的 x 和 y 坐标是否落在屏幕的特定区域内。

假设你正在创建一个平台游戏,玩家可以向左向右跑。你已经设置好了你的onTouchEvent(),每次玩家触摸屏幕时,你都要捕捉 x 和 y 坐标。你怎么能轻易地确定这些坐标应该把玩家推向左边还是右边呢?

答案是把屏幕分成触摸区。在这种情况下,我们希望一个区域位于屏幕的左侧,另一个区域位于屏幕的右侧。一些简单的if语句可以用来检查玩家在屏幕上触摸的位置。

以平台游戏为例,玩家只能向左和向右移动,你可以把屏幕分成两半,一个代表左边,一个代表右边。你也可以考虑把触摸区放在屏幕底部,玩家的拇指可能会在那里。

这意味着您必须忽略落在左右触摸区上方的任何触摸坐标。看一看图 5-1 和图 5-2 中的图,了解这一概念的直观表示。

9781430257646_Fig05-01.jpg

图 5-1 。具有左右触摸区的肖像模式

9781430257646_Fig05-02.jpg

图 5-2 。带左右触摸区的横向模式

创建触摸区域的第一步是获取屏幕的高度。为此,在公共类上创建一个新的Display属性,如下所示:

public static Display display;

在应用的主Activity上,使用WINDOW_SERVICE将默认显示复制到这个属性,如清单 5-2 所示。

清单 5-2 。使用WINDOW_SERVICE

MyClass.display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

你现在可以在你的游戏代码中决定屏幕的高度和宽度,如清单 5-3 所示。

清单 5-3 。确定高度和宽度

@Override
public boolean onTouchEvent(MotionEvent event) {
//Get the non-touchable area of the screen -
//the upper two-thirds of the screen
int height = MyClass.display.getHeight() / 3;

//The playable area is now the lower third of the screen
int playableArea = MyClass.display.getHeight() - height;
}

警告这种方法有效,但只有当你的游戏像这个一样使用全屏时才完全有效。如果你的游戏不打算使用全屏,等到游戏的视图加载后再调用<view>.getHeight()

使用值playableArea 作为 y 轴值,您可以很容易地判断出您的玩家是否触摸到了屏幕的正确部分。创建一个简单的if语句来测试玩家触摸坐标的位置(参见清单 5-4 )。

清单 5-4 。使用playableArea

@Override
public boolean onTouchEvent(MotionEvent event) {
//Get the non-touchable area of the screen -
//the upper two-thirds of the screen
int height = MyClass.display.getHeight() / 3;

//The playable area is now the lower third of the screen
 int playableArea = MyClass.display.getHeight() - height;

if (y > playableArea){

//This y coordinate is within the touch zone

}
}

现在你知道玩家已经触摸到了屏幕的正确区域,那么就可以通过测试 x 坐标是大于还是小于屏幕中心点来确定触摸区的左右和两侧(见清单 5-5 ) 。

清单 5-5 。测试触摸区域

@Override
public boolean onTouchEvent(MotionEvent event) {
//Get the non-touchable area of the screen -
//the upper two-thirds of the screen
int height = MyClass.display.getHeight() / 3;

//Get the center point of the screen
int center = MyClass.display.getWidth() / 2;

//The playable area is now the lower third of the screen
int playableArea = MyClass.display.getHeight() - height;

if (y > playableArea){

//This y coordinate is within the touch zone

if(x < center){
//The player touched the left
}else{
//The player touched the right
}

}
}

您已成功确定玩家触摸了屏幕的左侧还是右侧。将注释替换为您的特定代码,以根据玩家触摸的位置启动操作。

5.4 检测屏幕滑动

问题

你需要确定玩家是否滑动或投掷了屏幕,以及向哪个方向。

解决办法

使用SimpleOnGestureListener和然后计算投掷的方向。

它是如何工作的

对于一些游戏——比如《神庙逃亡》——你想让用户滑动或投掷屏幕来指示他们想要移动的方向。例如,向上一扔可能代表一次跳跃。这可能是一种更加通用的玩家输入方法,但是它也需要稍微多一点的设置代码。

实现这一点所需的代码将在与OnTouchEvent()相同的Activity上运行。事实上,你可以把这两个词——OnTouchEvent()SimpleOnGestureListener——结合起来使用。

打开您的Activity并实例化一个SimpleInGestureListener,如下所示:

GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
};

您需要在手势监听器中实现几个方法。然而,在这个解决方案中,你唯一要使用的是OnFling(),它在 清单 5-6 中提供。

清单 5-6OnFling()

GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEvent arg0) {
//TODO Auto-generated method stub
return false;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
//React to the fling action
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//TODO Auto-generated method stub
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//TODO Auto-generated method stub
return false;
}

};

现在,在您的Activity中创建一个新变量,如下所示:

private GestureDetector gd;

GestureDetector将用于抛出手势事件。将Activity、的onCreate()中的探测器初始化如下:

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gd = new GestureDetector(this,gestureListener);
}

最后,在OnTouchEvent()中,扔给gestureListener,像这样:

@Override
public boolean onTouchEvent(MotionEvent event) {
returngd.onTouchEvent(event);
}

当玩家抛出屏幕时,会执行OnFling()方法中的代码。这处理了什么和什么时候;接下来你需要确定什么方向。

注意OnFling()有两个MotionEvent属性。因为您之前使用过它,所以您知道MotionEvent包含一个getX()和一个getY(),用于获取事件各自的坐标。

这两个事件(e1e2)代表投掷的起点和终点。因此,使用每个事件的 x 和 y 坐标,可以计算出玩家向哪个方向移动(参见清单 5-7 ) 。

清单 5-7 。检测投掷运动

float leftMotion = e1.getX() - e2.getX();
float upMotion = e1.getY() - e2.getY();

float rightMotion = e2.getX() - e1.getX();
float downMotion = e2.getY() - e1.getY();

if((leftMotion == Math.max(leftMotion, rightMotion)) && (leftMotion > Math.max(downMotion, upMotion)) )
{
//The player moved left
}

if((rightMotion == Math.max(leftMotion, rightMotion)) && rightMotion > Math.max(downMotion, upMotion) )
{
//The player moved right
}
if((upMotion == Math.max(upMotion, downMotion)) && (upMotion > Math.max(leftMotion, rightMotion)) )
{
//The player moved up
}

if((downMotion == Math.max(upMotion, downMotion)) && (downMotion > Math.max(leftMotion, rightMotion)) )
{
//The player moved down
}

现在你可以为你在游戏中需要采取的行动填入适当的代码。

因为这个解决方案绕过了Activity一点点,清单 5-8 显示了完成的Activity看起来应该是的样子。

清单 5-8SBGGameMain的完整代码

public class SBGGameMain extends Activity {
private GestureDetector gd;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(myContentView);
gd = new GestureDetector(this,gestureListener);
}
@Override
protected void onResume() {
super.onResume();
}

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

@Override
public boolean onTouchEvent(MotionEvent event) {
return gd.onTouchEvent(event);
}

GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEvent arg0) {
//TODO Auto-generated method stub
return false;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {

float leftMotion = e1.getX() - e2.getX();
float upMotion = e1.getY() - e2.getY();

float rightMotion = e2.getX() - e1.getX();
float downMotion = e2.getY() - e1.getY();

if((leftMotion == Math.max(leftMotion, rightMotion)) && (leftMotion > Math.max(downMotion, upMotion)) )
{

}

if((rightMotion == Math.max(leftMotion, rightMotion)) && rightMotion > Math.max(downMotion, upMotion) )
{

}
if((upMotion == Math.max(upMotion, downMotion)) && (upMotion > Math.max(leftMotion, rightMotion)) )
{

}

if((downMotion == Math.max(upMotion, downMotion)) && (downMotion > Math.max(leftMotion, rightMotion)) )
{

}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//TODO Auto-generated method stub
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//TODO Auto-generated method stub
return false;
}

};
}

5.5 使用设备加速度计

问题

当玩家倾斜设备时,游戏角色不会移动。

解决办法

使用设备的内置加速度计来检测设备何时向特定方向倾斜,然后相应地移动角色。

它是如何工作的

大多数(如果不是全部的话)Android 设备都包含一个加速度计。这种传感器的一个普遍用途是作为游戏的另一个输入设备。使用来自加速度计的反馈,您可以检测玩家是否倾斜了设备,然后在代码中做出相应的反应。

在清单 5-9 中,你检测玩家是向左还是向右倾斜手机,然后设置适当的变量使角色向倾斜的方向移动。首先,在你的Activity类的中实现SensorEventListener。然后允许 Eclipse(或您选择的 IDE)添加所需的方法覆盖。

清单 5-9 。SensorEvenListener

public class SBGGameMain extends Activityimplements SensorEventListener{
@Override
public void onCreate(Bundle savedInstanceState) {
//TODO Auto-generated method stub
}
@Override
protected void onResume() {
//TODO Auto-generated method stub
}
@Override
protected void onPause() {
//TODO Auto-generated method stub
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
//TODO Auto-generated method stub

}
@Override
public void onSensorChanged(SensorEvent event) {
//TODO Auto-generated method stub
}

}

需要几个变量。prevXprevY跟踪先前的 x 和 y 轴倾斜位置,以确定倾斜是否有变化。布尔函数isInitialized确定先前是否检测到倾斜;如果不是,新值被存储在prevXprevY中。静态浮动NOISE保存一个值,让您根据环境设备的移动来确定实际的倾斜变化。最后,设置SensorManager和加速度计的变量。参见清单 5-10 。

清单 5-10 。传感器管理器

public class SBGGameMain extends Activity implements SensorEventListener{
private float prevX;
private float prevY;
private boolean isInitialized;
private final float NOISE = (float) 2.0;
private SensorManager sensorManager;
private Sensor accelerometer;
@Override
public void onCreate(Bundle savedInstanceState) {
//TODO Auto-generated method stub
}
@Override
protected void onResume() {
//TODO Auto-generated method stub
}
@Override
protected void onPause() {
//TODO Auto-generated method stub
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
//TODO Auto-generated method stub

}
@Override
public void onSensorChanged(SensorEvent event) {
//TODO Auto-generated method stub
}

}

接下来,在执行onSensorChanged()方法中的核心代码之前,在onCreate()onPause()onResume()方法中执行一些内务处理(参见清单 5-11 ) 。

清单 5-11 。onSensorChanged

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

isInitialized= false;
sensorManager= (SensorManager) getSystemService(this.SENSOR_SERVICE);
accelerometer= sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

}
Override
protected void onResume() {
super.onResume();

sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

gameView.onResume();

}

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

sensorManager.unregisterListener(this);

gameView.onPause();
}

现在是解决方案的核心。当检测到传感器的变化时,触发onSensorChanged()方法;在这种情况下,这就是加速度计。捕捉变化,并使用 x 和 y 向量来设置你的PLAYER_MOVE_LEFTPLAYER_MOVE_JUMP,如清单 5-12 中的所示。

清单 5-12 。设置玩家动作

public class SBGGameMain extends Activity implements SensorEventListener{
private float prevX;
private float prevY;
private boolean isInitialized;
private final float NOISE = (float) 2.0;
private SensorManager sensorManager;
private Sensor accelerometer;

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

isInitialized= false;
sensorManager= (SensorManager) getSystemService(this.SENSOR_SERVICE);
accelerometer= sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

}
@Override
protected void onResume() {
super.onResume();

sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

gameView.onResume();

}

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

sensorManager.unregisterListener(this);

gameView.onPause();
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
//TODO Auto-generated method stub

}
@Override
public void onSensorChanged(SensorEvent event) {
float x = event.values[0];
float y = event.values[1];
if (!isInitialized) {
prevX = x;
prevY = y;
isInitialized = true;
} else {
float deltaX = Math.abs(prevX - x);
float deltaY = Math.abs(prevY - y);
if (deltaX < NOISE) deltaX = (float)0.0;
if (deltaY < NOISE) deltaY = (float)0.0;
prevX = x;
prevY = y;
if (deltaX > deltaY) {
playeraction = PLAYER_MOVE_LEFT;
} else if (deltaY > deltaX) {
playeraction = PLAYER_MOVE_JUMP;
} else {

}
}
}
}

六、加载精灵表

很有可能到这个时候,你已经有了 Android 平台上游戏的外壳或雏形。也有可能你已经尝试了动画你的一个或所有的角色,武器,或其他屏幕上的对象没有运气。

如果您尝试过加载单独的图像,您无疑会发现翻转这些图像来创建动画的过程极其缓慢。这个问题的解决方案几乎和视频游戏本身一样古老:精灵表。大多数 2D 的视频游戏仍然采用一种久经考验的动画技术,并且非常适合这项任务;也就是说,创建游戏中需要的动画帧的 sprite 表。

在这一章中,你将使用 sprite sheets 完成一些常见问题的解决方案。

6.1 使用一个精灵表

问题

为动画加载多个独立的图像会占用太多空间,而且速度很慢。

解决办法

使用一个 sprite 表,在一个图像文件中包含所有动画帧。

它是如何工作的

让我们从基础开始。sprite sheet 是一个单独的图像文件,它保存了可用于创建动画 sprite 的所有不同图像。

我们示例游戏的主角——超级强盗——应该能够在屏幕上跑来跑去。这就要求超级强盗的精灵在奔跑时是动画的。动画中的每个图像都被加载到一个名为 sprite sheet 的单个文件中,而不是为动画的每一帧创建一个单独的图像(这样会非常耗费资源,以至于最终的游戏可能都无法加载)。图 6-1 显示了超级强盗家伙奔跑动画的精灵表的细节。

9781430257646_Fig06-01.jpg

图 6-1 。超级强盗家伙运行(详细)

请注意,动画的不同帧都放在一个文件中,因此减少了存储、调用、交换和显示单独图像所需的资源。

将图像放入您的res/drawable文件夹。这与用于任何其他图像文件的过程相同。所有图像文件都可以存储在res/drawable文件夹中,然后使用R.drawable.<imagename>通过 id 轻松调出。但是,请记住,所有图像名称必须小写,否则您将无法调用它们。

现在的问题是:如何一次只显示一帧,而不是一次显示整个 sprite 工作表?这实际上比看起来容易。使用 OpenGL ES,您将调整该图像或纹理的大小,以便您想要显示的一帧动画一次适合顶点(在下一个解决方案中解释)。记住,在 OpenGL ES 中,你的纹理和顶点可以有不同的大小。

注意仅仅因为 OpenGL ES 使用的所有图像都必须是正方形,并不意味着 sprite 表中的每个空间都必须包含一帧动画。虽然我们为超级强盗家伙使用的 4x4sheet 可以容纳 16 帧动画,但我们只使用了 10 帧。

图 6-2 显示了超级强盗正在使用的精灵表。

9781430257646_Fig06-02.jpg

图 6-2 。超级强盗(全精灵表)

注意为了在本书中展示,图像的背景被染成灰色。理想情况下,你的图片应该有透明的背景。

6.2 访问精灵表中的图像

问题

显示一个 sprite 表显示整个图像,而不是需要的单个图像。

解决办法

调整纹理映射以显示所需的 sprite sheet 部分。

它是如何工作的

为了理解这个解决方案是如何工作的,你需要首先理解你的纹理映射到的顶点和纹理对象本身是两个独立的实体,可以彼此独立地操作。这意味着您可以调整大小,移动或改变纹理,而不会影响顶点。

你已经知道图 6-2 中的精灵表包含了让超级强盗看起来像在跑的所有动画帧。然而,如果你尝试使用精灵作为纹理,两件事会立即变得明显。一是纹理出现倒挂;其次,整个 sprite 表被映射到顶点上,而不仅仅是一帧动画。

当 OpenGL 创建一个纹理时,图像被加载到一个字节数组中。当图像被加载到数组中时,图像的第一个字节被加载到数组的后面,接着是第二个字节,依此类推。当 OpenGL 开始从数组中读取纹理信息时,它读取的第一个字节(数组中的第一个字节)实际上是从文件中出来的最后一个字节。所以 OpenGL 的纹理是你原图的反转版。

你需要翻转 OpenGL 中的纹理,使其正面朝上。然后,您需要调整映射到顶点的纹理的大小,以便只有一帧 sprite 工作表是可见的。图 6-3 说明了这个概念。

9781430257646_Fig06-03.jpg

图 6-3 。将精灵纹理翻转并映射到顶点

首先,让我们注意翻转图像,使其正面朝上。

提示 OpenGL ES 以同样的方式处理所有图像到纹理的加载,不管它们是否是精灵片。因此,当你的所有图像变成纹理时,它们总是会反过来。解决方案的这一步应该在加载所有纹理时执行。

在您编写的将图像加载到纹理的代码中,实例化一个新的Matrix并使用postScale()方法创建新的矩阵,该矩阵沿 y 轴翻转纹理。新的矩阵被传递给通常用于加载纹理的createBitmap()方法。

在清单 6-1 中,texture代表你想要加载的图像的参考 id,在 drawable文件夹中可以找到。

清单 6-1 。使用postScale()

InputStreamimagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
Bitmap temp = null;

Matrix mtrx = new Matrix();
mtrx.postScale(1f, -1f);

temp = BitmapFactory.decodeStream(imagestream);
bitmap = Bitmap.createBitmap(temp, 0, 0, temp.getWidth(), temp.getHeight(), mtrx, true);

imagestream.close();
imagestream = null;

现在你的纹理已经以正确的方式翻转了,是时候调整纹理了,这样只有一帧可以明显地映射到你的顶点。同样,这可以在构建纹理和顶点时加载完成。

你目前用来加载纹理的代码,部分看起来应该像清单 6-2 。

清单 6-2 。纹理数组

privateFloatBuffervertexBuffer;
privateFloatBuffertextureBuffer;
privateByteBufferindexBuffer;

private float[] vertices = {
0f, 0f, 0f,
1f, 0f, 0f,
1f, 1f, 0f,
0f, 1f, 0f,
};

private float[] texture = {
0f, 0f,
1f, 0f,
1f, 1f,
0f, 1f,
};

因为 OpenGL ES 中的默认坐标系从 0 到 1,所以清单 6-2 中的数组使用整个纹理。使用这个数组,整个纹理将被映射到顶点上。然而,考虑到图 6-2 中的 sprite 工作表,你一次只想看到 sprite 工作表的四分之一。

图 6-2 中的 sprite 页被分成四行四幅图像(并未全部使用)。因此,每行是整个纹理高度的 25 %,每列是整个纹理宽度的 25%。

修正纹理数组,如清单 6-3 所示,只显示精灵表中的一帧动画。

清单 6-3 。新纹理数组

privateFloatBuffervertexBuffer;
privateFloatBuffertextureBuffer;
privateByteBufferindexBuffer;

private float[] vertices = {
0f, 0f, 0f,
1f, 0f, 0f,
1f, 1f, 0f,
0f, 1f, 0f,
};

private float[] texture = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

6.3 改变精灵图框

问题

图像需要从 sprite 表中的一帧变化到另一帧,而不是静态的。

解决办法

通过沿 x 和/或 y 轴平移纹理,从一个 sprite sheet 帧移动到另一个 sprite sheet 帧。

它是如何工作的

在 OpenGL ES 1 中使用glTranslatef()方法在坐标系中平移或移动矩阵。要从 sprite 工作表的第一帧切换到第二帧,需要将纹理矩阵沿 x 轴平移 25%。(这是假设你正在使用一个像图 6-2 中那样设置的 sprite 工作表)。

第一步是将 OpenGL ES 置于纹理矩阵模式,从而确保您修改的是纹理的坐标,而不是顶点。以下代码将 OpenGL ES 置于纹理矩阵模式,并将 sprite 工作表的第一帧(左上角)映射到顶点。

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

注意,传递给glTranslatef()的 y 坐标是. 75。在 0–1 的坐标范围内,. 75 对应于 sprite 工作表中第一行帧的左下角。在这个代码示例中,传递给glTranslatef()的 x 和 y 坐标分别是 0 和. 75。把这个带到图 6-4 中的图像,(0,. 75)是精灵表第一行第一帧的左下角。图 6-4 显示了 y 轴上的坐标是如何与精灵表对齐的。

9781430257646_Fig06-04.jpg

图 6-4 。具有 y 轴坐标的 Sprite 工作表

如果你想改变贴图到顶点的纹理到 sprite 表第一行的第二帧,使用glTranslatef()方法移动纹理到(. 25,. 75)。0.25 的 x 坐标表示第一行第二帧的 x 轴左下角。

gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(.25f,.75f, 0f);

如果您使用的是 OpenGL ES 2 或 3,更改 sprite 工作表框架的过程是不同的。您将需要添加一对浮点到您的片段着色器。这些浮动将接受框架位置的 x 和 y 坐标值,很像glTranslatef()

首先,将浮点添加到片段着色器代码,如清单 6-4 中的所示。

清单 6-4 。向片段着色器代码添加浮点

private final String fragmentShaderCode =
"precisionmediump float;" +
"uniformvec4vColor;" +
"uniformsampler2DTexCoordIn;" +
"uniform float posX;" +
"uniform float posY;" +
"varyingvec2TexCoordOut;" +
"void main() {" +
"}";

接下来,修改片段着色器的main()方法来调用texture2d()并向其传递清单 6-5 中posXposY, as shown in 和的值。

清单 6-5 。修改main()方法

private final String fragmentShaderCode =
"precisionmediump float;" +
"uniformvec4vColor;" +
"uniformsampler2DTexCoordIn;" +
"uniform float posX;" +
"uniform float posY;" +
"varyingvec2TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x + posX,TexCoordOut.y + posY));"+
"}";

着色器代码现已修改。您需要一种方法将posXposY的值传递到着色器代码中。这最终是使用glUniform1f()完成的。改变纹理的 x 和 y 位置的代码应该放在对象类的draw()方法中。修改方法签名,允许在调用draw()时传递坐标。

public void draw(float[] mvpMatrix,float posX, float posY) {
...
}

使用glGetUniformLocation()获得posXposY浮动在着色器中的位置,然后使用glUniform1f()分配新值,如清单 6-6 所示。

清单 6-6draw()

public void draw(float[] mvpMatrix, float posX, float posY) {
GLES20.glUseProgram(mProgram);

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

GLES20.glEnableVertexAttribArray(mPositionHandle);

intvsTextureCoord = 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]);
intfsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
intfsPosX = GLES20.glGetUniformLocation(mProgram, "posX");
intfsPosY = GLES20.glGetUniformLocation(mProgram, "posY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsPosX, posX);
GLES20.glUniform1f(fsPosY, posY);
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);
}

6.4 将 Sprite 表中的图像 制作成动画

问题

一个图像需要是一个随时间变化的动画 (就好像角色在跑)。

解决办法

以特定顺序浏览多个 sprite 工作表图像。

它是如何工作的

在这个解决方案中,您将构建在前一个解决方案中使用的glTranslatef()glUnifor1f()方法。OpenGL ES 1 的glTranslatef()方法已经显示了移动顶点上的贴图纹理,这样只有 sprite sheet 的特定部分是可见的。如果你足够快地执行这个动作,并且有足够的帧数,你将会有动画。

对于这个解决方案,你再次使用图 6-2 中所示的精灵表。这个解决方案也建立在第五章“读取玩家输入”的基础上

创建一个枚举,当玩家触摸屏幕的右侧或左侧时,可以设置该枚举,指示角色应该分别向右或向左跑(见清单 6-7 )。

这些变量应该放在你可以从渲染器和主Activity 访问它们的地方。

清单 6-7 。更新玩家移动

public static intplayerAction = 0;
public static final int PLAYER_MOVE_LEFT = 1;
public static final int PLAYER_STAND = 0;
public static final int PLAYER_MOVE_RIGHT = 2;

你还需要设置另外六个变量(清单 6-8 )。

清单 6-8 。设置另外六个变量

public static float playerCurrentLocation  = .75f;
public static float currentRunAniFrame  = 0f;
public static float currentStandingFrame   = 0f;

public static final float PLAYER_RUN_SPEED = .25f;
public static final float STANDING_LEFT = 0f;
public static final float STANDING_RIGHT = .75f;

playerCurrentLocation用于跟踪屏幕上精灵的当前位置。currentRunAniFrame用于跟踪精灵表中动画的当前帧,这使得角色看起来在运行。像currentRunAniFrame一样,currentStandingFrame被用来跟踪精灵表的哪一帧被用来使角色看起来是站着的。

PLAYER_RUN_SPEED将用于以特定的间隔在屏幕上移动精灵。结合动画,PLAYER_RUN_SPEED用来给人一种角色实际在跑的错觉。最后,STANDING_LEFTSTANDING_RIGHT变量保存代表角色站立的 sprite 表中两个帧的 x 轴左下角的值。一个框架面向左,另一个面向右。

回头参考第五章,清单 6-9 根据玩家是触摸了屏幕的右侧还是左侧来设置playerAction。游戏主ActivityonTouchEvent被修改为playerAction设置为PLAYER_MOVE_RIGHTPLAYER_MOVE_LEFTPLAYER_STAND

清单 6-9onTouchEvent()

@Override
publicbooleanonTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
DisplayMetricsoutMetrics = new DisplayMetrics();

display.getMetrics(outMetrics);

int height = outMetrics.heightPixels / 4;

int playableArea = outMetrics.heightPixels - height;
if (y >playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x <outMetrics.widthPixels / 2){
playerAction = PLAYER_MOVE_RIGHT;
}else{
playerAction = PLAYER_MOVE_LEFT;
}
break;
case MotionEvent.ACTION_UP:
playerAction = PLAYER_STAND;
break;
}
}

return false;
}

接下来,设置一个case语句来读取playerAction的值。游戏循环包含在RendereronDraw()方法中。该方法在一个常量循环中执行。因此,您可以在Renderer中创建一个名为movePlayer()的新方法,并从RendereronDraw()方法中调用它。

每次onDraw()方法执行,都会调用movePlayer()。在movePlayer()方法中,你所需要做的就是告诉 OpenGL ES 你想如何翻转精灵页面并“移动”角色。

首先,创建movePlayer()方法并设置一个case语句来遍历playerAction。在清单 6-10 所示的代码中,goodguy指的是SuperBanditGuy类的实例化。这可以代表你在 游戏中使用的任何职业。

清单 6-10movePlayer()

private void movePlayer(GL10gl){
if(!goodguy.isDead)
{
switch(playeraction){
case PLAYER_MOVE_RIGHT:

break;

case PLAYER_MOVE_LEFT:

break;

case PLAYER_STAND:

break;
}
}
}

在 Recipe 6.3 中,你学习了如何使用glTanslatef()glUniform1f()方法从 sprite 工作表的一帧移动到另一帧。这个解决方案中唯一的不同是您将自动化这个过程。这意味着因为onDraw(),也就是movePlayer(),是在一个循环中被调用的,你必须以这样一种方式编写对glTranslatef()的调用,它将在每次被调用时自动从一帧循环到下一帧。清单 6-11 和 6-12 展示了当您想要使用 OpenGL ES 1 和 OpenGL ES 2/3 将字符向右移动时,这段代码的样子。

清单 6-11 。用播放器移动画面(OpenGLES 1)

currentStandingFrame   = STANDING_RIGHT;
playerCurrentLocation  += PLAYER_RUN_SPEED;

currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playercurrentlocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.75f, 0.0f);
goodguy.draw(gl,spritesheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

清单 6-12 。用播放器移动帧(OpenGL ES 2/3)

currentStandingFrame   = STANDING_RIGHT;
playerCurrentLocation  += PLAYER_RUN_SPEED;

currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

goodguy.draw(mMVPMatrix, currentRunAniFrame, .75f );

首先,因为角色跑向右边,当他停止奔跑时,他应该面向右边。因此,currentStandingFrame被设置为STANDING_RIGHT。然后,PLAYER_RUN_SPEED被加到playercurrentlocation上,得到一个距离原始位置 0.25 的值。渲染时,精灵会移动到新位置。

下一个块保持动画循环移动。sprite 工作表有四个图像,左下角在 x 轴上分别为 0、. 25、. 50 和. 75。为了实现平滑的动画,您将从第一帧(0)开始,然后添加. 25 以到达第二帧,依此类推。当到达动画的最后一帧(. 75)时,需要从 0 重新开始。一个if()语句检查你是否在动画的最后一帧,并重置你回到第一帧。

最后,使用 OpenGL 绘制新的动画帧。注意glTranslatef()被调用了两次——一次在模型矩阵模式下,一次在纹理矩阵模式下。当在模型矩阵模式下调用它时,它会移动纹理映射到的顶点的物理位置,从而将角色向右移动。在纹理矩阵模式下调用glTranslatef()时,动画的帧前进。

清单 6-13 和 6-14 展示了完成的movePlayer()方法,同样使用了 OpenGL ES 1 和 OpenGL ES 2/3T6。

清单 6-13 。已完成movePlayer() ( OpenGL ES 1)

private void movePlayer(GL10gl){
if(!goodguy.isDead)
{
switch(playeraction){
case PLAYER_MOVE_RIGHT:
currentStandingFrame   = STANDING_RIGHT;
playerCurrentLocation  += PLAYER_RUN_SPEED;
currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.75f, 0.0f);
goodguy.draw(gl,spritesheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

case PLAYER_MOVE_LEFT:
currentStandingFrame   = STANDING_LEFT;
playerCurrentLocation  -= PLAYER_RUN_SPEED;
currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.50f, 0.0f);
goodguy.draw(gl,spritesheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

case PLAYER_STAND:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentStandingFrame,.25f, 0.0f);
goodguy.draw(gl,spritesheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
}
}

清单 6-14 。已完成movePlayer() (OpenGL ES 2/3)

private void movePlayer(GL10gl){
if(!goodguy.isDead)
{
switch(playeraction){
case PLAYER_MOVE_RIGHT:
currentStandingFrame   = STANDING_RIGHT;
playerCurrentLocation  += PLAYER_RUN_SPEED;
currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

goodguy.draw(mMVPMatrix, currentRunAniFrame, .75f );
break;

case PLAYER_MOVE_LEFT:
currentStandingFrame   = STANDING_LEFT;
playerCurrentLocation  -= PLAYER_RUN_SPEED;
currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

goodguy.draw(mMVPMatrix, currentRunAniFrame, .50f );
break;

case PLAYER_STAND:
goodguy.draw(mMVPMatrix, currentStandingFrame, .25f );
break;
}
}
}

七、滚动背景

本章的解决方案将帮助你创建一个游戏的滚动背景。许多游戏类型都有背景图像,当玩家玩游戏时,背景图像会滚动。你可能对如何让游戏的背景图像移动有一些疑问。

在某些情况下,图像会自动滚动。例如,滚动射击和其他“轨道”风格的游戏将有自动滚动的背景。这与其他游戏类型形成对比,例如侧滚平台游戏,其中背景图像将随着玩家的移动而滚动(这在第八章“滚动多个背景”中有所介绍)。

本章将介绍三种加载背景图像、垂直滚动图像和水平滚动图像的解决方案。

7.1 加载背景图像

问题

您的游戏无法使用 OpenGL ES 加载背景图像。

解决办法

创建一个类,可以将图像作为纹理加载,并将其映射到一组顶点 。

它是如何工作的

加载图像以供 OpenGL ES 使用的最简单方法是创建一个自定义类,该类创建所需的所有顶点,并将图像作为纹理映射到这些顶点。因为这个背景将会滚动,所以这个类还需要以一种能够重复的方式映射纹理。如果 OpenGL ES 可以在滚动时重复纹理,背景图像将会看起来好像无限延续。

用于无限滚动的最常见的背景类型之一,也是最容易操作的背景类型之一,是一个星空。星域是点的随机模式,很容易无缝重复。像侧滚射击游戏这样的游戏经常使用星域作为无限滚动的背景。

图 7-1 展示了将在本解决方案中使用的 星域图像。

9781430257646_Fig07-01.jpg

图 7-1 。星域图像

第一步是将图像添加到项目的正确的res/drawable文件夹中。我们已经讨论过将图像添加到项目中,以及可用于此目的的各种文件夹(参见第二章、第三章、第六章、【加载精灵表】了解更多具体信息)。将图像文件添加到项目中后,创建一个新的类。对于这个解决方案,新类的名称将是SBGBackground()

public class SBGBackground {

}

在第六章中创建了一个类似的类来加载 spritesheet 角色的图像和顶点。清单 7-1 (用于 OpenGL ES 1)和清单 7-2 (用于 OpenGL ES 2/3)的大部分代码直接来自第六章中的解决方案。

清单 7-1SBGBackground() (OpenGL 是 1)

public class SBGBackground {

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

清单 7-2SBGBackground() (OpenGL 是 2/3)

class SBGBackground{
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() {" +
"}";
private float texture[] = {
 0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
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;
static float squareCoords[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public SBGBackground() {
ByteBuffer bb = ByteBuffer.allocateDirect(
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);

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

ByteBuffer dlb = ByteBuffer.allocateDirect(
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);

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

public void draw(float[] mvpMatrix) {
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);
}
}

这个类以其当前的形式创建顶点、索引和纹理数组。它还包含一个初始化缓冲区的构造函数和一个在需要绘制背景图像时调用的draw()方法。基于你在本书之前的解决方案中看到的其他图像类,这个类应该看起来很熟悉。

请特别注意清单 7-1 中加粗的代码行。这一行创建了一个名为texturesint数组,但是只将其实例化为一个元素。其原因是现有的用于生成纹理名称的 OpenGL ES 方法(glGenTextures)只接受一组纹理,因为它是为处理多个纹理而构建的。

现在我们将使用 OpenGL ES 1 和 OpenGL ES 2/3 创建一个名为loadTexture()的新方法,这是加载图像文件并将其作为纹理映射到顶点所需的。对于 OpenGL ES 1,请使用以下内容:

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

}


对于 OpenGL ES 2/3,请使用以下内容:

```java
public void loadTexture(int texture, Context context) {

}

请注意,该方法的 OpenGL ES 1 版本接受 OpenGL ES 对象、要加载的图像的 ID 和当前 Android 上下文。在这个方法中,您需要从图像创建一个位图(使用传入的 ID),然后设置一些纹理参数,这些参数将指示 OpenGL ES 如何处理纹理(参见清单 7-3 和 7-4 ) )。

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

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

清单 7-4loadTexture() (OpenGL 是 2/3)

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

}catch(Exception e){
//Handle your exceptions here
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
 //Handle your exceptions 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();
}

请特别注意这个方法中加粗的代码。这段代码显式地将纹理设置为沿 x 和 y 轴重复。在 OpenGL ES 中,S 纹理坐标轴指的是 x 笛卡尔轴;t 是指 y 轴。在这个例子中,重复纹理是至关重要的,因为我们使用了一个将被无限重复的星域图像。

既然SBGBackground()类已经完成,那么需要将代码添加到利用新类的游戏循环中。完成这个解决方案还需要两个步骤。首先是实例化一个新的SBGBackground。然后图像 ID 必须传递给loadTexture()方法。

在您的游戏循环中,实例化一个新的SBGBackground,如下所示:

private SBGBackground background1 = new SBGBackground();

游戏循环包含在 OpenGL ES Renderer的实现中。因此,它有一些必需的方法,这些方法在前面的章节中已经详细介绍过了。其中一个方法是onSurfaceCreated(),这是加载纹理的代码应该被调用的地方。

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

...

background1.loadTexture(gl, R.drawable.starfield, context);
}

接下来的两个解决方案将包括滚动已经加载的背景纹理。

7.2 水平滚动背景

问题

背景目前是静态的,应该会水平滚动。

解决办法

在游戏循环中创建一个新类,将背景纹理在 y 轴上平移一个设定的量。

它是如何工作的

这个解决方案的 OpenGL ES 1 版本的第一步是创建两个变量,分别用于跟踪背景纹理的当前位置和平移纹理的值。

int bgScroll1 = 0;
float SCROLL_BACKGROUND_1 = .002f;

这些变量可以是您的Renderer类的本地变量,或者您可以将它们存储在一个单独的类中。

OpenGL ES Renderer实现中的onDrawFrame()方法会在游戏循环的每次迭代中被调用。你需要创建一个新的方法,叫做scrollBackground(),它又从onDrawFrame()方法中被调用(见清单 7-5 )。

清单 7-5scrollBackground() (OpenGL 是 1)

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

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

}

该方法的第一部分测试bgScroll1变量的当前值。考虑到浮动有一个上限,这个if语句是必要的,以确保你不超载你的浮动。

接下来,在开始使用纹理矩阵之前,将缩放和转换模型矩阵视图。注意,纹理模型的 y 坐标被bgScroll1中的值平移。这就是在屏幕上移动背景的原因。

最后,调用SBGBackground()类的draw()方法,用SCROLL_BACKGROUND_1变量中的值增加bgScroll1变量,为循环的下一次迭代做准备。

onDrawFrame()方法调用新的scrollBackground()方法,背景星域将在屏幕上水平平滑移动。

在 OpenGL ES 2/3 中完成同样的过程略有不同(见清单 7-6 )。控制滚动的变量在 object 类的draw()方法中设置。这个变量也可以传递到draw()方法中,就像在第六章中用于 spritesheet 解决方案的一样。然而,由于这个背景是自动滚动的,而且是无限滚动的,所以在方法中处理一切更有意义。

清单 7-6scrollBackground() (OpenGL 是 2/3)

class SBGBackground{
public float scroll = 0;
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 float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
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;
static float squareCoords[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

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

public void draw(float[] mvpMatrix) {
scroll += .01f;
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");
int fsScroll = GLES20.glGetUniformLocation(mProgram, "scroll");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsScroll, scroll);
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);
}
}

7.3 垂直滚动背景

问题

背景目前是静态的,它应该水平滚动。

解决办法

在游戏循环中创建一个新类,在 x 轴上将背景纹理平移一个设定的量。

它是如何工作的

在前一个解决方案的基础上,只需要做一个改变就可以垂直而不是水平滚动背景,如清单 7-7 和清单 7-8 所示。

清单 7-7 。垂直滚动(OpenGL ES 1)

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(bgScroll1, 0.0f, 0.0f);

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

}

清单 7-8 。垂直滚动(OpenGL ES 2/3)

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,TexCoordOut.y+ scroll));"+
"}";

注意 OpenGL ES 1 方法的scrollBackground()中加粗的代码。在glTranslatef()方法调用中,bgScroll1值已经从 y 轴位置移动到 x 轴位置。这就是使背景垂直滚动而不是水平滚动所需的全部内容。

OpenGL ES 2/3 唯一需要更改的代码是片段着色器。滚动浮动现在被添加到纹理的 y 坐标而不是 x 坐标。`

八、滚动多个背景

在第七章中,提出了创建可滚动背景的解决方案。虽然该解决方案将帮助您创建一个引人注目的游戏,它可以有更多的深度。

在这一章中,你将会看到在游戏背景中加载和使用两张图片的解决方案。这不仅会让你的游戏更有活力,还能让你以不同的速度滚动两幅图像。

在这一章的最后,给出了以不同速度滚动两幅不同背景图像的解决方案。这给了游戏一个更真实的外观,并增加了平面环境的深度。

8.1 加载两幅背景图像

问题

游戏的背景需要包含两个图像。

解决办法

使用 OpenGL 加载两幅图像来创建一个分层的背景,可以独立滚动以获得更动态的外观。

它是如何工作的

正如在第七章的中所讨论的,加载图像以供 OpenGL ES 使用的最简单的方法是创建一个自定义类来加载所有需要的顶点,并将图像作为纹理映射到这些顶点。

在这个解决方案中,您将复制两个图像到项目的res文件夹中。然后,您将实例化为第七章中的解决方案创建的类的两个副本。使用这两个独立的实例,您将在后台加载并绘制两个不同的图像。图 8-1 和 8-2 显示了将在本解决方案中使用的星域图像和碎片域图像。

9781430257646_Fig08-01.jpg

图 8-1 。星空图像

9781430257646_Fig08-02.jpg

图 8-2 。碎片区域图像

第一步是将图像添加到项目的正确的res/drawable文件夹中。我们之前讨论过将图像添加到项目,以及可用于此目的的各种文件夹。将图像文件添加到项目中后,您可以实例化在第七章中创建的类的两个副本。

这些类需要在包含游戏循环的类中实例化。包含游戏循环的类是 OpenGL ES Renderer的一个实现。背景类应该在一个所有的Renderer方法都可以访问的位置被实例化。

作为参考,清单 8-1 和 8-2 显示了来自第七章 的SBGBackground()类的完整代码。

清单 8-1SBGBackground (OpenGL 是 1)

public class SBGBackground {

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 SBGBackground() {
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(GL10gl) {
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(GL10gl,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();
}

}

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

class SBGBackground{
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 float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

Private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
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;
static float squareCoords[] = { -1f,  1f, 0.0f,
 -1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public void loadTexture(int texture, Context context) {
InputStreami magestream = 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){

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

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();
   }
public SBGBackground() {
ByteBuffer bb = ByteBuffer.allocateDirect(
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);

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

ByteBuffer dlb = ByteBuffer.allocateDirect(
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);

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

public void draw(float[] mvpMatrix, float scroll) {
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");
int fsScroll = GLES20.glGetUniformLocation(mProgram, "scroll");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsScroll, scroll);
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);
}
}

如果您将这段代码与第七章中的代码进行比较,您应该会注意到 OpenGL ES 2/3 版本的一个小变化。scroll 变量已被移动到构造函数中。这允许您传递滚动量,以便您可以以不同的速率滚动背景的多个实例化。

在你的游戏中实例化两个新的SBGBackground(),如下所示:

private SBGBackground background1 = new SBGBackground();
private SBGBackground background2 = new SBGBackground();

现在您需要加载图像,并使用SBGBackground()loadTexture()方法将它们映射为纹理。加载纹理的代码应该在RendereronSurfaceCreated()方法中调用。

public void onSurfaceCreated(GL10gl, EGLConfigconfig) {
//TODO Auto-generated method stub

...

background1.loadTexture(gl, R.drawable.starfield, context);
background1.loadTexture(gl, R.drawable.debrisfield, context);
}

接下来的两个解决方案将包括滚动背景纹理,现在它们已经被加载了。

8.2 滚动两个背景图像

问题

只有一个背景图像滚动。

解决办法

通过修改每个图像的滚动变量,修改游戏循环,使两个图像都滚动。

它是如何工作的

解决方案的第一步是创建四个变量,分别用于跟踪背景纹理的当前位置和平移纹理的值。

int bgScroll1 = 0;
int bgScroll2 = 0;

float SCROLL_BACKGROUND_1 = .002f;
float SCROLL_BACKGROUND_2 = .002f;

这些变量可以是您的Renderer类的本地变量,或者您可以将它们存储在一个单独的类中。这个解决方案借鉴了第七章中的一个解决方案。然而,如果你一开始不慢的话,在游戏中跟踪多个移动的元素可能会很棘手。尽量避免跳过这一步,因为很容易错过一个重要的细节。

OpenGL ES Renderer实现中的onDrawFrame()方法会在游戏循环的每次迭代中被调用。创建一个名为scrollBackgrounds()的新方法,它将从onDrawFrame()方法中被调用。参见清单 8-3 和清单 8-4 。

清单 8-3scrollBackgrounds() (OpenGL 是 1)

private void scrollBackgrounds(GL10gl){
if (bgScroll1 == Float.MAX_VALUE){
bgScroll1 = 0f;
}

if (bgScroll2 == Float.MAX_VALUE){
bgScroll2 = 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);

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

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

background2.draw(gl);
gl.glPopMatrix();
bgScroll2 +=  SCROLL_BACKGROUND_2;
gl.glLoadIdentity();

}

清单 8-4scrollBackgrounds() (OpenGL 是 2/3)

private void scrollBackgrounds(GL10gl){
if (bgScroll1 == Float.MAX_VALUE){
bgScroll1 = 0f;
}

if (bgScroll2 == Float.MAX_VALUE){
bgScroll2 = 0f;
}

background1.draw(mMVPMatrix, bgScroll1);
background2.draw(mMVPMatrix, bgScroll2);
bgScroll1 +=  SCROLL_BACKGROUND_1;
bgScroll2 +=  SCROLL_BACKGROUND_2;
}

该方法的第一部分测试bgScroll1bgScroll2变量的当前值。就像第七章中的一样,if声明对于确保你不会让你的浮存金超载是必要的。

视图矩阵和纹理矩阵模型被缩放和转换,以提供背景图像所需的“运动”。

最后,从onDrawFrame()方法调用新的scrollBackgrounds()方法,两个背景图像应该一起在屏幕上滚动。背景应该出现在图 8-3 中。

9781430257646_Fig08-03.jpg

图 8-3 。两个背景图像在一起

8.3 以不同的速度滚动两幅背景图像

问题

背景图像不会以不同的速度滚动。

解决办法

通过修改游戏循环以不同速度滚动多个背景图像来增加深度感。

它是如何工作的

在前一个解决方案的基础上,只需要做一个改变就可以以不同的速度滚动背景图像。

理想情况下,为了创造一种人工的深度感,您会希望前景图像(两幅图像中)的滚动速度比背景中最远的图像快。

为了实现这一效果,将先前解决方案中的SCROLL_BACKGROUND_2值更改为一个更大的数字。设置的数值越大,图像滚动的速度越快。

int bgScroll1 = 0;
int bgScroll2 = 0;

float SCROLL_BACKGROUND_1 = .002f;
float SCROLL_BACKGROUND_2 = .005f;

九、将背景与角色运动同步

在第八章中,介绍了创建可滚动的多层背景的解决方案。然而,如果你试图同步背景的滚动和角色的移动,你可能会遇到问题。

在这一章中,你会看到两个方法——第一个是在两个方向上滚动多图像背景,第二个是同步背景的滚动和可玩角色的移动。

9.1 向两个方向滚动背景

问题

当玩家可以两个方向奔跑时,背景只向一个方向滚动 。

解决办法

修改背景类以跟踪两个方向的运动。

它是如何工作的

这个解决方案假设你的游戏,可能是一个平台风格的游戏,有一个角色可以向两个方向移动。回想一下流行的平台风格游戏(如超级马里奥兄弟),很多时候可玩角色可以向右移动以在游戏中前进。人物也可以向左移动,通常是在有限的能力下,来追溯他们的脚步。

在背景可以与角色同步之前,它需要能够在两个方向上移动。这个解决方案将获取一个三幅图像的背景,加载它,向右滚动,然后反向向左滚动。

第一步是将用作背景的三幅图像复制到res/drawable文件夹中。图 9-1 到 9-3 代表了我在这个例子中使用的三张图片。请注意,它们是单一背景的图层,已经被拉开,以便它们可以以不同的速度滚动。

9781430257646_Fig09-01.jpg

图 9-1 。最远的背景层

9781430257646_Fig09-02.jpg

图 9-2。中间层

9781430257646_Fig09-03.jpg

图 9-3 。地面层

注意为了在本书中打印图像,图像的透明部分被涂成灰色。

一旦图像在项目中,实例化三个SBGBackground()类的实例——每个背景层一个。

private SBGBackground background1 = new SBGBackground();
private SBGBackground background2 = new SBGBackground();
private SBGBackground background3 = new SBGBackground();

下一步是创建三组变量来控制和跟踪每一层背景的速度和位置。

private float bgScroll1;
private float bgScroll2;
private float bgScroll3;
public static float SCROLL_BACKGROUND_1  = .002f;
public static float SCROLL_BACKGROUND_2  = .003f;
public static float SCROLL_BACKGROUND_3  = .004f;

在第七章(清单 7-1 和 7-2)中,给出了一个创建scrollBackground()方法的解决方案。

改变方法,允许背景向左或向右滚动,这取决于玩家移动的方向。

在本书的早期,提供了一个允许角色在屏幕上移动的解决方案(使用 spritesheet)。这个解决方案的一部分需要创建一些变量来跟踪玩家的动作。在这个解决方案中使用相同的变量。

public static int playerAction = 0;
public static final int PLAYER_MOVE_LEFT = 1;
public static final int PLAYER_MOVE_RIGHT = 2;

注意前面提到的变量应该在你收集玩家输入的时候设置。有关这方面的更多信息,请参见第五章和第六章。

使用 OpenGL ES 1 和 2/3 向接受playerAction变量的scrollBackground()方法添加一个新参数,如下所示。

对于 OpenGL ES 1:

private void scrollBackground1(GL10gl, int direction){

...

}

对于 OpenGL ES 2/3:

private void scrollBackground1(int direction){

...

}

传递给scrollBackground()方法的方向将用于决定如何滚动背景图像。在当前的scrollBackground()方法中,下面一行控制图像的滚动:

bgScroll1 +=  SCROLL_BACKGROUND_1;

这条线的关键部分是+=。要改变滚动方向,需要将操作者从+=切换到-=。创建一个switch...case语句,根据从玩家输入中收集的方向来改变这个操作符。

OpenGL ES 1 和 OpenGL ES 2/3 的scrollBackground()方法分别显示在清单 9-1 和清单 9-2 中。

清单 9-1scrollBackground() (OpenGL 是 1)

private void scrollBackground1(GL10gl, int direction){
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(bgScroll1,0.0f, 0.0f);

background1.draw(gl);
gl.glPopMatrix();
switch(direction)
{
case PLAYER_MOVE_RIGHT:
bgScroll1 +=  SCROLL_BACKGROUND_1;
break;
case PLAYER_MOVE_LEFT:
bgScroll1 -=  SCROLL_BACKGROUND_1;
break;
}
gl.glLoadIdentity();

}

清单 9-2scrollBackground() (OpenGL 是 2/3)

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

background1.draw(mMVPMatrix, bgScroll1);
switch(direction)
{
case PLAYER_MOVE_RIGHT:
bgScroll1 +=  SCROLL_BACKGROUND_1;
break;
case PLAYER_MOVE_LEFT:
bgScroll1 -=  SCROLL_BACKGROUND_1;
break;
}
}

背景现在将根据传递给scrollBackground()方法的方向向右或向左滚动。下一个解决方案将这个方法与玩家的移动联系起来。

9.2 移动背景以响应用户输入

问题

根据玩家的移动,背景不会开始或停止滚动。

解决办法

需要结合movePlayer()方法调用scrollBackground()方法来控制两个的移动。

它是如何工作的

在第六章中,清单 6-7 给出了一个解决方案,它创建了一个movePlayer()方法来促进角色的动画。这个方法需要修改,以允许背景的滚动同步到它。首先,更改它的名称以表明它的新用途。

在 OpenGL ES 1 中:

private void movePlayerAndBackground(GL10gl){

...

}

在 OpenGL ES 2/3 中:

private void movePlayerAndBackground(){

...

}

注意,在现有的movePlayer()方法中,有一个switch语句移动播放器(使用 spritesheet)。switch语句需要重写,以便当角色到达屏幕的大致中间时,它不再移动(参见清单 9-3 和 9-4 )。角色应该看起来在这一点上运行到位,背景应该滚动到近似运动。

清单 9-3movePlayerAndBackground() (OpenGL 是 1)

private void movePlayerAndBackground(GL10gl){
background1.draw(gl);
if(!goodguy.isDead)
{
switch(playerAction){
case PLAYER_MOVE_RIGHT:

currentStandingFrame = STANDING_RIGHT;

currentRunAniFrame += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame = .0f;
}

if(playerCurrentLocation>= 3f)
{
scrollBackground1(gl, playerAction);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.50f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

}else{
playerCurrentLocation += PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.50f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

}

break;

case PLAYER_MOVE_LEFT:

currentStandingFrame = STANDING_LEFT;

currentRunAniFrame += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame = .0f;
}

if(playerCurrentLocation<= 2.5f)
{
scrollBackground1(gl, playerAction);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.75f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

}else{
playerCurrentLocation -= PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.75f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

}
break;

case PLAYER_STAND:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentStandingFrame,.25f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
}
}

清单 9-4movePlayerAndBackground() (OpenGL 是 2/3)

private void movePlayerAndBackground(){
background1.draw(mMVPMatrix, bgScroll1);
if(!goodguy.isDead)
{
switch(playerAction){
case PLAYER_MOVE_RIGHT:

currentStandingFrame = STANDING_RIGHT;

currentRunAniFrame += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame = .0f;
}

if(playerCurrentLocation>= 3f)
{
scrollBackground1(playerAction);
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentRunAniFrame, .75f);
}else{
playerCurrentLocation += PLAYER_RUN_SPEED;
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentRunAniFrame, .50f);
}

break;

case PLAYER_MOVE_LEFT:
currentStandingFrame = STANDING_LEFT;

currentRunAniFrame += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame = .0f;
}

if(playerCurrentLocation<= 2.5f)
{
scrollBackground1(playerAction);
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentRunAniFrame, .75f);

}else{
playerCurrentLocation -= PLAYER_RUN_SPEED;
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentRunAniFrame, .50f);
}
break;

case PLAYER_STAND:
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentStandingFrame, .25f);

break;
}
}
}

角色动画大约在中途停止在屏幕上前进。然后该方法调用scrollBackground()方法开始移动背景。

十、使用瓷砖建造关卡

在这一章中,你将会看到两种用瓷砖建造关卡的方法。许多 2D 游戏(特别是侧滚平台和自上而下的冒险/RTS 风格的游戏)都实现了用可重复的瓷砖构建的关卡。

如果你在用瓷砖建造关卡时有困难,这一章应该会有帮助。第一个方法是从一个 sprite 表中加载图块并创建一个关卡贴图。第二个配方将使用精灵表和级别映射,然后从瓷砖创建一个完整的级别。

10.1 从子画面表加载图块

问题

用于创建关卡的图块存储在 sprite sheet 中,无法确定在哪个位置使用哪个图块。

解决办法

使用纹理加载器将瓷砖纹理映射到一组顶点,并使用级别映射来指定将哪些瓷砖放置在哪里。

它是如何工作的

这个解决方案需要使用两个类。第一个类包含创建顶点和索引的信息,以及绘制图块的方法。第二个类保存纹理信息。

在第六章的中,提供了加载子画面的解决方案。这些解决方案将纹理加载方法从对象类中分离出来,以允许在一个地方加载和保存多个子画面。这个解决方案将在纹理类上进行扩展,以保存新平铺子画面。像往常一样,首先将你的精灵表复制到项目中。图 10-1 中的所示的这个例子的 sprite 表中有两个图块。一个瓷砖是地面瓷砖,有一些草和一点天空;第二个瓦片是天空瓦片。记住,你的可能有几百个。

9781430257646_Fig10-01.jpg

图 10-1 。有两个图块的精灵表

SBGTile()类 类

将图像添加到项目中后,创建一个新类SBGTile()SBGTile()类将设置你的顶点和索引(见清单 10-1 和 10-2 )。该类的结构应该看起来非常熟悉,因为它现在已经在其他几个解决方案中使用;但是,加粗的代码已经更改,允许加载多个 sprite 表。

清单 10-1SBGTile() (OpenGL 是 1)

public class SBGTile {

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,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

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

public SBGTile() {
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.put(indices);
indexBuffer.position(0);
}

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

}

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

class SBGBackground{
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 posX;" +
"uniform float posY;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x+
posX,TexCoordOut.y + posY));"+
"}";
private float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
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;
static float squareCoords[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public SBGBackground() {
ByteBuffer bb = ByteBuffer.allocateDirect(

bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);

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

ByteBuffer dlb = ByteBuffer.allocateDirect(
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);

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

public void draw(float[] mvpMatrix, float posX, float posY,
int[] spriteSheet, int currentSheet)   {
GLES20.glUseProgram(mProgram);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, spriteSheet[currentSheet - 1]);
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);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsPosX = GLES20.glGetUniformLocation(mProgram, "posX");
int fsPosY = GLES20.glGetUniformLocation(mProgram, "posY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsPosX, posX);
GLES20.glUniform1f(fsPosY, posY);
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);
}
}

请特别注意加粗的行。这些行接受一个代表多个 sprite sheet 纹理的int数组,以及一个指示哪个 sprite sheet 用于特定绘制操作的int

SBGTextures()类类

现在您需要一个类来处理多个 sprite 表的加载。创建一个名为SBGTextures()的新类,如清单 10-3 和清单 10-4 所示。

清单 10-3SBGTextures() (OpenGL 是 1)

public class SBGTextures {

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

public SBGTextures(GL10gl){

gl.glGenTextures(2, textures, 0);

}
public int[] loadTexture(GL10gl,int texture, Context context,int textureNumber) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
Bitmap temp = null;

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

try {

temp = BitmapFactory.decodeStream(imagestream);
bitmap = Bitmap.createBitmap(temp, 0, 0, temp.getWidth(), temp.getHeight(), flip, true);
}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;
}
}

清单 10-4SBGTextures() (OpenGL 是 2/3)

public class SBGTextures {

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

public SBGTextures(){
}

public void loadTexture(int texture, Context context, int textureNumber) {
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);

}catch(Exception e){
//Handle your exceptions here
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
//Handle your exceptions here
}
}

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

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

同样,请注意每个清单中的粗体行。这里使用的int数组意味着你可以根据需要扩展你能容纳的独立 sprite 工作表的数量。

实例化所需的类,并在您的Renderer中创建一个 sprite 表数组。

private SBGTile tiles = new SBGTile();
private SBGTextures textureloader;
private int[] spriteSheets = new int[2];

然后,在RendereronSurfaceCreated()方法中,设置textureloader,用它来加载 tiles sprite 表。

textureloader = new SBGTextures(gl);
spriteSheets = textureloader.loadTexture(gl, R.drawable.tiles, context, 1);

现在瓷砖(作为一个纹理)可以使用了。但是游戏怎么知道瓷砖放在哪里呢?为此,您需要创建一个关卡地图。

创建关卡地图

关卡地图是游戏应该放置每一个方块的地方。该地图将是一个由int组成的二维数组

该图就像一个int值的矩阵。每个int值代表一个特定的图块。此解决方案中的示例只有两个不同的图块;因此,级别映射将仅由 0 和 1 组成。0 代表地面瓷砖,1 代表天空瓷砖。

将这些级别映射创建为二维数组是存储许多级别的体系结构的一种快速而简单的方法。以下是该解决方案的二维阵列级映射示例。

int map[][] = {
{0,0,0,0,0,0,0,0,0,0},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
};

在这里,我们创建了一个 10x 10 的 0 和 1 的数组来表示平铺在屏幕上的位置。在下一个解决方案中,您将编写一个 tiles 引擎来读取这个数组,并将 tiles 实际放置在屏幕上的正确位置。

10.2 从瓷砖创建一个级别

问题

您的游戏无法读取关卡地图int数组来使用瓷砖创建关卡。

解决办法

创建一个图块引擎,它读入数组并在所需位置写出图块。

它是如何工作的

该解决方案将带您构建一个 tile 引擎。平铺引擎读入级别贴图数组,一次一个维度,然后根据数组中的值绘制平铺。

在前一个解决方案中,我们创建了一个只有两个值的数组,即 0 和 1。这些值对应于 sprite 表中的两个图块。请记住,你可以很容易地有更多的瓷砖,给你一个看起来更精致的水平。

提示如果您使用更多的图块,从而在您的数组中有更多的int,您将不得不对该图块引擎进行的唯一更改是向switch...case语句添加更多的事例。

第一步是在您的Renderer中创建一个drawTiles()方法。

对于 OpenGL ES 1:

private void drawtiles(GL10 gl, int[][] map){

}

对于 OpenGL ES 2/3:

private void drawtiles(int[][] map){

}

drawTiles()方法将接受您的二维数组地图并遍历它。但是,在遍历数组之前,需要设置两个变量。

这些变量的目的是在您将图块设置到位时转换模型矩阵。这里的概念是,您读入地图数组的第一个元素,然后设置并绘制相应的图块。然后,您必须将模型矩阵平移到屏幕上的下一个位置,以便放置下一个图块。

float tileLocY = 0f;
float tileLocX = 0f;

现在,创建一个嵌套的for循环,它将迭代 map 数组的两个维度。

for(int x=0; x<10; x++){
for(int y=0; y<10; y++){

}
}

如果你使用的是 OpenGL ES 1,第一步就是缩放和转换模型矩阵,然后设置纹理矩阵。

for(int x=0; x<10; x++){
for(int y=0; y<10; y++){

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

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

注意在加粗的代码中,模型矩阵被之前设置的tileLocYtileLocX值转换。随着循环的进行,这些变量将递增,以便将下一个图块放置在正确的位置。

下一步是设置一个简单的switch...case语句来读取 map 数组的当前元素。

for(int x=0; x<10; x++){
for(int y=0; y<10; y++){

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

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

switch(map[x][y]){
case 1:
break;
case 0:
break;
}
}
}

因为,在这一点上,矩阵模式已经被设置为纹理,你在switch...case语句中唯一要做的事情就是将 sprite 表转换为正确的 tile 图像。

switch(map[x][y]){
case 1:
gl.glTranslatef(.75f,.75f, 0f);
break;
case 0:
gl.glTranslatef(.75f,1f, 0f);
break;
}

提示关于使用精灵工作表的更多信息,参见第六章“加载精灵工作表”

瓷砖是在适当的位置,纹理设置为正确的图像。现在绘制图块并增加tileLocY变量以移动到下一个位置。

switch(map[x][y]){
case 1:
gl.glTranslatef(.75f,.75f, 0f);
break;
case 0:
gl.glTranslatef(.75f,1f, 0f);
break;
}
tiles.draw(gl, spriteSheets, SBG_TILE_PTR);
tileLocY += .50;

剩余的嵌套循环在每个新行上弹出矩阵,并根据需要推进tileLocX变量。

如果您使用的是 OpenGL ES 2/3,概念保持不变,但过程略有不同。您仍然需要遍历地图的每个值,并使用一个switch语句来处理每种情况。不同之处在于,不像在 OpenGL ES 1 中那样翻译矩阵,您可以简单地将每个图块的位置传递给drawtiles()方法。这与你使用 sprite 工作表的过程是一样的(见第六章关于 sprite 工作表的更深入的讨论)。清单 10-5 显示了完成的方法应该是什么样子。完成的 OpenGL ES 2/3 版本的drawtiles()如清单 10-6 所示。

清单 10-5drawtiles() (OpenGL 是 1)

private void drawtiles(GL10gl){
float tileLocY = 0f;
float tileLocX = 0f;
for(int x=0; x<10; x++){
for(int y=0; y<10; y++){

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

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

switch(map[x][y]){
case 1:
gl.glTranslatef(.75f,.75f, 0f);
break;
case 0:
gl.glTranslatef(.75f,1f, 0f);
break;
}
tiles.draw(gl, spriteSheets, SBG_TILE_PTR);
tileLocY += .50;
}
gl.glPopMatrix();
gl.glLoadIdentity();
tileLocY = 0f;
tileLocX += .50;
}
}

清单 10-6drawtiles() (OpenGL 是 2/3)

private void drawtiles(){
float tileLocY = 0f;
float tileLocX = 0f;
Matrix.translateM(mTMatrix, 0, tileLocX, tileLocT, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0) ;
for(int x=0; x<10; x++){
for(int y=0; y<10; y++){

switch(map[x][y]){
case 1:
tiles.draw(mMPVMatrix, .75f, .75, spriteSheets, SBG_TILE_PTR);
break;
case 0:
tiles.draw(mMPVMatrix, .75f, .75, spriteSheets, SBG_TILE_PTR);
break;
}

tileLocY += .50;
}
tileLocY = 0f;
tileLocX += .50;
}
}

如果你正在使用 OpenGL ES 2/3,确保在你的Renderer中设置一个新的翻译矩阵(清单 10-6 中的mTMatrix)。平移矩阵的工作是移动瓷砖的位置。它是 OpenGL ES 2/3 版的glTranslatef()。下面的代码显示了转换矩阵。

public class SBGGameRenderer implements GLSurfaceView.Renderer {
...
private final float[] mTMatrix = new float[16];
...
@Override
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);
drawtiles();

...

}
}

由该贴图数组和子画面组合产生的级别显示在图 10-2 中。

9781430257646_Fig10-02.jpg

图 10-2 。一个简单的关卡,由关卡地图和方块组成

请记住,要利用更多的图块,只需扩展您的switch...case语句的范围。

十一、移动角色

在屏幕上移动角色——无论是人、动物、机器人还是车辆——是引人注目的游戏中最重要的部分之一。如果你试图创造一个在游戏中自由移动的角色,你可能会遇到一些问题。

本章将介绍帮助你移动角色的解决方案。本章中的解决方案包括让角色奔跑,以及在角色移动时改变角色动画。

第一种方法帮助你在屏幕上向四个方向移动你的角色。其余的解决方案帮助您以不同的速度移动角色,并在角色移动时为其设置动画。

11.1 向四个方向移动角色

问题

屏幕上的角色不会移动。

解决办法

使用游戏循环来控制角色的移动。

它是如何工作的

这个解决方案要求你追踪玩家想要角色移动到哪里,然后将这个意图转换到模型矩阵的 x 或 y 轴。换句话说,一旦你捕捉到玩家想要移动的位置,你就可以使用一个switch...case语句来确定在模型矩阵中平移哪个轴,从而相应地移动屏幕上的角色。

您分三步完成此解决方案。您需要确定玩家想要移动的方向,然后创建一个保存该值的标志,最后使用该值来移动屏幕上的角色。第一步是捕捉玩家想要移动的方向。我们将通过使用SimpleOnGestureListener() 来实现这一点。

玩家将向左、向右、向上或向下滑动来指示角色应该朝哪个方向跑(想想类似寺庙跑步风格的输入系统)。在游戏的主意图中,实例化一个新的SimpleOnGestureListener(),如清单 11-1 所示。

清单 11-1SimpleOnGestureListener()

GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEventarg0) {
//TODO Auto-generated method stub
return false;
}

@Override
public boolean onFling(MotionEvente1, MotionEvente2, float velocityX,
float velocityY) {

float leftMotion = e1.getX() - e2.getX();
float upMotion = e1.getY() - e2.getY();

float rightMotion = e2.getX() - e1.getX();
float downMotion = e2.getY() - e1.getY();

if((leftMotion == Math.max(leftMotion, rightMotion))&&

(leftMotion>Math.max(downMotion, upMotion)) )
{
}

if((rightMotion == Math.max(leftMotion, rightMotion))&&
(rightMotion>Math.max(downMotion, upMotion) )
{
}
if((upMotion == Math.max(upMotion, downMotion))&&
(upMotion>Math.max(leftMotion, rightMotion)) )
{
}

if((downMotion == Math.max(upMotion, downMotion))&&
(downMotion>Math.max(leftMotion, rightMotion)) )
{
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onScroll(MotionEvente1, MotionEvente2, float distanceX,
float distanceY) {
//TODO Auto-generated method stub
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//TODO Auto-generated method stub
return false;
}

};

注意这个实例化中的四个if语句。它们代表向左、向右、向上和向下的动作。现在创建一个可以从主意图和游戏循环中访问的int。根据SimpleOnGestureListener()检测到的方向设置int(参见清单 11-2 )。

清单 11-2SimpleOnGestureListener()

public static int playeraction = 0;
public static final int PLAYER_MOVE_LEFT = 1;
public static final int PLAYER_MOVE_RIGHT = 2;
public static final int PLAYER_MOVE_UP = 3;
public static final int PLAYER_MOVE_DOWN = 4;

...

if((leftMotion == Math.max(leftMotion, rightMotion)) &&
(leftMotion>Math.max(downMotion, upMotion)) )
{
playeraction = PLAYER_MOVE_LEFT;
}

if((rightMotion == Math.max(leftMotion, rightMotion)) &&
(rightMotion>Math.max(downMotion, upMotion) )
{
playeraction = PLAYER_MOVE_RIGHT;
}

if((upMotion == Math.max(upMotion, downMotion)) &&
(upMotion>Math.max(leftMotion, rightMotion)) )
{
playeraction = PLAYER_MOVE_UP;
}

if((downMotion == Math.max(upMotion, downMotion)) &&
(downMotion>Math.max(leftMotion, rightMotion)) )
{
playeraction = PLAYER_MOVE_DOWN;
}

...

最后,在Renderer中,创建一个方法来读取你刚刚设置的int的值,并相应地转换角色的模型矩阵,如清单 11-3 和清单 11-4 所示。

清单 11-3movePlayer() (OpenGL 是 1)

private void movePlayer(GL10gl){
switch(playeraction){
case PLAYER_MOVE_RIGHT:

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(0f, .75f, 0f);
character.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
case PLAYER_MOVE_LEFT:

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(0f, -.75f, 0f);
character.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;

case PLAYER_MOVE_UP:

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(.75f, 0f, 0f);
character.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
case PLAYER_MOVE_DOWN:

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(-.75f, 0f, 0f);
character.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;

}
}

清单 11-4movePlayer() (OpenGL 是 2/3)

private void movePlayer(GL10gl){
switch(playeraction){
case PLAYER_MOVE_RIGHT:

Matrix.translateM(mTMatrix, 0, 0, .75f, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);

break;
case PLAYER_MOVE_LEFT:

Matrix.translateM(mTMatrix, 0, 0, -.75f, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;

case PLAYER_MOVE_UP:

Matrix.translateM(mTMatrix, 0, .75f, 0, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;
case PLAYER_MOVE_DOWN:

Matrix.translateM(mTMatrix, 0, -.75f, 0, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;

}
}

glTranslatef()的调用在清单 11-3 (对于 OpenGL ES 代码)中用粗体突出显示,因为你应该用在你的特定游戏中效果最好的值来转换你的模型矩阵。

11.2 以不同的速度移动角色

问题

游戏角色需要以不同的速度行走和奔跑。

解决办法

使用游戏循环数来决定角色何时应该改变速度。

它是如何工作的

在这个解决方案中,您计算已经执行的游戏循环次数,并使用这个计数来确定角色的速度何时应该改变。例如,您的游戏是以这样的方式构建的,当玩家触摸屏幕的右侧时,角色将向右移动,当玩家触摸屏幕的左侧时,角色将向左移动,当玩家不触摸屏幕时,角色静止不动。如果玩家只是短时间触摸屏幕,你可以使用这种架构让角色行走,如果玩家触摸屏幕的时间更长,就让角色奔跑。

提示 第五章概述了设置基于触摸控制的游戏的解决方案。

第一步是创建两个变量,这两个变量可以从游戏中的任何类中读取。第一个变量跟踪已经执行的游戏循环次数,第二个变量跟踪角色的当前速度。

public static final float PLAYER_RUN_SPEED = .15f;
public static int totalGameLoops = 0;

接下来,在Renderer类中创建一个movePlayer()方法。到目前为止,这种方法已经在本书的多个解决方案中使用。如果你需要这个方法如何工作的基本解释,请参见第六章。

movePlayer()方法包含一个switch...case语句,该语句读取玩家的动作并相应地移动角色。修改这个方法来测试执行循环的次数,并在此基础上改变角色的速度(参见清单 11-5 和清单 11-6 )。

清单 11-5 。改变移动速度(OpenGL ES 1)

private void movePlayer(GL10gl){
if (totalGameLoops> 15)
{
PLAYER_RUN_SPEED += .5f;
}

switch(playeraction){
case PLAYER_MOVE_RIGHT:

playercurrentlocation += PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
case PLAYER_MOVE_LEFT:

playercurrentlocation -= PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

case PLAYER_STAND:

PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;

break;
}
}

清单 11-6 。改变移动速度(OpenGL ES 2/3)

private void movePlayer(GL10gl){
if (totalGameLoops> 15)
{
PLAYER_RUN_SPEED += .5f;
}
switch(playeraction){
case PLAYER_MOVE_RIGHT:
playercurrentlocation += PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0,playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);

break;
case PLAYER_MOVE_LEFT:
playercurrentlocation -= PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0,playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;

case PLAYER_STAND:

PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;

break;
}

}

最后,在RendereronDrawFrame()方法中,每次执行增加totalGameLoops int(参见清单 11-7 )。

清单 11-7totalGameLoops

public void onDrawFrame(GL10gl) {

...

totalGameLoops +=1;
movePlayer(gl);

...
}

11.3 当角色移动时制作动画

问题

当游戏角色移动时,它看起来不像是在行走。

解决办法

使用 spritesheet 动画使角色在移动时看起来像在行走。

它是如何工作的

这个解决方案将涉及到对您已经广泛使用的movePlayer()方法进行修改。在模型矩阵被转换后,转换纹理矩阵以显示 spritesheet 中的下一帧。

注意关于使用斜板的解决方案,参见第六章。

首先创建一个在所有类中都可见的作用域变量,该变量将用于跟踪 spritesheet 动画的当前帧。

public static float currentrunaniframe = 0f;

接下来,对movePlayer()方法进行加粗的修改(参见清单 11-8 和 11-9 )。

清单 11-8 。动画角色(OpenGL ES 1)

private void movePlayer(GL10gl){
if (totalGameLoops> 15)
{
PLAYER_RUN_SPEED += .5f;
}

currentrunaniframe += .25f;
if (currentrunaniframe> .75f)
{
currentrunaniframe = .0f;
}

switch(playeraction){
case PLAYER_MOVE_RIGHT:

playercurrentlocation += PLAYER_RUN_SPEED;
scrollBackground1(gl, playeraction);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentrunaniframe,.50f, 0.0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
case PLAYER_MOVE_LEFT:

playercurrentlocation -= PLAYER_RUN_SPEED;
scrollBackground1(gl, playeraction);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentrunaniframe,.75f, 0.0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

case PLAYER_STAND:

PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;

break;
}
}

清单 11-9 。制作角色动画(OpenGL ES 2/3)

private void movePlayer(GL10gl){
if (totalGameLoops> 15)
{
PLAYER_RUN_SPEED += .5f;
}
currentrunaniframe += .25f;
if (currentrunaniframe> .75f)
{
currentrunaniframe = .0f;
}

switch(playeraction){
case PLAYER_MOVE_RIGHT:
playercurrentlocation += PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0, playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix,currentrunaniframe, .50f );

break;
case PLAYER_MOVE_LEFT:
playercurrentlocation -= PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0, playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix,currentrunaniframe, .75f );
break;

case PLAYER_STAND:

PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;

break;
}

}

清单 11-8 中加粗的if语句基于四帧动画工作。如果 sprite 工作表中有四帧角色动画,则在四次循环后,动画需要重置为第一帧。if语句测试当前帧,并在到达第四帧时重置动画。

制作角色动画的关键(如果你在 OpenGL ES 2/3 中工作)是修改你的角色类的draw()方法,以传入你想要显示的 sprite sheet 图像的 x 和 y 位置(见第六章以获得完成这个的详细解决方案)。

最后,修改PLAYER_STAND案例,将动画从变为静态的“站立”图像。请记住,根据 spritesheet 的设置,该解决方案中显示的坐标可能需要更改(参见清单 11-10 和 11-11 )。

清单 11-10PLAYER_STAND (OpenGL 是 1)

case PLAYER_STAND:

PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(.25f,.25f, 0.0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

清单 11-11PLAYER_STAND (OpenGL 是 2/3)

case PLAYER_STAND:

PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;
playercurrentlocation -= PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0, playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix,.25f, .25f );

break;

十二、移动敌人

在屏幕上移动角色——无论是人、动物、机器人还是车辆——是引人注目的游戏中最重要的部分之一。如果你试图创造一个在游戏中自由移动的角色,你可能会遇到一些问题。

这一章将介绍帮助你在游戏中增加敌人的方法。本章的解决方案包括将敌人装载到游戏中预定的位置,并沿着特定的路径移动敌人。

12.1 将敌人装载到预定位置

问题

游戏没有在正确的位置装载敌人。

解决办法

使用一个类来确定敌人的产卵点在哪里。

它是如何工作的

许多游戏类型都有“繁殖点”,玩家可以在那里繁殖后代。要在这些预定的位置繁殖敌人,你需要在你的敌人职业中添加一些floats,然后使用这些floats将敌人的模型矩阵转换到繁殖位置。

本章的解决方案将基于一个基本的角色职业,而这个职业又是基于第七章和第八章中的SBGBackground职业。鉴于我们现在谈论的是游戏中的敌人,让我们重新命名这个职业SBGEnemy。该类的内容应该如清单 12-1 和清单 12-2 所示。

清单 12-1SBGEnemy() (OpenGL 是 1)

public class SBGEnemy {

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 SBGEnemy() {

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

}

清单 12-2SBGEnemy() (OpenGL 是 2/3)

public class SBGEnemy {

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 texX;" +
"uniform float texY;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x +
texX,TexCoordOut.y + texY));"+
"}";
private float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

private int[] textures = new int[1];
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;
static float vertices[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

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

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public SBGEnemy() {

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

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);

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

public void draw(float[] mvpMatrix, int texX, int texY, int[] spriteSheet) {
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, spriteSheet[0]);

int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsTexX = GLES20.glGetUniformLocation(mProgram, "texX");
int fsTexY = GLES20.glGetUniformLocation(mProgram, "texY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsTexX, texX);
GLES20.glUniform1f(fsTexY, texY);

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

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

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

GLES20.glDisableVertexAttribArray(mPositionHandle);
   }
}

然后修改这个类,添加两个 floats。一个浮动将跟踪 x 轴的繁殖位置,另一个浮动将跟踪 y 轴的繁殖位置(见清单 12-3 )。

清单 12-3 。追踪产卵位置的浮标

public class SBGEnemy {

public float posY = 0f;
public float posX = 0f;

...

}

SBGEnemy构造函数中将这些浮点数设置到期望的产卵位置(见清单 12-4 )。

清单 12-4 。给位置浮动赋值

public class SBGEnemy {

public float posY = 0f;
public float posX = 0f;
...
public SBGEnemy() {
posX = .25;
posY = .25;
...
}
...

}

现在你可以使用 OpenGL ES 1 的glTranslatef()方法调用中的SBGEnemy.posXSBGEnemy.posY在你绘制敌人的模型矩阵之前将它移动到产卵位置。您可以在 OpenGL ES 2/3 的Matrix.translateM()方法中使用相同的属性。在清单 12-5 和清单 12-6 中显示的spawnEnemy()方法,可以在你的游戏中创建,以帮助你在一个位置产生敌人。

清单 12-5spawnEnemy() (OpenGL 是 1)

private SFEnemy enemy = new SFEnemy();

spawnEnemy(){
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(enemy.posX, enemy.posY, 0f);
}

清单 12-6spawnEnemy() (OpenGL 是 2/3)

private SFEnemy enemy = new SFEnemy();

spawnEnemy(){
Matrix.translateM(mTMatrix, 0, enemy.posX, enemy.posY, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0);
}

12.2 将敌人装载到随机地点

问题

游戏需要在随机的地点产生敌人。

解决办法

修改最后一个解决方案,为产卵的敌人创造“随机”的位置。

它是如何工作的

许多游戏会在随机地点产生敌人。这增加了你游戏的难度,因为它剥夺了预先设定的产卵位置的可预测性。

上一个解决方案中的代码可以很容易地修改,以生成随机的产卵位置。清单 12-7 和 12-8 显示了应该进行的修改,以适应随机的产卵位置。

清单 12-7SBGEnemy()对于随机位置(OpenGL ES 1)

public class SBGEnemy {

public float posY = 0f;
public float posX = 0f;

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 SBGEnemy() {
Random randomPos = new Random();
posX = randomPos.nextFloat() * 3;
posY = randomPos.nextFloat() * 3;

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

}

清单 12-8SBGEnemy()对于随机位置(OpenGL ES 2/3)

public class SBGEnemy {

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 texX;" +
"uniform float texY;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x +
texX,TexCoordOut.y + texY));"+
"}";
private float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

private int[] textures = new int[1];
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;
static float vertices[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

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

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public SBGEnemy() {
Random randomPos = new Random();
posX = randomPos.nextFloat() * 3;
posY = randomPos.nextFloat() * 3;

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

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);

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

public void draw(float[] mvpMatrix, int texX, int texY, int[] spriteSheet) {
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, spriteSheet[0]);

int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsTexX = GLES20.glGetUniformLocation(mProgram, "texX");
int fsTexY = GLES20.glGetUniformLocation(mProgram, "texY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsTexX, texX);
GLES20.glUniform1f(fsTexY, texY);

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

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

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

GLES20.glDisableVertexAttribArray(mPositionHandle);
   }
}

这个解决方案建立在您在前一个解决方案中创建的posXposY属性的基础上。不再用静态值填充这些属性,SBGEnemy类的构造函数现在将随机填充位置到posXposY浮动中。结果就是现在当你从spanEnemy()方法调用SBGEnemy.posXSBGEnemy.posY的时候,敌人会在屏幕上的随机位置被创造出来。

12.3 沿着路径移动敌人

问题

敌人不会沿着预定的路径移动。

解决办法

使用算法为角色创建自动移动的路径。

它是如何工作的

这个解决方案是为了让你的敌人沿着一条特定的路径移动,这条路径被称为贝塞尔曲线。贝塞尔曲线通常在游戏中使用,因为它们可以通过一个相当简单的算法很容易地产生。他们也可以被修改来创造变化,使游戏变得有趣和不可预测。图 12-1 展示了贝赛尔曲线的样子。

9781430257646_Fig12-01.jpg

图 12-1 。二次贝塞尔曲线

为了让敌人以二次贝塞尔曲线从屏幕顶部移动到底部,你需要两种方法。您可以创建一个方法来获取贝塞尔曲线上的下一个 x 轴值,并创建一个方法来给出贝塞尔曲线上的下一个 y 轴值。每次你调用这些方法时,你会得到 x 和 y 轴上的下一个特定敌人需要移动到的位置。一旦你有了这些位置,你使用glTranslatef()移动模型矩阵到计算的位置。

幸运的是,在贝塞尔曲线上画点相当简单。要构建一条二次贝塞尔曲线,你需要四个笛卡尔点:一个起点,一个终点,以及曲线环绕的两个曲线点。现在我们来回顾一下如何做到这一点。

创建八个新的浮点数来跟踪这些点的 x 和 y 坐标,如清单 12-9 所示。

清单 12-9 。贝塞尔跟踪坐标

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;

修改SBGEnemy类,给现有的posXposY添加一个posT浮动,如清单 12-10 所示。

清单 12-10posT

public class SBGEnemy {
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

...
}

绘制点的关键值称为 t 位置。t 位置告诉公式您在曲线上的位置,从而允许公式计算该单个位置的 x 或 y 坐标。

提示如果你不理解以下公式背后的数学原理,有很多很好的资源,包括一个 wiki,可以找到贝塞尔曲线。

在你的SBGEnemy()类中创建两个方法(参见清单 12-11 )。一种方法用于获取下一个 x 轴值,一种方法用于获取下一个 y 轴值。此外,将随机值添加到posXposY浮点中,并将一个设定值添加到posT中。

清单 12-11 。播种位置值

public class SBGEnemy {
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 SBGEnemy() {
posY = (randomPos.nextFloat() * 4) + 4;
posX = randomPos.nextFloat() * 3;
posT = .012;

}

public float getNextPosX(){

}
public float getNextPosY(){

}

}

在 y 轴上的二次贝塞尔曲线上寻找一个点的公式如下:

(y1*(??)) + (y2 * 3 * (??) * (1-t)) + (y3 * 3 * t * (1-t)2) + (y4* (1-t)3)

注意要得到 x 轴点,只需将前面等式中的 y 替换为 x 即可。

在你的getNextPosY() 中使用这个公式来计算你的敌人的位置(见清单 12-12 )。

清单 12-12getNextPosY()

public class SBGEnemy {
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 SBGEnemy() {
posY = (randomPos.nextFloat() * 4) + 4;
posX = randomPos.nextFloat() * 3;
posT = .012;

}

public float getNextPosX(){

}

public float getNextPosY(){
return (float)((BEZIER_Y_1*(posT*posT*posT)) +
(BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) +
(BEZIER_Y_3 * 3 * posT * ((1-posT) * (1-posT))) +
(BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}

}

对 x 轴使用同样的公式,稍作修改,如清单 12-13 所示。

清单 12-13getNextPosX()

public class SBGEnemy {
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 SBGEnemy() {
posY = (randomPos.nextFloat() * 4) + 4;
posX = randomPos.nextFloat() * 3;
posT = sfengine.SCOUT_SPEED;

}

public float getNextPosX(){
return (float)((BEZIER_X_4*(posT*posT*posT)) +
(BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) +
(BEZIER_X_2 * 3 * posT * ((1-posT) * (1-posT))) +
(BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT))));

 }

public float getNextPosY(){
return (float)((BEZIER_Y_1*(posT*posT*posT)) +
(BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) +
(BEZIER_Y_3 * 3 * posT * ((1-posT) * (1-posT))) +
(BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}

}

注意,当计算 x 轴的右侧时,值是 x1、x2、x3,然后是 x4;然而,从左侧开始,这些点以相反的顺序使用,x4、x3、x2,然后是 x1。

现在,在游戏循环的每次执行中,将SBGEnemy.posX设置为SBGEnemy.getNextPosX()并将SBGEnemy.posY设置为SBGEnemy.getNextPosY(),然后将模型矩阵平移到posXposY点,就像你一直在做的那样。

十三、在有障碍物情况下移动角色

如果游戏,尤其是平台游戏,以人物从左到右无阻碍地奔跑为特色,它们不会长久地吸引玩家的兴趣。

平台游戏,如超级马里奥兄弟,小行星 2,和无数其他包含障碍,玩家必须导航。在你的游戏中使用障碍物是增加刺激和阻止动作的好方法。然而,在编写游戏代码时,障碍也会增加复杂性。

这一章将会介绍一些你在游戏中遇到障碍时可能会遇到的情况。第一个场景包括让一个角色在平台之间跳跃。

13.1 平台之间的跳跃

问题

该游戏不允许玩家在游戏关卡的平台之间跳跃。

解决办法

使用预定的距离,以及数学公式来调整跳跃动画。

它是如何工作的

每个人都知道一个人跳起来是什么样子。跳跃有一种特殊的运动和流畅,这在游戏中很难复制。在这个解决方案中,我们将修改早期解决方案中的一些代码,为可玩角色创建一个跳跃动作。

第一步是创建一个供用户“跳转”的控件在解决方案 5.4 中,我向你展示了如何使用SimpleOnGestureListeneronFling()方法创建一个手势。修改代码以设置一个公共变量来指示玩家想要跳转。

GestureDetector.SimpleOnGestureListener gestureListener = new
GestureDetector.SimpleOnGestureListener(){

@Override
publicbooleanonFling(MotionEvente1, MotionEvente2, float velocityX,
floatvelocityY) {

playeraction = PLAYER_JUMPING;
}
};

playeractionPLAYER_JUMPING变量是存储在项目可访问的类中的整数。

我们要跳跃的角色是SuperBanditGuy。在本书的前面,你创建了一个SuperBanditGuy类,它创建了游戏的主角并在屏幕上移动他。修改SuperBanditGuy类,添加两个浮点数(xy),用于跟踪角色在跳跃过程中的 x 和 y 坐标。

public class SuperBanditGuy {

public float x = .75f;
public float y = .75f;
//I like to start characters a little higher than the bottom of the screen so
//that the player can see some ground under them
...

}

现在,在游戏循环中添加以下浮动,Renderer

private float previousJumpPos = 0;
private float posJump = 0;

同样,在之前的解决方案中,我们在游戏循环中创建了一个case语句,允许您测试playeraction并相应地移动角色。现在让我们修改case语句来测试PLAYER_JUMPING ,然后开始计算跳跃。清单 13-1 和 13-2 (分别是 OpenGL ES 1 和 OpenGL ES 2/3)会让你的角色进行一次基本的跳跃。

清单 13-1PLAYER_JUMPING (OpenGL 是 1)

switch(playeraction){
case PLAYER_MOVE_RIGHT:

...

break;
case PLAYER_JUMPING:
previousJumpPos = posJump;

posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump<= Math.PI)
{
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;

}else{
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.y = .75f;
}
}
goodguy.x += PLAYER_RUN_SPEED;

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(goodguy.x, goodguy.y, 0f);
gl.glPopMatrix();
gl.glLoadIdentity();

break;
...
}

清单 13-2PLAYER_JUMPING (OpenGL 是 2/3)

switch(playeraction){
case PLAYER_MOVE_RIGHT:

...

break;
case PLAYER_JUMPING:
previousJumpPos = posJump;

posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump<= Math.PI)
{
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;

}else{
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.y = .75f;
}
}
goodguy.x += PLAYER_RUN_SPEED;

Matrix.translateM(RotationMatrix, 0, goodguy.x, goodguy.y, 0);

break;
...
}

无论 OpenGL ES 版本如何,以令人信服的跳跃动作移动角色的关键是以下公式:

posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump<= Math.PI)
{
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.y = .75f;
}
}

注意根据你的具体游戏,这个公式中有许多值你需要调整。这些值包括跳跃的高度和时间长度。

注意这个公式只作用于角色位置的 y 轴。x 轴位置将以角色确定的运行速度继续向左或向右移动。让我们检查这个公式的每一行。

previousJumpPos = posJump;

第一行设置了previousJumpPos ,供稍后在公式中使用。

posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);

这条线描绘了正弦波上跳跃的位置。这并不用于直接确定角色在屏幕上的跳转位置。相反,它用于在内存中确定角色何时到达跳跃的顶点。

.5的值是跳跃的时间长度。虽然它不代表特定的时间单位,但它可以增加或减少,以创建更长或更短的持续跳跃。这条线的Math.PI/2或半圆周率部分简单地表示了这样一个事实,即我们开始的平面已经是正弦波的一半。

当你处理正弦波时,圆周率是周期波所需要的时间。所以我们这波的时机是半π对π。当正弦波上的当前位置小于π时,我们知道角色正处于上升到跳跃顶点的过程中。一旦角色的位置大于圆周率,我们就可以开始让它回到地面。下一行的目的是测试这种情况。

if (posJump<= Math.PI)
{
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
...
}

最后,在if语句中,第一个条件在 y 轴上向上移动字符,第二个条件向下移动字符。

goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;

请注意,该角色的 y 轴位置会因公式而增加。这将在 y 轴上向上移动角色。与此同时,角色的 x 轴位置正在增加,就好像角色在 x 轴上朝着跳跃的方向正常移动一样。

下面一行将字符下移。

goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;

此公式减少 y 轴位置的值,以将角色向下移动到地平面。

注意两个移动语句中的值 1.5 代表跳跃的高度。同样,您可以根据需要调整该值,以获得更高或更短的跳跃。

当跳转结束时,只需测试角色的位置是否再次位于 y 轴上的 0.75 处(添加到渲染器的 float 中定义的起始位置),然后退出跳转,如下所示。

if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.y = .75f;
}

此代码的最佳位置是在正在减少 y 轴位置的if语句中。如果你把这段代码放在if语句之外,你就冒着让角色在一瞬间掉到地平面以下的风险。

最后,抽出角色,如列表 13-3 和 13-4 所示。

清单 13-3 。绘制角色 (OpenGL ES 1)

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(goodguy.x, goodguy.y, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();

清单 13-4 。绘制角色(OpenGL ES 2/3)

Matrix.translateM(RotationMatrix, 0, goodguy.x, goodguy.y, 0);

这段代码可以很容易地添加到您的渲染器中,以产生跳跃运动。

13.2 向上移动台阶

问题

角色需要跳上或跳下台阶,或者不平坦平面上的其他物体。

解决办法

使用跳转解决方案的修改版本来导航步骤。

它是如何工作的

在第十五章中,我将介绍碰撞检测的解决方案。虽然本章中的解决方案确实提出了冲突检测的主题,但它没有后面将要介绍的解决方案那么深入。相反,这个解决方案将专门处理导航步骤所需的来自上一个解决方案的跳转代码的修改。

如果您正在向上跳,您将从来自清单 13-1 和 13-2 的相同代码开始。

需要做的修改是在if语句中,该语句测试角色是否通过跳跃下降足够远以再次到达地面。将测试值替换为台阶的高度。

if (goodguy.y<=<height of step>){
playeraction = PLAYER_STAND;
goodguy.y =<height of step>;
}

通过测试角色是否下降到台阶的高度,可以停止角色在台阶上的移动。这种解决方案要求您知道或测试台阶的高度。

这种解决方案适用于您可以在代码中预测级别布局的级别。例如,如果你使用瓷砖来建造你的关卡,你可以测试瓷砖地图来知道台阶在哪里,从而知道停止下降的高度。如果关卡的布局可以动态改变,这种解决方案就不那么好了。

如果你的关卡中有碎片或者可能有移动的平台,而你不能使用这种方法,请参考第十五章,其中有更多关于碰撞检测的内容。

十四、发射武器

许多游戏要求玩家向障碍物或敌人开火或投掷武器。如果你曾经试图发射武器,你可能会遇到让你的投射物以可预测的方式离开你的角色,并沿着设定的路径到达目标的问题。

武器可以有多种形状、大小和功能。在许多游戏情况下,子弹是直线行进的,导弹、激光和大多数其他推进武器也是如此。投掷的武器,如石头、手榴弹,甚至在一定程度上是箭,都遵循更抛物线的轨迹。不管你为武器选择了什么样的图像或动画,从 A 点到 B 点的数学方法都是一样的。

本章将介绍触发武器的“按钮”的多种解决方案,以及在屏幕上制作武器动画的多种解决方案。很像第十三章,这一章在 OpenGL ES 中并不像过去的一些章节那么沉重。当你需要一个使用武器的角色时,需要更多的外围编码。

我们要看的第一个配方将提供一种在屏幕上连接“发射按钮”的方法。这个按钮将基于以前的解决方案,给你一个方法来控制游戏中武器的发射。

在很多游戏场景中,你可能不需要开火按钮。相反,这些武器可以自动发射,甚至可以持续发射。不断开火的武器在 top/down shooters 等游戏中相当受欢迎。如果你打算使用武器自动开火的游戏类型,请随意跳过本章的第一个配方。

14.1 为“火灾”按钮接线

问题

玩家没有办法发射武器。玩家需要一个按钮——或屏幕上的互动区域—来发射角色的武器。

解决办法

在屏幕上创建一个互动空间,玩家可以点击它来触发武器的发射。这将在两个不同的解决方案中演示。

它是如何工作的

我将从两个方面着手解决这个问题。第一种是基于配方 5.3 中的先前解决方案,其中屏幕区域被分成触摸区。我们现在将这些区域中的一个专用于射击。如果玩家触摸屏幕的这个区域,将会设置发射武器的标志。

这种方法适用于某些情况;然而,如果游戏类型要求屏幕上有多个触摸区域,这可能会导致武器被无意中发射。因此,第二个解决方案也将被探索,玩家可以双击屏幕上的任何地方来发射武器。

解决方案 1

对于第一种解决方案,覆盖游戏活动的onTouchEvent() 。请记住,这不一定是主要的活动,尤其是当你的游戏以菜单开始的时候。当此事件检测到触摸时,设置PLAYER_FIRE_WEAPONS标志,如清单 14-1 所示。

清单 14-1 。onTouchEvent()

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

display.getMetrics(outMetrics);

int height = outMetrics.heightPixels / 4;

int playableArea = outMetrics.heightPixels - height;
if (y >playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x <outMetrics.widthPixels / 2){
playeraction = PLAYER_FIRE_WEAPONS;
}
break;
}
}

return false;
}

在本书包含的许多解决方案中,您一直在使用playeraction int。这个int是在本书的前面建立的,作为当前动作的持有者。游戏循环包含一个case语句,它将读取这个int,并在playeraction = PLAYER_FIRE_WEAPONS时执行武器开火代码。

注意本解决方案中使用的display变量设置在游戏的主活动中。这是玩家启动游戏时开始的活动。因此,display变量设置如下:

display = ((WindowManager)
getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

如果这个解决方案不是你所需要的,你可以很容易地设置一个不同的解决方案,玩家可以双击屏幕上的任何地方来触发武器的发射。接下来我们将看看这个解决方案。

解决方案 2

要检测双击,需要实现GestureDetector 。清单 14-2 中的代码将允许玩家双击屏幕并发射武器。

清单 14-2 。使用GestureDetector的活动

public class SBGGameMain extends Activity {
private GestureDetector gd;

@Override
public void onCreate(Bundle savedInstanceState) {

...

gd = new GestureDetector(this,gestureListener);
}
@Override
protected void onResume() {
super.onResume();
gameView.onResume();
}

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

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

display.getMetrics(outMetrics);

int height = outMetrics.heightPixels/4;

int playableArea = outMetrics.heightPixels - height;
if (y >playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x <outMetrics.widthPixels/2){
playeraction = PLAYER_MOVE_LEFT;
}else{
playeraction = PLAYER_MOVE_RIGHT;
}
break;
case MotionEvent.ACTION_UP:
playeraction = PLAYER_STAND;
break;
}
}
else {
return gd.onTouchEvent(event);
}

return false;
}

GestureDetector.SimpleOnGestureListener gestureListener = new
GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEvent arg0) {
//TODO Auto-generated method stub
return false;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {

return false;
}
@Override
public void onLongPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//TODO Auto-generated method stub
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//TODO Auto-generated method stub
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {

playeraction = PLAYER_FIRE_WEAPONS;

return false;

};

};
}

解决方案 2 的关键是在您的活动中创建一个GestureDetector。然后建立一个新的SimpleOnGestureListener()并将事件从onTouchEvent()传递给它。然后SimpleOnGestureListener()将确定该事件是否是双击的结果,并将playeraction设置为PLAYER_FIRE_WEAPONS

14.2 制作导弹动画

问题

当玩家发射武器时,抛射体应该离开角色并沿直线行进,直到击中目标或离开屏幕。

解决办法

创建一个新的导弹类,并使用 OpenGL ES 将它从角色移动到目标。

它是如何工作的

第一步是为你的武器创建一个新的职业。这个类,像本书中其他解决方案中创建的许多类一样,将为图像的纹理绘制正方形,然后将纹理映射到正方形中。绘制武器的新类看起来应该如清单 14-3 (OpenGL ES 1)和清单 14-4 (OpenGL ES 2/3)所示。

清单 14-3SBGWeapon() (OpenGL 是 1)

public class SBGWeapon {

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

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

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

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

}

清单 14-4SBGWeapon() (OpenGL 是 2/3)

public class SBGWeapon {

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

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

private float texture[] = {
0f, 0f,
1f, 0f,
1f, 1f,
0f, 1f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int program;
private int positionHandle;
private int matrixHandle;

static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float vertices[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

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

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

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

}catch(Exception e){

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

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

public SBGWeapon() {

ByteBuffer byteBuff = ByteBuffer.allocateDirect(
byteBuff.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuff.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

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

ByteBuffer indexBuffer = ByteBuffer.allocateDirect(
indexBuffer.order(ByteOrder.nativeOrder());
drawListBuffer = indexBuffer.asShortBuffer();
drawListBuffer.put(indices);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);

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

public void draw(float[] matrix) {

GLES20.glUseProgram(program);

positionHandle = GLES20.glGetAttribLocation(program, "vPosition");

GLES20.glEnableVertexAttribArray(positionHandle);

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

GLES20.glVertexAttribPointer(positionHandle, 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]);
intfsTexture = GLES20.glGetUniformLocation(program, "TexCoordOut");
GLES20.glUniform1i(fsTexture, 0);

matrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");

GLES20.glUniformMatrix4fv(matrixHandle, 1, false, matrix, 0);

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

GLES20.glDisableVertexAttribArray(positionHandle);
}
}

除了 OpenGL ES 所要求的特性之外,SBGWeapon()类还包含三个关键特性。两个变量(xy)用于通过游戏循环跟踪武器的轴上坐标。shotFired变量用于确定SBGWeapon的具体实例化是否已经被触发,是应该被绘制在屏幕上还是被忽略。

为什么要用一个布尔值来表示是否开枪?玩家在游戏中快速连续射击多次是很常见的。这意味着,在任何时候,你的游戏都必须在游戏循环的一次迭代中跟踪许多镜头。通过使用shotFired boolean,你可以确定内存中的哪些SBGWeapons已经被触发,哪些正在等待被抽取。

接下来的计划是在您的Renderer类中实例化一个SBGWeapon()。然后,当您检测到PLAYER_FIRE_WEAPON时,在游戏循环的每次迭代中绘制SBGWeapon()并沿直线移动它,直到SBGWeapon()击中目标或到达屏幕的末端。

Renderer类中,实例化一个由SBGWeapon组成的数组。在清单 14-5 和清单 14-6 中,我将使用一个由四枚导弹组成的数组,这意味着一次只能有四枚导弹同时出现在屏幕上。

private SBGWeapon[] playerFire = new SBGWeapon[4];

不要忘记为你发射的任何武器的图像加载纹理。纹理被加载到RendereronSurfaceCreated()方法中(参见清单 14-5 和清单 14-6 )。

清单 14-5 。加载纹理(OpenGL ES 1)

for(int x = 0; x<4; x++){
playerFire[x].loadTexture(gl,R.drawable.weapon, context);
}

清单 14-6 。加载纹理(OpenGL ES 2/3)

for(int x = 0; x<4; x++){
playerFire[x].loadTexture(R.drawable.weapon, context);
}

最后,创建一个可以从游戏循环中调用的新方法。在本书的许多解决方案中,我引用了一个作用于playerActioncase语句。在这个语句中添加一个新的case来测试playerAction = PLAYER_FIRE_WEAPON。如果PLAYER_FIRE_WEAPON被检测到,调用你的新方法将武器绘制到屏幕上(见清单 14-7 和 14-8 )。

清单 14-7firePlayerWeapon() (OpenGL 是 1)

private void firePlayerWeapon(GL10gl){
for(int x = 0; x < 4; x++  ){
if (playerFire[x].shotFired){
int nextShot = 0;
if (playerFire[x].posY> 4.25){ //represents the top of the screen
playerFire[x].shotFired = false;
}else{
if (playerFire[x].posY> 2){
if (x == 3){//since we only have 4 should, recycle any that are no longer in use
nextShot = 0;
}else{
nextShot = x + 1;
}
if (playerFire[nextShot].shotFired == false){
playerFire[nextShot].shotFired = true;
//set the weapon x to the x of the character when it was fired
playerFire[nextShot].posX = player.x;
playerFire[nextShot].posY = 1.25f;
}

}
playerFire[x].posY += .12f; //the speed of the shot as it moves
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
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);
gl.glPopMatrix();
gl.glLoadIdentity();

}
}
}
}

清单 14-8firePlayerWeapon() (OpenGL 是 2/3)

private void firePlayerWeapon(GL10 unused, float[] rotationMatrix, float[] matrix){
for(int x = 0; x < 4; x++  ){
if (playerFire[x].shotFired){
int nextShot = 0;
if (playerFire[x].posY> 4.25){ //represents the top of the screen
playerFire[x].shotFired = false;
}else{
if (playerFire[x].posY> 2){
if (x == 3){//since we only have 4 should, recycle any that are no longer in use
nextShot = 0;
}else{
nextShot = x + 1;
}
if (playerFire[nextShot].shotFired == false){
playerFire[nextShot].shotFired = true;
//set the weapon x to the x of the character when it was fired
playerFire[nextShot].posX = player.x;
playerFire[nextShot].posY = 1.25f;
}

}
playerFire[x].posY += .12f; //the speed of the shot as it moves
Matrix.translateM(RotationMatrix, 0, playerFire[x].posX, playerFire[x].posY, 0);
playerFire[x].draw(matrix);
Matrix.multiplyMM(matrix, 0, rotationMatrix, 0, matrix, 0);

}
}
}
}

这个方法将从角色的位置发射一个镜头,一直向上直到它碰到屏幕的顶部边缘。修改SBGWeapon()xy值的赋值,使镜头向不同方向移动。通过增加或减少x值,您的镜头将向右或向左移动;通过增加或减少y值,你的镜头会上下移动。

在第十五章中,你将会看到实现碰撞检测的解决方案。碰撞检测是当你的镜头击中目标时采取行动的关键,而不是简单地让你的镜头离开屏幕边缘。

在下一个解决方案中,您将修改firePlayerWeapon()方法,以抛物线运动方式移动镜头,就好像是投掷而不是直线拍摄一样。

14.3 制作投掷武器的动画

问题

武器不会像投掷武器那样沿弧线飞行。

解决办法

使用一个公式,就像跳跃时使用的公式,来确定一个弯曲的轨迹。

它是如何工作的

要像投掷一样以弧形运动移动您的镜头,您需要修改firePlayerWeapon()方法。我们将使用《??》第十三章中让角色跳跃的相同数学公式,并将其放入firePlayerWeapons()公式中。这显示在清单 14-9 和清单 14-10 中。

清单 14-9 。拱形轨迹(OpenGL ES 1)

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

previousArcPos = arcJump;

arcJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (arcJump<= Math.PI)
{
playerFire[x].posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;

}else{
playerFire[x].posY -=(Math.sin((double)posArc) - Math.sin((double)previousArcPos))* 1.5;
if (playerFire[x].posY<= .75f){
playerFire[x].shotFired = false;
playerFire[x].posY = .75f;
}else{

if (x == 3){//since we only have 4 should, recycle any that are no longer in use
nextShot = 0;
}else{
nextShot = x + 1;
}
}

if (playerFire[nextShot].shotFired == false){
playerFire[nextShot].shotFired = true;
playerFire[nextShot].posX = player.x;
playerFire[nextShot].posY = player.y;
}

}

playerFire[x].posx += .12f;

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
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);
gl.glPopMatrix();
gl.glLoadIdentity();

}
}
}
}

清单 14-10 。拱形轨迹(OpenGL ES 2/3)

private void firePlayerWeapon(GL10 unused, float[] rotationMatrix, float[] matrix){
for(int x = 0; x < 4; x++  ){
if (playerFire[x].shotFired){
int nextShot = 0;
previousArcPos = arcJump;

arcJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (arcJump<= Math.PI)
{
playerFire[x].posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;

}else{
playerFire[x].posY -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (playerFire[x].posY<= .75f){
playerFire[x].shotFired = false;
playerFire[x].posY = .75f;
}else{

if (x == 3){//since we only have 4 should, recycle any that are no longer in use
nextShot = 0;
}else{
nextShot = x + 1;
}
}

if (playerFire[nextShot].shotFired == false){
playerFire[nextShot].shotFired = true;
playerFire[nextShot].posX = player.x;
playerFire[nextShot].posY = player.y;
}

}

playerFire[x].posx += .12f;
Matrix.translateM(RotationMatrix, 0, playerFire[x].posX, playerFire[x].posY, 0);
playerFire[x].draw(matrix);
Matrix.multiplyMM(matrix, 0, rotationMatrix, 0, matrix, 0);

}
}
}
}

通过这个小小的改动,你可以让你的武器产生一个抛出的弧线,而不是已经发射的射弹的直线。

摘要

在第十三章中,你回顾了允许你在游戏中添加敌人的食谱。然而,如果玩家没有办法保护自己,在游戏中加入敌人是不公平的。本章中的食谱帮助你为玩家提供了一种发射武器的方法。

十五、碰撞检测

碰撞检测是几乎所有游戏和所有游戏类型的关键组件。在一个没有碰撞检测的游戏中,物品、障碍物、角色和武器会在屏幕上四处移动,彼此漂浮而过,不会产生任何后果。

您的游戏代码需要能够确定屏幕上的对象是否相互接触或交叉。只有在你确定两个或更多的物体接触后,你才能对它们执行动作,比如施加伤害、停止运动、启动角色或摧毁一个物体。

本章将介绍一些解决碰撞检测问题的方法。碰撞检测可能很棘手,但是本章中的解决方案应该有助于使这个过程变得简单一些。

15.1 探测障碍物

问题

游戏角色可以通过屏幕上的物体来阻止他们。

解决办法

使用基本的碰撞检测来确定角色是否接触了障碍物或屏幕边缘。

它是如何工作的

如果您正在创建一个角色面临静态障碍物(如地板和平台、屏幕边缘或台阶)的游戏,则基本碰撞检测非常有用。测试静态对象的位置时,可以使用常数值。例如,在制作角色跳跃的配方 13.1 和 13.2 中,我使用了基本的碰撞检测来确定角色何时完成跳跃并回到地面,如清单 15-1 (OpenGL ES 1)和 15-2 (OpenGL ES 2/3)所示。

清单 15-1 。基本跳跃碰撞检测(OpenGL ES 1)

previousJumpPos = posJump;

posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);

if (posJump <= Math.PI)
{
goodguy.posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy. posY -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.posY<= .75f){
playeraction = PLAYER_STAND;
goodguy.posY= .75f;
}
}

goodguy. posX += PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(goodguy. posX, goodguy. posY, 0);
gl.glPopMatrix();
gl.glLoadIdentity();

清单 15-2 。基本跳跃碰撞检测(OpenGL ES 2/3)

previousJumpPos = posJump;

posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump <= Math.PI)
{
goodguy. posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;

}else{
goodguy. posY -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.posY= .75f;
}
}
goodguy. posX += PLAYER_RUN_SPEED;
Matrix.translateM(RotationMatrix, 0, goodguy. posX, goodguy. posY, 0);

在列表 15-1 和列表 15-2 中加粗的代码说明了在测试角色的 y 位置已经到达地面的水平时,如何使用常数. 75。因为我们知道游戏的地面总是在 y 轴的 0.75 处,所以这种简单的碰撞检测是有效的。

从屏幕边缘跑出来怎么办?如果您的游戏动作需要包含在一个屏幕中,并且 OpenGL ES 中的 x 轴已经被缩放到从 0(最左边)到 4(最右边)的范围内,您可以测试您的角色来阻止图像离开屏幕。

if(goodguy.posX<= 0 )
{
//the player has reached the left edge of the screen
goodguy. posX = 0; //correct the image's position and perform whatever action is necessary
}

如果您要测试与屏幕右边缘的碰撞,这个过程需要一个额外的步骤。OpenGL ES 中字符的 x 位置代表图像的左下角。因此,如果您正在测试字符的图像是否遇到了屏幕的右侧,则在整个图像已经离开屏幕之前,字符在左下角的 x 位置不会到达屏幕的右侧。

您可以通过将角色图像的大小添加到测试碰撞的if语句中来对此进行补偿。

if(goodguy. posX +.25f>= 4 )
{
//the player has reached the right edge of the screen
goodguy. posX = (4f - .25f); //correct the image's position and
                                //perform whatever action is necessary
}

碰撞检测的基本方法对于不太复杂的游戏逻辑是有效的,其中有许多静态对象,其大小和位置对于游戏循环来说是容易知道的。

如果你的游戏逻辑没那么简单怎么办?下一个解决方案帮助您检测正在移动且位置不可预测的对象之间的碰撞。

15.2 检测多个移动物体之间的碰撞

问题

该游戏需要检测两个或多个移动物体是否发生了碰撞。

解决办法

使用循环方法测试所有 OpenGL 图像边缘的碰撞。

它是如何工作的

要实现更健壮的碰撞检测,创建一个可以从游戏循环中调用的新方法。该方法将遍历屏幕上的所有活动项目,并确定是否有任何碰撞。

实现这种碰撞检测所需的关键字段是对象当前位置的 x 轴和 y 轴坐标,以及对象的状态。对象的状态是指该对象是否有资格被包括在碰撞检测中。这可以包括对象已经被破坏的标志,或者可能被测试的角色已经完成了一项成就,允许他们在特定的时间段内免于碰撞检测。

清单 15-3 和 15-4 描述了游戏中一个角色的职业。该类中添加了三个公共值:x 轴和 y 轴坐标各一个,用于跟踪角色的当前位置,还有一个布尔值,用于指示角色是否已经被破坏。

清单 15-3SBGEnemy() (OpenGL 是 1)

public class SBGEnemy {

public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;

private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBufferi ndexBuffer;

private float vertices[] = {
0.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 1.0, 0.0,
0.0, 1.0, 0.0,
};

private float texture[] = {
0.0, 0.0,
0.25f, 0.0,
0.25f, 0.25f,
0.0, 0.25f,
};

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

public SBGEnemy () {

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

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

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

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

}

清单 15-4SBGEnemy() (OpenGL 是 2/3)

public class SBGEnemy {

public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;

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

private float texture[] = {
0, 0,
1f, 0,
1f, 1f,
0, 1f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int program;
private int positionHandle;
private int matrixHandle;

static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float vertices[] = { -1f,  1f, 0.0,
-1f, -1f, 0.0,
1f, -1f, 0.0,
1f,  1f, 0.0 };

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

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

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

}catch(Exception e){

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

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

public SBGEnemy () {

ByteBuffer byteBuff = ByteBuffer.allocateDirect(
byteBuff.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuff.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

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

ByteBuffer indexBuffer = ByteBuffer.allocateDirect(
indexBuffer.order(ByteOrder.nativeOrder());
drawListBuffer = indexBuffer.asShortBuffer();
drawListBuffer.put(indices);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

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

public void draw(float[] matrix) {

GLES20.glUseProgram(program);

positionHandle = GLES20.glGetAttribLocation(program, "vPosition");

GLES20.glEnableVertexAttribArray(positionHandle);

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

GLES20.glVertexAttribPointer(positionHandle, 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(program, "TexCoordOut");
GLES20.glUniform1i(fsTexture, 0);

matrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");

GLES20.glUniformMatrix4fv(matrixHandle, 1, false, matrix, 0);

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

GLES20.glDisableVertexAttribArray(positionHandle);
}
}

现在,构建一个可以从游戏循环中调用的新类。在第十四章中,玩家可以使用武器。发射的武器排成一列,允许四发子弹同时出现在屏幕上。我们将在此基础上循环四个镜头中的每一个,检查它们是否被主动发射,然后检查它们是否与前面代码清单中的敌人角色发生碰撞。

完成碰撞测试最简单的方法是在每个活动对象周围创建一个边界框(在内存中),然后测试任何两个对象的边界框的边缘是否碰撞。为什么是边框?测试直线(如盒子边缘)比计算非常复杂形状的真实边缘更容易。此外,游戏中的物体通常会碰撞得如此之快,以至于眼睛无法察觉到碰撞发生在距离实际物体的可见边界不到一毫米的地方。

通过将大小(以坐标表示)添加到对象的当前 x 和 y 坐标位置来创建边界框。这意味着在坐标轴上缩放到 0.25 平方的对象将有一个从 x 到(x + .25)和从 y 到(y + .25)的边界框。任何进入那个空间的东西都会与那个物体相撞。在本例中,要测试碰撞,只需检查另一个对象的边界框是否包含介于(x 到(x + .25))和(y 到(y + .25))之间的点。如果是这样,那两个物体就相撞了。

在清单 15-5 中,正在发射的子弹有. 25 坐标值包围盒,敌人有 1 坐标值包围盒。

清单 15-5 。检测边界框

private void detectCollisions(){
for (int y = 1; y < 4; y ++){ //loop through the 4 potential shots in the array
if (playerFire[y].shotFired){ //only test the shots that are currently active
if(!enemy.isDestroyed){
//only test the shot against the enemy if it is not already destroyed
//test for the collision
if (((playerFire[y].posY  >= enemy.posY
&& playerFire[y].posY <= enemy.posY + 1f )  ||
(playerFire[y].posY +.25f>= enemy.posY
&& playerFire[y].posY + .25f<= enemy.posY + 1f )) &&
((playerFire[y].posX>= enemy.posX
&& playerFire[y].posX<= enemy.posX + 1f) ||
(playerFire[y].posX + .25f>= enemy.posX
&& playerFire[y].posX + 25f<= enemy.posX + 1f ))){
//collision detected between enemy and a shot
}
}
}
}
}

这种方法在检测一轮射击和单个敌人之间的碰撞时效果很好。为了测试一轮射击和许多敌人之间的碰撞,你需要稍微修改方法来循环通过你的敌人阵列(见清单 15-6 )。

清单 15-6 。穿过敌人

private void detectCollisions(){
for (int y = 1; y < 4; y ++){
if (playerFire[y].shotFired){
for (int x = 1; x < 10; x++ ){ //assumes you have an array of 10 enemies
if(!enemies[x].isDestroyed){
if (((playerFire[y].posY  >= enemies[x].posY
&& playerFire[y].posY <= enemies[x].posY + 1f )  ||
(playerFire[y].posY +.25f>= enemies[x].posY
&& playerFire[y].posY + .25f<= enemies[x].posY + 1f ))
&&  ((playerFire[y].posX>= enemies[x].posX
&& playerFire[y].posX<= enemies[x].posX + 1f) ||
(playerFire[y].posX + .25f>= enemies[x].posX
&& playerFire[y].posX + 25f<= enemies[x].posX + 1f ))){

//collision detected between enemy and a shot

}
}
}
}
}
}

这种碰撞检测方法将帮助您测试游戏中多个对象的边界框之间的碰撞。一旦检测到冲突,您就可以在注释区域中处理该冲突。你可能想要采取的一个行动是改变一个物体的轨迹——就像一个球从墙上弹回一样。

下一个食谱将包括改变一个物体的轨迹。

15.3 改变物体轨迹

问题

一个游戏对象,比如一个球,当它撞到墙上时不会改变方向。

解决办法

当一个对象与另一个对象碰撞时,使用碰撞检测来改变其轨迹。

它是如何工作的

有些游戏类型中的物体在与其他物体碰撞时不一定会停止或爆炸。一些游戏,如突破式砖块击碎器,包含碰撞时相互弹开的物体。

本解决方案中对detectCollisions()方法的修改有助于您检测两个对象(在本例中是一个球和一块砖)之间的碰撞,并在接触时改变球的轨迹。

清单 15-7 中的代码直接来自我写的一个旧的砸砖游戏,我在代码中留下了循环砖块来帮助你。在这个示例中,ball是一个Ball()类的实例化,它与我们在本章前面看到的SBGEnemy()类相同。另外,wall是一个包含行集合的类。行则是实例化砖块的集合,砖块的类也与SBGEnemy()相同。这为玩家创造了一堵由一排排砖块组成的墙。

最后,清单 15-7 不仅检查球和砖块之间的碰撞,还检查球和屏幕的边缘。如果球碰到屏幕的边缘,它会弹开,导致轨迹改变,并保持球在比赛中。

清单 15-7detectCollisions()

private void detectCollisions(){
if(ball.posY<= 0){
}
for (int x = 0; x <wall.rows.length; x++)
{ //cycle through each brick and see if the ball has collided with it
for(int y = 0; y <wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
if (((ball.posY>wall.rows[x].bricks[y].posY - .25f)  //the height of the brick is .25
&& (ball.posY<wall.rows[x].bricks[y].posY)
&& (ball.posX + .25f>wall.rows[x].bricks[y].posX)
&& (ball.posX<wall.rows[x].bricks[y].posX + 1.50))) //the legnthof the brick
{ //there is a collision, destroy the brick and change the trajectory
//of the ball

wall.rows[x].bricks[y].isDestroyed = true;
//change the trajectory by inverting the y axis
ballTargetY = ballTargetY * -1f;

//if the ball was originally moving to the left when it collided, move it to
//the right after the bounce - otherwise move it to the left
if(ballTargetX == -2f){
ballTargetX = 5f;
}else{
ballTargetX = -2f;
}

}
}

}
}

//Now check for collisions with the player's "paddle" and bounce the ball off accordingly
if((ball.posY - .25f<= .5f)
&& (ball.posX + .25f>player.PosX ) //the paddle has the same dimensions as a brick,
                                        //keep it simple
&& (ball.posX<player.PosX  + 1.50)){
//collision detected, change the Y trajectory of the ball, and the direction on the x axis
ballTargetY = ballTargetY * -1f;
if(ballTargetX == -2f){
ballTargetX = 5f;
}else{
ballTargetX = -2f;
}
}

//check for collision with edge of the screeen, change the x axis trajectory on impact
if(ball.posX< 0 || ball.posX + .25f>3.75f)
{
ballTargetX = ballTargetX * -1f;

}

}

15.4 碰撞时损坏物体,并清除损坏的物体

问题

游戏不会在碰撞后“损坏”物体。同样,一旦物体被损坏,它们在游戏中仍然可见。

解决办法

使用一个类来跟踪对象损坏并移除被破坏的对象。

它是如何工作的

在配方 15.2 中,设置了一个isDestroyed标志来表示该物体与另一个物体发生了碰撞,应该被摧毁,从而将其从游戏中移除。这是跟踪对象是否被破坏的一种方式。但是,如果你想创建一个系统,让一个物体在被摧毁之前可以被击中(碰撞)多次呢?

修改 objects 类。引用清单 15-1 中的SBGEnemy()类来包含一个damageCounter

public class SBGEnemy {

public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;
public int damageCounter = 0;
...
}

现在,碰撞时伤害计数器加 1。如果计数器达到预定阈值,则设置isDestroyed标志。

private void detectCollisions(){

...

//collision detected
character.damageCounter += 1;
if(character.damageCounter == 3){
character.isDestroyed = true;
}

...
} 

随着角色“被摧毁”,最后一步是从屏幕上删除它。最简单的方法就是不画它。在你的游戏循环中,在绘制之前测试一个角色或物体是否被破坏。

if(!character.isDestroyed){
character.draw(gl);
}

十六、得分

到目前为止,本书已经解决了许多与 Android 游戏开发相关的问题。从移动角色到碰撞检测,你的游戏应该进展顺利。然而,一个基本的问题还没有解决:你如何保持得分?

得分是大多数游戏不可或缺的一部分。没有分数,玩家将无法确定他们在游戏中的进展如何,也无法将他们的进展与其他玩家进行比较。从最早的电子游戏开始,分数一直是许多玩家吹嘘的中心。

在这一章中,我将介绍一些与游戏中记分相关的常见问题的解决方案。解决方案应该适用于大多数游戏类型。

16.1 给对象分配点值

问题

该游戏不会因为玩家破坏物品而给其评分。

解决办法

修改对象的类,为其分配分数。

它是如何工作的

这个解决方案包括给对象分配点值,并使用这些值作为玩家的分数。在游戏中给一个对象赋值非常容易,只需要修改对象的类。

在游戏中,物体可以是任何你想要赋值的东西。例如,敌人、易碎物品和屏幕上的目标(如关卡中的路点)都可以被分配一个分值,然后用于计算玩家的分数。

给敌人分配一个点值,修改敌人的职业增加一个名为pointValue的属性。在这个例子中,我们将修改在本书中多次使用的SBGEnemy()类,并给它赋值 3。

public class SBGEnemy {
public boolean isDead = false;

public int pointValue = 3;
...

}

同样的解决方案可以应用到你游戏中的所有职业,给每个职业分配一个分值,这个分值可以用于游戏的总分数。

这个解决方案的另一种实现方式是以毕业的形式。例如,我们可以使用一个类来创建多个对象,每个对象都有不同的点值。看一看下面的类。这个类取自我写的一个突破式游戏,其中一个砖块类被用来创建五种不同的砖块。

类是用 OpenGL ES 1 写的;然而,OpenGL ES 代码对于解决方案并不重要。如果你的游戏是在 OpenGL ES 2/3 中,不要担心,因为对这个类的修改不是特定于 OpenGL ES 版本的,可以很容易地跟随。

清单 16-1PBBrick()

public class PBBrick {

public float posY = 0f;
public float posX = 0f;
public float posT = 0f;

public boolean isDestroyed = false;

public int brickType = 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, .25f, 0.0f,
0.0f, .25f, 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 PBBrick(int type) {
brickType = type;

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

}

让我们修改这个类,以便为五种不同类型的砖块分配一个从 1 到 5 的不同分值。

清单 16-2PBBrick()修改

public class PBBrick {

public float posY = 0f;
public float posX = 0f;
public float posT = 0f;

public boolean isDestroyed = false;

public int brickType = 0;

public int pointValue = 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, .25f, 0.0f,
0.0f, .25f, 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 PBBrick(int type) {
brickType = type;

switch(type){
case 1:
pointValue = 1;
break;
case 2:
pointValue = 2;
break;
case 3:
pointValue = 3;
break;
case 4:
pointValue = 4;
break;
case 5:
pointValue = 5;
break;

}

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

...

}

在配方 16.2 中,你将获得分配给游戏对象的分值,并使用它们来创建玩家的分数。

16.2 添加并跟踪分数

问题

游戏不跟踪玩家的分数,即使每个对象都被分配了一个点值。

解决办法

使用游戏角色的职业来追踪整体分数。

它是如何工作的

在这个解决方案中,您将修改玩家角色的类来添加一个属性。该属性将用于跟踪玩家的总得分。修改玩家角色类后,您将修改碰撞检测方法,以便为新的分数属性指定正确的点值。

首先,修改玩家角色类,添加一个名为overallScore的新属性。

public class SuperBanditGuy {
public boolean isDead = false;

public int overallScore = 0;
...

}

在第十五章中,你创建了一个执行碰撞检测的方法。由于此解决方案假设奖励点数的基础是某种碰撞(例如,破坏一个对象),因此您将修改碰撞检测方法以在必要时指定点数。

清单 16-3detectCollisions()

private void detectCollisions(){
for (int y = 1; y <4; y ++){
if (playerFire[y].shotFired){
for (int x = 1; x <10; x++ ){ //assumes you have an array of 10 enemies
if(!enemies[x].isDestroyed){
if (((playerFire[y].posY>= enemies[x].posY
&& playerFire[y].posY<= enemies[x].posY + 1f )  ||
(playerFire[y].posY +.25f>= enemies[x].posY
&& playerFire[y].posY + .25f<= enemies[x].posY + 1f )) &&
((playerFire[y].posX>= enemies[x].posX
&& playerFire[y].posX<= enemies[x].posX + 1f) ||
(playerFire[y].posX + .25f>= enemies[x].posX
&& playerFire[y].posX + 25f<= enemies[x].posX + 1f ))){

//collision detected between enemy and a shot
goodguy.overallScore += enemies[x].pointValue;

}
}
}
}
}
}

如前所述,这个方法摘自《??》第十五章。这是一个跟踪十个不同敌人碰撞的基本方法。enemies[]数组是一个由SBGEnemy()类组成的数组。方法中的goodguy只是SuperBanditGuy()类的一个实例化。

使用这个解决方案,玩家角色的总分数将会在每次消灭一个敌人时增加。

16.3 将分数写入屏幕

问题

游戏不会在屏幕上显示玩家的分数。

解决办法

使用多个 OpenGL 形状和一个 sprite 表向用户显示分数。

它是如何工作的

若要跟踪乐谱,请在项目中添加一个新的 sprite 表单,其中包含用于显示乐谱的所有数字。在图 16-1 的中可以看到这个斜桅板。

9781430257646_Fig16-01.jpg

图 16-1 。分数位数 spritesheet

接下来,创建一个名为SBGScoreTile() 的新类。这个类将被用于在 OpenGL ES 1 和 OpenGL ES 2/3 中向屏幕显示一个纵向的分数块(参见清单 16-4 和清单 16-5 )。稍后,您将使用 sprite 工作表在图块上显示特定的分数数字。

清单 16-4SBGScoreTile() (OpenGL 是 1)

public class SBGScoreTile {

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

private float vertices[] = {
0.0f, 0.0f, 0.0f,
0.25f, 0.0f, 0.0f,
0.25f, 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 SBGScoreTile() {

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

}

清单 16-5SBGScoreTile() (OpenGL 是 2/3)

public class SBGScoreTile {

public float scoreX = 0;
public float scoreY = 0;

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 scoreX;" +
"uniform float scoreY;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x +
scoreX,TexCoordOut.y + scoreY));"+
"}";

private float texture[] = {
0f, 0f,
1f, 0f,
1f, 1f,
0f, 1f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int program;
private int positionHandle;
private int matrixHandle;

static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float vertices[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

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

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

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

}catch(Exception e){

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

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

}

public SBGScoreTile() {

ByteBuffer byteBuff = ByteBuffer.allocateDirect(
byteBuff.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuff.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

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

ByteBuffer indexBuffer = ByteBuffer.allocateDirect(
indexBuffer.order(ByteOrder.nativeOrder());
drawListBuffer = indexBuffer.asShortBuffer();
drawListBuffer.put(indices);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

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

public void draw(float[] matrix) {

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");
int fsScoreX = GLES20.glGetUniformLocation(mProgram, "scoreX");
int fsScoreY = GLES20.glGetUniformLocation(mProgram, "scoreY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsScoreX, scoreX);
GLES20.glUniform1f(fsScoreY, scoreY);
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);
}
}

绘制时,每个图块应默认为 0。这是在清单 16-6 中完成的,在 OpenGL ES 1 中对纹理矩阵中的坐标 0,0,0 执行glTranslatef(),并在 OpenGL ES 2/3 中间接将片段着色器的TexCoordOut.x设置为 0(关于 sprite sheets 如何工作的更详细信息,请参见第六章“加载 Sprite Sheet”)。

清单 16-6 。绘制图块(OpenGL ES 1)

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

清单 16-7 。绘制图块(OpenGL ES 2/3)

SBGScoreTile.scoreX = 0;
SBGScoreTile.scoreY = 0;

只需将精灵表推进到分数的正确数字。首先,创建一个switch...case语句来设置每个相应数字的 x 和 y 子画面坐标位置。

清单 16-8 。平铺switch语句

switch(SuperBanditGuy){
case 0:
x = 0;
y = 0;
break;
case 1:
x = 0;
y = .25;
break;
case 2:
x = 0;
y = .50;
break;
case 3:
x = 0;
y = .75;
break;

...

}

最后,使用在switch语句中设置的 x 和 y 坐标来显示正确的图块。

清单 16-9 。显示图块(OpenGL ES 1)

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

清单 16-10 。显示图块(OpenGL ES 2/3)

SBGScoreTile.scoreX = x;
SBGScoreTile.scoreY = y;

十七、计时

一些游戏,也许是你正在开发的游戏,是基于时间的。这可能意味着游戏中的特定挑战有一个需要完成的时间限制,或者整个游戏本身只能玩预定的时间。

本章的解决方案将帮助你在游戏中创建一个计时器。然后,您将使用该计时器写入屏幕,并在到期后退出游戏操作。游戏中追踪时间的关键组件是 Android 类CountDownTimer()CountDownTimer()是一个非常强大但易于实现的工具。

17.1 比赛中的赛道时间

问题

用户应该只有固定的时间来完成任务。

解决办法

在你的游戏中使用一个CountDownTimer() 来追踪时间的长短。

它是如何工作的

建立计时器的关键是实例化一个CountDownTimer()CountDownTimer()类是 Android 的核心类,不依赖于 OpenGL ES。这意味着无论你的游戏使用的是 OpenGL ES 1、2 还是 3,你都可以在你的游戏中轻松使用CountDownTimer()。因此,本章中的示例与 OpenGL ES 版本无关。

第一步是实例化该类。

newCountDownTimer(millisecondsInFuture, countDownInterval) {
}

CountDownTime()的构造函数有两个参数。第一个参数millisecondsInFuture,是计时器的总持续时间,单位为毫秒。如果你想让计时器持续 30 秒,你可以将millisecondsInFuture设置为 30000。

new CountDownTimer(30000, countDownInterval) {
}

第二个参数countDownInterval指定何时触发间隔或tick。假设你想要执行一个动作,比如更新一个屏幕或者定期检查一个游戏任务的进度。您可以将countDownInterval设置为小于millisecondsInFuture的值,如下所示:

new CountDownTimer(30000, 1000) {
}

这段代码设置了一个新的CountDownTimer(),它将在 30 秒后到期,并每 1 秒触发一次tick。然而,在计时器完成之前,还有一些编码工作要做。你需要覆盖两个方法(见清单 17-1 )。

清单 17-1CountDownTimer()

new CountDownTimer(30000,1000) {
@Override
public void onTick(long millisUntilFinished) {

//perform any interval-based calls here
}

@Override
public void onFinish() {

//perform any clean up or ending of tasks here
};
}

第一个方法onTick(),在每个countDownInterval到期后调用。您需要覆盖的第二个方法是onFinish()。在CountDownTimer()完全到期后,调用onFinish()方法。

最后,使用start()方法启动定时器。从CountDownTimer()调用start()方法来激活计时器并开始倒计时(见清单 17-2 )。

清单 17-2start()

newCountDownTimer(30000,1000) {
@Override
public void onTick(long millisUntilFinished) {

}

@Override
public void onFinish() {

};
}.start();

方法的一个很好的用途是将时间写到屏幕上。例如,使用第十六章中的解决方案,你可以用数字 0 到 9 建立一个精灵表。然而,它可以用来倒数时间,而不是记录分数(见清单 17-3 )。

清单 17-3 。显示时间

new CountDownTimer(30000,1000) {
@Override
public void onTick(long millisUntilFinished) {

switch(millisUntilFinished){
case(29000):
scoreTile.x = 0; //set the x and y to the location of the
scoreTile.y = 0; //correct sprite sheet image for the time digit
break;

...
}

}

@Override
public void onFinish() {

};
}.start();

17.2 当时间到期时停止动作

问题

当计时器到期时,游戏不会停止。

解决办法

当时间到时,使用CountDownTimer()onFinish()方法停止游戏。

它是如何工作的

要在时间到期时停止游戏,请使用onFinish()方法调用您的关闭例程。回头看看第四章中的食谱,你很可能有一个退出例程,可以从游戏菜单上的退出按钮调用。

当定时器到期时,你可以从onFinish()方法调用这个相同的例程(见清单 17-4 )。

清单 17-4 。游戏退出

new CountDownTimer(30000,1000) {
@Override
public void onTick(long millisUntilFinished) {

}

@Override
public void onFinish() {

gameView.exit(); //call the method that you established for exiting the game
};
}.start();

17.3 任务完成时停止定时器

问题

在玩家完成要求的任务后,游戏计时器继续运行。

解决办法

当玩家完成一个任务时,使用CountDownTimer()cancel()方法停止计时器。

它是如何工作的

您的游戏可能会被设置为要求玩家在给定的时间内完成一项任务或一系列任务。接下来的问题是,当任务完成时,如何停止计时器?

CountDownTimer包含一个cancel()方法,当您需要停止计时器时可以调用该方法。有效使用这个方法的关键是实例化一个CountDownTimer并限定它的范围,这样它就可以被游戏中的其他方法调用。清单 17-5 展示了如何实例化CountDownTimer(与配方 17.1 中的略有不同),然后使用cancel()停止计时器。

清单 17-5 。取消CountDownTimer

private CountDownTimer cdt;
...
cdt = new CountDownTimer(30000,1000) {
@Override
public void onTick(long millisUntilFinished) {

}

@Override
public void onFinish() {
//something bad happens to the player for failing
};
}.start();

... //rest of your game code

private checkTask(){
//this is a method that you create to check if the player
//has finished the required task
if(taskCompleted){
cdt.cancel();
}
}

posted @   绝不原创的飞龙  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示