Android-游戏开发入门指南-二-

177 阅读1小时+

Android 游戏开发入门指南(二)

原文:Beginning Android Games Development

协议:CC BY-NC-SA 4.0

八、测试和调试

  • 游戏测试的类型

  • 单元测试

  • 排除故障

  • Android Profiler

我们已经完成了项目的编程阶段;接下来,我们进行测试和调试。在这个阶段,我们必须找到代码中的所有错误和不一致之处。一个打磨过的游戏没有粗糙的边缘;我们需要测试它,调试它,并确保它不会占用计算资源。

游戏测试的类型

**功能测试。**一个游戏基本上就是一个 app。功能性测试 是测试一个应用的标准方式。它被称为 functional ,因为我们正在测试应用的功能(也称为功能),因为它们是在需求规格中指定的——需求规格是你(或游戏设计师)在游戏的规划阶段编写的。 这本来是要写在文档里的(通常称为功能需求说明书)。你可能在功能规范中发现的例子有“用户在进入游戏前必须登录到游戏服务器”和“用户可以选择或返回到已经完成的关卡;用户不能选择尚未完成的级别。测试人员,通常称为 QA 或 QC(分别是质量保证和质量控制的缩写),是执行这些测试的人。他们将创建测试资产,制定测试策略,执行它们,并最终报告执行的结果。失败的测试通常被分配给开发人员(您)来修复和重新提交。我在这里描述的是一个开发团队的典型实践,这个团队有一个单独的或者专门的测试团队;如果你是一个人的团队,QA 很可能也是你。测试是一种完全不同的技能;我强烈建议你寻求其他人的帮助,最好是那些有测试经验的人。

性能测试 。你可能从它的名字就能猜到这种测试是做什么的。它将游戏推向极限,并看到它在压力下的表现。这里你想看到的是游戏在高于正常水平的条件下是如何反应的。浸泡测试或 耐力测试 是一种性能测试;通常,你会让游戏在各种操作模式下运行很长一段时间,例如,在游戏暂停或出现标题屏幕时,让游戏运行很长一段时间。你在这里试图找到的是游戏如何响应这些条件,以及它如何利用系统资源,如内存、CPU、网络带宽等;您将使用类似于 Android Profiler 的工具来执行这些测量。

性能测试的另一种形式是 音量测试;如果您的游戏使用数据库,您可能想知道当数据加载到数据库时游戏将如何响应。你要检查的是系统在各种数据负载下的反应。

尖峰测试 或者可扩展性测试也是另一种性能测试。如果您的游戏依赖于中央服务器,该测试通常会增加连接到中央服务器的用户(设备端点)数量。你想要观察用户数量的激增如何影响玩家体验;游戏是否仍然响应迅速,是否对每秒帧数有影响,是否有滞后等等?

兼容性测试是检查游戏在不同设备和软硬件配置上的表现。这就是 AVDs (Android 虚拟设备)派上用场的地方;因为 avd 只是简单的软件仿真器,所以你不必购买不同的设备。尽可能使用 AVDs。有些游戏很难在模拟器上进行可靠的测试;当你处于这种情况下,你真的不得不为测试设备花钱。

**符合性或一致性测试 。这是你对照应用或游戏上的 Google Play 指南检查游戏的地方;请务必在 bit.ly/developerpo… 阅读 Google Play 的开发者政策中心。确保你也熟悉 PEGI(泛欧游戏信息)和 ESRB(娱乐软件评级委员会)。如果游戏中有不符合特定分级的不良内容,需要识别并报告。违规可能是拒绝的原因,这可能导致昂贵的返工和重新提交。

本地化测试 非常重要,尤其是当游戏面向全球市场时。游戏标题、内容和文本需要用支持的语言翻译和测试。

恢复测试。这将边缘案例测试带到了另一个高度。在这里,应用被迫失败,您将观察应用在失败时的行为以及失败后如何恢复。它会让你了解你是否写了足够多的 try-catch-finally 块。应用应该优雅地失败,而不是突然失败。只要有可能,运行时错误应该由 try-catch 块来保护;并且当异常发生时,尽量写日志,保存游戏状态。

**渗透或安全测试 。这种测试试图发现游戏的弱点。它模拟了潜在攻击者为了规避游戏的所有安全功能而进行的活动;例如,如果游戏使用数据库来存储数据,尤其是用户数据,则在 Wireshark 运行时,一支笔测试人员(进行渗透测试的专业人员)可能会从头到尾玩一遍游戏—Wireshark 是一种检查数据包的工具;这是一个网络协议分析器。如果您以明文形式存储密码,它会在这些测试中显示出来。

声音检测 。检查加载文件时是否有任何错误;此外,如果有破裂声或其他声音,请听听声音文件。

开发者测试 。这是你(程序员)在给游戏添加一层又一层代码时所做的测试。这包括编写测试代码(也用 Java)来测试你的实际程序。这就是所谓的单元测试。Android 开发者通常会进行 JVM 测试和仪器测试;我们将在接下来的章节中对此进行更多的讨论。****

****

单元测试

单元测试实际上是开发人员执行的功能测试,而不是 QA 或 QC。单元测试很简单;这是一个方法可能会做或产生的特定的东西。一个应用通常有许多单元测试,因为每个测试都是一组定义非常狭窄的行为。所以,你需要大量的测试来覆盖整个功能。Android 开发人员通常使用 JUnit 来编写单元测试。

JUnit 是由 Kent Beck 和 Erich Gamma 编写的回归测试框架;你可能记得他们分别是极限编程的创始人和四人帮(g of,设计模式)的成员。

Java 开发人员长期以来一直使用 JUnit 进行单元测试。Android Studio 是 JUnit 自带的,并且很好地集成在其中。我们不需要做太多的设置工作。我们只需要编写我们的测试。

JVM 测试与仪器测试

如果你观察任何一个 Android 应用,你会发现它有两个部分:一个基于 Java 的行为和一个基于 Android 的行为。

The Java part is where we code business logic, calculations, and data transformations. The Android part is where we actually interact with the Android platform. This is where we get input from users or show results to them. It makes perfect sense if we can test the Java-based behavior separate from the Android part because it’s much quicker to execute. Fortunately, this is already the way it’s done in Android Studio. When you create a project, Android Studio creates two separate folders—one for the JVM tests and another for the instrumented tests. Figure 8-1 shows the two test folders in Android view, and Figure 8-2 shows the same two folders in Project view.

![img/340874_4_En_8_Fig1_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c8a16f1ef7bd4b8592c4a69ad767cfb7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=PejbeVj6%2FWSUZKqoLfbnQSeGUZc%3D)
Figure 8-1

Android 视图中的 JVM 测试和插装测试

![img/340874_4_En_8_Fig2_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/990128deee3844808c5796a5e841d352~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=sWwgYA6VXHiSt95tG4%2BUbPKuZhY%3D)
Figure 8-2

项目视图中的 JVM 测试和插装测试

从图 8-1 或 8-2 中可以看出,Android Studio 为 JVM 和插装测试生成了样本测试文件。示例文件只是作为快速参考;它向我们展示了单元测试可能是什么样子。

简单的演示

To dive into this, create a project with an empty Activity. Create a class, then name it Factorial.java, and fill it up with the code shown in Listing 8-1.public class Factorial {public static double factorial(int arg) {if (arg == 0) {return 1.0;}else {return arg + factorial(arg - 1);}}}Listing 8-1

Factorial.java

Make sure that Factorial.java is open in the main editor, as shown in Figure 8-3; then, from the main menu bar, go to NavigateTest. Similarly, you can also create a test using the keyboard shortcut (Shift+Command+T on macOS and Ctrl+Shift+T for Linux and Windows).

![img/340874_4_En_8_Fig3_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1bfd478c2e814c47902a2c58c9dcd175~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=MMcog74Yis32uHioc7NFLtdnXbw%3D)
Figure 8-3

为 Factorial.java 创建一个测试

Right after you click “Test,” a pop-up dialog (Figure 8-4) will prompt you to click another link—click “Create New Test” as shown in Figure 8-4.

![img/340874_4_En_8_Fig4_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/42c240cacdcc4f2da900fdc98234e931~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=xkQk5YEpLp50Ozbr48KbiZ%2F5mRg%3D)
Figure 8-4

创建新的测试弹出窗口

Right after creating a new test, you’ll see another pop-up dialog, shown in Figure 8-5, which I’ve annotated. Please follow the annotations and instructions in Figure 8-5.

| -什么 | 您可以选择想要使用哪个测试库。您可以选择 JUnit 3、4 或 5。您甚至可以选择 Groovy JUnit、Spock 或 TestNG。我使用 JUnit4 是因为它是随 Android Studio 一起安装的。 | | ➋ | 命名测试类的约定是“要测试的类名”+“Test”。Android Studio 使用该约定填充该字段。 | | ➌ | 留空;我们不需要继承任何东西。 | | -你好 | 我们现在不需要 setUp() 和 tearDown() 例程,所以不要检查它们。 | | ➎ | 让我们检查一下 factorial() 方法,因为我们想为此生成一个测试。 |
![img/340874_4_En_8_Fig5_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/913345889dfb4f329b71b693edea9061~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=tvRW%2B6XOhIzhmIzbBPHytjhIQDY%3D)
Figure 8-5

创建因子测试

When you click the OK button, Android Studio will ask where you want to save the test file. This is a JVM test, so we want to save it in the “test” folder (not in androidTest). See Figure 8-6. Click “OK.”

![img/340874_4_En_8_Fig6_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ac209ee14bb24d20b7f08ec366df8c88~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=xGYKGWOHxsQMQu2zV16qlyr51R4%3D)
Figure 8-6

选择目的地目录

Android Studio will now create the test file for us. If you open FactorialTest.java, you’ll see the generated skeleton code—shown in Figure 8-7.

| -什么 | 文件*Factorial.java*被创建在*测试*文件夹下。 | | ➋ | 创建了一个 factorial() 方法,并将其注释为 @Test 。这就是 JUnit 知道这个方法是单元测试的方式。您可以在方法名前面加上“test”,例如 testFactorial(),但这不是必需的,有了 @Test 注释就足够了。 | | ➌ | 这是我们放置断言的地方。 |
![img/340874_4_En_8_Fig7_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b4c359ef2e594208a88619deae8b480d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=zJcmNk1t3mInONSQvZmPKk50a80%3D)
Figure 8-7

FactorialTest.java 在项目视图和主编辑器中

看到这有多简单了吗?就设置和配置而言,在 Android Studio 中创建一个测试用例实际上并不涉及我们太多。我们现在需要做的就是编写我们的测试。

实施测试

JUnit supplies several static methods that we can use in our test to make assertions about our code’s behavior. We use assertions to show an expected result which is our control data. It’s usually calculated independently and is known to be true or correct—that’s why you use it as a control data. When the expected data is returned from the assertion, the test passes; otherwise, the test fails. Table 8-1 shows the common assert methods you might need for your code.Table 8-1

常见断言方法

|

方法

|

描述

| | --- | --- | | assertEquals() | 如果两个对象或基元具有相同的值,则返回 true | | assertNotEquals() | assertEquals 的反义词() | | assertSame() | 如果两个引用指向同一个对象,则返回 true | | 断言紧急事件() | assertSame 的反向() | | assertTrue() | 测试布尔表达式 | | assertFalse() | assertTrue 的反函数() | | 断言 Null() | 测试空对象 | | 断言 NotNull() | assertNull 的反向() |

Now that we know a couple of assert methods, we’re ready to write some test. Listing 8-2 shows the code for FactorialTest.java.import org.junit.Test;import static org.junit.Assert.*;public class FactorialTest {@Testpublic void factorial() {assertEquals(1.0, Factorial.factorial(1),0.0);assertEquals(120.0, Factorial.factorial(5), 0.0);}}Listing 8-2

FactorialTest.java

我们的 FactorialTest 类只有一个方法,因为这只是为了举例说明。当然,真实世界的代码会有比这更多的方法。

注意,每个测试(方法)都由 @Test 注释。这就是 JUnit 如何知道 factorial() 是一个测试用例。还要注意的是, assertEquals() 是 Assert 类的一个方法,但是我们没有在这里写完全限定名,因为我们在 Assert 上有一个静态导入——这当然让事情变得更简单。

The assertEquals() method takes three parameters; they’re illustrated in Figure 8-8.

| -什么 | **期望值**是你的控制数据;这通常在测试中被硬编码。 | | ➋ | **实际值**是您的方法返回的值。如果预期值与实际值相同,则 assertEquals()通过——您的代码表现正常。 | | ➌ | **Delta** 意在反映*实际*和*预期*值能够有多接近并且仍然被认为是相等的。一些开发人员称这个参数为“模糊”因素。当期望值和实际值之间的差异大于“模糊因子”时,那么 assertEquals() 将会失败。我在这里使用 0.0 是因为我不想容忍任何形式的偏差。您可以使用其他值,如 0.001、0.002 等等;这取决于你的用例以及你的应用能够容忍多少模糊。 |
![img/340874_4_En_8_Fig8_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8035d385e49941cb8d5de7a117d003eb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=BLHsXpgWJo6bmoR%2BIn61Z620xRk%3D)
Figure 8-8

assertEquals 方法

现在,我们的代码完成了。如果你愿意,你可以在代码中插入更多的断言,这样你就可以更好地理解事物。

有几件事我没有包括在这个示例代码中。我没有重写 setUp() 和 tearDown() 方法,因为我不需要它。如果需要建立数据库连接、网络连接等等,通常会使用 setUp() 方法。使用 tearDown()方法关闭您在设置()中打开的任何东西。

现在,我们准备运行测试。

运行单元测试

You can run just one test or all the tests in the class. The little green arrows in the gutter of the main editor are clickable. When you click the little arrow beside the name of the class, that will run all the tests in the class. When you click the one beside the name of the test method, that will run only that test case. See Figure 8-9.

