安装 Android Studio
AndroidStudio 游戏开发指南(全)
原文:Android Studio Game Development
协议:CC BY-NC-SA 4.0
一、设置 Android Studio
欢迎来到安卓工作室游戏开发。这本书聚焦于在 Android Studio 中执行的游戏开发过程中的特定任务。在本章中,您将安装 Android Studio 和所需的 Java 开发工具包(JDK) 。本章结束时,你将拥有一个实用的 Android Studio 集成开发环境(IDE) ,你可以用它来开发令人惊叹的基于 Android 的游戏。开始吧!
安装 JDK
您要执行的第一步是下载并安装 JDK。因为 Android 应用——包括游戏——是用 Java 开发的,Android Studio 需要 JDK 来运行。JDK 包括很多 Java 工具,比如编译器( javac ) 、文档生成器( javadoc ) 、key tool ( keytool )。
注意虽然大部分安卓应用确实是完全用 Java 开发的,但是你可以通过使用安卓原生开发套件(NDK)用 C 或 C++来部分开发安卓应用和游戏。对于希望在不同版本之间共享一个公共库的开发人员来说,这是一个特别受欢迎的选择——例如,在为 Android 和 iPhone 开发同一款游戏时,您可以移植一个用两个系统都可以本地解释的公共语言编写的库。
在 www.oracle.com/technetwork/java/javase/downloads/index.html 的可以找到 JDK。图 1-1 说明了 JDK 下载页面。
图 1-1 。JDK 下载页面
选择下载 JDK 的选项,您将被引导至图 1-2 所示的页面。
图 1-2 。许可协议页面
接受许可协议后,下载 JDK 的链接将会激活。此时,您只需要下载 JDK,而不是演示或示例。和往常一样,确保你为你的系统下载了正确版本的 JDK,如果你在基于 32 位 Intel x86 的指令芯片组上运行 Linux,你下载 jdk- <版本> -linux-i586.tar.gz ,而如果你运行 64 位 Windows,你下载 jdk- <版本> -windows-x64.exe 。
下载了与您的系统兼容的 JDK 版本后,运行该文件并安装 JDK。我很幸运地简单执行了安装包并接受了所有的默认设置。
安装 JDK 后,您需要在特定系统上为 JDK 设置一个 path 环境变量。环境变量告诉应用在哪里可以找到 JDK。因为 Android Studio 依赖于 JDK,所以它需要知道在你的系统上哪里可以找到它。在这种情况下,变量需要命名为 JAVA_HOME 。
注意您的系统上可能已经有了 Java_HOME 环境变量,尤其是如果您过去使用过 JAVA、JAVA 软件开发工具包(SDK)或 JDK。然而,仔细检查也无妨。
在 Windows 10 上,通过按下 Win+Break 键来设置这个变量。在那里,选择高级系统设置环境变量。现在,创建一个名为 JAVA _ HOME的新环境变量,并设置为您的 JDK 文件夹的路径。
安装 JDK 后,您可以继续安装 Android Studio。
安装 Android Studio
安卓工作室下载可以在【http://developer.android.com/sdk/index.html】??【找到。图 1-3 说明了 Android Studio 下载页面。
图 1-3 。Android 开发者,Android Studio 下载页面
一旦你点击下载 Android Studio 按钮,你会看到一个条款和条件协议。接受条款和条件,激活下载链接,见图 1-4 。
图 1-4 。条款和条件页面
下载安装程序后,您可以将 Android Studio 安装到您的系统中。执行安装程序,按照提示安装 Android Studio。我非常幸运地接受了安装程序提供的所有默认选项。
这个安装程序设置 Android Studio IDE,并为环境的运行方式和位置设置默认选项。它还设置模拟器的默认大小。
注意虽然模拟器是一个很棒的工具,并且可以用于调试,但我发现使用 Android 手机(或其他 Android 设备)进行调试要容易得多,也快得多。模拟器加载运行应用的速度往往很慢。如果您正在开发商业风格的软件,这对您来说可能不是问题。然而,我发现对于游戏开发来说,模拟器运行速度太慢,GPU 模拟不够准确,无法运行完全模拟的游戏。因此,如果你有一个 Android 设备,将它置于开发者模式,并用于调试(将在本书后面讨论)。
既然已经安装了 Android Studio,是时候更新它了。
更新 安卓工作室
第一次打开 Android Studio。IDE 可能看起来不熟悉,但是我们现在不要担心它;您将在第三章中浏览 IDE。现在,让我们看看您可能已经收到的通知。
Android Studio 很可能会在 ide 的右侧弹出一个或多个通知。图 1-5 和 1-6 说明了这些通知。
图 1-5 。Android Studio 更新通知
图 1-6 。Android Studio SDK 更新通知
根据您下载和发布 Android Studio 新版本的时间,可能会有更新。Android Studio 更新的好处是它们相当无痛。
点击通知中的更新链接,开始图 1-5 所示的更新。点击这个链接会把你带回 Android 开发者网站,在那里所需的可执行文件会被自动下载,如图图 1-7 所示。
图 1-7 。Android 更新下载页面
一旦你的更新被下载,在你执行它之前,你必须关闭 Android Studio。如果没有,你会得到一个温和的提醒,如图 1-8 中的所示。
图 1-8 。关闭 Android Studio 的提醒
图 1-5 所示的这种更新是对 Android Studio 的整体更新。然而,图 1-6 中的更新略有不同:这是一次组件更新。此更新将更改您的 Android SDK。
注意只有在您的系统上安装了现有版本的 Android Studio 时,此步骤才适用。SDK 管理器虽然仍然是 Android Studio 的重要组成部分,但在最新版本的 Android Studio 中会略有不同。如果您安装了新版本的 Android Studio,请随意跳过这一部分。
通过单击通知中的更新链接,以与启动 Android Studio 更新相同的方式启动 SDK 更新。现在可以看出这两种类型的更新之间的差异。
Android Studio 提示您通过关闭 Android Studio 来完成更新,并允许它打开 Android SDK 管理器。Android SDK 管理器如图 1-9 所示。
图 1-9 。Android Studio,Android SDK 管理器
Android SDK 管理器跟踪和管理各种 Android SDK 的所有组件,这些组件适用于您的系统,并且已经安装在您的系统上。例如,如果您想了解您的应用在 Android Jelly Bean 下如何运行,您可以从这个屏幕安装 SDK。
现在,您只需点击标签为 Install Packages 的按钮来安装或更新推荐的组件。点击该按钮显示如图图 1-10 所示的许可协议窗口。
图 1-10 。许可受理窗口
接受许可协议并开始更新。更新完成后,重启 Android Studio。
应用所有更新后,您就可以开始探索 Android Studio IDE 了。在下一章中,你将会发现使 Android Studio 成为一个伟大的 IDE 的所有特性和工具。
二、创建新项目
在前一章中,您安装了 Android Studio。在这一章中,你将创建一个新的 Android 项目来突出 Android Studio 的一些特性。Android 项目是组成应用的所有文件的主要存储库。
第一次打开 Android Studio
如果这是你第一次打开 Android Studio,它首先会尝试更新一些功能组件。在 Android Studio IDE 打开之前,你可能最终会看到如图 2-1 所示的窗口。
图 2-1 。更新窗口
注意我知道 Android Studio 似乎正在进行大量更新。然而,直到现在,IDE 还没有运行,它需要确保您拥有自信运行所需的一切。
下载并安装所有更新后,你会看到 Android Studio 欢迎屏幕,如图 2-2 所示。
图 2-2 。Android Studio 欢迎屏幕
在下一节中,您将创建一个在 Android Studio 中运行的新项目。
创建新项目
Android Studio 欢迎屏幕为您提供了几个选项。在此窗口中,您可以创建一个新项目,打开或导入一个现有项目—从 Android Studio 或另一个兼容的 IDE,或者选择 Android Studio 的配置设置。让我们选择标记为开始一个新的 Android Studio 项目的选项。
选择该选项将打开新项目配置窗口,如图 2-3 所示。在此窗口中,您将输入项目的名称和位置。
图 2-3 。Android Studio 新项目配置窗口
在应用名称文本框中输入名称,将项目命名为 My Game 。接下来,在代表您的项目的公司域文本框中输入一个名称。Android Studio 试图通过使用您在公司域文本框中输入的内容来自动命名您的 Java 包。
如果要更改系统中保存项目的默认位置,可以在项目位置文本框中进行更改。点击下一步按钮,进入目标 Android 设备窗口。
目标 Android 设备窗口,如图 2-4 所示,让你选择应用运行的目标。如果您要创建 Android Wear 或 Android Auto 应用,请在此处选择。因为此项目将用于游戏开发,所以请选中“手机和平板电脑”复选框。
图 2-4 。目标 Android 设备窗口
最低 SDK 下拉列表配置应用运行的最低操作系统级别。通过项目的配置,Android 允许你瞄准特定的设备。这让您可以在开发过程中预测可以使用什么操作系统级别的工具集来创建您的应用。
Android Studio 为您显示每个目标 SDK 的安装统计数据,并自动为您选择安装基数最大的最小 SDK。截至本书写作之时,那个 SDK 还是果冻豆。
注意虽然棉花糖在图 2-4 中是可选的,但是你可以看到一个只在这个 SDK 上运行的项目只能在当前安装基础的不到 1%上运行。
单击“下一步”进入“将活动添加到移动设备”窗口。
在图 2-5 所示的“添加活动到手机”窗口中,你选择你希望 Android 默认为你创建的活动类型。为了创建游戏项目,请选择“不添加活动”,然后单击“完成”。
图 2-5 。向移动窗口添加活动
当 Android Studio 编译完您选择的选项后,Android Studio IDE 将会打开,如图图 2-6 所示。
图 2-6 。Android Studio IDE
在下一章中,您将浏览 IDE 的特性,并学习如何识别用于开发的窗口。
三、探索 IDE
在本章中,你将探索 Android Studio IDE 接口。Android Studio 中的许多工具和功能有助于简化 Android 应用的开发过程。本章包括以下内容:
Android Studio 的布局
IntelliJ
断点
请记住,Android Studio 是一个巨大的、功能齐全的 IDE,我不可能在这本迷你书中涵盖它所有令人惊叹的功能。然而,在本章结束时,你会对 Android Studio 的主要特性足够熟悉,从而使在 Android 平台上开发游戏的过程变得更加容易。
注意如果你熟悉任何其他 ide,比如 Eclipse、NetBeans 或 Visual Studio,本章将帮助你把你以前的经验用在 Android Studio 中。
打开 Android Studio 时,你会看到一个弹出的每日提示窗口,如图 3-1 所示。许多人会立即取消选中“启动时显示提示”复选框,但你可以通过在打开 Android Studio 时快速浏览这个弹出窗口来学习一些有用的技巧。我建议让它开着,至少开一小会儿。
图 3-1 。弹出每日提示
现在,我们来看看 Android Studio 及其特性是如何布局的。
Android Studio Windows
Android Studio 布局在一系列窗口中。这些窗口包含开发应用所需的工具和功能。打开 Android Studio,你会看到一个类似于图 3-2 的界面。
图 3-2 。Android Studio 界面
你在图 3-2 中看到的是你在 Android Studio 中开发时会用到的三个主要窗口中的两个:项目窗口和代码编辑器。
项目窗口
图 3-3 中的所示的项目窗口 为您列出了所有存在的项目及其各自的文件。这为您提供了一种在项目中导航的简单方法。
图 3-3 。项目窗口
注意所有的 Android 项目,不管最终的应用是什么,都应该有相同的文件结构。您的类位于 src 文件夹中,您的 XML 和资源位于 main 文件夹中,您引用的外部库位于 libs 文件夹中。
要添加一个新文件——不管是类、图像还是其他什么——你可以右键单击项目窗口的适当文件夹并选择要添加的新的 <文件类型>,或者你可以从 Android Studio 外部拖动一个现有的文件并将其放入项目窗口中所需的文件夹。
通过双击项目窗口中的文件,Android Studio 将尝试在其相应的编辑器窗口中打开该文件。例如,类和其他代码文件将在代码编辑器中打开,布局文件将在布局编辑器中打开,图像将在图像查看器中打开。
代码编辑器
代码编辑器 是你使用 Android Studio 进行大量工作的地方。这个窗口是执行所有类和 XML 开发的地方。图 3-4 说明了代码编辑器。
图 3-4 。Android Studio 代码编辑器
该窗口中的代码用颜色突出显示,以便于阅读。许多开发者对这个窗口做的一个改变是把它变暗。许多开发人员往往会因为长时间查看白色背景上的默认彩色文本而感到头痛和眼疲劳。将代码编辑器背景更改为黑色会有所帮助。
Android Studio 提供了一个编辑器主题来帮助开发者改变这个窗口的外观。从“文件”菜单中,选择“设置”。这将打开如图图 3-5 所示的设置窗口。
图 3-5 。设置窗口
从这里,选择编辑器和颜色&字体。“方案”下拉列表允许您选择 Darcula 主题。这会使代码编辑器背景变暗,如图 3-6 中的所示。
图 3-6 。Darcula 主题后的代码编辑器
在本章的后面,当我探索 IntelliJ 时,我会介绍更多关于代码窗口的编辑器。
布局编辑器
布局编辑器 是一个强大的图形工具,允许您创建和布局您的 Android 应用屏幕。虽然如果你只是专注于游戏开发,你可能不会经常使用这个编辑器,但是你仍然应该熟悉它的功能。图 3-7 说明了布局编辑器。
图 3-7 。版面编辑
在布局编辑器中,您可以将小部件拖放到不同 Android 设备的模型上。这使您可以直观地展示应用的设计,几乎可以立即看到它在成品设备上的外观。
然而,为了进一步调整,您总是可以通过选择编辑器底部的 text 选项卡将编辑切换到 XML 文本视图。(如前面的图 3-7 所示,文本选项卡在设计选项卡旁边。)
现在您已经找到了 Android Studio 中使用的两个主要编辑器,是时候探索它最强大的特性之一了:IntelliJ 集成。
IntelliJ
IntelliJ ,或 IntelliJ IDEA,是由 JetBrains 开发的 Java IDEA 。由于其强大的特性集,它已经成为 Java 开发的事实上的 IDE。Android Studio 基于 IntelliJ IDEA 的开源社区版。这意味着许多使 IntelliJ IDEA 成为 Java 开发的非凡 IDE 的特性也使 Android Studio 成为 Android 的 Java 开发的非凡 IDE。
让我们回到代码编辑器,看看 Android Studio 和 IntelliJ 是如何处理代码生成的。
代码生成
IntelliJ 通常有很多特性,它也有一整套工具来帮助代码生成。接下来的章节只涵盖了你在游戏开发过程中最有可能经常遇到的问题。
注要获得 IntelliJ 提供的功能的完整列表,请访问 www.jetbrains.com/idea/help/intellij-idea.html 的 ??。
Getters 和 Setters
当用 Java 或者其他语言创建属性时,不断地创建 getters 和 setters 会很乏味。IntelliJ 为你简化了这个过程。例如,在编辑器中编写以下代码:
private String myProperty;
将光标放在 myProperty 旁边,然后按 Alt+Insert。这将打开一个 IntelliJ 上下文窗口。在这个窗口中,您可以选择 getter 和 setter,Android Studio 会自动构建您合适的 Java getter 和 setter 代码:
public String getMyProperty() {
return myProperty;
}
public void setMyproperty(String myProperty) {
this.myProperty = myProperty;
}
自动完成
自动完成是 IntelliJ 在 Android 开发中最常用的特性之一。假设我们创建了一个函数,如下所示:
private void DoSomething(String someValue){
setMyProperty();
}
这个简单的函数接受字符串变量 someValue 。注意,该函数调用了 setMyProperty() 。我们知道 setMyProperty() ,它是 IntelliJ 为我们创建的 setter,接受一个字符串值。将光标放在 setMyProperty() 的括号内,按 Ctrl+Alt+Space。这将打开 IntelliJ 自动完成窗口,如图 3-8 所示。
图 3-8 。IntelliJ 自动完成窗口
关于这个窗口值得注意的一点是,它不仅列出了可以传递到 setMyProperty() 中的可用字符串值,而且还按照它认为您最有可能使用的值对它们进行了排序。在这种情况下,首先显示的是 some value——传递给我们函数的值。
断点
在本书的后面,我将带你调试你的游戏。然而,让我们花一点时间来讨论断点。断点 就像你代码中的书签,告诉 Android Studio 在你调试的时候你想在哪里暂停执行。
若要放置断点,请在代码编辑器的右边空白处,单击要暂停代码执行的行旁边。设置的断点如图图 3-9 所示。
图 3-9 。设置断点
Android Studio 中断点的一个很棒的地方就是你可以用条件来设置它们。如果你右击你的断点,你会得到一个可以展开的上下文菜单,如图 3-10 所示。
图 3-10 。断点上下文菜单
在此窗口中,您可以设置断点的条件。例如,如果您有一个名为 myInteger 的整数值,并在其上设置了断点,那么您可以设置一个条件,仅当 myInteger 的值大于 100 时才中断。任何可以评估为真或假的条件都可以用作断点条件。
在我们 Android Studio 之旅的最后一部分,第四章以设置 GitHub 作为你的版本控制系统结束。
四、作为您的 VCS 的 GitHub
在本章中,您将在 Android Studio 中设置一个版本控制系统(VCS) 。这将是接近游戏设计概念之前的最后一个设置步骤。
那么什么是版本控制系统呢?在最基本的层面上,VCS 是一个存储库,用于存储不同的版本,或者保存您的代码变更。例如,当您在计算机上处理一个 Word 文档时,您对该文档所做的任何更改都会覆盖您系统上该文档的任何先前版本;只留给您最近的一组更改。在软件开发中,这是一个不太理想的结果。很多时候,在你意识到有一个更好的方法来基于你在保存你的改变之前所拥有的东西做一些事情之前,你可能已经进行了几天的改变。
VCS 可以让您返回并访问您以前保存的任何内容。然而,这并不是好的 VCS 的唯一伟大特征。VCS 的另一个特点是它允许你在所有的项目上进行合作。朋友、同事和公众信任的成员可以被允许查看甚至分支你的基本代码的变更。这使得创建和使用软件的过程成为一种共享的体验。如果您不希望其他人查看或更改您的代码,您只需使用一个私有存储库——一个只有您可以访问的存储库。
虽然有许多版本控制系统可供您使用,但我们在本书中重点介绍的是 GitHub。
要使用 GitHub,你首先需要的是 Git。Git 是 GitHub 给你的版本控制库。Git 可以从 http://git-scm.com/download 的 下载安装。Git 安装向导如图 4-1 所示。
图 4-1 。Git 安装向导
虽然你通常可以接受所有的默认设置,但如果你运行的是基于 Windows 的系统,我会建议你从 Windows 命令提示符选项中选择使用 Git,如图 4-2 所示。
图 4-2 。从 Windows 命令提示符选项中选择使用 Git
一旦 Git 安装在您的系统上,您就可以设置一个 GitHub 帐户。
设置 GitHub 帐户
在您可以将 GitHub 添加为您的版本控制系统之前,您必须在 http://github.com 创建一个帐户。账户创建界面位于 GitHub 的主页上,如图 4-3 所示。
图 4-3 。 GitHub 的账户创建页面
一旦您创建了您的帐户,您必须指定一个计划。计划从免费到每月 50 美元不等。这两个计划的主要区别在于您获得的私有存储库的数量。GitHub 的免费版本不允许你使用私有库。计划选择页面如图 4-4 所示。
图 4-4 。GitHub 计划选择页面
一旦您选择了您的计划,请单击页面底部的“完成注册”按钮。这就是设置 GitHub 的全部内容。现在我们来设置 Android Studio 端。
在 Android Studio 中设置 VCS
在 Android Studio 中将 GitHub 设置为你的 VCS 应该是一个相当轻松的过程。首先,点击文件菜单,进入设置。在设置菜单中,展开版本控制并选择 GitHub,如图 4-5 所示。
图 4-5 。在版本控制设置窗口选择 GitHub
在窗口的右侧,系统会提示您输入在上一节中创建的 GitHub 帐户信息。将主机保留为默认设置—github.com。提供您的登录名和密码,然后单击“应用”。
鉴于这应该是你第一次在 Android Studio 中添加带密码的东西,你应该会收到一个弹出窗口,要求你设置主密码——如下图 4-6 所示。此主密码用于存储您所有帐户密码的密码数据库。我建议不要把这个密码设置成和你在 GitHub 上使用的密码一样。
图 4-6 。主密码弹出
设置好您的主密码后,您可以单击“确定”来完成该过程。
注意 GitHub 可能会要求您在添加任何存储库之前验证您的电子邮件地址。
在 GitHub 上分享项目
现在 GitHub 已经配置好了,需要启用 Git 来允许您使用 GitHub 共享您的 Git。点击 Android Studio 菜单栏中的 VCS 菜单项,选择启用版本控制集成,如图所示图 4-7 。
图 4-7 。启用版本控制集成
这将打开启用版本控制集成弹出窗口,如图图 4-8 所示。从该弹出窗口的下拉列表中选择 Git。
图 4-8 。启用版本控制集成弹出窗口
注意如果设置 Git 后,你收到 Android Studio 找不到 git.exe 的错误通知,不要害怕。单击标记为修复它的链接。这将打开 Git 的设置。从这里你可以将 Android Studio 指向你的 git.exe 的位置,如果你接受默认的安装,它应该是 Program Files\Git\bin。一旦您将 Android Studio 指向您的 git.exe,您必须按照步骤再次启用版本控制集成。
现在您可以将您的第一个项目保存到 GitHub。为此,从 Android Studio 菜单栏中选择 VCS,然后点击 GitHub 上的导入到版本控制共享项目,如图 4-9 中的所示。
图 4-9 。在 GitHub 上分享项目
GitHub 现在会要求你命名你的库并提供一个简短的描述。如果您要公开这个存储库(默认),请尝试将其命名为其他人能够识别的名称。图 4-10 中显示了一个例子。
图 4-10 。命名 GitHub 存储库
在您命名了您的存储库之后,系统会提示您希望添加项目中的哪些文件。这通常是所有的文件,但是如果你的任何文件中有任何敏感信息,在将这些文件包含在任何公共存储库中之前,请记住这一点。
现在,如果您检查您的 GitHub 配置文件,您应该会看到您的新存储库。包含本书代码的 GitHub 位于github.com/jfdimarzio/AndroidStudioGameDev。
在下一章,你将学习游戏开发的概念。
五、游戏开发简介
在 Android 上开发游戏有利有弊。在开始之前,你应该意识到这些利弊。第一,安卓游戏是用 Java 开发的,但不是全 Java。您在过去的 Java 开发中可能用过的许多包都包含在 Android SDK 中;但是一些对游戏开发者,尤其是 3D 游戏开发者有帮助的包并不包括在内。因此,并不是所有你以前用来制作游戏的包都可以在 Android 中使用。当然,这是假设你已经在某种程度上尝试过 Java 游戏开发。如果这是你第一次接触任何语言的游戏开发概念,你可能会有优势,因为你不会有那种依赖甚至可能不适用于 Android 的包的精神支柱。
随着每个新的 Android SDKs 的发布,越来越多的包变得可用。您将需要知道您必须使用哪些包;我们将涉及这些,因为它们与章节中的主题相关。
另一个支持者和另一个反对者关注 Android 的熟悉度和它的缺乏力量。Android 在编程的熟悉性和易用性方面提供了什么,它可能在速度和功能方面有所欠缺。大多数视频游戏,如为 PC 或控制台编写的游戏,都是用低级语言开发的,如 C 语言,甚至(部分)汇编语言。这使得开发人员能够最大程度地控制处理器如何执行代码以及代码运行的环境。处理器说的是非常低级的代码,你越接近处理器的母语,你需要跳过的解释器就越少。Android 虽然提供了有限的底层编码能力,但它通过自己的执行系统解释和线程化 Java 代码。这使得开发者对游戏运行环境的控制更少。
这本书不会带你经历游戏开发的低级方法。Java ,尤其是为一般 Android 开发而呈现的,广为人知,易于使用,并且可以自己创建一些有趣、有益的游戏。
本质上,如果你已经是一个经验丰富的 Java 开发人员,那么你的技能在应用到 Android 上时不会在翻译中丢失。如果您还不是经验丰富的 Java 开发人员,不要害怕。Java 是一种很好的开始学习的语言。
既然你已经了解了 Android 开发的利弊,那么让我们来讨论游戏开发的第一个基本概念:游戏引擎。
游戏引擎
每个视频游戏的核心都是游戏引擎 ,而那个游戏引擎的一部分就是游戏循环。顾名思义,游戏引擎是为游戏提供动力的代码,而游戏循环是游戏引擎中的一段代码,它重复运行并执行组成游戏的所有功能。
每一款游戏,无论是什么类型的游戏——无论是角色扮演游戏(RPG) ,第一人称射击游戏,还是平台游戏——都需要一个功能齐全的游戏引擎和游戏循环来运行。游戏引擎通常在自己的线程上运行,这使得它可以访问尽可能多的资源。游戏循环通常从应用的主活动开始执行。
游戏引擎处理游戏执行的所有繁重工作:从播放音效和背景音乐到在屏幕上渲染图形。以下是典型游戏引擎执行的部分功能列表:
图形渲染
动画
声音
冲突检出
人工智能
物理学(非碰撞)
线程和内存
建立关系网
命令解释程序
所有游戏都在一个代码循环中执行。这个循环执行得越快,游戏运行得就越好,对玩家的反应就越快,屏幕上的动作就越流畅。
游戏循环
构建和绘制屏幕、移动游戏对象、计算分数、检测碰撞以及验证或无效项目所需的所有代码都在游戏循环中执行。
典型的游戏循环可以执行以下操作:
解释输入设备的命令
跟踪人物和/或背景,以确保没有人移动到他们不应该移动的地方
测试对象之间的碰撞
根据需要移动背景
绘制背景
画任意数量的固定物品
计算任何移动物体的物理性质
移动任何重新放置的武器/子弹/物品
拔出武器/子弹/物品
独立移动角色
画人物
播放音效
剥离连续背景音乐的线程
追踪玩家的分数
跟踪和管理联网或多个玩家
这可能不是一个全面的列表,但它是一个相当好的列表,列出了游戏循环中预期要做的所有事情。
提炼和优化你所有的游戏代码是很重要的。你的循环越优化,它执行所有调用的速度就越快,给你最好的游戏体验。
下一章将介绍 OpenGL ES,它对你的游戏意味着什么,以及它的作用。
六、OpenGL ES 和多边形
OpenGL for Embedded Systems(OpenGL ES)是一个开源图形 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 through Marshmallow 开发者可以使用强大的 OpenGL ES 2 进行游戏开发。
那么 OpenGL ES 到底为你做了什么,又是怎么做到的呢?让我们找出答案。
了解 OpenGL ES 如何与 Android 一起工作
Open GL ES 与图形硬件的通信方式比核心 Android 调用更直接。这意味着您将数据直接发送到负责处理数据的硬件。核心 Android 调用在到达图形硬件之前必须通过核心 Android 进程、线程和解释器。为 Android 平台编写的游戏只能通过直接与图形处理单元(GPU) 通信来实现可接受的速度和可玩性。
当前版本的 Android 能够使用 OpenGL ES 1 或 OpenGL ES 2 调用,有些能够使用 OpenGL ES 3。不同版本之间有很大的差异,你使用哪个版本在决定谁可以运行你的游戏方面起着重要作用。
虽然 Android 从 4.3 版本开始就支持 OpenGL ES 3 ,但是实现能够使用 OpenGL ES 3 的特定硬件取决于手机制造商。截至本书撰写之时,只有这些手机具有与 OpenGL ES 3 兼容的硬件:
Nexus 7 (2013 年)
Nexus 4
网络 5
Nexus 10
HTC 蝴蝶 S
HTC One/One Max
LG G2
LG G Pad 8.3
三星 Galaxy S4(骁龙版)
三星 Galaxy S5
三星 Galaxy 注 3
三星 Galaxy Note 10.1 (2014 版)
索尼体验 m
索尼 Z/ZL 体验
索尼 Z1 体验
索尼超级 z 体验
索尼体验平板电脑 z
本书中的代码,以及从本书的 GitHub 资源库中获得的代码,利用 OpenGL ES 2 在硬件方面为您提供了最广泛的应用。
决定哪些手机可以运行哪些版本的 OpenGL 的硬件差异在于 GPU。较新的设备具有使用着色器的 GPU。着色器仍然是一种专门的硬件,但它比它的前身固定函数流水线灵活得多。OpenGL ES 2 和 3 通过使用一种称为 OpenGL 着色语言(GLSL) 的编程语言来与着色器一起工作,以执行任意数量的可编程任务。
使用着色器
一个着色器 是一个软件实现的助手,用着色器语言编写,执行以前由固定功能硬件处理的所有功能。OpenGL ES 2 与两种类型的着色器一起工作:顶点着色器和片段着色器。
顶点着色器
一个顶点着色器 对顶点执行功能,如顶点的颜色、位置和纹理的变换。着色器在传递给它的每个顶点上运行。这意味着,如果你有一个由 256 个顶点组成的形状,顶点着色器将在每个顶点上运行。
顶点可大可小。然而,在所有情况下,顶点都由许多像素组成。顶点着色器以相同的方式处理单个顶点中的所有像素。单个顶点内的所有像素被视为单个实体。当顶点着色器完成时,它将顶点向下游传递到光栅化器,然后传递到片段着色器。
片段着色器
顶点着色器处理整个顶点的数据,片段着色器——有时也称为像素着色器——处理每个像素。片段着色器对光照、阴影、雾、颜色和其他影响顶点中单个像素的事物进行计算。渐变和光照的处理是在像素级别上执行的,因为它们可以跨顶点不同地应用。
设置你的游戏循环
前一章简要讨论了游戏循环的作用。是时候建立一个了。
到本章结束时,你的项目中应该有四个新的类: MainActivity 、 GameView 、 GameRenderer 和 Starfield 。
注意如果由于在第二章中创建了您的项目,您的项目还没有一个主活动,继续添加一个(也可以从本书的 GitHub 的github.com/jfdimarzio/AndroidStudioGameDev中获得)。
将 MainActivity 设置成这样:
public class MainActivity extends AppCompatActivity {
private GameView myGameView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myGameView = new GameView(this);
setContentView(myGameView);
}
@Override
protected void onPause() {
super.onPause();
// The following call pauses the rendering thread.
// If your OpenGL application is memory intensive,
// you should consider de-allocating objects that
// consume significant memory here.
myGameView.onPause();
}
@Override
protected void onResume() {
super.onResume();
// The following call resumes a paused rendering thread.
// If you de-allocated graphic objects for onPause()
// this is a good place to re-allocate them.
myGameView.onResume();
}
}
游戏视图 是游戏视图类的实例化,你现在需要创建它。在 Android Studio 的项目视图中右键单击包名。通常,您的包名类似于 com。你的名字。<项目名称> 。目前,您应该只看到您的包下面列出的 MainActivity 。右键单击后,选择 New Java Class。将新类命名为游戏视图。
游戏视图类应该扩展 GlSurfaceViewT4,如下所示:
public class GameView extends GLSurfaceView {
private final GameRenderer gameRenderer;
public GameView(Context context) {
super(context);
setEGLContextClientVersion(2);
gameRenderer = new GameRenderer(context);
setRenderer(gameRenderer);
}
}
需要注意的是 GameRenderer 是你需要创建的另一个类(很快)。它实现了一个作为游戏循环的 OpenGL ES 渲染器。使用 GLSurfaceViewer 查看 OpenGL ES 渲染器,这是 GameView 类的目的。
行 setEGLContextClientVersion(2)将 OpenGL ES 的版本设置为版本 2。
让我们创建渲染器。再次,在 Android Studio 的项目窗口中右键单击您的包,并选择 New Java Class。将这个类命名为 game render。
游戏玩家应该实现的 GLSurfaceView。渲染器,设置如下:
public class GameRenderer implements GLSurfaceView.Renderer {
private static final String TAG = "GameRenderer";
private Context context;
public static float[] mMVPMatrix = new float[16];
public static float[] mProjectionMatrix = new float[16];
public static float[] mViewMatrix = new float[16];
private Starfield starfield;
public GameRenderer(Context gameContext) {
context = gameContext;
}
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
starfield = new Starfield();
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
@Override
public void onDrawFrame(GL10 unused) {
float[] matrix = new float[16];
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
starfield.draw(mMVPMatrix);
}
public static int loadShader(int type, String shaderCode){
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
public static void checkGlError(String glOperation) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e(TAG, glOperation + ": glError " + error);
throw new RuntimeException(glOperation + ": glError " + error);
}
}
}
马上您可以看到这个类实例化了一个 Starfield 类,这是第四个也是最后一个需要设置的类。然而,在设置这个类之前,在游戏玩家中还有很多事情要做。
这个 onDrawFrame()就是你的游戏循环。OpenGL 在一个循环中调用 onDrawFrame() ,因此它可以作为所有矩阵转换的中心区域。这是在屏幕上创建、移动、销毁和测试碰撞的地方。
此时, onDrawFrame() 使用 glClear() 调用清空屏幕,并使用 matrix . setlookatm(mview matrix,0,0,0,-3,0f,0f,0f,0f,0f,1.0f,0.0f) 和 matrix . multiplymm(mMVPMatrix,0,mProjectionMatrix,0,mViewMatrix,0) ,
在你的包中添加一个新类,并将其命名为 Starfield 。该类应该按如下方式设置:
public class Starfield {
static float squareCoords[] = {
-1f, 1f, 0.0f, // top left
-1f, -1f, 0.0f, // bottom left
1f, -1f, 0.0f, // bottom right
1f, 1f, 0.0f }; // top right
private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };
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, vec2(TexCoordOut.x ,TexCoordOut.y));" +
"}";
private float texture[] = {
-1f, 1f,
-1f, -1f,
1f, -1f,
1f, 1f,
};
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final int mProgram;
private int mPositionHandle;
private int mColorHandle;
private int mMVPMatrixHandle;
static final int COORDS_PER_VERTEX = 3;
private final int vertexStride = COORDS_PER_VERTEX * 4;
public Starfield() {
ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
int vertexShader = GameRenderer.loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = GameRenderer.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);
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
GameRenderer.checkGlError("glGetUniformLocation");
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
GameRenderer.checkGlError("glUniformMatrix4fv");
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
Starfield 类创建一个占据整个屏幕的正方形。然而,现在这个广场只是一个空的框架。在下一章中,你将把一幅图像映射到这个正方形中,并创建一个滚动的星域。
七、加载图像和子图片
如果没有图像,安卓游戏就不会那么有趣了。我们都能记住标志性的视频游戏图像,如马里奥、《我的世界》的史蒂夫和军士长。在本章中,你将加载一个图像到你的游戏中,并把它作为纹理映射到你在前一章中创建的多边形上。
你正在处理的图像是一个星域,如图 7-1 所示。
图 7-1 。星域图像
将图像添加到项目中
在使用图像之前,您需要将其添加到项目中。将图像添加到项目时,有两件事必须确认。
首先,图像的大小需要是 2 的倍数,OpenGL ES 才能正常工作。这意味着你的形象也必须是方的。图 7-1 中的星域是一幅 512×512 的图像。我已经养成了以 512×512 的分辨率加载所有图片的习惯。OpenGL ES 可以处理任何符合这个参数的图像,所以 32×32 甚至 256×256 都是有效的。
第二,Android Studio 项目窗口需要在项目视图中,而不是 Android 视图中,以便能够通过使用拖放向您的项目添加图像。要确认这一点,请查看项目窗口的右上角;一个下拉控件默认指示 Android 或者 Project。还有其他选项,但是现在您只想确认您的窗口是否在项目视图中。
现在,您可以将图像添加到您的项目中。
展开你的项目树,打开资源文件夹 res ,如图图 7-2 所示。
图 7-2 。项目窗口中的 res 文件夹
resources 文件夹中应该有一个名为 drawable 的文件夹。如果该文件夹不存在,请随意创建。您所有的图像都将被放入 drawable 文件夹中。将您的图像从桌面拖放到此文件夹中。
图像在你的项目中之后,你可以把它作为一个纹理,加载到游戏中。
加载图像
在前一章中,你创建了星域级。这个类在当前状态下创建一个多边形。在本节中,您将向该类添加一个 loadTexture() 方法,以允许您的图像被加载到 Starfield 类中。然后将它映射到多边形上。
将以下方法添加到您的 Starfield 类中:
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();
}
这个类接受一个 int 纹理,它将是一个指向 res/drawable 文件夹中资源的指针。在本节的后面,您将从游戏循环中传递这个内容。
需要注意的一点是:在行中 gles 20 . gltexparameter f(gles 20。2D 纹理,GLES20。GL_TEXTURE_WRAP_S,GLES20。【GL _ REPEAT】, GL_REPEAT 使纹理(图像)在你映射到的多边形上移动时自我重复。把它想象成你在盒子外面移动的包装纸:当你向任何方向移动纸时,图像将继续重复。
为了防止图像重复,使用 GL_CLAMP_TO_EDGE 。然而,在这种情况下,您使用重复使星域看起来好像是无限滚动。
接下来,向类中添加一些变量来处理纹理操作:
private float texture[] = {
-1f, 1f,
-1f, -1f,
1f, -1f,
1f, 1f,
};
private int[] textures = new int[1];
private final FloatBuffer textureBuffer;
static final int COORDS_PER_TEXTURE = 2;
public static int textureStride = COORDS_PER_TEXTURE * 4;
Add a new texture buffer to the Starfield constructor.
bb = ByteBuffer.allocateDirect(texture.length * 4);
bb.order(ByteOrder.nativeOrder());
textureBuffer = bb.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
注意如果看起来令人困惑,这些类的完整文本包含在本节的末尾。
现在,编辑 Starfield 的 draw() 方法 ,将纹理绑定到多边形上。这里展示了整个 draw() 方法,因为操作的顺序很重要:
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");
GameRenderer.checkGlError("glGetUniformLocation");
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
GameRenderer.checkGlError("glUniformMatrix4fv");
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
在下一节中,您将添加使图像滚动所需的代码。
使图像滚动
需要做两个改变来允许你的图像滚动。第一个是对片段着色器,第二个是对 draw() 方法。
片段着色器是图像实际滚动的地方。将片段着色器更改为如下所示:
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));" +
"}";
注意,您已经添加了一个名为 scroll 的浮点变量,并且您正在将该变量的值添加到 TexCoordOut.y 中。这有效地沿 y 轴移动了图像。要沿着 x 轴滚动,您可以将滚动条中的值添加到 TextCoordOut.x 中。
随着着色器逻辑的完成,您需要一种方法来更改滚动值。这是在 draw() 方法中完成的:
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");
GameRenderer.checkGlError("glGetUniformLocation");
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
GameRenderer.checkGlError("glUniformMatrix4fv");
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
您已经向 draw() 方法添加了一个新的参数浮动滚动。使用 glGetUniformLocation(),可以访问着色器中的 scroll 变量,然后使用 glUniform1f() 为其赋一个新值。
最后一步是返回到 GameRenderer 类并调用 Starfield ,给它传递一个图像指针和滚动值。
图像指针应该从 onSurfaceCreated() 传入 loadTexture() 方法 :
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
starfield = new Starfield();
starfield.loadTexture(R.drawable.starfield, context);
}
onDrawFrame() 方法 现在可以调用 Starfield 的 draw 方法并传入变量进行滚动。请注意,因为您在不断地向 scroll 变量添加值,所以您必须对其进行测试,以确保您没有使 float 达到最大值并造成溢出:
@Override
public void onDrawFrame(GL10 unused) {
float[] matrix = new float[16];
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
starfield.draw(mMVPMatrix, starfieldScroll);
if(starfieldScroll == Float.MAX_VALUE){
starfieldScroll = 0;
}
starfieldScroll += .001;
}
完成的星域职业和玩家 应该如下图所示。如果你有 GitHub 的权限,你也可以从github.com/jfdimarzio/AndroidStudioGameDev下载。
public class GameRenderer implements GLSurfaceView.Renderer {
private static final String TAG = "GameRenderer";
private Context context;
public static float[] mMVPMatrix = new float[16];
public static float[] mProjectionMatrix = new float[16];
public static float[] mViewMatrix = new float[16];
private Starfield starfield;
float starfieldScroll = 0;
public GameRenderer(Context gameContext) {
context = gameContext;
}
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
starfield = new Starfield();
starfield.loadTexture(R.drawable.starfield, context);
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
@Override
public void onDrawFrame(GL10 unused) {
float[] matrix = new float[16];
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
starfield.draw(mMVPMatrix, starfieldScroll);
if(starfieldScroll == Float.MAX_VALUE){
starfieldScroll = 0;
}
starfieldScroll += .001;
}
public static int loadShader(int type, String shaderCode){
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
public static void checkGlError(String glOperation) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e(TAG, glOperation + ": glError " + error);
throw new RuntimeException(glOperation + ": glError " + error);
}
}
}
这是完成的星域级:
public class Starfield {
static float squareCoords[] = {
-1f, 1f, 0.0f, // top left
-1f, -1f, 0.0f, // bottom left
1f, -1f, 0.0f, // bottom right
1f, 1f, 0.0f }; // top right
private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };
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 ,TexCoordOut.y + scroll));" +
"}";
private float texture[] = {
-1f, 1f,
-1f, -1f,
1f, -1f,
1f, 1f,
};
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 mColorHandle;
private int mMVPMatrixHandle;
static final int COORDS_PER_TEXTURE = 2;
static final int COORDS_PER_VERTEX = 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 Starfield() {
ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
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(drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
int vertexShader = GameRenderer.loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = GameRenderer.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");
GameRenderer.checkGlError("glGetUniformLocation");
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
GameRenderer.checkGlError("glUniformMatrix4fv");
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
使用 Spritesheets
spritesheet 是一种特殊的图像,在一个位图中包含多个图像。图 7-3 显示了一个斜桅板。
图 7-3 。一个人物 spritesheet
使用 spritesheet 的好处是你可以放大聚焦到一个图像上,然后通过滚动到表单中的另一个图像来改变角色的外观。
这是另一个名为 Hero 的类,基于 Starfield 类,它使用一个 spritesheet 来显示一艘宇宙飞船。将这个职业添加到你的游戏中。这个类和 Starfield 的唯一区别是纹理的大小和移动纹理以显示正确图像的方式。
图 7-3 中的位图显示了一个能够显示 4×4 图像的 spritesheet。这意味着可以使用 x,y 坐标在矩阵上绘制每个图像左上角的位置,如下所示:(0,0)表示第一行中的第一个图像,(0,25)表示第一行中的第二个图像,(25,0)表示第二行中的第一个图像,依此类推。
在 onDrawFrame() 方法 中,您将在矩阵乘法中使用这些坐标来显示正确的图像:
public class Hero {
static float squareCoords[] = {
-0.25f, 0.25f, 0.0f, // top left
-0.25f, -0.25f, 0.0f, // bottom left
0.25f, -0.25f, 0.0f, // bottom right
0.25f, 0.25f, 0.0f }; // top right
private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };
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, 0.25f,
0f, 0f,
0.25f, 0f,
0.25f, 0.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_TEXTURE = 2;
static final int COORDS_PER_VERTEX = 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_LINEAR);
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_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
}
public Hero() {
ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
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(drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
int vertexShader = GameRenderer.loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = GameRenderer.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) {
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);
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");
GameRenderer.checkGlError("glGetUniformLocation");
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
GameRenderer.checkGlError("glUniformMatrix4fv");
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
GLES20.glDrawElements(GLES20.GL_TRIANGLES,
drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
将游戏页面修改成这样:
public class GameRenderer implements GLSurfaceView.Renderer {
private static final String TAG = "GameRenderer";
private Context context;
public static float[] mMVPMatrix = new float[16];
public static float[] mProjectionMatrix = new float[16];
public static float[] mViewMatrix = new float[16];
public static float[] mTranslationMatrix = new float[16];
private Starfield starfield;
private Hero hero;
float starfieldScroll = 0;
float heroSprite = 0;
public GameRenderer(Context gameContext) {
context = gameContext;
}
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
starfield = new Starfield();
hero = new Hero();
starfield.loadTexture(R.drawable.starfield, context);
hero.loadTexture(R.drawable.ships, context);
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
@Override
public void onDrawFrame(GL10 unused) {
float[] matrix = new float[16];
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
starfield.draw(mMVPMatrix, starfieldScroll);
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
Matrix.setIdentityM(mTranslationMatrix,0);
Matrix.translateM(mTranslationMatrix, 0,0,-.5f,0);
Matrix.multiplyMM(matrix, 0, mMVPMatrix, 0, mTranslationMatrix, 0);
hero.draw(matrix,0,0);
GLES20.glDisable(GLES20.GL_BLEND);
if(starfieldScroll == Float.MAX_VALUE){
starfieldScroll = 0;
}
starfieldScroll += .001;
}
public static int loadShader(int type, String shaderCode){
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
public static void checkGlError(String glOperation) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e(TAG, glOperation + ": glError " + error);
throw new RuntimeException(glOperation + ": glError " + error);
}
}
}
最后要注意的一点是:行 GLES20.glBlendFunc(GLES20。GL_SRC_ALPHA,GLES20。 onDrawFrame() 方法中的 GL_ONE_MINUS_SRC_ALPHA) 有助于透明度。spritesheet 有一个透明的背景,可以透过它看到星空。如果不使用混合功能,在这种情况下,spritesheet 的背景将呈现为黑色。
在下一章,你将测试玩家输入。
八、读取用户输入
如果你从未为移动设备或平板电脑编写过游戏代码,一个问题很快就会出现。不像游戏控制台,甚至台式机,明显缺乏输入选项来将玩家的意图反馈到游戏代码中。如果没有游戏控制器、键盘或鼠标,为玩家提供复杂的输入系统会很困难。
玩家的大部分输入将来自设备的触摸屏。连接游戏来检测和响应设备上的触摸事件并不像表面上看起来那么困难。
使用 onTouchEvent()
在你的游戏视图类中,覆盖 onTouchEvent() 如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
}
onTouchEvent() 接受一个运动事件。当事件调用生成时,这个运动事件由系统自动传入。
MotionEvent 包含了所有你需要的信息来帮助你判断和解读玩家的动作。从动作事件中,你可以获得诸如玩家触摸的 x 和 y 坐标,以及触摸的压力和持续时间等信息。你甚至可以决定滑动的方向。
在本例中,您将获得玩家的触摸坐标:
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
}
现在,您可以根据需要对 x 和 y 坐标做出反应。
如果您想要检测多个触摸点,请使用 getPointerCount()和 PointerCoords 来帮助检索用于检测多点触摸输入的指针对象。
传递给 onTouchEvent() 的 MotionEvent 可以跟踪多达五个不同的同时屏幕触摸。这里的概念是遍历所有使用 getPointerCount() 检测到的指针。在循环内部,您将使用 getPointerID()来检索每个指针所需的信息。
首先设置您的 onTouchEvent() 并遍历检测到的指针:
@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]);
}
}
现在,您可以从检测到的每个指针中获得所需的所有信息。
假设你正在创建一个平台游戏,玩家可以向左向右跑。您已经设置了您的 onTouchEvent() ,并且您正在捕捉玩家每次触摸屏幕时的 x 和 y 坐标。你怎么能轻易地确定这些坐标应该把玩家推向左边还是右边呢?
答案是将屏幕分成 个触摸区域——在这种情况下,一个区域在屏幕左侧,一个区域在右侧。几个简单的 if 语句可以用来检查玩家在屏幕上触摸的位置。
以平台游戏为例,玩家只能向左和向右移动,你可以把屏幕分成两半,一个代表左边,一个代表右边。你也可以考虑将触摸区放在屏幕底部,玩家的拇指可能会在那里。
这意味着您必须忽略落在左右触摸区上方的任何触摸坐标。看一看图 8-1 和图 8-2 中的图,了解这一概念的直观表示。
图 8-1 。肖像模式左右触摸区
图 8-2 。横向模式左右触摸区
第一步是获得屏幕的高度:
@Override
public boolean onTouchEvent(MotionEvent event) {
//get the non-touchable area of the screen -
//the upper 2/3rds of the screen
Point size = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(size);
int width = size.x;
int height = size.y / 3;
//the playable area is now the lower 3rd of the screen
int playableArea = size.y - height;
}
使用值 playableArea 作为 y 轴值,您可以很容易地判断您的玩家是否触摸了屏幕的正确部分。创建一个简单的 if 语句来测试玩家触摸坐标的位置:
@Override
public boolean onTouchEvent(MotionEvent event) {
//get the non-touchable area of the screen -
//the upper 2/3rds of the screen
Point size = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(size);
int width = size.x;
int height = size.y / 3;
//the playable area is now the lower 3rd of the screen
int playableArea = size.y - height;
if (y > playableArea){
//this y coordiate is within the touch zone
}
}
现在您知道玩家已经触摸了屏幕的正确区域,可以通过测试 x 坐标是大于还是小于屏幕的中心点来确定触摸区的左侧和右侧:
@Override
public boolean onTouchEvent(MotionEvent event) {
//get the non-touchable area of the screen -
//the upper 2/3rds of the screen
Point size = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(size);
//get the center point of the screen
int width = size.x / 2;
int height = size.y / 3;
//the playable area is now the lower 3rd of the screen
int playableArea = size.y - height;
if (y > playableArea){
//this y coordiate is within the touch zone
if(x < center){
//The player touched the left
}else{
//The player touched the right
}
}
}
您已成功确定玩家触摸了屏幕的左侧还是右侧。将注释替换为您的特定代码,以根据玩家触摸的位置启动操作。
对于一些游戏(比如《神庙逃亡》),你想让玩家滑动或投掷屏幕来指示他们想要移动的方向。例如,向上一扔可能代表一次跳跃。这可能是一种更加通用的玩家输入方法,但是也需要更多的设置代码。
添加手势监听器
打开你的 游戏视图并实例化一个简单的斯图利斯坦纳 :
GestureDetector.SimpleOnGestureListener gestureListener = new
GestureDetector.SimpleOnGestureListener(){
};
您需要在手势监听器中实现几个方法。然而,在这个解决方案中,您唯一要使用的是 OnFling() :
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;
}
};
现在,创建一个新变量:
private GestureDetector gd;
手势检测器将用于抛出手势事件。在活动的 onCreate() 中初始化检测器:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gd = new GestureDetector(this,gestureListener);
}
最后,在 OnTouchEvent() 中,抛出给的手势监听器:
@Override
public boolean onTouchEvent(MotionEvent event) {
return gd.onTouchEvent(event);
}
当玩家甩出屏幕时,会执行 OnFling() 中的代码。这就解决了“什么”和“什么时候”的问题现在你需要确定哪个方向。
注意 OnFling() 有两个运动事件属性。因为您之前使用过它,所以您知道运动事件包含一个 getX() 和一个 getY() 用于获取事件各自的坐标。
e1 和 e2 这两个事件代表了投掷的起点和终点。因此,使用每个事件的 x 和 y 坐标,可以计算出玩家向哪个方向移动。
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
}
现在你可以为你在游戏中需要采取的行动填入适当的代码。
在下一章中,你将会根据被检测到的触摸来移动屏幕上的图像。
九、游戏内的运动
在前一章中,你看到了几种检测玩家是否与屏幕交互的方法。在这一章中,你将让你的角色对那个动作做出反应。让我们再来看看来自第七章的英雄职业。
英雄级的是一艘宇宙飞船,玩家可以在屏幕上四处移动。在屏幕上移动图像将会比听起来更容易。当画出英雄 ?? 时,你需要做的就是在转换矩阵中增加(或减少)一个值。在这种情况下,您只更新了一个值,因为您将只在一个轴上移动船只。如果你在两个轴上移动船,你需要更新矩阵中的两个值。
让我们首先为名为 heroMove 的 float 的 GameRenderer 类添加一个 getter 和 setter。这个浮标跟踪船应该移动多少:
float heroMove = 0;
public void setHeroMove(float movement){
heroMove = movement;
}
public float getHeroMove(){
return heroMove;
}
现在,使用游戏视图中 onTouchEvent() 的 getter 和 setter 更新 heroMove float。对于这个例子,我选择了一个简单的 onTouchEvent() 。它只检测玩家触摸了屏幕的左侧或右侧:
public boolean onTouchEvent(MotionEvent e) {
float x = e.getX();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
if (x < getWidth() / 2) {
gameRenderer.setHeroMove(gameRenderer.getHeroMove() + .1f);
}
if (x > getWidth() /2){
gameRenderer.setHeroMove(gameRenderer.getHeroMove() - .1f);
}
}
return true;
}
}
每次玩家触摸屏幕时, heroMove float 都会更新。让我们通过修改游戏玩家中的变换矩阵来更新英雄的位置:
public void onDrawFrame(GL10 unused) {
float[] matrix = new float[16];
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
starfield.draw(mMVPMatrix, starfieldScroll);
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
//debris.draw(mMVPMatrix, debrisScroll);
Matrix.setIdentityM(mTranslationMatrix,0);
Matrix.translateM(mTranslationMatrix, 0,heroMove,-.5f,0);
Matrix.multiplyMM(matrix, 0, mMVPMatrix, 0, mTranslationMatrix, 0);
hero.draw(matrix,0,0);
GLES20.glDisable(GLES20.GL_BLEND);
if(starfieldScroll == Float.MAX_VALUE){
starfieldScroll = 0;
}
if(debrisScroll == Float.MAX_VALUE){
debrisScroll = 0;
}
starfieldScroll += .001;
debrisScroll += .01;
}
float heroMove 乘以平移矩阵,在 x 轴上移动图像。在这个例子中,y 轴上有一个硬编码值 -.5f ,用于将船只保持在屏幕底部。然而,如果你也想在 y 轴上移动船只,你可以简单地创建另一个浮动,并把它放在 -.5f 的位置。
在这本迷你书的最后一章,你会看到一些碰撞检测的解决方案。
十、碰撞检测
碰撞检测是几乎所有游戏和所有游戏类型的关键组件。在一个没有碰撞检测的游戏中,物品、障碍物、角色和武器会在屏幕上四处移动,彼此漂浮而过,不会产生任何后果。
您的游戏代码需要能够确定屏幕上的对象是否相互接触或交叉。只有在你确定两个或更多的物体接触后,你才能对它们执行动作,比如施加伤害、停止运动、启动角色或摧毁一个物体。
使用基本碰撞检测
如果您正在创建一个角色面临静态障碍物(如地板和平台、屏幕边缘或台阶)的游戏,则基本碰撞检测非常有用。测试静态对象的位置时,可以使用常数值。例如,可以使用基本碰撞检测来确定角色何时完成跳跃并回到地面。这段代码可以放在单独的跳转方法中,或者放在 onTouchEvent() 中的中:
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);
从屏幕边缘跑出来怎么办?如果您的游戏动作需要包含在一个屏幕中,并且 OpenGL ES 中的 x 轴已经被缩放到从 0(最左边)到 4(最右边)的范围内,您可以测试您的角色来阻止图像离开屏幕。
if(goodguy.posX <= 0 )
{
//the player has reached the left edge of the screen
//correct the image's position and perform whatever action is necessary
goodguy. posX = 0;
}
如果您要测试与屏幕右边缘的碰撞,这个过程需要一个额外的步骤。 OpenGL ES 中字符的 x 位置代表图像的左下角。因此,如果您正在测试字符的图像是否遇到了屏幕的右侧,则在整个图像已经离开屏幕之前,字符在左下角的 x 位置不会到达屏幕的右边缘。
您可以通过将角色图像的大小添加到测试碰撞的 if 语句中来对此进行补偿:
if(goodguy. posX +.25f >= 4 )
{
//the player has reached the right edge of the screen
//correct the image's position and perform whatever action is necessary
goodguy. posX = (4f - .25f);
}
碰撞检测的基本方法对于不太复杂的游戏逻辑是有效的,其中有许多静态对象,其大小和位置对于游戏循环来说是容易知道的。
如果你的游戏逻辑没有那么简单,你在处理多个移动的物品怎么办?
使用更强大的碰撞检测
为了实现一种更加健壮的碰撞检测形式,创建一个可以从你的游戏循环中调用的新方法。该方法将遍历屏幕上的所有活动项目,并确定是否有任何碰撞。
实现这种碰撞检测所需的关键字段是对象当前位置的 x 和 y 轴坐标,以及对象的状态。状态是指对象是否有资格被包括在碰撞检测中。这可以包括对象已经被破坏的标志,或者可能被测试的角色已经通电,允许他们在特定时间段内免于碰撞检测。
下面的代码描绘了一个版本的英雄职业,叫做敌人。该类中添加了三个公共值:x 和 y 轴坐标各一个,用于跟踪角色的当前位置,还有一个布尔值,用于指示角色是否已被破坏。
public class Enemy {
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 Enemy () {
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 + 0.25)和从 y 到(y + 0.25)的边界框。任何进入那个空间的东西都会与那个物体相撞。在本例中,要测试碰撞,只需检查另一个对象的边界框是否包含介于(x 到(x + 0.25))和(y 到(y + 0.25))之间的点。如果是这样,那两个物体相撞了。
在下面的代码示例中,正在发射的镜头有一个 0.25 坐标值的边界框,而敌人有一个 1 坐标值的边界框。
下面的代码假设英雄可以一次在屏幕上发射多达四发子弹,这意味着有可能有四个物体会被敌人击中:
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
}
}
}
}
}
这种方法在检测一轮射击和单个敌人之间的碰撞时效果很好。要测试一轮射击和众多敌人之间的碰撞,您需要稍微修改该方法,以循环通过您的敌人阵列:
private void detectCollisions(){
for (int y = 1; y < 4; y ++){
if (playerFire[y].shotFired){
//assumes you have an array of 10 enemies
for (int x = 1; x < 10; x++ ){
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
}
}
}
}
}
}
这种碰撞检测方法将帮助您测试游戏中多个对象的边界框之间的碰撞。
我希望你已经发现这本迷你书中的概念是有用的。虽然我们没能从头到尾涵盖完整的游戏开发过程,但是大量的信息已经被塞进了这些页面。你可以从本书的 GitHub 网站【https://github.com/jfdimarzio/AndroidStudioGameDev】随意下载代码。