![img/340874_4_En_8_Fig9_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/795aa34981e844b1b8a35ff927eed6c8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=eLyrCyrmgBSd4nTUJnBp5a%2FH7T8%3D)
Figure 8-9

FactorialTest.java 在主编辑

同样,您也可以从主菜单栏运行测试;前往运行运行

Figure 8-10 shows the result of the text execution.

![img/340874_4_En_8_Fig10_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/48dfc43a3be44d14bbb3ceed1789eb5f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=jVs3C%2Bfupi2lk06Nd4do16ZYjuo%3D)
Figure 8-10

运行 FactorialTest.java 的结果

Android Studio 为您提供了大量的提示,因此您可以判断您的测试是通过还是失败。我们的第一次运行告诉我们Factorial.java有问题; assertEquals() 失败。

Tip

当测试失败时,最好使用调试器来调查代码。FactorialTest.java 与我们项目中的其他职业没有什么不同;这只是另一个 Java 文件,我们肯定可以调试它。在测试代码的关键位置设置一些断点,然后运行“调试器”而不是“运行”它,这样你就可以遍历它了。

我们的测试失败了,因为 1 的阶乘不是 2,而是 1。如果你仔细观察Factorial.java,你会注意到阶乘值没有正确计算。

Edit the Factorial.java file, then change this line:return arg + factorial(arg - 1);to this linereturn arg * factorial(arg - 1);If we run the test again, we see successful results, as shown in Figure 8-11.

![img/340874_4_En_8_Fig11_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/593a4174a60842d3964f1c3a21066eb1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=rXe4nvQtgpz95erKnuj0aOGyiyo%3D)
Figure 8-11

成功测试

我们现在看到的不是黄色的感叹号,而是绿色的复选标记。我们现在看到的不是“测试失败”,而是“测试通过”现在我们知道我们的代码按预期工作。

排除故障

我们写代码已经有一段时间了;我敢肯定,到目前为止,您的代码已经遇到了一些问题,并且已经看到了 Android Studio 提醒您注意这些错误的各种方式。

句法误差

你会经常遇到的一个错误是语法错误。它们发生是因为你在代码中写了一些不应该出现的东西;或者你忘了写什么(比如分号)。这些错误可能是良性的,如忘记了右花括号,也可能是复杂的,如在使用泛型时将错误类型的参数传递给方法或参数化类。幸运的是,Android Studio 非常善于发现这类错误。这几乎就像 IDE 在不断地读取代码并编译它。

Syntax errors are simple enough to solve, and you’ve probably figured it out by now. Whenever you see red squiggly lines or red-colored text in the IDE (as shown in Figure 8-12), just hover the mouse on top of the red-colored text or red squiggly lines, and you should see Android Studio’s tips.

![img/340874_4_En_8_Fig12_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e257919f6dc5448b91ff7d73032106ba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=2OQ0mfoU1mqdV396wjqdgWJ1M3I%3D)
Figure 8-12

语法错误显示在编辑器中

The tips typically tell you what’s wrong with the code. In Figure 8-12, the error is Cannot resolve symbol ‘Button’, which means you haven’t imported the Button class just yet. To resolve this, position the mouse cursor on the offending word (Button, in this case), then use the Quick Fix feature (Option+Enter in Mac, Alt+Enter in Windows). Quick Fix in action is shown in Figure 8-13.

![img/340874_4_En_8_Fig13_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f9436e9f520a49788306aa05a50b77c8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=sFpGT%2F5e0WVO2GEeAMwqDn7FrUk%3D)
Figure 8-13

权宜之计

运行时错误

Runtime errors happen when your code encounters a situation it doesn’t expect; and as its name implies, that errant condition is something that appears only when the program is running—it’s not something you or the compiler can see at the time of compilation. Your code will compile without problems, but it may stop running when something in the runtime environment doesn’t agree with what your code wants to do. There are many examples of these things; here are some of them:

  • 这个应用从互联网上获得一些东西,一张图片或一个文件等等,所以它假设互联网是可用的,并且有网络连接。一直都是。经验应该告诉你,情况并不总是这样。网络连接有时会中断,如果您不在代码中考虑这一点,它可能会崩溃。

  • 该应用需要从文件中读取。就像我们前面的第一个案例一样,您的代码假设文件将一直存在。有时,文件会损坏,可能变得不可读。这也应该在代码中考虑。

  • 该应用执行数学计算。它使用用户输入的值,有时也使用其他计算得出的值。如果您的代码碰巧执行了除法,并且在其中一个除法中,除数为零,这也将导致运行时问题。

在大多数情况下,在处理运行时错误时,Java 是你的后盾。异常处理在 Java 中不是可选的。只要确保你没有在你的 try-catch 块上吝啬;总是放异常处理代码,你应该没问题。

逻辑错误

逻辑错误最难发现。顾名思义,这是你逻辑上的错误。当你的代码没有做你认为它应该做的事情时,那就是逻辑错误。有许多方法可以解决这个问题,但最常用的方法是(1)使用日志语句和(2)使用断点和遍历/单步执行代码。

Printing log messages is a simple way of marking the footprints of the program; you can do it with the simple System.out.println() statement, but I’d encourage you to use the Log class instead. Listing 8-3 shows a basic usage of the Log class.public class MainActivity extends AppCompatActivity {final String TAG = getClass().getName();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// ...}void doSomething() {Log.d(TAG, "Log message, doSomething");}}Listing 8-3

Log 类的基本用法

You can define the TAG variable anywhere in the class, but in Listing 8-3, I defined it as a class member; Log.d() prints a debug message. You can use the other methods of the Log class to print warnings, info, or errors. The other methods are shown here:Log.v(TAG, message) // verboseLog.d(TAG, message) // debugLog.i(TAG, message) // infoLog.w(TAG, message) // warningLog.e(TAG, message) // error

在每种情况下,标签是一个字符串或变量。您可以使用标记来过滤 Logcat 窗口中的消息。消息也是一个字符串或变量,它包含了您真正想在日志中看到的内容。

When you run your app, you can see the Log messages in the Logcat tool window. You can launch it either by clicking its tab in the menu strip at the bottom of the window (as shown in Figure 8-14) or from the main menu bar, ViewTool WindowsLogcat.

![img/340874_4_En_8_Fig14_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bb7cff93f88e41818ae7e7a55664badf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=4QDYWKg%2BaIRDT%2FuALCadEEekIhA%3D)
Figure 8-14

Logcat 工具窗口

遍历代码

Android Studio 包括一个交互式调试器,它允许你在代码运行时一步一步地调试代码。使用交互式调试器,我们可以在代码中的特定位置和特定时间点检查应用的快照—变量值、运行的线程等。代码中的这些特定位置被称为断点;你可以选择这些断点。

To set a breakpoint, choose a line that has an executable statement, then click its line number in the gutter. When you set a breakpoint, there will be a pink circle icon in the gutter, and the whole line is lit in pink—as shown in Figure 8-15.

![img/340874_4_En_8_Fig15_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9355711f3bd94b23903f5014e5ea67c4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=TdJFrToUxIx60Gavc2SV%2B5CErtY%3D)
Figure 8-15

调试器窗口

设置断点后,您必须在调试模式下运行应用。如果应用当前正在运行,请将其停止,然后从主菜单栏中单击运行调试“应用”。

Note

在调试模式下运行应用并不是调试应用的唯一方式。您还可以在当前运行的应用中附加调试器进程。在有些情况下,第二种技术是有用的;例如,当您试图解决的错误发生在非常特定的条件下时,您可能希望运行应用一段时间,当您认为您接近错误点时,您可以附加调试器。

照常使用该应用。当执行到您设置断点的一行时,该行将从粉红色变为蓝色。这就是你如何知道代码在断点处执行。此时,调试器窗口打开,执行停止,Android Studio 进入交互式调试模式。当您在这里时,应用的状态显示在调试工具窗口中。在此期间,您可以检查变量值,甚至看到应用中运行的线程。

您甚至可以通过单击带有眼镜图标的加号,在“监视”窗口中添加变量或表达式。将有一个文本字段,您可以在其中输入任何有效的表达式。当你按下输入时,Android Studio 会对表达式进行求值,并向你显示结果。要删除监视表达式,请选择表达式,然后单击“监视”窗口上的减号图标。

要恢复程序执行,您可以单击调试器工具栏顶部的“恢复程序”按钮—它是指向右侧的绿色箭头。或者,您也可以从主菜单栏运行恢复程序中恢复程序。如果你想在程序自然结束前暂停它,你可以点击调试器工具栏上的“停止应用”按钮;这是红色方块图标。或者,您也可以从主菜单栏运行停止应用中执行此操作。

仿形铣床

分析器让我们了解我们的应用/游戏如何使用计算资源,如 CPU、内存、网络带宽和电池。

The Profiler is new in Android Studio 3. It replaces the Android monitor with its new unified and shared timeline view for the CPU, memory, network, and energy graphs. Figure 8-16 shows the Profiler.

![img/340874_4_En_8_Fig16_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d3ddd08c0f35444480e8ab105b872eca~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=sjo5hYDcSfWUpU%2FuS48CU4nOerw%3D)
Figure 8-16

仿形铣床

You can get to the Profiler by going to the main menu bar, then selecting ViewTool WindowsProfiler.

| -什么 | 它显示正在分析的进程和设备。 | | ➋ | 它会显示要查看的会话。您还可以通过单击+按钮添加新的会话。 | | ➌ | 使用缩放按钮来控制要查看多少时间线。 | | -你好 | 新的共享时间线视图允许您查看 CPU、内存、网络和能源使用情况的所有图表。在顶部,您还会看到重要的应用事件,如用户输入或活动状态转换。 |

当您启动一个应用时,无论是在连接的设备上还是在仿真器上,您都会在 Profiler 上看到它的图形。

Note

如果您尝试使用低于 API 级别 26 的版本来分析 APK,您将会看到一些警告,因为 Android Studio 需要完全检测您的代码。您需要启用“高级分析”;但是,如果你的 APK 是奥利奥或更高,你不会看到任何警告。

如果您单击任何图表,Profiler 窗口将带您进入其中一个详细视图。例如,如果您单击 CPU,您将看到 CPU 利用率的详细视图。

中央处理器

Figure 8-17 shows the detailed view for the CPU utilization on the sample app I was running.

![img/340874_4_En_8_Fig17_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5df949a79ea64843a1849b0dec1a4966~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=xiBdh7c3QJ4clJf0yl0ken2VXNU%3D)
Figure 8-17

CPU 视图

除了实时利用率图,CPU 详细视图还显示了应用中所有线程及其状态的列表,您可以看到线程是否正在等待 I/O 或它们何时处于活动状态。

You might have noticed the “Record” button in Figure 8-17; if you click that button, you can get a report on all the methods that were executed in a given period. Notice also the selected trace type in the drop-down (Sample Java Methods); this trace type has a smaller overhead but not as detailed nor as accurate as the instrumented type (Trace Java Methods), meaning the sampled type may miss the execution of a very short-lived method. You might think, “just always use the instrumented type then”—you have to remember, though, that while instrumented type can record every method call, on Android devices before version 8, there is a limit on how much data can be captured; so, if you use the instrumented trace, that limit will be reached quickly. You can change that limit by editing the configuration for the instrumented capture. On the trace type drop-down, choose “Edit Configurations” as shown in Figure 8-18.

![img/340874_4_En_8_Fig18_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bdda4582acbb4fbb981f2290b5e38deb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=tdbw7E9T8CTrX6aGldVVn2FNUqY%3D)
Figure 8-18

编辑配置

![img/340874_4_En_8_Fig19_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/44c59687b7d9482397698e101a338a5d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=76SzyBXbUQ2ajxwuA1FuzSO6fNY%3D)
Figure 8-19

CPU 记录配置

图 8-19 显示了“采样间隔”和“文件大小限制”设置,您可以使用它们来调整采样的频率以及您想要分配给录像的文件大小。只是重申一下,文件大小限制只存在于运行 Android 8.0 或更低版本(< API level 26)的 Android 设备上。如果你的设备有更高的 Android 版本,你就不会受到这些限制。

If you click record, Android Studio will begin capturing data. Click the “Stop” button when you’d like to stop recording, as shown in Figure 8-20.

![img/340874_4_En_8_Fig20_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6df25eda519642de98bd659951fc2d2a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=lQ1Wr4XW7B%2F1cYhkB7Qy5%2B5%2Br7E%3D)
Figure 8-20

录制会话

When you hit stop, you can take a look at the individual threads, as shown in Figure 8-21.

![img/340874_4_En_8_Fig21_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d3f7217632af41389ef340fdff77d7ea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=q2aBRKs6ytElnnLdepyn%2Fwt%2B3pU%3D)
Figure 8-21

检查螺纹

记忆

The memory profiler shows, in real time, how much memory your app is consuming. Figure 8-22 shows a snapshot of the memory view as I captured the memory footprint of a test app. As you can see, not only does the graph show how much memory your app is gulping, it also shows the breakdown, for example, how much memory is used by the code, stack, graphics, Java, and so on.

![img/340874_4_En_8_Fig22_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/abd6c0fe7b5142a59da9cfae31039745~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=B2Gm2alZSwyOsRJFrLzbpwv1wbo%3D)
Figure 8-22

内存视图

You can force garbage collection (GC) in the memory view. See that garbage can icon at the top? Yup, if you click that, it’ll force a GC. The button to its right is also useful—the icon with a down-pointing arrow inside a box is a memory dump . If you click that, the Java heap will be dumped, and then you can inspect it, as shown in Figure 8-23.

![img/340874_4_En_8_Fig23_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/254ab3edb5754434891806919d548ce2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=JKh7%2Btrkbchpekuq%2BDUf%2B8HTLuk%3D)
Figure 8-23

Java 堆

The heap is a preserved amount of storage memory that the Android runtime allocates for our app. When we dumped the heap, it gave us a chance to examine instance properties of objects, as shown in Figure 8-24.

![img/340874_4_En_8_Fig24_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3e9e7a932e744090a5c7270c2e18df97~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=%2BK8MQmgNjOTwwVT%2Bkwzq5wUJmUQ%3D)
Figure 8-24

实例视图,参考选项卡

Reference 选项卡在查找内存泄漏时非常有用,因为它显示了指向您正在检查的对象的所有引用。

Another useful tool in the memory view is the Allocation tracker, shown in Figure 8-25.

| -什么 | 单击内存图时间线中的任意位置,查看分配跟踪器。这将向您显示在该时间点分配和释放的所有对象的列表。 | | ➋ | 这显示了应用在某个时间点正在使用的所有类的列表。 | | ➌ | 这显示了在特定时间点分配和释放的所有对象的列表。 | | -你好 | 跟踪器甚至包括分配的调用堆栈。 |
![img/340874_4_En_8_Fig25_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/744b6785c0694fe49cdf5c7c2602a7ae~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=U7TtRly8OxdsN7%2BOeQgNrnqjyUM%3D)
Figure 8-25

分配跟踪器

网络

Like the other views in the Profiler, the network view also shows real-time data. It lets you see and inspect data that is sent and received by your app; it also shows the total number of connections. Figure 8-26 shows a snapshot of the network profiler .

![img/340874_4_En_8_Fig26_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/abe82c1d192f4409974b98faef7a1606~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=w1yVkCuYXm1N%2FK0LcL5Nmua1G7w%3D)
Figure 8-26

网络分析器

每次你的应用向网络发出请求,它都使用 WiFi 无线电来发送和接收数据——无线电不是最节能的;它很耗电,如果你不注意你的应用如何发出网络请求,那肯定会比平时更快地耗尽设备电池。

当您使用网络分析器时,一个好的开始方式是寻找网络活动的短峰值。当您看到急剧上升和下降的尖峰信号,并且它们分散在整个时间线上时,似乎您可以通过批处理网络请求来进行一些优化,以减少 WiFi 无线电需要唤醒和发送或接收数据的次数。

活力

By now you’re probably seeing a pattern on how the Profiler works. It shows you real-time data. In the case of the Energy profiler, it shows data on how much energy your app is guzzling—though it doesn’t really show the direct measure of energy consumption, the Energy profiler shows an estimation of the energy consumption of the CPU, the radio, and the GPS sensor. Figure 8-27 shows a snapshot of the Energy profiler .

![img/340874_4_En_8_Fig27_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/48e4b2e3d21d46afbdfa661247c13ef3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=%2FrrH09UzuI85AY20iKDZZtcvr38%3D)
Figure 8-27

能量分析器

You can also use the Energy profiler to find system events that affect energy consumption, for example, wake locks, jobs, and alarms.

  • 唤醒锁是一种在设备进入睡眠状态时保持屏幕 CPU 开启的机制,例如,当应用播放视频时,它可能会使用唤醒锁来保持屏幕开启,即使没有用户交互——使用唤醒锁没有问题,但忘记释放唤醒锁会有问题;它让 CPU 的运行时间超过了必要的时间,这无疑会更快地耗尽电池。

  • 警报可用于以特定的时间间隔运行应用上下文之外的后台任务。当警报响起时,它可以运行一些任务;如果它运行一段高能耗的代码,您肯定会在能量分析器中看到它。

  • 当满足某些条件时,例如,当网络变得可用时,一个作业可以执行动作。您通常会使用 JobBuilder 创建一个作业,并使用 JobScheduler 来调度执行;当一个任务开始时,您也可以在能源配置文件中看到它们。

这是 Android Studio Profiler 的一个简单介绍;务必在 查看官方文档 https://developer . Android . com/studio/profile/Android-profiler。使用分析器可以让你了解游戏代码的哪一部分占用了资源。优化使用资源可以节省电池;你的用户会感谢你的。

关键要点

  • 我们已经讨论了你可以为你的游戏做的各种各样的测试;你不必做所有的测试,但是要确保你做的测试适用于你的游戏。

  • 开发测试(单元测试)应该是核心开发任务;试着养成将测试用例与实际代码一起编写的习惯。

  • Android Studio Profile 可以从底层检查您的应用的行为。它可以让你了解应用是如何消耗资源的;当您进行性能测试时,请使用这个工具。

****

九、OpenGL ES 简介

  • 关于 OpenGL ES

  • OpenGL 专家系统理论

  • GLSurfaceView 和 GLSurfaceView。渲染器

  • 在 OpenGL ES 中使用 Blender 数据

从 API level 11 (Android 3)开始,2D 渲染管道已经支持硬件加速。当你在画布上画图的时候(这是我们上两个游戏搭建的时候用的),画图操作已经在 GPU 上完成了;但这也意味着应用会消耗更多的 RAM,因为实现硬件加速需要更多的资源。

如果你构建的游戏不是那么复杂,那么使用画布构建游戏是一个不错的技术选择;但是,当视觉复杂性水平上升时,画布可能会耗尽能量,无法满足您的游戏需求。你需要更实在的东西。这就是 OpenGL ES 的用武之地。

什么是 OpenGL ES

开放图形库(OpenGL)来自硅图形(SGI);他们是高端图形工作站和大型机的制造商。最初,SGI 有一个名为 IRIS GL 的专有图形框架(后来成为行业标准),但随着竞争的加剧,SGI 选择将 IRIS GL 转变为一个开放框架。IRIS GL 去掉了与图形无关的功能和硬件相关的特性,变成了 OpenGL。

OpenGL 是一种用于渲染 2D 和 3D 图形的跨语言、跨平台的应用编程接口(API)。这是一个渲染多边形的精益平均机器;它是用 C 编写的 API,用于与图形处理单元(GPU)进行交互,以实现硬件加速渲染。这是一个非常低级的硬件抽象。

随着小型手持设备变得越来越普遍,用于嵌入式系统的 OpenGL(OpenGL ES)被开发出来。OpenGL ES 是桌面版的精简版;它移除了许多更冗余的 API 调用,并简化了其他元素,使其能够在市场上功能较弱的 CPU 上高效运行;因此,OpenGL ES 在许多平台上被广泛采用,如 HP webOS、任天堂 3DS、iOS 和 Android。

OpenGL ES 现在是 3D 图形编程的行业标准。它由 Khronos Group 维护,Khronos Group 是一个行业联盟,其成员包括 ATI、NVIDIA 和 Intel 等;这些公司共同定义并扩展了标准。

Currently, there are six incremental versions of OpenGL ES: versions 1.0, 1.1, 2.0, 3.0, 3.1, and 3.2.

  • OpenGL ES 1.0 和 1.1—此 API 规范受 Android 1.0 及更高版本支持。

  • OpenGL ES 2.0—此 API 规范受 Android 2.2 (API level 8)及更高版本支持。

  • OpenGL ES 3.0—此 API 规范受 Android 4.3 (API level 18)及更高版本支持。

  • OpenGL ES 3.1—此 API 规范受 Android 5.0 (API 等级 21)及更高版本支持。

There are still developers, especially those who focus on games that run on multiple platforms, who write for OpenGL ES 1.0; this is because of its simplicity, flexibility, and standard implementation. All Android devices support OpenGL ES 1.0, some devices support 2.0, and any device after Jelly Bean supports OpenGL ES 3.0. At the time of writing, more than half of activated Android devices already support OpenGL ES 3.0. Table 9-1 shows the distribution and Figure 9-1 shows a nice pie chart to go with it; this data was taken from developer.android.com/about/dashboards#OpenGL.Table 9-1

OpenGL ES 版本发布

|

OpenGL 是版本

|

分配

| | --- | --- | | 仅限 GL 1.1 | 0.0% | | GL 2.0 | 14.5% | | GL 3.0 | 18.6% | | GL 3.1 | 9.8% | | GL 3.2 | 57.2% |

![img/340874_4_En_9_Fig1_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9faef37259d43a690dc9c9eade331da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=8ODB3eJZh%2Bw42%2Fn3i5%2F2apzEgLU%3D)
Figure 9-1

OpenGL ES 版本发布

Note

对 OpenGL ES 的一个特定版本的支持也意味着对任何更低版本的支持(例如,对 2.0 版本的支持也意味着对 1.1 版本的支持)。

值得注意的是,OpenGL ES 2.0 打破了与 1.x 版本的兼容性。您可以使用 1.x 或 2.0,但不能同时使用两者。原因是 1.x 版本使用一种称为 固定功能管道 的编程模型,而 2.0 及更高版本允许您通过着色器以编程方式定义部分渲染管道。

OpenGL ES 是做什么的

The short answer is OpenGL ES just renders triangles on the screen, and it gives you some control on how those triangles are rendered. It’s probably best also to describe (as early as now) what OpenGL ES is not. It is not

  • 一个场景管理 API

  • 射线追踪仪

  • 物理引擎

  • 游戏引擎

  • 一个真实感渲染引擎

OpenGL ES 只是渲染三角形。没别的了。

Think of OpenGL ES as working like a camera. To take a picture, you have to go to the scene you want to photograph. Your scene is composed of objects that all have a position and orientation relative to your camera as well as different materials and textures. Glass is translucent and reflective; a table is probably made out of wood; a magazine has some photo of a face on it; and so on. Some of the objects might even move around (e.g., cars or people). Your camera also has properties, such as focal length, field of view, image resolution, size of the photo that will be taken, and a unique position and orientation within the world (relative to some origin). Even if both the objects and the camera are moving, when you press the shutter release, you catch a still image of the scene. For that small moment, everything stands still and is well defined, and the picture reflects exactly all those configurations of position, orientation, texture, materials, and lighting. Figure 9-2 shows an abstract scene with a camera, light, and three objects with different materials.

![img/340874_4_En_9_Fig2_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f5f5b4e656544d89b17359a7ed7b75df~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=4VdaMKvQjKZtGKYcgEm157av%2FHQ%3D)
Figure 9-2

抽象场景

每个对象都有相对于场景原点的位置和方向。由眼睛指示的摄像机也具有相对于场景原点的位置。图 9-2 中的金字塔被称为视体视见体 ,它显示了摄像机捕捉了多少场景以及摄像机是如何定向的。带有光线的小白球是场景中的光源,它也有一个相对于原点的位置。

我们可以将这个场景映射到 OpenGL ES,但是要这样做,我们需要定义(1)模型或对象,(2)灯光,(3)相机,和(4)视口。

模型或对象

OpenGL ES 是一个三角形渲染机器。OpenGL ES 对象是 3D 空间中的点的集合;它们的位置由三个值定义。这些值连接在一起形成面,面是看起来很像三角形的平面。三角形然后被连接在一起形成物体或物体的块(多边形)。

The resolution of your shapes can be improved by increasing the number of polygons in it. Figure 9-3 shows various shapes with varying number of polygons.

![img/340874_4_En_9_Fig3_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80024715634a46d19ad520722dd109c1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=0vQcziR3qDzM3jzreHlLaKd6ep8%3D)
Figure 9-3

从简单形状到复杂形状

图 9-3 最左边的是一个简单的球体;如果你仔细观察,你会发现它并不像一个球体。它旁边的形状(右)也是一个球体,但有更多的多边形。这些形状向右延伸,形成复杂的轮廓;这可以通过增加形状中多边形的数量来实现。

OpenGL ES 提供了几个不同的灯光类型和不同的属性。它们只是在 3D 空间中具有位置和/或方向的数学对象,加上诸如颜色之类的属性。

照相机

这也是一个在 3D 空间中具有位置和方向的数学对象。此外,它还有控制我们看到多少图像的参数,类似于真正的相机。所有这些共同定义了一个视见体或视见平截头体(在图 9-2 中用顶部被切掉的金字塔表示)。这个金字塔里面的任何东西都可以被摄像机看到;外面的任何东西都不会进入最终的画面。

视口

这定义了最终图像的尺寸和分辨率。可以把它想象成你放入模拟相机的胶片类型,或者你用数码相机拍摄的照片的图像分辨率。

预测

OpenGL ES 可以从相机的角度构建场景的 2D 位图。虽然一切都是在 3D 空间中定义的,但 OpenGL 通过所谓的 投影 将 3D 空间映射到 2D。单个三角形在 3D 空间中定义了三个点。为了渲染这样的三角形,OpenGL ES 需要知道这些 3D 点在基于像素的帧缓冲区坐标系中的坐标,这些点位于三角形内部。

矩阵

OpenGL ES expresses projections in the form of matrices. The internals are quite involved; for our introductory purposes, we don’t need to bother with the internals of matrices; we simply need to know what they do with the points we define in our scene.

  • 矩阵对要应用于点的变换进行编码。变换可以是投影、平移(其中点四处移动)、围绕另一个点和轴的旋转或缩放等。

  • 通过将这样的矩阵乘以一个点,我们将变换应用于该点。例如,将一个点与编码 x 轴上 10 个单位的平移的矩阵相乘,将使该点在 x 轴上移动 10 个单位,从而修改其坐标。

  • 我们可以通过矩阵相乘将存储在不同矩阵中的变换连接成一个矩阵。当我们用一个点乘以这个单个连接矩阵时,存储在该矩阵中的所有变换都将应用于该点。应用变换的顺序取决于矩阵相乘的顺序。

There are three different matrices in OpenGL ES that apply to the points in our models:

  • 模型-视图矩阵—该矩阵用于将模型放置在“世界”的某个地方例如,如果您有一个球体模型,并希望它位于东面 100 米处,您将使用模型矩阵来完成此操作。我们可以使用这个矩阵来移动、旋转或缩放三角形的点(这是模型-视图矩阵的模型部分)。这个矩阵也用于指定我们的摄像机的位置和方向(这是视图部分)。如果你想观察我们的球体,它在东边 100 米处,我们也必须将自己向东移动 100 米。另一种思考方式是,我们保持静止,世界的其他部分向西移动 100 米。

  • 投影矩阵—这是我们相机的视锥。由于我们的屏幕是平面的,我们需要做最后的转换,将我们的视图“投影”到我们的屏幕上,并获得漂亮的 3D 视角。这就是投影矩阵的用途。

  • 纹理矩阵—这个矩阵允许我们操作纹理坐标。

在 OpenGL ES 编程中,我们需要吸收更多的理论,但是让我们通过一个简单的编码练习来探索其中的一些理论。

渲染一个简单的球体

OpenGL ES APIs 内置在 Android 框架中,因此我们不需要导入任何其他库或者将任何其他依赖项包含到项目中。

OpenGL ES is widely supported among Android devices, but just to be prudent, if you want to exclude Google Play users whose device do not support OpenGL ES, you need to add a uses-feature in the Android Manifest file, like this:<uses-feature android:glEsVersion="0x00020000"android:required="true" />

清单条目基本上是说,应用希望设备支持 OpenGL ES 2,这实际上是编写时的所有设备。

Additionally (and optionally), if your application uses texture compression, you must also declare it in the manifest so that the app only installs on compatible devices; Listing 9-1 shows how to do this in the Android Manifest.Listing 9-1

AndroidManifest.xml,纹理压缩

Assuming you’ve already created a project with an empty Activity and a default activity_main layout file, the first thing to do is to add GLSurfaceView to the layout file. Modify activity_main.xml to match the contents of Listing 9-2.<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="schemas.android.com/apk/res/and…<android.opengl.GLSurfaceView****android:layout_width="400dp"****android:layout_height="400dp"android:id="@+id/gl_view"/></androidx.constraintlayout.widget.ConstraintLayout>Listing 9-2

activity_main.xml

我移除了默认的 TextView 对象,并插入了一个 400dp 乘 400dp 大小的 GLSurfaceView 元素。现在让我们保持它均匀的正方形,这样我们的形状就不会倾斜。OpenGL 假设绘图区域总是正方形的。

Figure 9-4 shows the activity_main layout in design mode.

![img/340874_4_En_9_Fig4_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e05d49ab9aea407b8c7ab6ad7e1df378~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=hs3aLOotvvlwmK7XrX56pixFElM%3D)
Figure 9-4

在设计模式下的 activity_main.xml

GLSurfaceView 是 SurfaceView 类的一个实现,它使用一个专用图面来显示 OpenGL 渲染;这个对象管理一个 surface,它是一个特殊的内存块,可以合成到 Android view 系统中。GLSurfaceView 运行在一个专用线程上,将渲染性能与主 UI 线程分开。

Next, in MainActivity, let’s get a reference to the GLSurfaceView we just created. We can create a member variable on MainActivity that’s of type GLSurfaceView, then in the onCreate() method, we’ll get a reference to it using findViewByID. The code is shown in Listing 9-3.public class MainActivity extends AppCompatActivity {private GLSurfaceView glView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);glView = findViewById(R.id.gl_view);}}Listing 9-3

获取对 GLSurfaceView 的引用

Next, still on MainActivity, let’s determine if there’s support for OpenGL ES 2.0. This can be done by using an ActivityManager object which lets us interact with the global system state; we can use this to get the device configuration info, which in turn can tell us if the device supports OpenGL ES 2. The code to do this is shown in Listing 9-4.ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);ConfigurationInfo ci = am.getDeviceConfigurationInfo();boolean isES2Supported = ci.reqGlEsVersion > 0x20000;Listing 9-4

确定对 OpenGL ES 2.0 的支持

Once we know if the device supports OpenGL ES 2 (or not), we tell the surface that we’d like an OpenGL ES 2 compatible surface, and then we pass it in a custom renderer. The runtime will call this renderer whenever it’s time to adjust the surface or draw a new frame. Listing 9-5 shows the annotated code for MainActivity.import android.app.ActivityManager;import android.content.Context;import android.content.pm.ConfigurationInfo;import android.opengl.GLES20;import android.opengl.GLSurfaceView;import android.os.Bundle;import javax.microedition.khronos.egl.EGLConfig;import javax.microedition.khronos.opengles.GL10;import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity {private GLSurfaceView glView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);glView = findViewById(R.id.gl_view);ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);ConfigurationInfo ci = am.getDeviceConfigurationInfo();boolean isES2Supported = ci.reqGlEsVersion > 0x20000;if(isES2Supported) { ❶glView.setEGLContextClientVersion(2); ❷glView.setRenderer(new GLSurfaceView.Renderer() { ❸@Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); ❹// statements ❺}@Overridepublic void onSurfaceChanged(GL10 gl10, int width, int height) {GLES20.glViewport(0,0, width, height); ❻}@Overridepublic void onDrawFrame(GL10 gl10) {// statements ❼}});}else {}}}Listing 9-5

MainActivity,创建 OpenGL ES 2 环境

| -好的 | 一旦我们知道支持 OpenGL ES 2,我们就开始创建 OpenGL ES 2 环境。 | | ❷ | 我们告诉表面视图,我们想要一个 OpenGL ES 2 兼容的表面。 | | -你好 | 我们使用匿名类创建一个自定义渲染器,然后将该类的一个实例传递给表面视图的 **setRenderer()** 方法。 | | (a) | 我们将渲染模式设置为只有在图形数据发生变化时才进行绘制。 | | (一) | 这是创建用于绘图的对象的好地方;可以认为这相当于活动的 **onCreate()** 方法。如果我们丢失了表面上下文并在以后被重新创建,这个方法也可能被调用。 | | ❻ | 当图面已经创建,并且随后由于某种原因图面的大小发生变化时,运行库调用此方法一次。这是你设置视窗的地方,因为当这个被调用时,我们已经得到了表面的尺寸。可以认为这相当于视图类的 **onSizeChanged()** 。这也可以在设备切换方向时调用,例如,从纵向切换到横向。 | | ❼ | 这是我们画画的地方。当要画一个新的框架时,这个函数被调用。 |

渲染器的 onDrawFrame() 方法 就是我们告诉 OpenGL ES 在表面上画东西的地方。我们将通过传递表示位置、颜色等的数字数组来实现这一点。在我们的例子中,我们要画一个球体。我们可以手工编码数字数组——代表顶点的 X,Y,Z 坐标——我们需要将它们传递给 OpenGL ES,但这可能无法帮助我们想象我们要画的是什么。所以,相反,让我们使用一个 3D 创作套件像 Blender(【www.blender.org】)来绘制一个形状。

Blender is open source; you can use it freely. Once you’re done with the download and installation, you can launch Blender, then delete the default cube by pressing X; next, press Shift+A and select MeshIco Sphere, as shown in Figure 9-5.

![img/340874_4_En_9_Fig5_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9a41d077de446e3b347d32632044efa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=9%2F4hLWjcDfktQsBJyQZn%2F1dHTgw%3D)
Figure 9-5

创建一个 Icosphere

现在我们有了一个有几个顶点的中等有趣的对象——手工编码这些顶点会很麻烦;所以我们走了搅拌机这条路。

要在我们的应用中使用球体,我们必须将其导出为波前对象。波前对象是一种几何定义文件格式。这是一种开放格式,被 3D 图形应用供应商所采用。这是一种简单的数据格式,表示 3D 几何图形,即每个顶点的位置;构成每个多边形的面被定义为一系列顶点。出于我们的目的,我们只对顶点和面的位置感兴趣。

In Blender, go to FileExport Wavefront (.obj) as shown in Figure 9-6. In the following screen, give it a name (sphere.obj) and save it in a location of your choice. Don’t forget to note the export settings of Blender; check only the following:

  • 作为 OBJ 对象导出

  • 三角测量人脸

  • 保持顶点顺序

![img/340874_4_En_9_Fig6_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aafcd5bb20fe4d0e9cd2ab2b32e280b2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=8nWTy3KaS422IaING0G3BXV81lg%3D)
Figure 9-6

将球体导出到波前对象格式

这些是我发现很容易使用的设置,尤其是当您要解析导出的顶点和面数据时。

The resulting object file is actually a text file; Listing 9-6 shows a partial listing of that sphere.obj.# Blender v2.82 (sub 7) OBJ File: 'sphere.blend'# www.blender.orgo Icospherev 0.000000 -1.000000 0.000000v 0.723607 -0.447220 0.525725v -0.276388 -0.447220 0.850649v -0.894426 -0.447216 0.000000v -0.276388 -0.447220 -0.850649v 0.723607 -0.447220 -0.525725v 0.276388 0.447220 0.850649s offf 1 14 13f 2 14 16f 1 13 18f 1 18 20f 1 20 17f 2 16 23f 3 15 25f 4 19 27f 5 21 29Listing 9-6

Partial sphere.obj

注意每一行是如何以“v”或“f”开头的。以“v”开头的线代表单个顶点,以“f”开头的线代表面。顶点线具有顶点的 X、Y 和 Z 坐标,而面线具有三个顶点的索引(它们一起形成一个面)。

为了让事情有条理,让我们创建一个代表我们的球体对象的类——我们并不真的想现在就在 onDrawFrame() 方法中编写所有的绘图代码,不是吗?

Let’s create a new class and add it to the project. You can do this by using Android Studio’s context menu; right-click the package name (as shown in Figure 9-7), then choose NewJava Class.

![img/340874_4_En_9_Fig7_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cff9805f5e50454ab590e65b2f421eec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=7oPFP3%2FPjXLy1GnCarb%2FwsOSvkI%3D)
Figure 9-7

创建一个新的类

In the screen that follows, provide the name of the class (Sphere), as shown in Figure 9-8.

![img/340874_4_En_9_Fig8_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/90ebcf0a0b364b858bb317a649c6e19c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=CmV7e4Q2pNcKTBIngn%2B%2BkExO8bY%3D)
Figure 9-8

为类提供一个名称

We’ll build the Sphere class a basic POJO that contains all the data that OpenGL ES requires to draw a shape. Listing 9-7 shows the starting code for Sphere.java.public class Sphere {private List vertList;private List facesList;private Context ctx;private final String TAG = getClass().getName();public Sphere(Context context) {ctx = context;vertList = new ArrayList<>();facesList = new ArrayList<>();}}Listing 9-7

Sphere.java

The Sphere class has two List objects which will hold the vertices and faces data (which we will load from the OBJ file). Apart from that, there’s a Context object and a String object:

  • Context 对象将被我们的一些方法所需要,所以我把它作为一个成员变量。

  • 字符串标签—我只需要一个识别字符串,用于我们做一些日志记录的时候。

The idea is to read the exported Wavefront OBJ file and load the vertices and faces data into their corresponding List objects. Before we can read the file, we need to add it to the project. We can do that by creating an assets folder. An assets folder gives us the ability to add external files to the project and make them accessible to our code. If your project doesn’t have an assets folder, you can create them. To do that, use the context menu; right-click the “app” in the Project tool window (as shown in Figure 9-9), then select NewFolderAssets Folder.

![img/340874_4_En_9_Fig9_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/83c09321754b4c84b482c367e4e8fa5d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=AfSiZvY%2FIkP6B%2BYqsA2PPFH4RgI%3D)
Figure 9-9

创建一个资产文件夹

In the window that follows, click Finish, as shown in Figure 9-10.

![img/340874_4_En_9_Fig10_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5105d5c61cb642e180b8ae098336bd7e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=djj%2BHpRe4egA%2B9YPWYDwOgZOFMg%3D)
Figure 9-10

新的安卓组件

Gradle will perform a “sync” after you’ve added a folder to the project. Figure 9-11 shows the Project tool window with the newly created assets folder.

![img/340874_4_En_9_Fig11_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/67e5c1e1e8e34bdcbb1b69b306730802~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=BFlelJAo4QrpuXVyjPMwfMiFZrI%3D)
Figure 9-11

资产文件夹已创建

Next, right-click the assets folder, then choose Reveal in Finder (as shown in Figure 9-12)—this is the prompt I got because I’m using macOS. If you’re on Windows, you will see “Show in Explorer**”** instead.

![img/340874_4_En_9_Fig12_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8dbdd6d94d524093950e3e00811aa132~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=1n60w8nC%2FVdF%2BDekLdUuSltFN%2B8%3D)
Figure 9-12

在 Finder 中显示或在资源管理器中显示(适用于 Windows 用户)

现在您可以将 sphere.obj 文件转移到项目的 assets 文件夹中。

Alternatively, you can copy the sphere.obj file to the assets folder using the Terminal of Android Studio (as shown in Figure 9-13).

![img/340874_4_En_9_Fig13_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2abc73a2553d402b8104d26e8e3f24a5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=1UuMyVrnS2CmAB8B%2FV3J8z9YIOY%3D)
Figure 9-13

使用终端复制文件

用哪种方式对你更方便。有些人喜欢 GUI 方式,有些人喜欢命令行方式。使用您更熟悉的工具。

Now we can read the contents of the OBJ file and load them onto the ArrayList objects. In the Sphere class, add a method named loadVertices() and modify it to match Listing 9-8.import java.util.Scanner;// class definition and other statementsprivate void loadVertices() {try {Scanner scanner = new Scanner(ctx.getAssets().open("sphere.obj")); ❶while(scanner.hasNextLine()) { ❷String line = scanner.nextLine(); ❸if(line.startsWith("v ")) {vertList.add(line); ❹} else if(line.startsWith("f ")) {facesList.add(line); ❺}}scanner.close();}catch(IOException ioe) {Log.e(TAG, ioe.getMessage()); ❻}}Listing 9-8

load vertices()

| -好的 | 创建一个新的扫描仪对象并打开 **sphere.obj** 文本文件。 | | ❷ | 虽然我们还没有到达文件的末尾, **hasNextLine()** 将总是返回 true。 | | -你好 | 读取当前行的内容并保存到*行*变量中。 | | (a) | 如果该行以“v”开头,则将它添加到 **vertList** ArrayList 中。 | | (一) | 如果该行以“f”开头,将其添加到 **facesList** ArrayList 中。 |

我们使用 Java 语言编写应用,但是你需要记住 OpenGL ES 实际上是一堆 C APIs。我们不能简单地将顶点和面的列表直接传递给 OpenGL ES。我们需要将我们的顶点和面数据转换成 OpenGL ES 能够理解的东西。

Java and the native system might not store their bytes in the same order, so we use a special set of buffer classes and create a ByteBuffer large enough to hold our data and tell it to store its data using the native byte order. This is an extra step we need to do before passing our data to OpenGL. To do that, let’s add another method to the Sphere class; Listing 9-9 shows the contents of the createBuffers() method .private FloatBuffer vertBuffer; ❶private ShortBuffer facesBuffer;// some other statementsprivate void createBuffers() {// BUFFER FOR VERTICESByteBuffer buffer1 = ByteBuffer.allocateDirect(vertList.size() * 3 * 4); ❷buffer1.order(ByteOrder.nativeOrder());vertBuffer = buffer1.asFloatBuffer();// BUFFER FOR FACESByteBuffer buffer2 = ByteBuffer.allocateDirect(facesList.size() * 3 * 2); ❸buffer2.order(ByteOrder.nativeOrder());facesBuffer = buffer2.asShortBuffer();for(String vertex: vertList) { ❹String coords[] = vertex.split(" "); ❺float x = Float.parseFloat(coords[1]);float y = Float.parseFloat(coords[2]);float z = Float.parseFloat(coords[3]);vertBuffer.put(x);vertBuffer.put(y);vertBuffer.put(z);}vertBuffer.position(0); ❻for(String face: facesList) {String vertexIndices[] = face.split(" "); ❼short vertex1 = Short.parseShort(vertexIndices[1]);short vertex2 = Short.parseShort(vertexIndices[2]);short vertex3 = Short.parseShort(vertexIndices[3]);facesBuffer.put((short)(vertex1 - 1)); ❽facesBuffer.put((short)(vertex2 - 1));facesBuffer.put((short)(vertex3 - 1));}}Listing 9-9

创建缓冲区()

| -好的 | 您必须向 Sphere 类添加 FloatBuffer 和 ShortBuffer 成员变量。我们将用它来保存顶点和面的数据。 | | ❷ | 使用**allocated direct()**方法初始化缓冲区。我们为每个坐标分配 4 个字节(因为它们是浮点数)。一旦创建了缓冲区,我们就通过调用 **asFloatBuffer()** 方法将其转换为 FloatBuffer。 | | -你好 | 类似地,我们为面初始化一个 ByteBuffer,但是这一次,我们只为每个顶点索引分配 2 个字节,因为索引是无符号的 short。接下来,我们调用 **asShortBuffer()** 方法将 ByteBuffer 转换为 ShortBuffer。 | | (a) | 为了解析顶点列表对象,我们使用 Java 的增强 for-loop 遍历它。 | | (一) | 顶点列表对象中的每个条目都是保存顶点的 X,Y,Z 位置的一条线,像**0.723607-0.447220 0.525725**;它被一个空格隔开。因此,我们使用字符串对象的 **split()** 方法,使用空格作为分隔符。这个调用将返回一个包含三个元素的字符串数组。我们将这些元素转换成浮点数并填充 FloatBuffer。 | | ❻ | 重置缓冲器的位置。 | | ❼ | 和我们在顶点列表中做的一样,我们把它们分成数组元素,但是这次把它们转换成 short。 | | ❽ | 索引从 1(非零)开始;因此,在将转换后的值添加到 ShortBuffer 之前,我们将它减去 1。 |

下一步是创建着色器。如果我们不创建着色器,就无法渲染我们的 3D 球体;我们需要一个顶点着色器和一个片段着色器。着色器是用类似 C 的语言编写的,称为 OpenGL 着色语言(简称 GLSL)。

顶点着色器负责 3D 对象的顶点,而片段着色器(也称为像素着色器)处理 3D 对象像素的着色。

To create the vertex shader, add a file to the project’s assets folder and name it vertex_shader.txt , as shown in Figure 9-14.

![img/340874_4_En_9_Fig14_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4fe31d3af7904ad1b281934ce8b54431~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=D3PNZDj5sHXXie1cyoHAtSGO73Q%3D)
Figure 9-14

新的文件

In the window that follows (Figure 9-15), enter the name of the file.

![img/340874_4_En_9_Fig15_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/516637d26e3845a1a99c81742a1741de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=5PLAGv9%2BG7EL7K05e5CxRcP00hM%3D)
Figure 9-15

输入新的文件名

Modify the newly created vertex_shader.txt to match the contents of Listing 9-10.attribute vec4 position; ❶uniform mat4 matrix; ❷void main() {gl_Position = matrix * position; ❸}Listing 9-10

vertex_shader.txt

| -好的 | **属性**全局变量从我们的 Java 程序接收顶点位置数据。 | | ❷ | 这是来自我们 Java 代码的**统一**全局变量视图-项目矩阵。 | | -你好 | 在 **main()** 函数中,我们将 **gl_position** (一个 GLSL 内置变量)的值设置为统一和属性全局变量的乘积。 |

Next, we create the fragment shader. Like what we did in vertex_shader, add a file to the project and name it fragment_shader.txt. Modify the contents of the fragment shader program to match Listing 9-11.precision mediump float;void main() {gl_FragColor = vec4(0.481,1.000,0.865,1.000);}Listing 9-11

fragment_shader.txt

这是一个极简的片段着色器代码;它基本上给所有的像素分配一个浅绿色。

The next step is to load these shaders into our Java program and compile them. We will add another method to the Sphere class named createShaders(); its contents are shown in Listing 9-12.// class definition and other statementsprivate int vertexShader; ❶private int fragmentShader;private void createShaders() {try {Scanner scannerFrag = new Scanner(ctx.getAssets().open("fragment_shader.txt")); ❷Scanner scannerVert = new Scanner(ctx.getAssets().open("vertex_shader.txt")); ❸StringBuilder sbFrag = new StringBuilder(); ❹StringBuilder sbVert = new StringBuilder();while (scannerFrag.hasNext()) {sbFrag.append(scannerFrag.nextLine()); ❺}while(scannerVert.hasNext()) {sbVert.append(scannerVert.nextLine());}String vertexShaderCode = new String(sbVert.toString()); ❻String fragmentShaderCode = new String(sbFrag.toString());Log.d(TAG, vertexShaderCode);vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER); ❼GLES20.glShaderSource(vertexShader, vertexShaderCode);fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);GLES20.glShaderSource(fragmentShader, fragmentShaderCode);GLES20.glCompileShader(vertexShader); ❽GLES20.glCompileShader(fragmentShader);}catch(IOException ioe) {Log.e(TAG, ioe.getMessage());}}Listing 9-12

createShaders()

| -好的 | 为**顶点着色器**和**片段着色器**添加成员变量声明。 | | ❷ | 打开 **fragment_shader.txt** 进行读取。 | | -你好 | 打开 **vertex_shader.txt** 进行读取。 | | (a) | 创建一个 StringBuffer 来保存我们将从 Scanner 对象中读取的部分字符串;对 fragment_shader.txt 和 vertex_shader.txt 都执行此操作。 | | (一) | 将当前行追加到 StringBuffer(对两个 StringBuffer 对象都这样做)。 | | ❻ | 当 Scanner 对象中的所有行都被读取并追加到 StringBuffer 后,我们创建一个新的 String 对象。对两个 StringBuffers 都这样做。 | | ❼ | 着色器的代码必须添加到 OpenGL ES 的着色器对象中。我们使用 **glCreateShader()** 方法创建一个新的着色器,然后我们使用新创建的着色器和着色器程序代码设置着色器源;对顶点着色器和片段着色器都执行此操作。 | | ❽ | 最后,编译着色器。 |

在我们可以使用着色器之前,我们需要将它们链接到一个程序。我们不能直接使用着色器。这是连接顶点着色器的输出和片段着色器的输入的部分。它也让我们传递来自程序的输入,并使用着色器来绘制我们的形状。

We’ll create a new program object, and if that turns out well, we’ll attach the shaders. Let’s add a new method to the Sphere class and name it runProgram(); the code for this method is shown in Listing 9-13.private int program; ❶// other statementsprivate void runProgram() {program = GLES20.glCreateProgram(); ❷GLES20.glAttachShader(program, vertexShader); ❸GLES20.glAttachShader(program, fragmentShader); ❹GLES20.glLinkProgram(program); ❺GLES20.glUseProgram(program);}Listing 9-13

运行时程序()??

| -好的 | 您需要创建**程序**作为 Sphere 类中的成员变量。 | | ❷ | 使用 **glCreateProgram()** 方法创建一个程序。 | | -你好 | 将顶点着色器附加到程序中。 | | (a) | 将片段着色器附加到程序。 | | (一) | 要开始使用这个程序,我们需要使用 **glLinkProgram()** 方法链接它,并通过 **glUseProgram()** 方法使用它。 |

Now that all the buffers and the shaders are ready, we can finally draw something to the screen. Let’s add another method to the Sphere class and name it draw(); the code for this method is shown in Listing 9-14.import android.opengl.Matrix; ❶// class definition and other statementspublic void draw() {int position = GLES20.glGetAttribLocation(program, "position"); ❷GLES20.glEnableVertexAttribArray(position);GLES20.glVertexAttribPointer(position, 3, GLES20.GL_FLOAT, false, 3 * 4, vertBuffer); ❸float[] projectionMatrix = new float[16]; ❹float[] viewMatrix = new float[16];float[] productMatrix = new float[16];Matrix.frustumM(projectionMatrix, 0, -1, 1, -1, 1, 2, 9); ❺Matrix.setLookAtM(viewMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0f); ❻Matrix.multiplyMM(productMatrix, 0, projectionMatrix, 0, viewMatrix, 0);int matrix = GLES20.glGetUniformLocation(program, "matrix"); ❼GLES20.glUniformMatrix4fv(matrix, 1, false, productMatrix, 0);GLES20.glDrawElements(GLES20.GL_TRIANGLES, facesList.size() * 3,GLES20.GL_UNSIGNED_SHORT, facesBuffer); ❽GLES20.glDisableVertexAttribArray(position);}Listing 9-14

draw()

| -好的 | 您需要导入矩阵类。 | | ❷ | 如果你还记得在 **vertex_shader.txt** 中,我们定义了一个 **position** 变量,它应该从我们的 Java 代码中接收顶点位置数据;我们将要把数据发送到这个**位置**变量。为此,我们必须首先在 vertex_shader 中获取一个对 **position** 变量的引用。我们使用**glgetattributelocation()**方法来实现这一点,然后使用**glEnableVertexAttribArray()**方法来启用它。 | | -你好 | 将**位置**手柄指向顶点缓冲区。**glvertexattributepointer()**方法也期望每个顶点的坐标数和每个顶点的字节偏移量。每个坐标是一个浮点数,所以字节偏移量是 **3 * 4** 。 | | (a) | 我们的顶点着色器需要一个视图投影矩阵,它是视图和投影矩阵的乘积。一个**视图矩阵**允许我们指定摄像机的位置和它正在看的点。一个**投影矩阵**让我们映射 Android 设备的方形坐标,并指定视见平截头体的远近平面。我们简单地为这些矩阵创建浮点数组。 | | (一) | 使用 matrix 类的**frustrum()**方法初始化投影矩阵。您需要向该方法传递一些参数;它需要左、右、下、上、近和远裁剪平面的位置。当我们在 activity_main 布局文件中定义 GLSurfaceView 时,它已经是一个正方形了,所以我们可以使用值 **-1 和 1** 来表示近裁剪平面和远裁剪平面。 | | ❻ | **setLookAtM()** 方法用于初始化视图矩阵。它期望摄像机的位置和它正在观察的点。然后使用**multiplym()**方法计算乘积矩阵。 | | ❼ | 让我们使用 **glGetUniformLocation()** 方法将乘积矩阵传递给着色器。当我们得到句柄(**矩阵**变量)时,使用 **glUniformMatrix4fv()** 方法将其指向乘积矩阵。 | | ❽ | glDrawElements() 方法让我们使用 faces 缓冲区来创建三角形;它的参数期望顶点索引的总数、每个索引的类型以及面缓冲区。 |

Now that we’ve got the methods to load the vertices from a blender file, create all the buffers, compile the shaders, and create an OpenGL program, we can now tie all these methods together in the constructor of the Sphere class, as shown in Listing 9-15.public Sphere(Context context) {ctx = context;vertList = new ArrayList<>();facesList = new ArrayList<>();loadVertices();****createBuffers();****createShaders();****runProgram();}Listing 9-15

球体类的构造函数

After adding all these methods, it may be difficult to keep the code straight. So, I’m showing all the contents of the Sphere class in Listing 9-16, for your reference.import android.content.Context;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.nio.FloatBuffer;import java.nio.ShortBuffer;import java.util.ArrayList;import java.util.List;import java.util.Scanner;import android.opengl.GLES20;import android.opengl.Matrix;import android.util.Log;public class Sphere {private FloatBuffer vertBuffer;private ShortBuffer facesBuffer;private List vertList;private List facesList;private Context ctx;private final String TAG = getClass().getName();private int vertexShader;private int fragmentShader;private int program;public Sphere(Context context) {ctx = context;vertList = new ArrayList<>();facesList = new ArrayList<>();loadVertices();createBuffers();createShaders();runProgram();}private void loadVertices() {try {Scanner scanner = new Scanner(ctx.getAssets().open("sphere.obj"));while(scanner.hasNextLine()) {String line = scanner.nextLine();if(line.startsWith("v ")) {vertList.add(line);} else if(line.startsWith("f ")) {facesList.add(line);}}scanner.close();}catch(IOException ioe) {Log.e(TAG, ioe.getMessage());}}private void createBuffers() {// BUFFER FOR VERTICESByteBuffer buffer1 = ByteBuffer.allocateDirect(vertList.size() * 3 * 4);buffer1.order(ByteOrder.nativeOrder());vertBuffer = buffer1.asFloatBuffer();// BUFFER FOR FACESByteBuffer buffer2 = ByteBuffer.allocateDirect(facesList.size() * 3 * 2);buffer2.order(ByteOrder.nativeOrder());facesBuffer = buffer2.asShortBuffer();for(String vertex: vertList) {String coords[] = vertex.split(" ");float x = Float.parseFloat(coords[1]);float y = Float.parseFloat(coords[2]);float z = Float.parseFloat(coords[3]);vertBuffer.put(x);vertBuffer.put(y);vertBuffer.put(z);}vertBuffer.position(0);for(String face: facesList) {String vertexIndices[] = face.split(" ");short vertex1 = Short.parseShort(vertexIndices[1]);short vertex2 = Short.parseShort(vertexIndices[2]);short vertex3 = Short.parseShort(vertexIndices[3]);facesBuffer.put((short)(vertex1 - 1));facesBuffer.put((short)(vertex2 - 1));facesBuffer.put((short)(vertex3 - 1));}facesBuffer.position(0);}private void createShaders() {try {Scanner scannerFrag = new Scanner(ctx.getAssets().open("fragment_shader.txt"));Scanner scannerVert = new Scanner(ctx.getAssets().open("vertex_shader.txt"));StringBuilder sbFrag = new StringBuilder();StringBuilder sbVert = new StringBuilder();while (scannerFrag.hasNext()) {sbFrag.append(scannerFrag.nextLine());}while(scannerVert.hasNext()) {sbVert.append(scannerVert.nextLine());}String vertexShaderCode = new String(sbVert.toString());String fragmentShaderCode = new String(sbFrag.toString());Log.d(TAG, vertexShaderCode);vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);GLES20.glShaderSource(vertexShader, vertexShaderCode);fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);GLES20.glShaderSource(fragmentShader, fragmentShaderCode);GLES20.glCompileShader(vertexShader);GLES20.glCompileShader(fragmentShader);}catch(IOException ioe) {Log.e(TAG, ioe.getMessage());}}private void runProgram() {program = GLES20.glCreateProgram();GLES20.glAttachShader(program, vertexShader);GLES20.glAttachShader(program, fragmentShader);GLES20.glLinkProgram(program);GLES20.glUseProgram(program);}public void draw() {int position = GLES20.glGetAttribLocation(program, "position");GLES20.glEnableVertexAttribArray(position);GLES20.glVertexAttribPointer(position, 3, GLES20.GL_FLOAT, false, 3 * 4, vertBuffer);float[] projectionMatrix = new float[16];float[] viewMatrix = new float[16];float[] productMatrix = new float[16];Matrix.frustumM(projectionMatrix, 0, -1, 1, -1, 1, 2, 9);Matrix.setLookAtM(viewMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0f);Matrix.multiplyMM(productMatrix, 0, projectionMatrix, 0, viewMatrix, 0);int matrix = GLES20.glGetUniformLocation(program, "matrix");GLES20.glUniformMatrix4fv(matrix, 1, false, productMatrix, 0);GLES20.glDrawElements(GLES20.GL_TRIANGLES, facesList.size() * 3, GLES20.GL_UNSIGNED_SHORT, facesBuffer);GLES20.glDisableVertexAttribArray(position);}}Listing 9-16

球体类的完整代码

Now that all of the code for the Sphere class is complete, we can go back to MainActivity. Remember in MainActivity that we created a Renderer object using an anonymous inner class. We created that renderer because a GLSurfaceView needs a renderer object so that it can, well, render 3D graphics. Listing 9-17 shows the complete code for MainActivity.public class MainActivity extends AppCompatActivity {private GLSurfaceView glView;private Sphere sphere; ❶@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);glView = findViewById(R.id.gl_view);ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);ConfigurationInfo ci = am.getDeviceConfigurationInfo();boolean isES2Supported = ci.reqGlEsVersion > 0x20000;if(isES2Supported) {glView.setEGLContextClientVersion(2);glView.setRenderer(new GLSurfaceView.Renderer() {@Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);sphere = new Sphere(getApplicationContext()); ❷}@Overridepublic void onSurfaceChanged(GL10 gl10, int width, int height) {GLES20.glViewport(0,0, width, height);}@Overridepublic void onDrawFrame(GL10 gl10) {sphere.draw(); ❸}});}else {}}}Listing 9-17

主要活动,完成

| -好的 | 创建一个成员变量作为我们将要创建的球体对象的引用。 | | ❷ | 创建球体对象;将当前上下文作为参数传递。 | | -你好 | 调用球体的 **draw()** 方法。 |

At this point, you’re ready to run the app. Figure 9-16 shows the app at runtime.

![img/340874_4_En_9_Fig16_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0088f64dfea148aabf80fd71e9a1a4f8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=cGnFCVaDbk4r3i6u9fb7vKyHaJQ%3D)
Figure 9-16

OpenGL 中渲染的 icosphereES

在将近 300 行代码之后,我们得到的只是一个没有太多定义的绿色小 Icosphere。欢迎来到 OpenGL ES 编程。这应该让你知道一个 OpenGL ES 游戏有多复杂,需要做多少工作。

关键要点

  • 从 Android 3 (API level 11)开始,在画布上完成的绘图已经享受到了硬件加速,因此对于游戏编程来说,这是一个不错的技术选择。但是,如果游戏的视觉复杂性超过了画布的能力,您应该考虑使用 OpenGL ES 来绘制图形。

  • OpenGL ES 真的只擅长画三角形,别的不多。它给了你很多控制力来画这些三角形。有了它,你可以控制相机,光源和纹理等。

  • Android SDK 已经内置了对 OpenGL ES 的支持。GLSurfaceView 已经包含在 SDK 中,这是您通常用于绘制 OpenGL ES 对象的视图。

十、定价

  • 定价模型

  • Freemium

  • 广告

  • 可发现性

付费或免费

你必须决定游戏是付费的还是免费的。app 一旦免费发布,就不能再改成付费了。然而,付费应用可能会在以后转为免费。付费发布应用是一种直接获得回报的方式;建 app,发布 app,要钱。

你也可以免费发布你的游戏,但是如果你免费发布的话,你怎么能建立一个收入流呢?一些开发商已经采取了“免费广告”的路线,虽然许多人仍在这样做,但由于纯粹的竞争,你可能要考虑其他形式的收入流。普遍的想法是,有很多移动应用广告,但广告背后没有多少钱,所以钱分散了。基本的想法是你免费提供游戏,你展示广告来创收。这里需要关注的关键指标是点击率,它是点击率的缩写。CTR 是被点击的广告数量除以显示的广告(印象)数量,以百分比表示。如果你展示了 100 个广告,用户点击了两次,点击率是 2%;玩你游戏的人越多,你展示的广告就越多,获得更高点击率的机会也就越多;这是基本的想法。

广告并不都是平等的;有些人比其他人更有潜力。一个 banner 广告,比方说,点击率 0.02%;如果你的游戏显示 100,000 次展示,你得到 200 次点击;每次点击 0.05 美元,就是 10 美元。如果你的应用每月获得 100,000 次展示,你可能要在放弃日常工作之前先考虑一下。仅仅做一下数学计算,你就可以算出一款游戏需要多少印象才能获得 1000 美元的收入。

幸运的是,广告并不是免费游戏赚钱的唯一途径。大约十年前,免费增值定价模式进入主流意识。免费增值是“免费”和“高级”两个词的组合。这是一种定价策略,你可以免费发布游戏,并从其他地方获得收入,如应用内购买、虚拟货币等。

Freemium

If you look at Google’s top grossing apps (bit.ly/topgrossingapps), you’ll find that quite a few of them are free; to be more precise, they’re freemium. They’re free to use and download, but they also have in-app purchases that cost real money. These purchases allow the users to buy extra content, for example, levels, new characters, costumes, virtual currencies, or coins, which can be used for upgrades. There are many more that you can buy in an in-app purchase, but these are the popular ones. The freemium model is very successful, but it requires more development work. On the side of the users, it works to their advantage because there is no cost in trying out the game. If they like it and they’ve invested some playing hours already, they’re more likely to spend real money to buy more content. Going freemium is more work because of two things:

  1. 1.

    Extra content is not defined in the game itself, but defined somewhere in Google Play, which means that you need to spend time to manage Google Play. When the items are defined, your game can query Google Play to get the list of items available for purchase.

  2. 2.

    When the new game content has been purchased (and downloaded), the game needs to change its behavior. The change of game behavior depending on available content needs to be considered in the overall structure of the game; This increases the complexity of programming.

顺便说一下,走向免费增值模式与广告并不相互排斥,与付费应用也不相互排斥。有些可疑的开发者可能会发布带有广告的付费应用,然后提供应用内购买来移除广告。不难看出这会适得其反。当用户开始发表评论,告诉其他用户这个令人讨厌的策略时,游戏就结束了。

应用内购买

In-app purchases (IAP) or in-app products refer to the buying of goods and services from inside an application on a mobile device. The idea is that the player wants something that’s offered in your game, and he’s willing to pay a small amount of money to get it. There are two types of in-app product options given on the Google Play Store:

  • —只能购买一次的物品。它们附属于购买者,而不是设备。Google Play 跟踪这些购买,这允许用户在以后查询这些项目以进行恢复;此外,如果买家试图购买他们已经购买的物品,Google Play 将回应“该物品已经被购买。”管理项目的例子有等级、角色或能力。

*** 未被管理的物品—这些是被用户用完的物品,如硬币、虚拟货币(VC)或任何需要“补充”的东西未被管理的项目不会被 Google Play 追踪;用户不能在以后“恢复”这些购买。如果你想追踪未被管理的物品,你需要在你的游戏中为其编写代码。像被管理的项目一样,这些项目也附加到 Google 帐户,而不是设备。**

另一个与 IAPs 相关的货币化选项是“订阅”Google Play 允许您设置定期计费的订阅。应用会简单地将订阅视为“开启”或“关闭”当它“开启”时,用户可以付费连续访问内容或服务。玩家可以享受你的游戏所提供的一切,只要他们订阅了。

**

虚拟货币

虚拟货币是游戏内的货币。它们有很多名字。在一些游戏中,他们被称为黄金,硬币,红宝石,信用,等等。VC 是你的游戏为玩家存储的点数或数字;它允许玩家在游戏中做事情或买东西。有了 VCs,玩家可以购买提示、升级武器、更多生命值等等。

风投可以(通常)通过在玩游戏时获得,或者通过用真金白银购买(从 Google Play,作为非托管项目)获得。

**

**

广告

If you’re considering putting ads on the game, you need to get familiar with the ad providers; they deliver the ads from advertisers and pay you for the clicks. The money is split between you and the ad provider (it’s not split in the middle). A portion of the money goes to you (the game publisher) and the rest of the money goes to the ad provider, which is how the provider makes money. You’ll need to configure some keywords for the app, so the ads are more relevant; this is where you need a bit of SEO background and keyword wizardry. The ads can be in a variety of formats, but the common ones are banner and full-page ads. Here are some of the major ad providers and aggregators:

这些服务有自己的 API,通常很容易使用。请访问他们的网站获取技术文档。

实施广告时很容易激动,可能会做过头。只要记住展示广告的目的是为了赚钱。在显示广告和激怒用户之间有一个平衡点;收益递减法则显然适用于此。如果广告带来太多的干扰,用户可能会感到恼火;当这种情况发生时,用户群可能会缩小——你的收入也会随之减少。

发现你的游戏

已经有成千上万的游戏可供选择(大约。在撰写本文时,Google Play 中有 300,000 个用户),更多用户还在路上。这是红海的领土;这是一个非常拥挤的地方;但也不乏成功的故事。那么,你如何让你的游戏受到关注呢?你如何让人们意识到你的游戏已经在 Google Play 上发布了,而且很棒?嗯,你总是可以在广告上花很多钱,或者你可以试试本节概述的东西。

社交网络

脸书和推特是社交媒体的重量级人物。我假设您现在已经使用过这些平台了。有很多策略可以利用社交媒体让你的游戏获得一些关注。你总是可以做很多人已经在做的事情,比如建立一个脸书页面并“提升”页面(你必须为此付费)。与此同时,你可以告诉你的朋友喜欢这个网页。这可能会给你带来一些下载,但仅此而已——除非你有数百万的朋友或粉丝。我想你没有那么多,所以我们继续找吧。

从营销的角度来看,这两个社交网站的一些优点是,几乎每个人都使用它们,它们可以免费使用,并且它们对更有创意的解决方案很友好。这里有一些你如何利用这些网站来营销你的游戏的例子:给在脸书上“喜欢”你的游戏的用户 50 个免费的风险投资信用。给在推文中提到你的游戏的用户 50 个免费 VC 积分。

每月举行一次高分竞赛,奖品是一台新的安卓设备,只允许在脸书上喜欢你的人注册。在最后一个例子中,你必须真的购买一个设备作为奖品,当然,但是就激励“喜欢”而言,这样的策略真的很有效。很容易创造激励让人们互相分享你的游戏,这些网络是这种信息分享的完美平台。

脸书和 Twitter 都提供了 Android SDKs,你可以下载并使用它们来将网络与你的游戏整合在一起。API 集成文档通常很容易理解,所以一定要尝试一下。

发现服务

有 AppBrain(【https://appbrain.com】)这样的公司,他们的唯一目的就是帮助你让你的游戏被发现。其他公司,如 tap joy()和 Flurry(www.flurry.com),也有发现服务。这些服务中的大部分都提供了将你的游戏“放到网络中”的方法,这样它就会被其他游戏所推广。你可以支付安装费,并控制一场运动,让你的游戏进入许多人的手中。

不同的公司提供不同的发现方法,但是,简而言之,如果你想让你的游戏被发现,并且你有预算,你可能想看看这些服务中的一个或多个。当你把这样的服务和一个好的社交网络策略结合起来,你可能会让雪球越滚越大,为你的游戏制造轰动。

博客和网络媒体

让你的游戏被发现的另一个策略是为故事收集试点,为演示创建视频,并将所有这些发送到评论新的 Android 应用和游戏的博客。这些网站的编辑被审查应用和游戏的请求轰炸,所以要为他们做尽可能多的工作,提前给他们所有他们需要的信息。

游戏设计

I mentioned earlier that a game with in-app purchase capabilities is more complex to develop and administer. It’s better to anticipate the structural complexities warranted on the outset if you want to monetize the game rather than retrofitting an already finished game for monetization. A game that’s designed for monetization may have one or more of the following elements:

  • 影响游戏性的可选修改器

    • 促进

    • 升级

    • 欺骗

  • 不影响游戏性的可选内容

    • 外皮

    • 特性

  • Additional content

    • New level

    • New film art

    • New parts

    • Checkpoint

  • Virtual currency that

    • You can get

    • You can buy

    • You can buy the in-game upgrade

    • can be used to purchase additional content

Also, during the early planning stage, make the game discoverable by design; these kinds of games provide incentives for players to tell other people about the game. Much like a game that’s designed to be monetized, a game that’s designed to be discoverable incorporates most or all of the same elements (virtual currency, virtual goods, unlockables, additional content, etc.) as incentives for telling other people about the game. Here are some ideas on how to do this:

  • 制作一个只能通过输入从另一个玩家处收到的推荐代码来解锁的内容。

  • 提供额外的内容或风险资本,用于在脸书上发布关于该游戏的微博或分享或喜欢该游戏。

  • 奖励所有推荐给其他玩家的 VC 玩家。

  • 整合脸书或其他社交媒体来发布成就和新的高分。

  • 创建游戏的另一部分,作为一个脸书应用来玩,但以某种方式与移动游戏联系在一起。

关键要点

  • 有很多方法可以让你的游戏赚钱;你可以直接把它卖几美元一个。仅此而已。你可以免费发布它,并在游戏中提供应用内购买。你也可以免费发布游戏,并通过展示广告获得收入;或者你可以三者结合使用。

  • 在推广你的游戏时,创造性地使用社交网络;除了简单地在广告上砸钱,还有更划算的方法。

  • 货币化游戏更复杂,因此更难开发;但是要确保游戏赚钱不是事后的想法。在游戏开发和设计的规划阶段,你需要包括盈利策略。

**

十一、发布游戏

你可以相当自由地发布你的游戏,没有太多的限制;你可以让你的用户从你的网站、Google Drive、Dropbox 等等下载;如果你愿意,你甚至可以把游戏《APK》直接发给用户;但许多开发者选择在谷歌或亚马逊这样的市场上分发他们的应用或游戏,以最大限度地扩大影响。

In this chapter, we’ll discuss the things you need to do to get your game out in Google Play. Here’s what we’ll cover:

  • 准备发布

  • 签署应用

  • 谷歌游戏

  • 应用捆绑包

准备项目发布

There are three things you need to keep in mind when preparing for release; these are

  • 准备发布的材料和资产

  • 为发布配置项目

  • 构建一个发布就绪的应用

准备材料和资产用于发布

你的代码很棒,你甚至可能认为它很聪明,但是用户永远看不到它。他们将看到您的视图对象、图标和其他图形资产。你应该擦亮它们。

如果你认为应用的图标没什么大不了的,那可能是个错误。图标可以帮助用户识别您的应用,因为它位于主屏幕上。这个图标还出现在其他区域,如启动窗口和下载部分,更重要的是,它出现在 Google Play 上。图标在创造用户对你的游戏的第一印象中起着很大的作用。这是一个很好的主意,你可以在这里找到谷歌的图标指南:【http://bit.ly/androidreleaseiconguidelines】

如果你要在 Google market place 上发布,其他要考虑的是图形资产,比如屏幕截图和促销文本。请务必阅读谷歌的图形资产指南,可以在这里找到:【http://bit.ly/androidreleasegraphicassets】

配置要发布的应用

  1. 1.

    Check the package name —You may want to check the package name of the application. Make sure it is still not com.example.myapp .. Package name makes the application unique in Google marketplace; Once you decide the name of a bag, you can't change it again. So, think about it.

  2. 2.

    Processing debugging information -Make sure that you have deleted the Android: Debugable attribute in the < application > tab of the manifest file.

  3. 3.

    Delete the log statement —Different developers will do different things. Some people will bother to check the code and delete statements manually. Some people will write sed or awk programs to delete log statements. Some people will use ProGuard, while others will use third-party tools, such as Timber, to deal with logging activities. Which one you will use depends on yourself; But make sure your users don't accidentally see the log information.

  4. 4.

    Check the permissions of the application —At some point in the development process, you may have tested some functions of the application, and you may have set permissions on the list, such as using the network and writing to external storage. Check the < uses-permission > label on the list to ensure that the game is not granted unnecessary permissions.

  5. 5.

    Check the remote server and URL —If the game depends on web APIs or cloud services, make sure that the release version uses the production URL instead of the test path. During the development process, you may have obtained the sandbox and test URLs, and you need to upgrade them to the production version.

构建发布就绪的应用

During development, Android Studio did quite a few things for you; it

  • 创建了调试证书

  • 将项目的所有资产、配置文件和运行时二进制文件组装到一个 APK 中

  • 使用调试证书签署了 APK

  • 将 APK 部署到模拟器或连接的设备

这些事情都发生在背景;除了写代码,你不需要做任何其他事情。现在,你需要保管好那个证书。Google Play 和其他类似的市场不会发布带有调试证书的应用。它需要是一个适当的证书。不需要去 Thawte 或者 Verisign 这样的认证机构;自签名证书就足够了。此外,请确保保留该证书;当您对应用进行更新时,您需要使用相同的证书对其进行签名。

在接下来的步骤中,您将看到如何生成一个签名包或 APK;您已经知道什么是 APK——它是包含您的应用的包。而是你上传到 Google Play 的内容。另一方面,bundle 很像 APK,但它是一种更新的上传格式。像 APK 一样,它也包括所有应用的编译代码和资源,但它推迟了 APK 一代。这是 Google Play 的新应用服务模式,称为动态交付。它使用您的应用捆绑包为每个用户的设备配置生成和提供优化的 APK,因此他们只需下载运行您的应用所需的代码和资源。您不再需要构建、签署和管理多个 apk。

在 Android Studio 中,生成 APK 和 bundle 的步骤几乎相同。在下面的步骤中,我们将看到如何生成包和 APK。

Launch Android Studio, if you haven’t done so yet. Open the project, then from the main menu bar, go to BuildGenerate Signed Bundle/APK, as shown in Figure 11-1.

![img/340874_4_En_11_Fig1_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ec7f6935526f4bbcbf0c7e41bf56a123~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=Q4YE9MWdiS56d%2FUBE0NiyqtydNI%3D)
Figure 11-1

生成签名的 APK

Choose either Bundle or APK, then click Next; in this example, I chose to create a bundle. When you click Next, you will see the “Keystore” dialog, as shown in Figure 11-2.

![img/340874_4_En_11_Fig2_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a43f0c7348cf4b76a303f1d3bf448b87~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=BF6gw3fRQ6IDXnOSrXF1I987pLg%3D)
Figure 11-2

密钥库对话框

The Key store path is asking where the Java Keystore (JKS) file is. At this point, you don’t have it yet. So, click Create New. You’ll see the dialog window for creating a new keystore, as shown in Figure 11-3.

![img/340874_4_En_11_Fig3_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c4547b2400984557b6771d08191bb151~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=7YT3rc2SLIXHf2tTF%2F6v3p54HQQ%3D)
Figure 11-3

新密钥存储

Table 11-1 shows the description for the input items of the keystore.Table 11-1

密钥库项和描述

|

密钥库项目

|

描述

| | --- | --- | | 密钥库路径 | 要保存密钥库的位置。这完全取决于你。只要确保你记得这个位置 | | 密码 | 这是密钥库的密码 | | 别名 | 此别名标识密钥。这只是它的一个友好的名字 | | (钥匙)密码 | 这是钥匙的密码。这与密钥库的密码不同(但是如果您愿意,也可以使用相同的密码) | | 有效期,以年计 | 默认为 25 年;你可以接受默认值。如果在 Google Play 上发布,证书的有效期必须到 2033 年 10 月,所以 25 年应该没问题 | | 其他信息 | 只有名字和姓氏字段是必需的 |

When you’re done filling up the New Key Store dialog, click OK. This will bring you back to the Generate Signed Bundle or APK window, as shown in Figure 11-4; but now, the JKS file is created and the Keystore dialog is populated with it.

![img/340874_4_En_11_Fig4_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/283c3ec6a7bd490bb0e1896d635d94c1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=wyIlmPT1PDyVb6E7kdogHZvg41Y%3D)
Figure 11-4

生成签名包或 APK,填充

Click Next. Now we choose the destination of the signed bundle as shown in Figure 11-5.

![img/340874_4_En_11_Fig5_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/49dec8685ceb44ccaf02fc3e5a645642~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=bVtsg%2BMm%2FbwRAKpWb0qLJmRPLXQ%3D)
Figure 11-5

签名 APK APK 目的地文件夹

你需要记住“目标文件夹”的位置,如图 11-5 所示。这是 Android Studio 存储签名包的地方。同样,确保构建变体被设置为“发布”

当您点击完成时,Android Studio 将为您的应用生成签名包。这是您将提交给 Google Play 的文件。

发布应用

Before you can submit an app to Google Play, you’ll need a developer account. If you don’t have one yet, you can sign up at developer.android.com. There’s a lot of assumptions I’m making about the next activities. I’m assuming that

  1. 1.

    You already have a Google account (Gmail).

  2. 2 .

    范思哲,范思哲,范思哲【https://developer . Android . com】

  3. 3.

    Your Google account is logged into Chrome.

If your Google account isn’t logged in to Chrome, you might see something like Figure 11-6. Chrome will ask you to go select an account (or create one).

![img/340874_4_En_11_Fig6_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a37e61b812a4b1192e224e69e3daafa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=Ilo9CS0VirfWXs3o%2B%2FAshpp065w%3D)
Figure 11-6

选择一个账户

When you get your Google account sorted out, you’ll be taken to the developer.android.com website, as shown in Figure 11-7.

![img/340874_4_En_11_Fig7_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c4a34b7451bb4ab1b4e9baeb309213e9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=BiKnvV%2F8Kd3N8D4gidxqj7rATdk%3D)
Figure 11-7

developer.android.com

点击 Google Play ,如图 11-7T5。

Click Launch Play Console, as shown in Figure 11-8.

![img/340874_4_En_11_Fig8_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2cfcb5d5e1344736b80a8445e19cef62~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=YMv4NTgDAA%2BjayNuK0e3MNMbMsk%3D)
Figure 11-8

启动游戏控制台

You need to go through four steps to complete the registration, (shown in Figure 11-9):

  • 使用您的 Google 帐户登录。

  • 接受开发者协议。

  • 交报名费。

  • 填写您的帐户详细信息。

![img/340874_4_En_11_Fig9_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6f5939a24b8d490c970395ff993afe14~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=G5WuODhTtqm9Mv3wMaF47sr3jxk%3D)
Figure 11-9

Google Play 控制台,注册

Once you have completed the registration and payment, you will now have access to the Google Play Console, as shown in Figure 11-10.

![img/340874_4_En_11_Fig10_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/080f5c1e523c445d98fb8072551546e2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=2ZwU4hvi3qLSfoAfSQWklXm4T8M%3D)
Figure 11-10

播放控制台

您可以从这里开始向商店提交应用的流程。单击“创建应用”按钮开始。

关键要点

  • 在用户体验你的游戏之前,他们会先看到图标和其他图形资产——确保图形资产和你的代码一样完美。

  • 在构建一个版本之前,去掉代码中的所有调试信息和日志语句。

  • 对你自己的工作进行代码审查。如果你有伙伴或者其他人可以和你一起审查代码,那就更好了。如果您的应用使用服务器、RESTful URLs 等等,请确保它们是生产就绪的,而不是沙箱。

  • 在将你的应用上传到 Google Play 之前,你需要使用适当的证书对你的应用进行签名。

  • 如果你想在 Google Play 上销售你的应用,你需要一个 Google Play 帐户。我一次性支付了 25 美元的费用。

  • 别忘了在真实的设备上测试游戏,尽可能多的种类和大小。

十二、下一步是什么

在学习了 11 章 Android 编程的基础知识、Android Studio、一些游戏开发的理论以及两个从零开始构建的游戏之后,我们正准备做出结论。

我相信你从零开始制作这两款游戏后,已经获得了一些新的自信。当你看到你的作品在模拟器或设备上运行时,那是一种温暖的感觉;但是游戏编程的学习曲线很陡。如今,对游戏质量的要求已经很高了。

In this chapter, we’ll look at some areas of interest that you can add to your game programming arsenal. We’ll cover the following:

  • 安卓 NDK

  • Vulkan 简介和基本设置

  • 游戏引擎和游戏框架

安卓 NDK

你在游戏编程中会遇到的相当多的游戏资源、库、框架甚至引擎都是用 C 或 C++编写的。因此,您需要知道如何很好地使用这些库和语言本身。Android 有办法与 C/C++并肩工作。那是 NDK,是本地开发工具包的缩写。

NDK 是 Android SDK 的一个补充,它让你可以编写 C/C++和汇编代码,然后集成到你的 Android 应用中。NDK 包括一组特定于 Android 的 C 库,一个基于 GNU 编译器集合(GCC) 的交叉编译器工具链,它可以编译 Android 支持的所有不同的 CPU 架构(ARM、x86 和 MIPS),以及一个定制的系统(【https://developer.android.com/ndk/guides/ndk-build),与编写自己的 make 文件相比,它应该会使编译 C/C++代码更容易。

NDK 没有公开大多数 Android APIs,比如 UI 工具包。它主要是为了加速一些代码,这些代码可以通过用 C/C++编写并在 Java 中调用它们而受益。从 Android 2.3 开始,使用 NativeActivity 类代替 Java activities,几乎可以完全绕过 Java。NativeActivity 类是专门为全窗口控制的游戏设计的,但它根本不提供对 Java 的访问,所以它不能与其他基于 Java 的 Android 库一起使用。许多来自 iOS 的游戏开发人员选择这条路线,因为这让他们可以重用 Android 上的大部分 C/C++,而不必深入研究 Android Java APIs。然而,脸书认证或 ads 等服务的集成仍然需要用 Java 来完成,因此将游戏设计为在 Java 中启动并通过 JNI (Java 本地接口)调用 C++通常是最首选的方式。也就是说,如何使用 JNI 呢?

JNI 是让虚拟机与 C/C++代码通信的一种方式。这是双向的;可以从 Java 调用 C/C++代码,也可以从 C/C++调用 Java 方法。Android 的许多库使用这种机制来公开本机代码,如 OpenGL ES 或音频解码器。

Once you use JNI, your application consists of two parts: Java code and C/C++ code. On the Java side, you declare class methods to be implemented in native code by adding a special qualifier called native. The code could look like the one in Listing 12-1.class NativeSample {public native void doSomething(String a);}Listing 12-1

原生样本. java

如您所见,我们声明的方法没有方法体。当运行 Java 代码的 JVM 在方法上看到这个限定符时,它知道相应的实现是在共享库中找到的,而不是在 JAR 文件或 APK 文件中。

共享库非常类似于 Java JAR 文件。它包含编译的 C/C++代码,任何加载这个共享库的程序都可以调用这些代码。在 Windows 上,这些共享库通常带有后缀。dll 在 Unix 系统上,它们以. so 结尾。

On the C/C++ side, we have a lot of header and source files that define the signature of the native methods in C and contain the actual implementation. The header file for our class in the preceding code would look something like Listing 12-2./* DO NOT EDIT THIS FILE - it is machine generated /#include <jni.h>/ Header for class NativeSample /#ifndef _Included_NativeSample#define _Included_NativeSample#ifdef __cplusplusextern "C" {#endif/* Class: NativeSample* Method: doSomething* Signature: (Ljava/lang/String;)V*/JNIEXPORT void JNICALL Java_NativeSample_doSomething(JNIEnv *, jobject, jstring);#ifdef __cplusplus}#endif#endifListing 12-2

NativeSample.h .原始样本

Before Java 10, programmers used javah to generate header files like the preceding code, but javah became obsolete when Java 10 came about. To generate this header files for JNI, we now usejavac NativeSample.java -h .

该工具将一个 Java 类作为输入,并为它找到的任何本机方法生成一个 C 函数签名。这里发生了很多事情,因为 C 代码需要遵循特定的命名模式,并且需要能够将 Java 类型封送到它们对应的 C 类型(例如,Java 的 int 变成了 C 中的 jint)。我们还获得了 JNIEnv 和 jobject 类型的两个附加参数。第一个可以被认为是虚拟机的句柄。它包含与 VM 通信的方法,比如调用类实例的方法。第二个参数是调用该方法的类实例的句柄。我们可以将它与 JNIEnv 参数结合使用,从 C 代码中调用这个类实例的其他方法。

当然,您仍然需要编写实际实现该函数的 C 源文件,并在 Java 代码可以使用它之前编译它。

To install the NDK, you need to go to the SDK manager. If you have an open project in Android Studio, go to Preferences or Settings (Windows and Linux); then choose Android SDK, then check the boxes NDK (Side by side) and CMake, as shown in Figure 12-1, then click OK.

![img/340874_4_En_12_Fig1_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b687b6c788504235a9bc338d3f286d41~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=lK1iKRb8ZVkT59cedUUUhCgMgB0%3D)
Figure 12-1

安装 CMake 和 NDK (并排)

In the window that follows (Figure 12-2.), click OK to confirm the change and proceed.

![img/340874_4_En_12_Fig2_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5e349ec0492b4856a9284d5a62ca5719~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=bi%2FSFKlikhhHk0e0VbNZceA4zvU%3D)
Figure 12-2

确认更改

In the window that follows (Figure 12-3), click Finish.

![img/340874_4_En_12_Fig3_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80f8a4113d6444f08e0e82d070cafa27~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=%2BRxmO5hUk5ygnbZsLBpEjLVqH4E%3D)
Figure 12-3

组件安装程序

现在,您已经准备好在您的项目中使用 NDK 了。

瓦肯

Vulkan 是 Khronos 小组(也是给了我们 OpenGL 的小组)的新 API,它为现代图形卡提供了更好的抽象。这个新接口允许我们更好地描述应用的意图,与现有的 API(如 OpenGL 和 Direct3D)相比,这可以带来更好的性能和更少令人惊讶的驱动程序行为。Vulkan 背后的想法类似于 Direct3D 12(只能在 Windows 上使用)和 Metal (只能在苹果生态系统上使用的图形 API),但 Vulkan 的优势是完全跨平台,允许你同时为 Windows、Linux 和 Android 开发。

这些好处的代价是我们必须使用一个更加冗长的 API。与图形 API 相关的每个细节都需要由您的应用从头开始设置,包括初始帧缓冲区创建和缓冲区和纹理图像等对象的内存管理。图形驱动程序将会减少很多手持操作,这意味着我们需要在应用中做更多的工作来确保正确的行为。

Vulkan 可能不适合所有人。如果你对高性能显卡着迷,并愿意投入一些工作,这可能是你的拿手好戏。另一方面,如果你对游戏开发而不是计算机图形更感兴趣,你可以一直使用 OpenGL ES——它不会很快被弃用而支持 Vulkan。

Android 平台包括一个特定于 Android 的 Vulkan API 实现。

To get started with Vulkan on Android, you can download the LunarG Vulkan repository. You’ll need to download the project from GitHub. You can simply download the git file from github.com/LunarG/VulkanSamples. Click the “Clone or download” button as shown in Figure 12-4.

![img/340874_4_En_12_Fig4_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ed7d2dc9a85e4291ac9bec91e601454b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=EIDs%2FJkkA8KDlFLS1Kvwh0UQF2k%3D)
Figure 12-4

VulkanSamples.git

Or use git on a command line, like this (this was done on a Mac; same commands will work on Linux):mkdir vulkancd vulkangit clone --recursive github.com/LunarG/Vulk… VulkanSamples/API-Samplescmake -DANDROID=ON -DABI_NAME=abicd androidpython3 compile_shaders.pyNote

如果你还没有 Python 3,你需要在你的系统上安装它。可以从 Python 网站www.python.org/downloads/获取。

Next, open Android Studio, if you haven’t launched it yet. Choose FileOpen and select VulkanSamples/API-Samples/android/build.gradle. The project looks like the window shown in Figure 12-5.

![img/340874_4_En_12_Fig5_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc477056265a400385160737ca3cf5e8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=ISOAwqBFuAPL7M1gDbwGPv6jhtw%3D)
Figure 12-5

项目窗格显示导入后的个样本

We need to configure the SDK and NDK directories; to do that, go to FileProject Structure and then ensure that the SDK and NDK locations are set (as shown in Figure 12-6).

![img/340874_4_En_12_Fig6_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/930a213ec9254221b826ab6e8cffa190~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=l%2F2%2FFgo9lPszsLR7FwRhJmLsBuY%3D)
Figure 12-6

项目结构、NDK 和 SDK

如果您的 NDK 尚未设置,请单击下拉箭头(省略号附近,右侧的三个点)。下拉菜单应该建议推荐的目录。如果 Android Studio 没有建议的目录,你需要检查你是否已经安装了 NDK。请参阅本章前面几节中关于 NDK 安装的讨论。

You can now compile the individual modules in the project. Select the project you want to compile in the Project tool window, as shown in Figure 12-7.

![img/340874_4_En_12_Fig7_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9e3c94e5aaf49ff983ac4aaa7355500~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=tF%2BMlOJfFQ5ZiqIoEfwkrY7ABtc%3D)
Figure 12-7

制作模块

From the Build menu, choose Make Module . Resolve any dependency issues, then compile. Most of the samples have simple functionality. The drawcube example is one of the visually interesting examples (shown in Figure 12-8).

![img/340874_4_En_12_Fig8_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/622f2585d0f7425d89d19e1c359682ac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=xIGhfRgBLWexW3Z6dLs5zC7F61E%3D)
Figure 12-8

drawcube 模块

这些关于如何在 Android 中设置 Vulkan 环境的说明来自 Android 开发网站()https://developer . Android . com/ndk/guides/graphics/getting-started);本书付印时,说明可能会改变;因此,在设置 Vulkan 环境时,请务必访问该页面。

游戏引擎和框架

在第六章和 第七章 中,你只看到了一个游戏开发者生活的一小部分,因为我们制作了两个小游戏,但我们是从零开始制作的。尽管游戏的规模不是很大,但就代码行和资产而言,我们必须做所有的事情。我们必须告诉程序从哪里获取图形文件,将它们加载到屏幕上的特定坐标,在游戏中的特定时间播放一些音频,等等。这就像用牙刷粉刷房子一样——是的,你对游戏的每个方面都有很大的控制权,但这也是很大的工作量。你可以打赌,你玩过的大多数 AAA 游戏都不是那样构建的。

大多数现代游戏要么使用游戏框架,要么使用游戏引擎。游戏引擎是一个完整的包。这是一套全面的工具,帮助您从头开始构建游戏。引擎通常包含一些场景或关卡编辑器,导入游戏资源(模型,纹理,声音,精灵等)的工具。)、动画系统以及对游戏逻辑进行编程的脚本语言或 API。您仍然需要编写代码来使用引擎,但是大部分代码将集中在游戏逻辑上。游戏引擎将为您提供系统级样板代码。

Android SDK 为游戏提供了一个不错的框架。还记得我们使用视图对象和 ImageView 对象吗?Android SDK 也提供了一些不错的支持,所以我们可以处理事件,让窗口达到最大尺寸,并在屏幕上绘制一些基本的图形。这些是框架要做的事情;但是除了 Android SDK 提供的框架之外,还有其他框架。

说实话,你并不真的需要游戏引擎,也不需要框架;但是在游戏编程过程中,它们确实让你的生活变得容易多了。在没有引擎或框架的情况下,构建一个不平凡的游戏可能是艰巨而危险的。如果你的最终目标是构建一个游戏,考虑使用第三方工具会更好。

外面有许多框架和引擎;我只编译了那些包含 Android 作为目标平台的应用;并不是所有的公司都会使用 Java 或 Android SDK 进行开发。你应该记住,这个列表并不全面,但它应该让你开始。

结构

HaxeFlixel 。【haxeflixel.com/】[](haxeflixel.com/)

这是一个 2D 游戏框架。您可以在 HTML5、Android、iOS 和桌面上部署它。如果你不介意学习 Haxe 语言,你可以试试这个。

勒夫【https://love 2d . org/

It’s also a 2D framework. You’ll have to use the Lua language, but you can deploy it on Android, iOS, Linux, macOS, and Windows. This framework has already been used on some commercial games; check out Figure 12-9.

![img/340874_4_En_12_Fig9_HTML.jpg](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7e4241a691604e89bcecb2dbbce2a414~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1775488942&x-signature=YdjaAB8krFW8vDOzGFrYR6%2BFiug%3D)
Figure 12-9

用 love 完成的商业游戏

一夫一妻制。【www.monogame.net/】

这是另一个针对 iOS、Windows、Android、macOS、PS4、PSVita、Xbox One 和 Switch 的 2D 框架。使用的语言是 C#(它与 Java 有很多语言元素的相似之处)。

发动机

Cocos2D 。【cocos2d.org/】[](cocos2d.org/)

这是一个针对 Android(开发中)、PC、macOS 和 iOS 的 2D 引擎。根据您的平台,您必须使用 C++、C#或 Objective-C。

铜立方体。【www . ambira . com/copper cube】

这是一个 3D 引擎,你可以用它来运行在 Windows、macOS、Android 和网络上的游戏。它支持 C++、JavaScript 和可视化脚本语言。

去折叠。【www.defold.com/】

如果你不介意使用 Lua 语言,你可以用这个 2D 引擎来瞄准 Windows、macOS、Linux、iOS、Android 和 HTML。

埃森瑟尔。【www.esenthel.com/】

这是一个面向 Windows、Xbox、Mac、Linux、Android、iOS 和 Web 的 2D/3D 引擎。你必须用 C++编写代码。

GameMaker Studio 2 。【www.yoyogames.com/】

这是一个针对 Windows,Mac,Android,iOS,Windows Phone 8,HTML5,Ubuntu,Tizen 和 Windows UWP 的商业 2D 引擎。它使用一种叫做 GML 的定制语言。有一个免费(但有限)的试用。

团结【http://unity 3d . com/

这是一个面向 Windows、macOS、Linux、HTML5、iOS、Android、PS4、XB1、N3DS、Wii U 和 Switch 的 2D/3D 引擎。C#是这里的首选语言。这是免费使用,直到第一个 100,000 美元的收入。查看他们的网站了解更多详情。

虚幻引擎 4 。【www.unrealengine.com/】

你可以针对 Windows,iOS,Mac,PS4,XB1,Switch,HTML5,HoloLens,鲁珉,Android 和 Linux。这是一个 2D/3D 引擎。你必须使用 C++或者 Blueprints 可视化脚本(JavaScript 语言可以和一些插件一起使用)。在该项目盈利超过 100 万美元之前,它是免费使用的。查看网站了解更多详情。

关键要点

在这最后一章,我们学习了一些关于 NDK、Vulkan 和游戏引擎和框架的知识。游戏编程是个大话题;我们只是触及了这本书的表面。我希望你继续你的旅程,构建有趣和引人入胜的游戏。愿原力与你同在!