Marmalade-SDK-移动游戏开发精要-全-

Marmalade SDK 移动游戏开发精要(全)

原文:zh.annas-archive.org/md5/9d559b55e7ff104c2fd7c103507ed1d1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

现代移动设备是一台功能强大的设备。技术已经发展到这样的程度,当前一代的手机和平板电脑能够运行图形和声音都令人印象深刻的游戏和应用,甚至可以与家用游戏机和电脑上的游戏相媲美。

然而,编写能够在众多可用设备上运行的游戏是困难的。有许多不同的平台需要支持(例如,Android 和 iOS),并且每个平台都可以有在能力和处理能力方面有很大差异的设备。

这就是 Marmalade SDK 发挥作用的地方!Marmalade 是一个跨平台解决方案,它允许我们用 C++(大多数视频游戏开发者已经熟悉的语言)编写一次视频游戏的源代码,然后将其部署到包括 iOS、Android 和 BlackBerry PlayBook 在内的多个平台。

在本书中,我们将学习如何使用 Marmalade SDK 来实现现代移动视频游戏所需的所有功能。

本书涵盖的内容

第一章, Marmalade 入门: 我们通过学习如何安装 SDK 并构建一个简单的“Hello World”应用程序开始我们的 Marmalade SDK 之旅。然后我们将发现如何部署和运行最终程序到多个不同的移动平台。

第二章, 资源管理和 2D 图形渲染: 大多数视频游戏都是媒体丰富的体验,充满了出色的图形和惊人的音效和音乐。在本章中,我们首先将探讨 Marmalade 如何通过使用内置的资源管理系统使我们能够轻松地将图形和其他资源加载到内存中。然后我们将发现如何在屏幕上渲染简单的二维图形。

第三章, 用户输入: 我们的游戏需要允许用户提供输入来控制动作;因此,在本章中,我们将探讨如何响应键盘、触摸屏和加速度计输入。

第四章, 3D 图形渲染: 移动设备现在配备了图形处理单元,这使得它们能够轻松渲染美丽的 3D 图形。在简要概述 3D 渲染的基本原理之后,我们将学习如何使用 Marmalade 渲染一个旋转的立方体,首先是通过在代码中生成 3D 模型数据,然后通过发现我们如何可以使用 3D 建模软件包创建和导出可以在 Marmalade 应用程序中加载和绘制的 3D 模型。

第五章, 动画 3D 图形: 在前一章的基础上,我们将介绍如何通过使 3D 图形动画化来使其更有趣。

第六章,实现字体、用户界面和本地化:在用户甚至能够玩我们的游戏的第一关之前,他们首先需要导航其菜单系统。本章涵盖了 Marmalade 对字体渲染的支持、用户界面可以构建的方式,并以查看如何本地化游戏以支持多种语言结束。

第七章,添加声音和视频:声音和音乐都是视频游戏非常重要的方面,可以使游戏感觉更加沉浸和刺激,因此学习如何将这些元素添加到游戏中是本章的主要目标。Marmalade 还允许我们显示全动作视频剪辑,因此我们也会简要地看看这一点。

第八章,支持广泛的设备:不同的移动设备具有不同的功能,在主要处理器和图形渲染能力方面也可能有所不同。在本章中,我们将探讨 Marmalade 如何通过允许我们的游戏适应其运行的硬件,帮助我们支持尽可能广泛的设备。我们还将查看使用 Marmalade 内置的压缩和解压缩文件支持来减小游戏安装包大小的功能。

第九章,添加社交媒体和其他在线服务:由于大多数设备现在都永久连接到互联网,本章探讨了我们可以添加到我们的游戏中的在线功能的一些选项,从与 Facebook 集成到显示广告作为可能的收入来源。

第十章扩展 Marmalade"),使用扩展开发工具包(EDK)扩展 Marmalade:虽然 Marmalade SDK 在标准化编写视频游戏的大部分正常要求方面做得很好,例如显示图形或播放声音,但有时会出现需要访问 Marmalade 不支持直接的功能的设备功能。本章展示了我们如何访问 Windows、iOS 和 Android 上的底层 SDK,以便访问我们否则无法访问的设备功能。

你需要这本书的东西

为了充分利用本书的内容,你需要以下几样东西:

  • 至少 1GB RAM 的互联网连接 PC,运行 Windows XP Service Pack 2、Vista 或 Windows 7

  • Microsoft Visual Studio 2005/2008/2010(C++ Express 版本适合此用途)

  • Marmalade SDK 的授权副本

第一章将解释如何安装合适的 Microsoft Visual Studio 版本,以及如何获取 Marmalade 并为其购买许可证。

对于制作 iOS 部署,你还需要注册 Apple 的 iOS 开发者计划;有关此内容的更多详细信息也提供在第一章中。

对于 3D 图形章节,如果你有支持的建模软件包,那将是有益的。Marmalade 直接支持 Autodesk 3DS Max 和 Autodesk Maya,但一个更便宜的替代方案是开源的 Blender 软件包。

本书面向对象

本书旨在向您展示如何使用 Marmalade 来实现视频游戏所需的功能。它不是编写视频游戏的指南,尽管本书提供了与每个章节同步增长的简单游戏的示例代码。

由于 Marmalade SDK 是用 C++实现的,因此你预计已经能够编写这种编程语言。

你还应该对涉及 2D 和 3D 图形渲染的概念有实际了解。相关章节提供了简要概述,但它们仅作为复习和介绍书中后续部分使用的术语。

惯例

在这本书中,你会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些风格的示例,以及它们含义的解释。

文本中的代码词汇将如下所示:“用于platform的值通常是操作系统的名称。”

代码块将以如下方式设置:

{OS=IPHONE}
Message="Hello iOS!"
{}
{OS=QNX}
Message="Hello BlackBerry!"
{}

当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:

{OS=IPHONE}
Message="Hello iOS!"
{}
{OS=QNX}
Message="Hello BlackBerry!"
{}

任何命令行输入或输出将如下所示:

C:\PlayBook> blackberry-debugtokenrequest -cskpass <password> -keystoresigtool.p12 -storepass <password> -deviceId 0x<device id> debugtoken.bar

新术语重要词汇将以粗体显示。你在屏幕上看到的,例如在菜单或对话框中的文字,将以如下方式显示:“”

注意

警告或重要注意事项将以如下方式显示。

小贴士

小贴士和技巧将以如下方式显示。

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大收益的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

您可以从www.PacktPub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support并注册,以便直接将文件通过电子邮件发送给您。

下载本书彩色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/3363OT_images.pdf下载此文件。

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中找到错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/support,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站,或添加到该标题的错误清单中。任何现有的错误清单都可以通过从www.packtpub.com/support选择您的标题来查看。

盗版

网络上版权材料的盗版是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过链接mailto:copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

咨询

如果您在本书的任何方面遇到问题,可以通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决。

第一章. Marmalade 入门

在本章中,我们将首先学习如何为开发设置 Marmalade SDK。虽然 Marmalade 既有 Windows 版本也有 Mac 版本,但 Windows 版本是两个版本中开发最完善的,也是本书将主要涵盖的内容。在本章结束时,我们将知道如何完成以下操作:

  • 使用 Marmalade SDK 设置 Windows PC 以进行开发

  • 创建并构建 "Hello World" 项目

  • 在多个移动平台上部署和运行 "Hello World" 项目

所以,无需多言,让我们开始吧!

安装 Marmalade SDK

以下部分将向您展示如何使用 Marmalade 将您的 PC 设置为开发环境,从安装合适的开发环境到许可、下载和安装您的 Marmalade 版本。

安装开发环境

在我们开始编码之前,我们首先需要安装 Microsoft Visual C++ 的一个版本,这是 Marmalade 使用的 Windows 开发环境。如果您还没有安装版本,您可以免费下载一个副本。在撰写本文时,Express 2012 版本刚刚发布,但 Marmalade 直接支持的最新免费版本仍然是 Visual C++ 2010 Express,可以从以下 URL 下载:

www.microsoft.com/visualstudio/en-us/products/2010-editions/visual-cpp-express

按照此网页上的说明下载和安装产品。

注意

对于 Marmalade 的 Apple Mac 版本,支持的开发环境是 Xcode,它可以从 Mac App Store 免费下载。在本书中,我们将假设使用的是 Marmalade 的 Windows 版本,除非特别说明。

选择您的 Marmalade 许可类型

在设置好合适的开发环境后,我们现在可以开始下载 Marmalade 本身。首先,您需要使用以下 URL 访问 Marmalade 网站:

www.madewithmarmalade.com

网站顶部有两个按钮,分别标有 购买免费试用。点击其中一个(哪个都行,因为它们都指向同一个地方!)您将看到一个页面,解释许可选项,这些选项也在以下表格中描述:

许可类型 描述
评估版 这是免费的,但有时间限制(目前为 45 天),虽然您可以将其部署到所有支持的平台,但您不允许分发使用此版本构建的应用程序。
Community 这是开始使用 Marmalade 最低成本的方式,但您只能将其发布在 iOS 和 Android 上,并且您的应用程序在启动时也会显示 Marmalade 启动画面。
Indie 此版本消除了基本许可证的限制,没有启动画面,并且可以针对任何受支持的平台。
Professional 此版本在开发过程中遇到任何问题时,提供 Marmalade 的专属支持,并允许提前访问 Marmalade 的新版本。

当您选择了许可证级别后,您首先需要通过提供电子邮件地址和密码在 Marmalade 网站上注册。

注意

您注册的电子邮件地址将与您的许可证相关联,并将在稍后用于激活。请确保在注册时使用有效的电子邮件地址。

一旦您完成注册,您将被带到网页,您可以在此选择所需的许可证级别。确认付款后,您将收到一封电子邮件,允许您激活许可证并下载 Marmalade 安装程序。

下载和安装 Marmalade

现在您已经拥有有效的许可证,请使用我们之前使用的相同 URL 返回 Marmalade 网站。

  1. 如果您尚未登录到网站,请使用网页右上角的登录链接进行登录。

  2. 点击下载按钮,您将被带到可以下载 Marmalade 安装程序最新和以前版本的页面。点击您所需的版本按钮,开始下载。下载完成后,运行安装程序并按照说明操作。安装程序将首先要求您通过选择单选按钮接受最终用户许可协议,然后要求输入安装位置。

  3. 接下来,输入您想要安装的文件位置。默认安装目录会丢弃次要修订号(因此版本 6.1.1 将安装到名为 6.1 的子目录中)。您可能希望将次要修订号重新添加回去,以便更容易同时安装多个版本的 Marmalade。

  4. 一旦安装程序将文件复制到您的硬盘驱动器后,它将显示 Marmalade 配置实用程序,这在下一节中有更详细的描述。一旦配置实用程序关闭,安装程序将提供选项,在退出前启动一些有用的资源,例如 SDK 文档。

    注意

    同时安装多个版本的 Marmalade SDK 并按需切换版本是可能的,因此有关安装目录的建议变得非常有用。当 Marmalade 的新版本修复了特定设备的 bug,但您仍然需要支持需要不同版本 Marmalade 的旧项目时,这一点尤其有用。

使用 Marmalade 配置实用程序

Marmalade 配置实用程序窗口在安装过程结束时出现,但也可以通过其快捷图标启动:

使用 Marmalade 配置实用程序

注意

在 Windows Vista 或 Windows 7 上启动 Marmalade 配置实用程序时,您应该在图标上右键单击并选择 以管理员身份运行 选项,否则您所做的任何更改可能不会被应用。

最重要元素是 License Information 框。在此框下方有一个标签为 Activate License... 的按钮,允许您激活您的 Marmalade 安装。按照以下步骤进行激活:

  1. 点击 Activate License... 按钮以显示一个对话框,该对话框要求您输入您在获取许可证时使用的电子邮件地址和密码。

  2. 对话框还有一个带有标签 Machine ID (Ethernet MAC address) 的下拉框,您应该确保将其设置为始终存在于您计算机上的以太网端口的 MAC 地址。通常您不需要更改此设置。

  3. 点击 OK 按钮连接到 Marmalade 许可证服务器。您将被要求选择您想要安装的许可证。(通常只有一个选项可用。)选择它并点击 OK

  4. 将显示适用于您所使用许可证类型的 End User License Agreement(EULA)摘要,因此请点击 OK 以接受它。对话框中也提供了完整 EULA 的引用。

  5. 您现在应该会看到一个确认成功安装许可证的消息。此时 Marmalade 准备就绪!

在我们完成之前,让我们看看其他可用的选项。第一个选项标签为 Marmalade System (S3E) Base,它由一个下拉框组成,允许您选择您想要使用的 Marmalade SDK 版本,当然,如果您安装了多个版本的话!

注意

S3E 是 Segundo Embedded Execution Environment 的缩写,这是 Marmalade SDK 的最低层。这个命名约定是在 SDK 开发初期采用的,并且一直沿用至今。正如您将在本书后面看到的那样,有许多以这个名称为前缀的 API。

Default Build Environment 允许您选择您希望使用的开发环境,假设您安装了多个受支持的 Visual C++ 版本。

带有标签 RVCT ARM Compiler 的下拉框允许您更改在制作设备构建时将使用的编译器。(大多数移动设备都包含 ARM CPU,因此我们必须为这种处理器类型编译我们的代码。)Marmalade 随 GCC 编译器一起发货,并默认使用它,但它也可以使用 ARM 的 RVCT C++ 编译器,这是一项额外购买,可以生成更好的优化代码。我们通常不需要更改此设置,可以将其保留在第一个选项 不使用 RVCT 上。

Advanced Options... 按钮提供了访问更详细的项目构建选项以及 SDK 的一些实验性部分。您通常不需要在此处进行任何更改。

管理您的 Marmalade 账户和许可证

在我们开始实际编码之前,值得提一下如何管理您的 Marmalade 许可证和账户。如果您回到 Marmalade 网站并登录,您会注意到网站右上角有一个标记为我的账户的链接。

将鼠标指针悬停在链接上,将出现一个选项菜单,允许您更改账户详情和许可证使用情况。以下各节提供了有关这些选项的更多信息。

查看您的账户概览

菜单选项概览将带您到一个页面,您可以在其中查看您的个人详细信息以及您账户下设置的许可证和用户的总结。从该屏幕,有按钮允许您更新您的个人资料,修改注册用户信息,购买新的许可证,以及管理现有的许可证。

更新您的个人资料信息

点击我的账户菜单中的个人资料选项或点击个人资料概览屏幕上的更新个人资料信息按钮,将显示一个页面,允许您更改诸如您的姓名、联系信息、地址和账户登录密码等信息。还有一个复选框,允许您订阅 Marmalade 的电子邮件新闻更新。

管理您的许可证

点击我的账户菜单中的许可证链接或在概览屏幕上点击标记为管理的按钮,将带您到一个页面,您可以在其中升级许可证级别或为新的团队成员购买更多许可证。

本页底部的管理许可证部分显示了您账户中所有当前活跃的许可证,并允许您释放当前正在使用的许可证,以便将其转移到另一台电脑。

如果您需要因某些原因在另一台电脑上工作,或者如果您有一台新的开发电脑希望将许可证转移到上面,那么释放许可证是有用的。您可以随时释放许可证,但单个许可证一次只能在一台电脑上使用。

管理您的用户列表

如果您在一个团队中工作,那么您显然需要不止一个 Marmalade 许可证,但您还需要管理谁可以访问这些许可证。点击我的账户菜单中的用户选项或点击概览页面上的管理用户按钮,您就可以这样做。

此页面显示分配给您的所有用户的列表,还有一个邀请用户部分,允许您将新用户添加到您的账户。在提供的框中输入他们的电子邮件地址,然后点击发送邀请按钮,发送一封邮件告诉他们如何激活他们自己的 Marmalade 账户。

创建 Marmalade 项目

安装了 Marmalade 后,我们现在可以开始进行一些编码工作了,还有什么比从经典的“Hello World”程序开始更好的地方呢。

创建“Hello World”项目

要开始一个新的项目,我们首先必须创建一个 MKB 文件,这是 Marmalade 的自有项目文件格式。我们使用 MKB 文件来指定构建我们的项目所需的所有源文件、库和构建选项。

MKB 文件实际上用于生成一个 Visual C++ 项目文件,但每次我们想要向我们的项目添加或删除源文件时,我们必须通过编辑 MKB 文件,并使用 Marmalade 的 makefile 构建脚本从它重新生成 Visual C++ 项目文件,我们将在稍后查看这个脚本。

除了源文件外,MKB 文件还允许你列出你的应用程序运行所需的所有其他数据文件和资源文件,因为这些信息在将你的应用程序部署到不同的移动平台时将会被需要。

Marmalade 确实附带了一个名为 LaunchPad 的小工具,可以用来创建新项目,但为了学习如何构建 Marmalade 项目,我们将从头开始创建一切。

小贴士

下载示例代码

你可以从你在 www.PacktPub.com 的账户中下载你购买的所有 Packt 书籍的示例代码文件。如果你在其他地方购买了这本书,你可以访问 www.PacktPub.com/support 并注册以直接将文件通过电子邮件发送给你。

“Hello World”项目的 MKB 文件

让我们从“Hello World”项目开始。创建一个新目录来存放项目文件,然后在这个目录中创建一个名为 Hello.mkb 的文件,并将以下内容输入到其中:

#
# Main MKB file for Hello project
#

# Modules used by this project
subprojects
{
  iwgx
}

# The files that make up the project (source, data etc.)
files
{
  [SourceCode]
  (source)

  Main.cpp
}

# Settings to configure the deployment process 
deployments
{
  name="Hello"
  caption="Hello World"
}

Hello.mkb 的第一个部分是 subprojects 部分,它列出了我们的应用程序使用的所有附加代码模块。在这个例子中,一个代码模块是一个可以作为一组 C 或 C++ 源文件添加到我们的项目中的库,或者作为与头文件一起的预编译、可链接的对象文件。在先前的例子中,只有一个,iwgx,这是负责渲染图形的 Marmalade 代码模块。

Marmalade 中的所有高级模块都以这种方式引用,你也可以使用这个系统创建自己的模块,以实现跨项目代码的重用。要创建一个子项目模块,我们使用一个 MKF 文件,这实际上比 MKB 文件多了一个不同的文件扩展名!当我们向 subprojects 列表中添加条目时,Marmalade 的 makefile 构建脚本将搜索一个合适的 MKF 文件,该文件描述了每个子项目。我们将在本书后面的详细示例中看到如何做到这一点。

下一个部分被标记为 files,这是我们列出我们项目中的所有源代码文件的地方。你有可能将你的源文件拆分到不同的目录中。为了方便,你只需在括号中放入目录名(在我们的例子中是 (source)),然后列出目录下的所有源文件。

还可以将相关的文件分组到子部分中,我们使用方括号(例如我们示例中的[SourceCode])来做到这一点。任何位于此以下的源文件都将被添加到该部分,并随后在 Visual C++解决方案资源管理器中的单独文件夹中显示。不需要目录和组名匹配,实际上,如果你愿意,一个组中可以有多个目录。

最后,我们来到了deployments部分,这是设置各种控制将我们的应用程序部署到不同设备类型的各种设置的地点。

在我们的示例中,我们设置了两个设置。name设置提供了我们最终可执行文件的文件名,并且也被用于 Marmalade 为我们创建的文件和目录名,而caption设置则在设备上安装应用程序图标时显示的名称。

上述两种设置都是适用于所有设备类型的通用设置示例,但还有许多其他设置可用,这些设置特定于特定平台,如 iOS 或 Android。这些设置的完整列表可以在作为 Marmalade SDK 一部分安装的Marmalade Documentation帮助文件中找到,我们还将在本书的第八章“支持广泛的设备”以及尚未为这个示例展示的 MKB 文件的附加部分中查看。

在 MKB 文件中使用空白完全取决于你个人的喜好。尽管大多数 Marmalade 示例倾向于在块内缩进条目,但也可以使用制表符或空格。

也可以使用井号(#)字符添加注释。然后,从井号字符到当前行末尾的所有内容都被视为注释。

"Hello World"项目的源文件

现在,我们可以为我们的项目使用 MKB 文件,但我们仍然无法对它做任何事情,因为我们已经告诉 Marmalade 存在一个名为Main.cpp的源文件,而这个文件还不存在。如果我们尝试使用 MKB 文件来构建项目,我们会收到关于此缺失文件的错误报告,所以让我们创建它。

你会记得我们说过,我们的Main.cpp文件将位于 MKB 文件中的source目录下,因此首先在项目目录中创建这个新的子目录。现在,在源目录中创建一个名为Main.cpp的文件,并将以下内容输入其中:

//---------------------------------------------------------
// Learning Mobile Game Development with Marmalade
// Chapter 1 - Hello
//---------------------------------------------------------

// Marmalade SDK includes
#include "IwGx.h"
#include "s3eConfig.h"
#include "s3eDevice.h"

//---------------------------------------------------------
// Main entry point
//---------------------------------------------------------
int main()
{
  // Initialise Marmalade modules
  IwGxInit();

  // Set a default message, then check the ICF file to see if
  // a proper message has been specified
  char lMessage[S3E_CONFIG_STRING_MAX] = "Hello!";
  s3eConfigGetString("APP", "Message", lMessage);

  // Set screen clear colour to black
  IwGxSetColClear(0, 0, 0, 255);

  // Draw text at double default size
  IwGxPrintSetScale(2);

  // Loop until we receive a quit message
  while (!s3eDeviceCheckQuitRequest())
  {
    // Allow device to process its internal events
    s3eDeviceYield(0);

    // Clear the screen
    IwGxClear();

    // Display our message on screen
    IwGxPrintString(10, 10, lMessage);

    // Flush IwGx draw buffers to screen
    IwGxFlush();

    // Swap screen double buffer
    IwGxSwapBuffers();
  }

  // Terminate Marmalade modules
  IwGxTerminate();

  return 0;
}

代码应该相当简单易懂,但这里有一个快速分解。

首先,我们引用包含文件,以便我们可以使用 Marmalade 中对我们应用程序必要的部分,然后我们创建我们的主要入口点函数main。这相当于标准 C 或 C++程序中的main()函数,但因为它不接受参数,因为 Marmalade 不接受命令行参数。在移动设备上指定命令行参数相当困难,所以实际上并没有必要!

我们应用程序需要做的第一件事是通过调用 IwGxInit() 初始化 Marmalade 的渲染模块,这将初始化屏幕并设置标准行为,例如显示的双缓冲。

接下来,我们分配一个字符缓冲区,它将包含我们将在屏幕上显示的消息。我们将其初始化为默认消息以确保有内容显示,但随后我们使用对 s3eConfigGetString 函数的调用来查看是否在应用程序的配置文件中指定了另一条消息,这将在稍后详细解释。

以下对 IwGxSetColClear 的调用将所需的屏幕背景色设置为黑色,然后对 IwGxPrintSetScale 的调用告诉 Marmalade 使用其内置字体(字体大小相当小)以双倍默认分辨率显示文本。

现在我们进入主处理循环,它将持续进行,直到 s3eDeviceCheckQuitRequest 函数返回一个真值,这将在用户退出应用程序或设备因任何原因向应用程序发送退出请求时发生。

我们主循环的第一行是对 s3eDeviceYield 的调用。这是一个非常重要的函数,必须在应用程序运行期间定期调用,以便允许设备的操作系统执行重要任务,如处理事件——用户输入、来电等。在大多数情况下,主循环中对该函数的单次调用就足够了。

传递给 s3eDeviceYield 的值是我们应用程序将让给操作系统处理的最大时间(以毫秒为单位)。通常这个值被设置为零,这足以让设备处理事件,但一旦所有事件都处理完毕,就会将控制权返回给我们的应用程序。

接下来,我们调用 IwGxClear 清除屏幕,然后使用 IwGxPrintString 在屏幕上显示一条消息。IwGxFlush 导致 Marmalade 引擎处理所有我们的绘图请求,以生成最终的屏幕图像,然后我们可以使用 IwGxSwapBuffers 将其显示给世界。

最后,在主循环之外,我们调用 IwGxTerminate 关闭 Marmalade 的渲染引擎,并最终返回零以指示我们的应用程序在没有任何错误的情况下完成。

构建 "Hello World" 项目

现在我们已经设置了项目并编写了必要的代码,现在是时候构建它了。为此,打开一个 Windows 资源管理器窗口,导航到包含 Hello.mkb 文件的文件夹,然后只需双击该文件。你可能看到命令提示符窗口的短暂闪烁,但经过短暂的延迟后,Visual C++ 应该会自动启动并打开我们的项目。

双击 MKB 文件实际上会执行 Marmalade 的 makefile 构建脚本。这是一个用 Python 语言编写的脚本,它接收 MKB 文件并输出一个 Visual C++解决方案文件和其他必需的元素。在安装 Marmalade 时,会自动设置文件关联,因此你可以双击文件,或者使用命令提示符通过切换到项目目录并输入Hello.mkb来创建你的项目。

在我们继续编译和运行项目之前,让我们快速看一下 Marmalade 为我们创建了什么。

如果你查看项目目录,应该有两个新目录,以下部分将描述它们。

构建目录

MKB 文件创建的目录之一将被命名为build_hello_vcxx,其中“xx”部分取决于你使用的 Visual C++版本。

此目录是 Marmalade 的工作目录,所有在构建过程中创建的对象文件都存储在这里。在制作设备构建时,它也将成为我们的部署包的存放地。

从 MKB 文件创建的 Visual C++解决方案文件也位于此目录中,虽然你可以使用这些文件在项目之间切换,但你绝不应该使用 Visual C++ IDE 中的选项添加文件或更改项目设置。

注意

总是修改 MKB 文件中的项目更改,然后关闭 Visual C++并双击 MKB 文件以重新构建解决方案,或者使用命令提示符在 Visual C++内部执行构建以更新解决方案文件中的任何更改。你不应该在 Visual C++ IDE 中直接进行更改,因为它们将在下一次使用 MKB 文件重新创建项目文件时丢失。

数据目录

MKB 文件还会生成一个名为data的目录,这是 Marmalade 要求你放置任何应用程序需要加载的文件的地方,例如图像、3D 模型数据、声音文件等。虽然你可以自己创建此目录和这些文件,而且不会造成问题,但我们还是让 makefile 构建器为我们完成这项工作吧!

如果你查看data目录,你会看到构建过程还创建了两个名为app.icfapp.config.txt的文件。这些文件用于配置你的应用程序,并在以下部分中解释。

app.config.txt 文件

此文件提供了在app.icf文件中可以设置的所有应用程序特定设置的列表,以及每个设置的作用描述和如何使用。使用此文件有两个原因:

  1. 向此文件添加条目可以保持你的项目设置在一个地方记录,这样其他编码者可以检查此文件以了解特定设置的作用。

  2. app.icf文件中包含的任何设置,如果不在app.config.txt文件中记录,当你尝试在程序中指定或访问它时,将会生成一个运行时错误信息。

此外,app.config.txt文件还要求你为你的设置定义一个组名,这可以通过使用方括号来指定。

如果你查看“Hello World”项目代码中的s3eConfigGetString函数调用,你将看到这个示例。这个调用试图从APP组中访问一个名为Message的设置,所以现在让我们将它添加到app.config.txt文件中,以防止在运行我们的应用程序时触发任何断言。编辑文件并在底部添加以下行:

[APP]
Message      The message we want to display on screen

app.icf 文件

app.icf文件用于将配置设置添加到你的应用程序中,如前所述,这些设置必须在你的项目app.config.txt文件中记录,或者可以定义在应用程序使用的子项目中的一个类似文件中。

添加配置设置只是简单地添加设置名称后跟一个等号,然后是你希望使用的值。你还必须确保使用与app.config.txt文件中相同的方括号符号将设置添加到正确的组中。以下是一个示例:

[APP]
Message="Hello World"

app.icf文件中创建的设置可以通过s3eConfigGetInts3eConfigGetString函数在代码中访问。

app.icf文件还有一些其他技巧,因为也可以创建特定于某个平台或甚至特定于单个设备的设置。以下是实现方法:

  • 要将应用程序限制在特定的平台上,添加行{OS=platform},并且在此之后的所有设置将仅适用于该设备平台。platform值通常是操作系统的名称,例如ANDROIDBADAWINDOWS,尽管值得提一下,你应该使用IPHONE来指代 iPhone、iPod 和 iPad!如果有疑问,你可以使用调用s3eDeviceGetString(S3E_DEVICE_OS)来发现特定操作系统需要使用的值。

  • 要将应用程序限制在特定的设备或设备上,添加行{ID=platform id},并且任何后续设置只有在运行在指定的设备上时才会应用。platform值与之前使用的相同,而id是特定设备的标识符。id值的格式取决于操作系统,但你可以通过调用s3eDeviceGetString(S3E_DEVICE_ID)来发现特定设备应使用什么值。如果你需要将设置应用于多个设备,也可以提供以逗号分隔的id值列表。

注意,这两个设置将继续生效,直到指定了新的操作系统或 ID 值。如果你希望将所有设置全局应用,只需在你最后的操作系统或 ID 特定设置后添加{}

小贴士

确保您的 OS-或 ID 特定的部分始终以{}结束是一个好习惯,否则在部署到设备并发现您刚刚更改的一些设置似乎没有生效时,可能会导致您大伤脑筋。

为了说明{}的使用,让我们向“Hello World”项目的app.icf文件添加一些设置。打开文件,将其以下几行添加到文件底部:

[APP]
{OS=ANDROID}
Message="Hello Android!"
{}

{OS=BADA}
Message="Hello Bada!"
{}

{OS=IPHONE}
Message="Hello iOS!"
{}

{OS=QNX}
Message="Hello BlackBerry!"
{}

{OS=WINDOWS}
Message="Hello Windows!"
{}

您应该能从中看出,我们已经为每个我们希望支持的不同的平台类型指定了不同的消息字符串。

在 Windows 模拟器中构建和运行

现在是时候看看“Hello World”项目在实际中的运行情况了。这只是一个简单的问题,就是在 Visual C++中编译代码并运行它。

要编译代码,只需选择构建 | 构建解决方案或按F7键。Visual C++将编译并链接Main.cpp文件。

现在我们可以执行程序了。选择调试 | 开始调试或简单地按F5。Marmalade Windows 模拟器将被启动,它将加载并执行我们的程序。以下图像显示了在 Windows 模拟器中运行时“Hello World”项目应该看起来像什么:

在 Windows 模拟器中构建和运行

您会注意到 Windows 模拟器包含许多菜单选项。这些选项允许 Windows 模拟器尽可能接近您选择的任何设备运行。最好的方法是亲自探索菜单选项,但以下是一些您可能想做的更有用的事情:

  • 加速度计:在 Windows 上测试加速度计输入是不可能的,没有配置 | 加速度计...。这将弹出一个对话框,允许您使用鼠标旋转手机的 3D 图像,以模拟加速度计输入。

  • OpenGL ES 版本:Windows 模拟器选项配置 | GL...允许您模拟不同版本的 OpenGL ES,这使得您能够轻松地看到您的游戏在不同类型的硬件上可能看起来像什么。它还允许您完全禁用 OpenGL ES 支持,这将迫使 Marmalade 使用其内置的软件渲染器。

  • 屏幕分辨率:移动设备支持广泛的屏幕分辨率,因此配置 | 表面...允许您选择您想要的任何屏幕大小。

  • 设备挂起和恢复的模拟:很容易忘记许多设备的主要功能实际上是电话而不是游戏平台,这意味着你的应用程序可能会在任何时候被来电中断。Marmalade 负责处理大多数自动处理这种情况的繁琐细节,但仍然可能存在需要在这种情况下执行特殊操作的情况。Windows 模拟器允许你通过事件 | 模拟挂起事件 | 模拟恢复来测试你的应用程序是否正确响应。

部署 Marmalade 项目

我们现在已经成功创建并运行了我们的第一个 Marmalade 应用程序,但将其在 Windows 上运行并不是我们的最终目标。Marmalade SDK 的整个目的就是为了让我们能够一次性开发应用程序,然后将其部署到一系列移动设备平台。

当然,我们可能需要更改一些资产,例如,因为我们针对的是广泛的屏幕分辨率,希望我们的应用程序始终看起来最佳,代码本身应该不需要修改才能成功运行。

为了说明这一点,我们现在将“Hello World”项目部署到多个不同的移动设备平台。

为 ARM CPU 编译“Hello World”项目

在 Windows 上运行我们的项目意味着我们使用标准的 Visual C++编译器编译代码,因此生成 Intel x86 代码。然而,一个事实是,今天大多数可用的移动设备都包含某种版本的 ARM 处理器,因此我们首先需要为 ARM 编译我们的代码。

幸运的是,Marmalade 为我们简化了这一点。在 Visual C++窗口的顶部,你应该看到一个默认设置为(x86)调试的下拉菜单。

如果你打开下拉菜单,你会看到为预配置了几个针对我们的构建类型,但我们感兴趣的是GCC (ARM) Release选项。选择此选项并再次构建解决方案(构建 | 构建解决方案或按F7),Visual C++将使用 GCC 编译器创建我们应用程序的 ARM 版本。

现在我们只需要将代码上传到设备上!

部署“Hello World”项目

现在我们已经有了代码的 ARM 编译版本,我们需要创建一个安装包,以便我们可以在真实移动设备上测试它。为此,我们需要使用Marmalade 系统部署工具。按照以下步骤进行部署过程:

  1. 要启动工具,确保选择了GCC (ARM) Release构建类型并且代码已经编译。选择调试 | 开始调试(或按F5)。由于 Visual C++调试器只能调试 Intel x86 可执行文件,所以没有太多意义,Marmalade 系统部署工具将被启动。部署“Hello World”项目

  2. 程序将首先要求我们选择要部署的构建类型,并将有多个单选按钮可供选择。只有当前已构建的构建类型才是可选择的。在我们的情况下,我们需要选择 ARM GCC 发布 单选按钮,然后点击标有 下一阶段 > 的按钮,以继续到下一步。

  3. 下一页要求我们选择部署配置。我们可以指定我们想要创建部署包文件的目录,并且还有一个可供选择的可用部署配置的复选框列表。Marmalade 允许我们创建不同的配置,这意味着我们可以将不同的资源包部署到不同的设备上。目前我们不会关注这些,所以请确保 默认 设置被选中,然后再次点击 下一阶段 > 按钮。

  4. 现在,我们面前是一个列出我们可以部署的所有设备类型的页面。使用复选框选择要部署的平台,然后再次点击 下一阶段 >,即可进入最终页面。

  5. 在最后一页的顶部,我们可以看到即将制作的不同的部署类型的简要描述,每个都有下拉框,用于指定我们是否只需生成必要的包文件,完全忽略该构建类型,或者可选地安装并运行包。

    注意

    Marmalade 系统配置工具中可用的 包和安装 选项通常依赖于你的系统已经设置了额外的第三方软件,这些软件不是作为 Marmalade SDK 的一部分自动安装的。因此,在这本书中,我们将一般保持使用 选项,并使用手动方法安装和运行部署包。

  6. 既然我们已经配置了想要的部署类型,只需按下 部署全部 按钮,Marmalade 将为所有选定的不同目标创建包。

默认的部署包位置位于 Marmalade 的 Build 目录中。如果你使用 Windows 资源管理器查看此目录,你会看到已创建一个名为 deployments 的新目录。反过来,此目录包含一个名为 default 的文件夹,它来自我们使用的部署配置。

注意

可以通过点击部署实用程序的最终页面上的 探索… 按钮来打开 Windows 资源管理器到部署文件夹。

default 目录包含我们选择的每个部署平台的子目录,并且每个子目录都将包含一个 release 目录,因为我们是从发布构建创建部署的。请注意,也可以部署调试构建,这在调试时可能很有用。进入 release 文件夹,在那里你会找到我们刚刚制作的部署包。

现在剩下的只是安装并在设备上运行它。

在 Android 设备上安装

让我们先看看如何安装 Android 构建。

注意

在能够使用部署工具进行 Android 部署之前,有一个先决条件,即必须安装 Java JDK。你可以从以下网页下载它:

www.oracle.com/technetwork/java/javase/downloads/index.html

"Hello World"项目的 Android 包文件名为Hello.apk,要安装它,我们首先需要将其复制到 Android 设备上。这可以通过将文件复制到 SD 卡上完成,或者如果你的设备有内置存储内存,可以将文件复制到那里。

在我们能够安装我们的包之前,我们首先需要确保 Android 设备允许我们这样做。进入你的设备的设置程序并选择应用程序选项。在这里有一个未知来源选项,它允许我们安装自己的包。确保这个选项被勾选。

接下来,在你的设备上找到文件管理应用程序。不同的设备可能为这个应用程序有不同的名称,但通常它有一个图标,上面有一个档案文件夹的图片。导航目录以找到你复制的Hello.apk文件的位置,然后点击列表中的文件条目。

屏幕将切换以显示应用程序请求访问的大列表,以及安装取消按钮。点击安装按钮,包将被安装。然后你可以选择打开按钮来启动你的应用程序,或者如果你现在不想运行它,可以选择完成按钮。点击打开,我们应该会看到我们的Hello Android消息。

注意

通过安装 Android SDK,还可能通过允许部署工具自动打包、安装和运行部署的包来加速设备上的测试。关于如何设置 Android SDK 以使其工作的说明可以在本书的第十章扩展 Marmalade") 使用扩展开发工具包(EDK)扩展 Marmalade 中找到。

在 iOS 设备上安装

如果你尝试创建"Hello World"项目的 iOS 构建,你会注意到它目前无法完成,因为签名错误。这是因为你需要向 Marmalade 提供一些证书文件,这些文件只能通过成为注册的苹果开发者来生成。

加入iOS 开发者计划目前每年需要 99 美元,你可以在以下网页上找到更多详细信息:

developer.apple.com/programs/which-program/

一旦注册,您将能够访问iOS Dev Center,这将允许您创建所需的证书。通常您需要一个 Apple Mac 来生成这些证书,但方便的是,Marmalade 提供了一个名为iPhone Sign Request Tool的小工具,可以解决这个问题。以下是您需要执行的操作:

  1. 启动 Marmalade iPhone Sign Request Tool,并按照以下方式填写字段:

    1. 证书请求文件:选择一个位置来保存此文件。您需要稍后将其上传到 iOS Dev Center。

    2. 密钥文件:从下拉框中选择developer_identity.key

    3. 通用名称:您在注册 iOS Developer Program 时使用的名称。

    4. 电子邮件地址:您在注册 iOS Developer Program 时使用的电子邮件地址。

  2. 现在,登录到 iOS Dev Center 并点击iOS Provisioning Portal链接。

  3. 在左侧面板中,点击Certificates链接。点击Development选项卡,然后点击Request Certificate按钮以打开一个包含说明的页面。在新页面上,按Choose File按钮上传在第 1 步中生成的文件。

  4. 再次点击左侧面板中的Certificates链接,然后点击提示您下载 WWDR 中间证书的链接。将此文件保存到您的 Marmalade SDK 安装目录下的子目录s3e\deploy\plugins\iphone\certificates中。

  5. 在您的网络浏览器中刷新Certificates页面,直到您在Action列中看到Download按钮。点击此按钮并将文件保存到步骤 4 中的目录,将其重命名为developer_identity.cer

  6. 在左侧面板中,点击Devices链接,通过点击Add Devices按钮并输入设备及其 40 位十六进制设备 ID 的描述来将您的测试设备注册到 iOS Dev Center。有一个标记为Finding the Device ID的链接,它告诉您如何为特定设备发现此值。

  7. 接下来点击App IDs链接,然后点击New App ID按钮,注册一个新的 App ID 以供您的应用程序使用。您只需输入一个描述(可以是您想要的任何内容)和一个形式为com.domainname.appname的包标识符。domainname部分可以是您喜欢的任何内容(它不必与真实的 URL 相关),而appname部分应该是您应用程序的名称,或者您可以使用一个星号,这样就可以使用该 App ID 为任何应用程序。

  8. 点击Provisioning链接,然后点击New Profile按钮。为配置文件输入一个描述性名称,并勾选您的证书名称旁边的复选框。从下拉框中选择在第 7 步中生成的 App ID,然后勾选所有您希望此配置文件应用于的设备的复选框。

  9. 再次点击配置文件链接,并不断刷新此页面,直到在您的新配置文件旁边出现下载按钮。

在 Marmalade 文档的帮助文件中可以找到有关此过程的信息,大多数 iOS 配置文件门户页面上也有如何标签,解释了涉及的过程,尽管大多数这些假设您正在使用 Apple Mac 生成各种文件。

在克服所有这些障碍之后,我们可以使用Marmalade 系统部署工具生成一个正确签名的 iOS 安装包,该包将被命名为Hello.ipa。现在,要将它安装到设备上!

可以使用 iTunes 来安装您的构建版本,但请注意,它可能有点儿运气成分,不一定能成功。有时 iTunes 无法识别新的构建版本,需要同步到设备上。根据我的经验,一个更可靠的选项是使用iPhone 配置实用工具,这是一个可以从以下网址免费下载的 Apple 工具:

support.apple.com/kb/DL1466

在 iOS 设备上安装

首先,您需要让 iPhone 配置实用工具了解您的配置文件。在左侧最远端面板中点击配置文件,然后在工具栏中点击添加按钮。导航到步骤 9 中创建的配置文件文件,并点击打开按钮将其添加到可用配置文件列表中。或者,您也可以从 Windows 资源管理器中将文件拖放到列表中。

接下来,在左侧面板中点击应用程序条目,然后点击工具栏中的添加按钮。在部署的发布目录中找到Hello.ipa文件,并点击确定将其添加到已知应用程序列表中,或者您也可以从 Windows 资源管理器中将文件拖放到列表中。

现在,使用 USB 线将您的 iOS 设备连接到计算机。在短暂延迟后,它应该出现在左侧面板的底部。点击您的设备名称,您应该在主面板中看到五个标签出现。点击配置文件标签,然后点击您配置文件旁边的安装按钮。完成后,此按钮将更改为移除按钮。

我们接下来的步骤是安装应用程序本身,因此点击应用程序标签以查看设备上已安装或可安装的所有应用程序的列表。找到标记为Hello的条目,并点击其安装按钮,一旦应用程序安装完成,该按钮将更改为显示移除

在完成所有这些之后,我们最终可以通过在设备上找到其图标并点击它来运行我们的应用程序。您应该会看到消息Hello iOS,我承认在经过如此漫长的过程后,这可能显得有些令人失望。

在 BlackBerry QNX 设备上安装

Marmalade 还可以部署到 BlackBerry QNX 设备,其中最著名的是 PlayBook 平板电脑。为了部署到 PlayBook,我们首先需要进行一些设置工作。在接下来的步骤中,需要输入一些命令行。在这些命令行中,有一些用尖括号括起来的参数。在每个命令组之后,有一个表格解释了如何替换尖括号内的参数。

  1. 首先转到以下 URL 下载 BlackBerry Native SDK,其中包含一些用于签名构建的工具:

    developer.blackberry.com/native/download/

  2. 通过在 Windows 资源管理器中右键单击安装程序的图标并选择以管理员身份运行选项来运行安装程序,以避免在安装 SDK 时出现权限问题。

  3. 安装完成后,请转到以下 URL 请求一些签名密钥:

    www.blackberry.com/SignedKeys/nfc-form.html

  4. 我们需要请求一个设备代码签名密钥,因此选择第一个单选按钮以请求此密钥类型,然后输入所需的个人资料信息(你的姓名、公司名称等)。你还将被要求输入一个由八个字母数字字符组成的 PIN 值。确保你记住在这个阶段输入的内容,因为你需要它来完成密钥的注册。

  5. 在勾选许可协议复选框并单击提交按钮之前,请确保已勾选标记为适用于 BlackBerry PlayBook OS 和 BlackBerry 10 及以上版本的复选框。

  6. 密钥文件将通过电子邮件发送给你,但它们到达之前可能会有几小时的延迟。当你拥有密钥文件时,在你的 PC 上创建一个空文件夹并将它们保存到该文件夹中。以下步骤将假设文件夹名为C:\PlayBook

  7. 打开一个命令提示符窗口,将当前目录更改为你在第 6 步中创建的目录,并输入以下命令。第一个命令设置PATH环境变量,以便其他命令可以执行。这些命令将你的密钥文件注册到 BlackBerry 签名服务器,并允许你的 PC 生成调试令牌:

    C:\PlayBook> C:\bbndk-2.0.1\bbndk-env.bat
    C:\PlayBook> blackberry-keytool -genkeypair -keystore sigtool.p12 -storepass <password> -dname "cn=<company name>" -alias author
    C:\PlayBook> blackberry-signer -csksetup -cskpass <password>
    C:\PlayBook> blackberry-signer -register -csjpin <pin> -cskpass <password> <RDK file>
    C:\PlayBook> blackberry-debugtokenrequest.bat -register -cskpass <password> -csjpin <pin> <PBDT file>
    
    参数 需要输入的值
    <公司名称> 在第 4 步请求密钥文件时指定的公司名称。
    <密码> 你选择的密码。在每个命令中使用相同的值。
    <PBDT 文件> 这是第 6 步中通过电子邮件发送给你的 PBDT 密钥文件的文件名。该文件名应采用client-PBDT-1234567.csj的形式,其中数字部分将唯一标识你的密钥文件。
    <pin> 在第 4 步请求密钥文件时指定的 PIN 值。
    <RDK 文件> 这是第 6 步中通过电子邮件发送给你的 RDK 密钥文件的文件名。该文件名应采用client-RDK-1234567.csj的形式,其中数字部分将唯一标识你的密钥文件。
  8. 我们现在可以使用以下命令行生成调试令牌文件:

    C:\PlayBook> blackberry-debugtokenrequest -cskpass <password> -keystore sigtool.p12 -storepass <password> -deviceId 0x<device id> debugtoken.bar
    
    参数 需要输入的值
    <设备 ID> 您 BlackBerry 设备的设备 ID。例如,您可以在 PlayBook 的设置屏幕中找到此值,进入设置屏幕,在左侧面板中点击关于项,然后在下拉框中选择硬件选项。标记为PIN的值是设备 ID。当您在命令行中指定此值时,确保您以0x为前缀,就像在 C++源代码中一样,以表示十六进制值。
    <密码> 这是您在上一组命令中使用的相同密码。
  9. 要在设备上安装调试令牌,首先确保设备通过 WiFi 连接到与您的 PC 相同的网络,然后查看设备设置以确定分配给它的 IP 地址。这可以通过转到关于面板并在下拉框中选择网络来找到。

  10. 接下来,我们必须在设备上启用开发模式,这可以在设置屏幕中完成。选择安全面板,然后在该屏幕上点击开发模式条目。将使用开发模式旁边的切换控制设置为开启。您将被要求输入用于开发模式的密码,因此输入一个到编辑框中,然后点击上传调试令牌按钮。

  11. 现在我们必须输入一个额外的命令行来在设备上安装调试令牌。

    C:\PlayBook> blackberry-deploy -installDebugToken debugtoken.bar -device <ip address> -password <device password>
    
    参数 需要输入的值
    <设备密码> 在第 10 步设置的设备密码。
    <IP 地址> 在第 9 步发现的设备 IP 地址。
  12. 现在我们必须将以下两行添加到 MKB 文件的deployments部分,以便我们可以创建一个有效的部署包:

    playbook-author="<company name>"
    playbook-authorid="<author id>"
    
    参数 需要输入的值
    <公司名称> 在第 4 步请求密钥文件时指定的公司名称值。
    <作者 ID> 找到这个值有点复杂。首先复制第 8 步生成的debugtoken.bar文件,将其重命名为.zip扩展名。现在您可以使用归档程序查看此文件。进入META_INF目录并提取MANIFEST.MF文件。在文本编辑器中打开此文件,查找名为Package-Author-Id的条目。此条目后面的字符串就是您需要在 MKB 文件中输入的值。
  13. 如果 Visual C++已打开,请关闭它。双击项目 MKB 文件以使用之前所做的更改重新构建它,并启动 Visual C++。执行项目的GCC (ARM) 发布构建,然后按F5启动部署工具。

  14. 在部署工具中,更改设置以创建 BlackBerry QNX 部署。当您到达标记为部署摘要的页面时,在点击部署所有按钮之前,将下拉框更改为包和安装选项。

  15. 在部署工具的最后页面将显示,对于 BlackBerry 部署,有两个额外的字段。第一个字段标记为设备主机名(或 IP 地址),您应提供您想要安装包的设备的 IP 地址。另一个字段标记为设备密码,您应输入在第 10 步中设置的密码。然后再次点击部署全部按钮,部署将通过 WiFi 传输并安装到设备上。

构建现在应该已经安装到设备上,所以松一口气,然后在应用程序列表中寻找它的图标。触摸图标以运行程序,你应该会看到一个愉快的短消息Hello BlackBerry

在 Bada 设备上安装

在 Bada 设备上安装构建是一个例子,说明在Marmalade 系统部署工具中使用包、安装和运行选项实际上是一个非常不错的选择,因为无法手动将包复制到 Bada 设备上以进行安装。

首先,您需要为您的 Bada 设备安装一些设备驱动程序,以便部署工具可以连接到它。Marmalade 确实附带了一些 Bada 驱动程序,但确定三个可能的驱动程序中哪一个与您的设备匹配可能很困难。因此,最好首先安装 Samsung Kies 工具,它附带了许多驱动程序,并将自动为您安装正确的驱动程序。您可以从以下网址下载 Kies:

www.samsung.com/uk/support/usefulsoftware/KIES/JSP

安装 Kies 后,使用 USB 线缆将您的设备连接到计算机,并运行部署工具。当您到达平台选择页面时,您将看到有四个可能的 Bada 选项。您必须选择适合您设备的正确选项,这取决于您的设备所具有的 Bada 版本以及其屏幕分辨率。

选择正确的 Bada 平台选项后,点击下一阶段 >按钮,然后在窗口顶部的下拉框中选择包、安装和运行。点击部署全部按钮,将创建一个安装包,该包将安装到您的设备上并执行,然后您应该会看到所有单色的Hello Bada消息!

摘要

“Hello World”项目可能非常简单,但它已经展示了 Marmalade 的惊人力量。我们现在知道如何创建一个新的 Marmalade 项目,构建它,并为其创建和应用我们自己的应用程序特定设置。

我们然后在 Windows 模拟器中运行我们的项目,并学习了如何在多个不同的移动平台上部署和运行它。

在屏幕上显示一些文本虽然不是世界上最令人兴奋的事情,但接下来的一章我们将学习如何使用 Marmalade SDK 在屏幕上绘制简单的 2D 图像。我们还将了解 Marmalade 是如何使我们轻松使用位图图像的。

第二章:资源管理和 2D 图形渲染

除非你恰好正在编写一个老式的文本冒险游戏(甚至即使你正在这样做),否则你很可能会希望在屏幕上显示的不仅仅是简单的调试字体中的文本。绘制漂亮的图形要求我们也能够将这些图形加载到内存中以便显示;因此,在本章中,我们将探讨以下内容:

  • 使用 Marmalade 的资源管理器加载游戏资源

  • 使用我们自己的自定义类扩展资源管理系统

  • 我们可用于渲染目的的编程选择

  • 如何使用 IwGx API 在屏幕上显示位图图像

Marmalade ITX 文件格式

ITX 文件是 Marmalade 的内置文件格式,可用于将各种数据加载到我们的程序中。扩展名 ITX 是 Ideaworks TeXt 的缩写;Ideaworks 是在重新品牌为 Marmalade 之前创建 SDK 的公司的原始名称。

ITX 文件具有简单的文本格式,并用作资源加载的基础。虽然我们可以自己加载资源,但这样做就像是在重新发明轮子,因为 Marmalade 已经为这一真正繁琐的编码方面提供了大量的支持。

Marmalade 有一个名为 IwUtil 的 API,它包含了一系列广泛的有用实用函数,从内存管理和调试到对象的序列化和随机数生成。它还包含一个名为 CIwTextParserITX 的类,它允许我们加载和处理 ITX 文件。

要将此功能添加到我们的项目中,我们只需将 iwutil 添加到 MKB 文件的 subprojects 列表中,然后在程序开始时添加对 IwUtilInit 的调用,并在关闭代码中添加对 IwUtilTerminate 的调用。

在我们可以使用文本解析器之前,我们需要通过使用 new CIwTextParserITX 来创建它的一个实例。这个类是一个单例类,因此我们可以在程序开始时创建它的一个实例,然后在代码的其余部分中尽可能多地重用它(不要忘记在关闭时释放它!)。该实例可以通过 IwGetTextParserITX 函数访问,然后我们可以使用以下代码加载和解析 ITX 文件:

IwGetTextParserITX()->ParseFile("myfile.itx");

ITX 文件不过是一个由类定义组成的庞大集合。类的实例通过首先写出类的名称,然后跟随着用花括号括起来的该实例的参数列表来定义。假设我们有一个名为 WidgetClass 的类,其定义如下(现在不必担心 CIwManaged 类和 IW_MANAGED_DECLARE 宏,我们稍后会讨论这些):

class WidgetClass : public CIwManaged
{
public:
  IW_MANAGED_DECLARE(WidgetClass)
  WidgetClass();
private:
  uint8         mColor[3];
  int32         mSize;
  bool          mSparkly;
  WidgetClass*  mpNextWidget;
  uint32        mNextWidgetHash;
};

下面是一个示例,说明我们如何在 ITX 文件内部实例化这个类:

WidgetClass
{
  name     "red_widget"
  color    { 255 0 0 }
  size     10
  sparkly  true
}

WidgetClass
{
  name     "green_widget"
  color    { 0 255 0 }
  size    20
  sparkly  false
  next    "red_widget"
}

这个示例声明了两个WidgetClass的实例,并用名称、颜色值、大小以及一个标志来初始化这些实例,该标志指示所讨论的小部件是否闪闪发光。每个这些设置都被称为属性,它们可以是任何我们想要的类型——字符串、整数、浮点数、布尔值或值的数组(color属性提供了一个例子)。

希望您正在查看这个,并思考这个格式是如何被 Marmalade 文本解析器神奇地加载和实例化的,因为它显然对WidgetClass一无所知。这是一个很好的问题!答案是,您希望从 ITX 文件中解析的任何类都必须首先从 Marmalade 类CIwManaged派生。

CIwManaged 类

CIwManaged类是在 Marmalade SDK 中使用的基类,以及我们自己的类,每当我们要能够通过从文件加载来创建它们的实例时。

该类提供了一些虚拟方法,我们可以覆盖这些方法以允许解析器识别我们自己的自定义类,并将它们序列化为二进制格式,并解决对其他类或资源的引用。它还提供了在运行时实例化我们类副本所需的编码粘合剂。

这个功能对我们来说非常有用,因为它允许我们使我们的代码更加数据驱动。比如说,我们有一个描述玩家可以收集的物品的类。在我们的游戏中可能有多种不同的物品类型,所以我们不必在源代码中创建它们的实例,这只有程序员才能更改,我们反而可以从 ITX 文件中实例化它们,这样没有编程知识的游戏设计师就可以编辑它们。

使用类工厂实例化类

CIwTextParserITX在 ITX 文件中遇到的第一件事是类名,它将使用这个类名来创建我们类的一个全新实例。它是通过使用类工厂来实现的,这是 IwUtil API 的另一个部分。

类工厂是一种编程模式,它允许我们通过要求另一个类(所谓的工厂)为我们创建一个相关的类实例来在运行时生成新对象实例。

Marmalade 类工厂系统允许我们通过注册一个标识类的唯一哈希值和一个创建该类新实例的方法来将我们自己的类添加到 SDK 本身提供的类中。

哈希值通常是通过将类名转换为数字来得到的,这是通过将类名作为字符串传递给 IwUtil API 的函数IwHashString来实现的。虽然这并不能保证产生一个唯一的数字,但对于我们的目的来说通常已经足够好了,并且与其他类名的哈希值发生冲突的情况很少。

要将我们自己的自定义CIwManaged派生类添加到类工厂中,我们只需执行以下操作(如果您想查看这个完整的示例以及接下来几节将要涉及的内容,请查看本章伴随的 ITX 项目的源代码):

  1. 将宏 IW_MANAGED_DECLARE(CustomClassName) 添加到类的公共部分。此宏声明了一个名为 GetClassName 的方法,它将返回类的名称作为字符串,并添加了一些类型定义,以便更容易地使用 CIwArray 类,这是 IwUtil 提供的另一个功能。

  2. 将宏 IW_MANAGED_IMPLEMENT_FACTORY(CustomClassName) 添加到类的源文件中。此宏实现了 GetClassName 方法,并创建了必要的类工厂函数,该函数将用于创建我们类的新的实例。

  3. 最后,我们必须通过在初始化代码中添加宏 IW_CLASS_REGISTER(CustomClassName) 来将我们的类注册到类工厂本身。

完成这些后,我们现在可以将我们的类包含在 ITX 文件中。CIwTextParserITX 类现在可以通过调用类工厂函数 IwClassFactoryCreate("CustomClassName") 来创建它的全新实例。

解析一个类

在处理完我们类的新的实例创建后,下一步是允许 CIwTextParserITX 通过修改其成员来配置该实例。这是通过以下 CIwManaged 类的虚拟方法完成的:

方法 描述
ParseOpen 当文本解析器达到类定义的开始花括号时,将调用此方法。它可以用来初始化在解析对象过程中可能需要的任何内部内容。重要的是不要使用此方法将类的所有成员变量初始化为某些默认值。类构造函数是做这件事的更好地方,因为它保证在实例以任何方式创建时都会被调用。
ParseAttribute 当在对象定义中遇到属性时,将调用此方法。该属性作为标准 C 风格字符串传递给此方法,然后可以按需处理它。文本解析器可以在此方法中使用,以多种不同方式提取任何数据元素,包括字符串、整数和布尔值。
ParseClose 当遇到类定义的结束花括号时,将调用此方法。

| ParseCloseChild | 在 ITX 文件中,可以在其他类定义内部嵌入类定义。如果一个类没有实现 ParseClose 方法,那么当遇到其结束花括号时,将调用父类的 ParseCloseChild 方法,并传递子类的指针。在这种情况下,父类和子类并不指代类继承层次结构,而是指在 ITX 文件中类的定义方式。例如:

ParentClass
{
    name "parent"

    ChildClass
    {
        name "child"
    }
}

|

当重写这些方法中的任何一个时,通常应该调用超类中的方法版本,无论是 CIwManaged 还是其他从它派生的类。例如,name 属性是由 CIwManaged::ParseAttribute 解析的,它不仅读取类的名称,还生成名称的哈希值。当涉及到后续的序列化和解析类实例时,哈希值非常重要。

下面的图示展示了本章前面定义的 WidgetClass 实例如何被 ITX 解析器处理:

解析一个类

对于 WidgetClass,我们唯一肯定需要实现的方法是 ParseAttribute 方法,它可能看起来像以下代码:

bool WidgetClass::ParseAttribute(CIwTextParserITX* apParser,
const char* apAttribute)
{
  if (!stricmp(apAttribute, "color"))
  {
    apParser->ReadUInt8Array(mColor, 3);
  }
else if (!stricmp(apAttribute, "size"))
  {
    apParser->ReadInt32(&mSize);
  }
else if (!stricmp(apAttribute, "sparkly"))
  {
    apParser->ReadBool(&mSparkly);
  }
else if (!stricmp(apAttribute, "next"))
  {
  CIwStringL lNextWidget;
  apParser->ReadString(lNextWidget);
  mNextWidgetHash = IwHashString(lNextWidget.c_str());
  }
else
    return CIwManaged::ParseAttribute(apParser, apAttribute);
return true;
}

序列化一个类

序列化对象实例是将对象的当前状态转换为(或从)二进制格式的过程。

虽然在解析 ITX 文件时并非绝对必要,但它仍然是 CIwManaged 提供的功能中的一个非常有用的部分,并且构成了我们在本章后面将要看到的资源处理过程的一个基本部分。

当保存当前游戏进度或高分表等数据时,序列化功能也非常有用,尽管当然我们也可以选择使用常规的文件处理操作来完成这项工作。

我们类的序列化是通过重写虚拟方法 Serialise 来处理的。然后,该方法可以使用 IwUtil 提供的序列化函数,这些函数都以前缀 IwSerialise 开头。

例如,IwSerialiseInt32 将序列化一个 int32 值。所有这些函数都使用了 Marmalade 的基本变量类型定义,因为这些定义在变量的内存占用方面更为明确。有关 IwSerialise 函数和变量类型的信息,请参阅 Marmalade SDK 安装目录中的头文件 IwSerialise.hs3eTypes.h

我们必须确保调用我们的超类 Serialise 实现以确保对象的每个部分都被序列化。通常,这将是我们在 Serialise 实现中做的第一件事,但只要它在某个时候被调用,就不必这样做。

我们可以通过调用 IwSerialiseOpen 来将我们的对象序列化到我们选择的文件中。这允许我们指定文件名和一个布尔标志,该标志指示我们是在读取还是写入文件。然后我们调用我们想要序列化的每个对象的 Serialise 方法,最后调用 IwSerialiseClose 来完成这个过程。

IwSerialise 函数的一个很好的特性是,在大多数情况下,我们不必担心 Serialise 方法是否被调用以将数据写入文件,或者是否被调用以从文件中读取数据。我们只需调用该函数,它就会根据需要读取或写入值。

有时候我们会关心读取或写入文件中的值;例如,如果我们需要分配一块内存来读取一些值。函数IwSerialiseIsReadingIwSerialiseIsWriting允许我们做出适当的决定。

以下代码片段通过展示Serialise方法可能的样子来说明序列化函数的使用:

void WidgetClass::Serialise()
{
  CIwManaged::Serialise();
  IwSerialiseUInt8(mColor[0], 3);
  IwSerialiseInt32(mSize);
  IwSerialiseBool(mSparkly);
}

解析类

解析类实例的行为是在从 ITX 文件解析对象或从序列化过程中创建它时修复我们类中未正确初始化的任何部分。

这种情况可能发生在什么时候?需要解析我们的实例的最常见原因是当实例需要指向另一个类时,而这个类在第一次创建时可能不存在。

这一点最好通过一个例子来说明。假设我们的类包含指向另一个类实例的指针,以便实现链表。当我们读取实例时,我们可能会引用一个尚未创建的实例,因此我们无法立即创建链表。

为了解决这个问题,我们可以在数据中存储一个值,以便我们稍后查找所需的实例。这可能是一个表示实例名称的字符串,或者可能是一个唯一的标识符号码。

一旦所有实例都已读取,我们就可以依次调用CIwManaged类的虚拟方法Resolve,并使用我们认为合适的方法获取指向正确实例的指针。例如,我们可能维护一个所有类实例的列表,每当创建新实例时,都会添加到这个列表中。然后我们可以使用这个列表来查找所需的实例。

并非总是需要创建我们自己的Resolve实现,但如果我们这样做,我们必须确保从我们的超类中调用方法的重载版本。

我们将再次审视WidgetClass以总结这一切。你可能记得它有一个成员mpNextWidget,它指向另一个WidgetClass实例。在 ITX 文件中,我们通过指定另一个WidgetClass实例的名称来为这个成员提供一个值。在ParseAttribute方法中,我们读取这个名称并从中计算出一个哈希值,该值存储在mNextWidgetHash成员变量中。

我们可以实现Resolve方法并查找指向正确实例的指针,但我们也需要维护所有WidgetClass实例的列表才能做到这一点。一种方法是在实现ParseClose时将每个实例存储在列表中。以下代码展示了如何实现这一点:

void WidgetClass::ParseClose(CIwTextParserITX* apParser)
{
  // Add this instance to a list.  gpWidgetList is an instance of a
  // Marmalade class called CIwManagedList which is very useful
  // for storing lists of objects derived from CIwManaged!
  gpWidgetList->Add(this);
}

void WidgetClass::Resolve()
{
  // Look up an instance of WidgetClass with the given hash
  if (mNextWidgetHash)
  {
    mpNextWidget = static_cast<WidgetClass*>
                   (gpWidgetList->GetObjHashed(mNextWidgetHash));
  }
}

Marmalade 资源管理器

大多数位图艺术包都能够以多种不同的文件格式保存图像,但我们真正需要的是访问实际的位图数据本身,这很可能是以压缩格式存储的,与任何特定的文件格式无关。

Marmalade 通过 IwResManager API 简化了加载图像的任务。这个 API 依赖于我们刚刚讨论的 ITX 文件格式,并且不仅限于加载图像。它还可以用于加载 3D 模型和动画等数据,我们还可以用它来跟踪我们自己的自定义类。

注意

之前我们必须创建自己的 CIwTextParserITX 实例来解析 ITX 文件。当需要时,IwResManager 会创建自己的 CIwTextParserITX 实例,所以我们不需要担心创建自己的实例。

将 IwResManager 添加到项目中

要使 IwResManager API 可用于项目,所有需要做的只是将 iwresmanager 添加到 MKB 文件中的子项目列表中。

要初始化 API,只需添加对 IwResManagerInit 的调用,这将创建 Marmalade 资源管理器类 CIwResManager 的单例实例。这个类用于加载、释放,当然还有访问我们的项目资源,无论它们是什么。可以使用函数 IwGetResManager 访问单例。

当我们的项目终止时,我们应该调用 IwResManagerTerminate,这将销毁资源管理器单例以及它可能仍在内存中加载的任何资源。

使用 GROUP 文件指定资源

Marmalade 允许我们将不同类型的资源收集到资源组中。我们可以自由混合图像、声音、3D 模型以及我们可能需要使用的任何其他数据类型。

我们为什么要将资源分组在一起呢?比如说,你正在编写一个包含多个不同级别的游戏。每个级别将有一些公共资源(例如,玩家图形),但可能还有特定于该级别的独特元素,所以当级别正在播放时,只将这些资源保留在内存中是有意义的。因此,你可以为玩家图形创建一个资源组,并为每个级别创建单独的组。

为了将资源组加载到我们的程序中,我们首先需要创建一个 GROUP 文件。实际上,GROUP 文件是一个扩展名为 .group 的 ITX 文件,它允许我们列出我们想要聚集的所有资源。

让我们从查看一个示例 GROUP 文件开始:

CIwResGroup
{
  name  "game_resources"

  "./images/titlescreen.png"
  "./sounds/sounds.group"
  "./levels/levels.itx"
}

该文件的 第一行 定义了一个新的 CIwResGroup 类实例,这是用于实现资源组的类,我们在定义的大括号内做的第一件事就是给资源组命名。这个名称将用于稍后允许我们访问资源组。

注意

GROUP 文件应只包含一个 CIwResGroup 定义。Marmalade SDK 文档指出,如果你指定了多个,行为将是未定义的。实际上这并不是一个问题,因为 GROUP 文件是可以在一次加载的资源的最底层块,所以指定多个 CIwResGroup 实际上也没有真正的益处。

示例定义的剩余行指定了我们想要包含在此组中的资源,并且通常这些只是相关资源的文件名。随着我们继续阅读本书,我们将看到一些额外的功能,这些功能由组文件提供,但到目前为止,我们将专注于加载资源的主要任务。

在示例中,我们指定了三个我们希望成为此资源组一部分的文件。第一个是一个保存为 PNG 文件格式的位图图像。下一个资源是对另一个 GROUP 文件的引用。当这个 GROUP 文件被加载时,sounds.group 文件也将被加载到内存中。

我们要包含的最后一个文件是 levels.itx,这是一个标准的 ITX 文件,将被用来创建我们自己的类的实例。

加载组和访问资源

在我们的程序中加载 GROUP 文件,我们需要做以下操作:

CIwResGroup* pResGroup;
pResGroup = IwGetResManager()->LoadGroup("groupfile.group");

这将在项目的 data 目录中查找指定的 GROUP 文件,并将其加载到内存中。LoadGroup 方法返回创建的 CIwResGroup 实例的指针,我们可以将其存储起来,以便稍后释放资源组及其所有资源。

当资源组在内存中时,我们可以通过两种方式访问单个资源。第一种方式是要求 CIwResGroup 实例本身为我们定位特定的资源。以下是我们的操作方法:

CIwResource* pResource;
pResource = pResGroup->GetResNamed(name, type, flags);

GetResNamed 调用中,name 参数是一个以空字符终止的字符串,包含我们想要访问的资源名称。这是在 ITX 文件中使用 name 属性指定的值。如果没有明确指定 name 值,将使用 GROUP 文件中遇到的第一个资源名称(不包括任何扩展名)。在上一个示例的 GROUP 文件中,这个名称将变为 titlescreen,因为文件中的第一个资源是 titlescreen.png 文件。

type 参数指示我们正在尝试定位的资源类别。此参数也是一个字符串,简单地说就是资源类型的类名。

最后还有 flags 参数,我们通常可以完全省略它,因为它默认值为零。我们可以使用各种标志来改变搜索资源的方式。例如,IW_RES_PERMIT_NULL_F 将防止在找不到所需资源时触发断言。不过,有关这些标志的更多信息,请查阅 Marmalade 文档,尽管在大多数情况下,我们需要的默认值是零。

如果找不到资源,GetResNamed 调用将返回 NULL,否则它将返回我们的资源,作为一个指向 CIwResource 实例的指针,然后我们可以将其转换为所需的类类型。

访问资源的第二种方式是要求资源管理器通过搜索所有当前加载的组来找到它。这可能非常有用,因为它意味着我们不必确切知道要搜索哪个资源组。显然,搜索所有当前加载的资源组将更慢,但这意味着我们不必跟踪我们加载的每个资源组。毕竟,这就是资源管理器的作用!搜索所有加载组以查找特定资源的调用如下:

CIwResource* pResource;
pResource = IwGetResManager()->GetResNamed(name, type, flags);

参数与调用 CIwResGroup::GetResNamed 方法的参数完全相同。

最后,我们可以通过以下调用从内存中删除一个资源组和它包含的所有内容:

IwGetResManager()->DestroyGroup(pResGroup);

每当我们不再需要在内存中保留那些资源时(例如,仅当在玩游戏的一个特定级别时,包含该级别资源的组才需要保留在内存中),我们应该销毁一个组。然而,在关闭时销毁所有组并不是绝对必要的,因为 Marmalade 将确保在应用程序终止时释放所有已分配的资源。

CIwResource 类

我们已经看到 CIwManaged 类可以用来通过从文件中加载它们来轻松创建我们自己的类的实例。通过 CIwResource 类,这一功能得到了进一步的改进,它允许我们将自己的类包含到资源组中。

在前一个章节中展示的 GROUP 文件示例中,我们指定了 levels.itx 文件,该文件可能包含我们自定义类的定义。如果我们的自定义类使用 CIwResource 作为其基类(或者当然,任何从 CIwResource 继承而来的类),那么我们所有的资源都可以添加到资源组中,这样我们就无需自己跟踪它们。

GROUP 文件序列化

我们可以轻松加载不同类型的资源真是太好了,但最终我们可能不希望将我们的应用程序与一组易于识别或编辑的文件一起部署。这有几个原因:

  • 加载速度:解析一个文本文件并将其转换为类是一个比直接加载已解析序列化版本更慢的操作。还可能需要对我们原始数据进行某种转换,以便使其在游戏中可用,因此如果我们能避免这样做,我们将提高我们游戏的加载时间。

  • 为了防止黑客攻击:如果我们分发一组文本文件和常见的文件格式,如 PNG 文件,我们将使某人很容易地黑客攻击并修改我们的游戏或未经授权使用游戏资源。

  • 代码大小更小:如果我们正在加载的资源数据已经是我们游戏代码可以直接使用的格式,那么就没有必要包含任何将原始数据格式转换为我们的内部格式的代码。这使得代码大小更小,同时也稍微有助于防止黑客。

  • 部署大小:文本文件通常比它们的序列化二进制等效文件大得多,因此发送二进制版本可以减少我们的安装包大小。

Marmalade 通过使用 CIwManaged 类提供的序列化功能,自动将我们加载的每个 GROUP 文件转换为它的二进制等效文件来解决所有这些问题。

在资源组完全加载后,资源管理器将对组内每个资源的每个实例调用 Serialise 方法,创建一个文件,其文件名在原始 GROUP 文件名后添加 .bin。例如,名为 images.group 的文件中的资源将被序列化到名为 images.group.bin 的文件中。

一旦创建了 GROUP 文件的序列化版本,资源管理器将销毁资源组,然后从新序列化的版本中重新创建它。这一步骤之所以存在,是因为它使得发现问题,例如忘记序列化类的成员变量,变得更加容易。

存在一个有用的 ICF 设置,用于控制资源构建过程。只需将以下内容添加到 ICF 文件中(参考第一章,使用 Marmalade 入门,以了解 ICF 文件是什么):

[RESMANAGER]
ResBuild=1

当设置为 1 时,ResBuild 设置将确保资源管理器始终加载 GROUP 文件并将其序列化。将其设置为 0,则跳过 GROUP 文件解析阶段,而是直接加载 GROUP 文件的现有序列化版本。这在开发期间非常有用,既可以增加没有添加或更改资源时的应用程序启动时间,也可以更接近设备上的加载过程。

注意

如果你已经对你的游戏资源进行了更改,但运行时它们没有出现,那么 ResBuild 标志始终是一个好的起点。令人惊讶的是,要做出资源更改并忘记你已禁用资源构建是多么容易!

资源处理器

值得一提的是,IwResManager API 的最后一个部分,这就是 资源处理器 的概念。

你可能想知道资源管理器是如何加载和处理不同类型的文件的。我们可以在 GROUP 文件中列出一些文件名,这很好,但 PNG 格式的图像文件究竟是如何被加载成我们可以用于渲染的形式的呢?当然是通过资源处理器!

资源处理器是 CIwResHandler 的子类,用于加载和处理特定类型的资源,该类型由一个或多个文件扩展名标识。

当文本解析器在 GROUP 文件中遇到文件名时,它会查看文件扩展名,然后检查是否已为该扩展名注册了资源处理器。如果没有找到合适的处理器,将引发错误;否则,文件名将被传递给相关的资源处理器类,该类将执行对文件所需的任何操作,使其在我们的代码中可用。

Marmalade 中的整个资源管理系统都依赖于资源处理器才能工作。GROUP 文件、ITX 文件和位图图像文件都由从 CIwResHandler 派生的类处理,如果我们想使用核心 Marmalade SDK 不支持的某些文件类型,我们可以创建自己的自定义资源处理器。

当我们谈到在 第七章 添加声音和视频 中实现声音时,我们将再次回到资源处理器的话题,因为 Marmalade 核心 SDK 不支持任何声音文件格式。

Marmalade SDK 提供的图形 API

现在我们已经熟悉了资源管理,我们可以继续进行更有趣的任务,即在显示上显示图片。

Marmalade 通过提供多种在屏幕上绘制图形的方法来宠坏了我们。以下各节提供了我们可用的不同选项的概述。

s3eSurface API

显示访问的最低级别是 s3eSurface API。它通过使用内存指针提供对显示的访问,然后您可以使用该指针直接读取或修改像素。

您可以找到显示的宽度和高度(以像素为单位),以及步长,即您需要跳过多少字节才能到达显示图像的下一行。

音调受显示的像素格式(16 位、24 位或 32 位显示都是可能的)的影响,并且通常还会添加额外的填充字节,以便每行从字对齐的内存地址开始,这可以提高显示内存访问时间。

实际上,这个 API 很少被使用,部分原因是因为它不提供绘制位图图像或线条的支持,但主要是因为它在许多现代设备上由于显示是由 图形处理单元GPU)绘制的而非常慢,这可能会限制 CPU 如何以及何时访问此内存。

我们不会在本书中任何地方使用此 API,但如果您想使用它,您不需要向您的项目添加任何内容,因为它始终在 Marmalade 项目的任何地方可用。

IwGL API 和 OpenGL ES

如上所述,今天大多数移动设备都包含一个用于加速绘图操作并释放 CPU 以执行其他任务的 GPU,例如更新游戏当前状态。在大多数移动平台上采用的标准 API 是 OpenGL ES。

OpenGL ES API 是 OpenGL API 的衍生品,OpenGL API 已经在许多桌面系统上使用了多年。OpenGL ES 被构想为 OpenGL 的简化版本,专为嵌入式系统设计(因此得名 ES 部分!)。

OpenGL ES 有两个主要版本。1.x 标准是为具有固定、功能渲染管道的设备设计的,这意味着虽然可以控制如何将 3D 点转换成 2D 坐标,以及如何将多边形及其关联的纹理(如果有)光栅化到屏幕上,但你完全受限于硬件提供的选项。

OpenGL ES 的 2.x 标准是为 GPU 硬件设计的,其中 3D 点的转换和结果多边形的光栅化可以通过 着色器 来编程。着色器是一个可以非常快速应用于转换 3D 点(顶点着色器)或计算渲染像素所需颜色的简短程序(像素或片段着色器)。

在大多数情况下,支持 OpenGL ES 2.x 的设备也将支持 OpenGL ES 1.x,但两者不能混合。当初始化 OpenGL 时,您请求创建一个或另一个接口作为 OpenGL 上下文。上下文实际上不过是一个大结构,它存储了 OpenGL 运作所需的所有信息,例如当前帧缓冲区、像素混合模式以及可用的着色器。

那么,IwGL API 究竟是什么呢?简单来说,它是对 OpenGL ES 的包装,允许我们直接调用常规的 OpenGL ES 函数调用,但它还提供了一些其他非常有用的功能:

  • IwGL 简化了初始化 OpenGL ES 的过程,只需一个函数调用——IwGLInit。这个函数调用将初始化帧缓冲区并设置 OpenGL 上下文,使其准备好并准备好运行,使用适合可用硬件的设置。还提供了对初始化的精细控制,允许使用应用程序的 ICF 文件中设置的设置来选择显示和深度缓冲区格式。

  • 它提供了上下文状态缓存函数,例如保留所有当前上传到 OpenGL ES 的纹理的副本。在您的应用程序被挂起的情况下(例如,由来电引起)所有纹理和其他资源可能会丢失,通常您需要负责重新加载所有需要的资源。IwGL 自动为我们处理所有这些。

  • 任何 OpenGL ES 扩展函数(特定 GPU 可能提供的超出 OpenGL ES 基础级别要求的功能)都映射到可以直接调用的函数,如果该函数实际上不受支持,则不会引发错误。通常,在尝试调用它之前,您需要专门检查是否存在扩展。

  • 它还提供了一个虚拟分辨率系统,这使得将现有代码(这些代码是硬编码到特定分辨率或屏幕方向)调整为不同分辨率或方向变得容易,只需调整或旋转渲染的图像即可。

当您正在将使用 OpenGL ES 编写的现有代码移植到 Marmalade 时,IwGL 是 Marmalade SDK 的一个无价部分,因为它允许您利用 Marmalade 将项目部署到多个平台的能力,而无需完全重写整个项目。

然而,我们在这本书中也不会使用 IwGL。虽然我们无法阻止自己使用这个 API 来开发新的项目,但这确实意味着我们只能针对具有 GPU(或支持 OpenGL 软件模拟版本)的设备进行开发,我们仍然需要自己处理诸如加载纹理等问题。

您可以通过在 MKB 文件的subprojects部分添加iwgl来在自己的项目中使用 IwGL API。

Iw2D API

由于这是一章关于 2D 图形渲染的章节,Iw2D API 肯定是我们应该选择的方式,对吧?

嗯,是的,也不是。它确实有很多优点,如下所示:

  • 它使我们能够渲染平面着色的基本图形,如线条、弧线、矩形和多边形,可以是轮廓或填充形状。

  • 它使我们能够轻松加载位图图像并在屏幕上渲染它们,还可以对这些图像应用缩放或旋转变换。

  • 它使我们能够在屏幕上绘制看起来比我们目前看到的默认调试字体好得多的文本。

  • 它提供了一些优化,使我们能够加快渲染速度。例如,它可以将绘制特定图像的多个请求批量处理为单个调用,这在许多设备上可以带来良好的性能提升。

然而,正如你可能已经从本节的语气中推断出的那样,我们在这本书中也不会使用 Iw2D。

如果您只对渲染 2D 图形感兴趣,Iw2D 可能完全能满足您的需求,但如果您将来想要过渡到 3D 图形,您最终会发现 Iw2D API 并不能满足您的所有需求,比如渲染任何形状的纹理多边形,而不仅仅是矩形。

由于我们将在本书的后面部分处理 3D 图形,因此我们使用 Marmalade 开始我们的渲染之旅,本身使用 3D 图形是有意义的。

如果您想在您的项目中使用这个 API,只需在 MKB 文件的subprojects部分添加iw2d

IwGx API

最后,我们来到了本书中将使用的 API;实际上,我们在创建我们的“Hello World”项目时已经使用了一小部分。女士们,先生们,我向大家介绍 IwGx API!

此 API 非常灵活,并具有以下功能:

  • 它支持硬件和软件渲染管线,因此您的代码可以在具有 GPU 的现代硬件上未经修改地运行,同时在较老或功能较弱的硬件上回退到基于软件的渲染器。您甚至可以混合这两个管线,例如,您可以使用 GPU 进行光栅化,但仍然使用 CPU 进行变换和光照操作。

  • 它为我们处理了一些繁琐的事情,例如初始化显示和纹理管理,这与 IwGL API 类似。

  • 它允许我们在任意多边形上使用纹理映射和平滑或高洛德着色等特性。

  • 它提供了一些调试功能,例如简单的文本渲染(如我们的“Hello World”项目所示)和渲染形状,如矩形和圆形。

  • 它使得针对 OpenGL ES 2.x 设备变得更容易,因为它提供了必要的着色器程序来模拟 Open GL ES 1.x 的固定功能管线,同时仍然允许我们在需要时提供自己的自定义着色器。

通过从一开始就使用 IwGx 进行 2D 图形的渲染,我们将发现稍后绘制 3D 多边形要容易得多,因为涉及的技术非常相似。

注意

随着 Marmalade 版本 6.1 的发布,IwGx API 进行了一些现代化改造,并标准化了使用浮点值来指定多边形信息。在此版本之前,一些信息(例如,纹理 UV 值)是以定点整数格式指定的。还有一个基于软件的渲染引擎,用于针对没有 GPU 硬件的旧设备。如果您有仍然需要旧定点方式执行操作的现有代码,可以通过在项目 MKB 文件中添加 define IW_USE_LEGACY_MODULES 来回退。

现在应该不会让人感到惊讶,我们只需在 MKB 文件的 subprojects 部分添加 iwgx,就可以在我们的项目中使用 IwGx。

使用 IwGx 渲染 2D 图形

现在我们已经知道了如何加载资源,我们可以开始做一些有趣的事情。我们将探讨如何在屏幕上绘制位图图像。

IwGx 初始化和终止

我们已经在“第一章”的“Hello World”项目中看到了如何做到这一点,即 Marmalade 入门。我们只需在程序开始时调用 IwGxInit 来设置 IwGx,并在结束时调用 IwGxTerminate 来关闭它。

渲染一个多边形

在 IwGx 中,最常用的多边形类型是线条、三角形和四边形(基本上是两个共享公共边的三角形)。

还支持精灵,它们总是矩形的形状,不允许纹理缩放,以及 n-多边形,可以包含多达 63 个顶点。

由于三角形和四边形更加灵活,精灵很少被使用,尽管在软件渲染模式下它们可以更快地绘制。对于软件渲染器来说,n-多边形也可能比一系列三角形更快地绘制,但它们通常最好避免,因为它们需要即时转换为三角形才能使用硬件渲染进行绘制。

要在屏幕上渲染多边形,我们至少需要指定它想要出现在屏幕上的位置和它想要的颜色。此外,我们可能还想使用位图图像来绘制多边形。以下章节将展示我们如何提供这些信息。

材料和纹理

首先,我们让 IwGx 知道我们想要应用到我们的多边形上的颜色(或者确实是颜色)和图像。我们通过指定我们想要使用的材料来完成这项工作,这是一个CIwMaterial类的实例,它将此信息组合在一起。为了设置我们想要使用的材料,我们必须使用以下函数调用向 IwGx 提供一个指向相关CIwMaterial实例的指针:

IwGxSetMaterial(pMaterial);

如果我们在绘制一个没有应用图像的多边形,那么材料至少需要提供我们想要使用的颜色信息。

一种材料实际上包含四种不同的颜色,如果你对 3D 图形渲染稍有了解,你可能会认出它们。它们是环境色、漫反射色、发射色和镜面色。对于 2D 渲染目的,我们只关心环境色。当我们进入第四章 3D Graphics Rendering 中的 3D 渲染时,我们会探讨其他颜色。

材料还指定了我们想要应用的纹理。纹理指定了一个我们要应用到我们的多边形上的位图图像,在 Marmalade 中由CIwTexture类表示。

CIwTexture类实际上是对CIwImage类的包装,它实际上存储了图像的像素信息。CIwTexture增加了控制图像如何渲染的功能,支持启用和禁用诸如双线性过滤和米波映射等功能。

材料还提供了控制其他多边形渲染功能的能力,例如多边形是渲染为平面还是高洛德着色,以及绘制时如何与当前屏幕内容混合。

材料可以通过代码创建,也可以由资源管理器实例化。以下章节将说明这一点。

在代码中创建材料

在代码中创建材料只需要创建一个新的CIwMaterial实例,并使用可用的方法设置颜色、纹理和其他设置。例如,为了创建一个渲染明亮的红色、半透明多边形的材料,我们可以使用以下代码:

CIwMaterial* lpRedMaterial = new CIwMaterial;
lpRedMaterial->SetColAmbient(255, 0, 0, 128);
lpRedMaterial->SetAlphaMode(CIwMaterial::ALPHA_BLEND);

注意,如果你尝试在程序堆栈上创建一个本地的 CIwMaterial 实例,Marmalade 会抛出一个断言消息。这是因为渲染并不是在你调用绘图函数的那一刻发生的,所以当渲染真正发生时,材质数据可能会被其他函数重用相同堆栈空间时破坏。

使用 MTL 文件创建材质

虽然在代码中创建材质很简单,但有一种更简单的方法,尤其是在指定带有纹理的材质时。这涉及到我们朋友 ITX 文件的另一种用途。

材质文件具有 .mtl 扩展名,并且使用与 ITX 文件相同的格式化规则。我们可以在 MTL 文件中创建任意数量的 CIwMaterial 实例,并用所需的颜色、纹理和其他设置初始化它们。

作为额外的好处,我们在 MTL 文件中引用的任何纹理也将自动加载,这意味着我们不需要在 GROUP 文件中单独列出它。为了使这一点生效,所有源图像文件必须位于名为 textures 的子目录中,该子目录位于 MTL 文件所在的同一目录中,或者它们必须已经从另一个 GROUP 文件或在同一 GROUP 文件中在引用 MTL 文件之前加载到内存中。

注意

Marmalade 本地支持 PNG、TGA、GIF 和 BMP 图像文件格式。如果你想要加载任何其他类型的位图,你需要提供自己的自定义资源处理程序来做到这一点。

下面是一个 MTL 文件可能的样子:

CIwMaterial
{
  name      "red"
  colAmbient  { 255 0 0 128 }
  alphaMode    BLEND
}

CIwMaterial
{
  name      "grid"
  colAmbient  { 128 128 128 128 }
  texture0    "grid.png"
  alphaMode    ADD
  shadeMode    FLAT
  filtering    false
}

这个例子生成了一种半透明的红色材质,相当于上一节中创建的材质,以及一个使用名为 grid.png 的纹理的材质,该材质以平面着色和加性透明度绘制,亮度为原始图像的一半,并且没有双线性过滤。

注意

你可能已经注意到,图片是通过一个名为 texture0 的属性来指定的。Marmalade 材质实际上可以分配两个纹理,当渲染多边形时它们可以混合在一起,它们被称为 texture0texture1。在这本书中,我们只关注单纹理材质。

这里要列出的属性名称太多,所以为了获得完整的列表,请查看 Marmalade 文档页面中的 CIwMaterial 类。该页面列出了所有这些属性。

要使这些材质在我们的代码中可用,我们只需要在加载的 GROUP 文件中引用 MTL 文件。然后我们可以通过使用本章前面描述的资源管理器函数按名称搜索它们来获取材质。

注意

建议在创建使用 MTL 文件的材料时,不要使用CIwMaterial类中的方法修改它们的任何设置。相反,使用CIwMaterial::Copy方法制作材料的副本。虽然这样做是可能的,但如果相同的材料用于渲染多个不同的事物,可能会出现问题,因为渲染并不在绘制函数调用时立即发生。因此,最终结果是不可预测的,因为它将取决于CIwMaterial在最终渲染时如何配置。

顶点流

为了在屏幕上显示多边形,我们需要指定一个定义角点的屏幕坐标列表。由于我们目前只进行 2D 渲染,每个坐标都指定为一个CIwSVec2实例,这是一个在另一个名为IwGeom的 Marmalade API 中定义的向量类。在渲染多边形时使用的任何数据项列表,无论是顶点、颜色还是其他,通常被称为,因此顶点列表被称为顶点流

虽然我们可以通过将iwgeom添加到 MKB 文件的subprojects部分并调用IwGeomInitIwGeomTerminate来指定此 API 作为我们项目的一部分,但实际上并不需要这样做,因为 IwGx 本身依赖于这个 API。

CIwSVec2类使用有符号 16 位整数定义一个两分量向量,因此非常适合指定屏幕坐标。

IwGx 中的默认屏幕坐标系将原点放置在屏幕的左上角,x 分量水平向右增加,y 分量垂直向下增加。然而,可以通过传递一个包含原点所需位置的CIwSVec2实例到函数IwGxSetScreenSpaceOrg来改变原点的位置。

以下图表说明了我们如何在标准 iPhone 分辨率屏幕(320 x 480 像素)上指定三角形的坐标。屏幕的左上角是原点,坐标位置为(0,0),而右下角的位置为(320,480)。

顶点流

要渲染这个三角形,我们只需填充一个包含坐标的CIwSVec2数组的数组,并将其提交给 IwGx,如下所示:

CIwSVec2* v = new CIwSVec2[3];
v[0].x = 160;    v[0].y = 120;
v[1].x = 20;     v[1].y = 360;
v[2].x = 300;    v[2].y = 360;
IwGxSetVertStreamScreenSpace(v, 3);

函数调用IwGxSetVertStreamScreenSpace允许我们指定用于渲染的屏幕空间(即像素)坐标列表,但我们必须明确声明我们提交了多少个顶点。在我们的三角形的情况下,这是三个。

还可以使用IwGxSetVertStreamScreenSpaceSubPixel函数调用指定我们的坐标,使用子像素定位。虽然输入可能有点长,但使用子像素定位确实提供了屏幕上更平滑移动的优势,因为我们不再仅限于以整个像素为单位在屏幕上移动事物。

使用子像素坐标也可以提高最终渲染图像的质量,因为如果我们在使用双线性过滤进行渲染时,缓慢移动的对象不会在像素位置之间跳跃。

IwGx 每个像素只支持八个子像素位置;因此,为了将我们的坐标转换为使用子像素定位,我们只需将屏幕坐标乘以八或使用位运算符将左移三位。

颜色流

如果我们想使用平面着色来绘制多边形,使得渲染的每个像素都是相同的颜色,我们只需设置我们材质的环境颜色,我们的工作就完成了。

然而,如果我们想使用高光着色渲染多边形,我们需要指定每个顶点使用的颜色。这不能通过材质来完成,因此我们需要通过提供自己的颜色流来覆盖材质的颜色信息。

我们通过创建一个 CIwColour 对象数组来完成这项工作,这是 Marmalade 选择表示颜色的方法。这个类有四个公共成员变量,类型为 uint8(一个无符号字节),分别称为 rgba,它们(可能不会令人惊讶)代表颜色的红色、绿色、蓝色和 alpha 值。

注意

注意,由于 Marmalade 是在英国开发的,API 中所有单词 color 的实例实际上都会拼写成 colour

CIwColour 还提供了一些方法,使设置和操作颜色更加容易。

返回到之前图中定义的三角形,如果我们想将其顶部着色为红色,右下角为绿色,左下角为蓝色,我们可以使用以下代码:

CIwColour* c = new CIwColour[3];
c[0].Set(255, 0, 0, 255);
c[1].Set(0 255, 0, 255);
c[2].Set(0, 0, 255, 255);
IwGxSetColStream(c);

注意,IwGxSetColStream 不需要我们指定流中的颜色数量。这是因为 IwGx 预期找到的颜色数量与顶点的数量相同。如果我们不想指定颜色流,我们只需将 NULL 传递给 IwGxSetColStream 函数,将使用所选材质的颜色。

UV 流

当使用纹理渲染多边形时,我们需要以某种方式指示该纹理应该如何映射到多边形上。我们通过指定一个 UV 流来完成,这允许我们声明纹理的哪个部分应该出现在每个顶点上。然后,渲染引擎可以通过在多边形表面插值 UV 值来计算出每个渲染像素所需的纹理部分。

在 IwGx 中,使用浮点数指定 UV 坐标。单个 UV 值通常写作 (u, v),在 IwGx 中使用 CIwFVec2 类表示,这是我们在之前遇到的 CIwSVec2 的浮点等效类。向量的 x 分量代表 u,y 分量代表 v

UV 值映射到纹理上,使得 (0.0, 0.0) 是图像的左上角,(1.0, 1.0) 是右下角。我们可以通过使用大于一的值,将纹理重复平铺到多边形上,最多重复八次。

UV 流

注意

在 Marmalade 版本 6.1 之前,UV 值以 12 位定点表示法使用 16 位有符号整数给出。值 4096 相当于 1.0,8192 相当于 2.0,而 2048 相当于 0.5。IwGeom API 为我们提供了定义IW_GEOM_ONE,我们可以用它来避免在代码中到处出现难看的魔法数字。通过回滚到本章前面详细说明的 IwGx API 的旧版本,此功能仍然可以使用。

通过这种方式映射 UV 值,我们使它们与纹理图像的实际大小无关。如果我们出于任何原因更改图像的大小,它不会破坏渲染,因为我们的 UV 值不需要更改。

与顶点流一样,我们只需分配一个CIwSVec2数组,填充该数组,并将其提交给 IwGx,就可以指定一组 UV 值。我们不需要指定我们提交的 UV 值数量,因为 IwGx 期望看到与顶点相同的 UV 数量。以下是一些可能想要使用的示例代码,用于将纹理应用到三角形上:

CIwSVec2* uv = new CIwSVec2[3];
uv[0].x = IW_GEOM_ONE / 2;    uv[0].y = 0;
uv[1].x = 0;                  uv[1].y = IW_GEOM_ONE;
uv[2].x = IW_GEOM_ONE;        uv[2].y = IW_GEOM_ONE;
IwGxSetUVStream(uv, 0);

IwGxSetUVStream的第二个参数指示 UV 值应用于哪个纹理。如果我们使用的材质只有一个纹理,我们可以完全省略此参数,因为它将默认为0,但如果材质确实有第二个纹理,我们需要通过将IwGxSetUVStream的第二个参数更改为 1 来提供用于它的 UV 流。此 UV 流可以与第一个纹理的流相同,也可以是完全不同的 UV 值集。

如果我们的材质没有应用纹理,没有必要将 UV 流设置为NULL,因为它将被完全忽略。

绘制多边形

我们现在已经看到了如何设置几乎所有我们需要的信息来指定我们想要的多边形如何出现,因此我们最终可以指示 IwGx 绘制它。为此,我们需要让 IwGx 知道我们应该如何使用以下函数调用来解释各种输入流:

IwGxDrawPrims(polygon_type, indices, num_indices);

polygon_type参数指示我们是在绘制三角形、四边形、线条、精灵还是 n-多边形,而indices参数是一个uint16值的数组,显示了我们应该按什么顺序访问输入流中的元素。这被称为索引流num_indices参数只是indices数组中元素数量的计数。

以下图表显示了 IwGx 支持的多边形类型。请注意,可以通过提供更长的数据流一次绘制多个多边形。这是我们应尽可能尝试做的事情,因为它可以防止 GPU 在等待提供新的多边形信息时闲置。

绘制多边形

图中标注顶点的数字对应于索引流中的元素。在渲染时,indices数组按照每个多边形类型显示的顺序遍历,它包含的值指示应使用输入流中的哪个元素来渲染每个顶点。

要绘制我们一直在构建的三角形,我们可以使用以下代码片段:

uint16* indices = new uint16[3];
indices[0] = 0;  indices[1] = 1;  indices[2] = 2;
IwGxDrawPrims(IW_GX_TRI_STRIP, indices, 3);

我们可以进一步简化这一点,因为在这个实例中索引流实际上不是必需的,因为我们的输入流是按流中出现的顺序逐个元素访问的,所以我们可以像这样指定NULL作为indices参数:

IwGxDrawPrims(IW_GX_TRI_STRIP, NULL, 3);

在创建索引流时,还有一个需要注意的点,那就是我们提供顶点的顺序。因为 IwGx 也可以用于在屏幕上渲染 3D 多边形,它支持背面裁剪,这可以防止任何朝向观察者远离的多边形被渲染。

如何将多边形分类为朝向或远离观察者?如果我们给多边形的每个顶点标注一个递增的数字,从第一个顶点的零开始,然后沿着多边形的边缘从顶点到顶点依次标注,那么当在屏幕上渲染并按升序数字顺序考虑时,如果多边形的顶点形成一个逆时针模式,则该多边形朝向观察者。顶点提供的顺序称为绕行顺序,以下图表更清楚地说明了这一点:

绘制多边形

将顶点按正确顺序排列不是解决这个问题的唯一方法,但当我们进步到渲染 3D 多边形时,养成按这种方式排列顶点的习惯是值得的。我们可以通过调用CIwMaterial::SetCullMode方法并使用以下枚举值之一来禁用或反转每个材质的背面裁剪操作:CULL_FRONTCULL_BACKCULL_NONE。默认值为CULL_BACK

注意

如果你试图绘制一个多边形,但就是无法显示出来,首先应该检查你的顶点的绕行顺序。它们可能只是被 GPU 裁剪而没有被绘制出来!

我们的多边形信息现在已经提交用于渲染;但我们还看不到它在屏幕上显示。

显示渲染的图像

在屏幕上显示某物的最终步骤是将所有绘图请求刷新到屏幕上,然后显示最终图像。

IwGx 自动为我们提供了一个双缓冲显示设置。这意味着我们所有的渲染都是在离屏缓冲区完成的,然后在所有绘图完成后切换到显示这个缓冲区。如果我们不这样做,我们可能会看到一个不完整、闪烁的屏幕显示,因为我们的图形可能在不完整的状态下被显示。

要完成绘制周期,我们只需添加以下两行代码:

IwGxFlush();
IwGxSwapBuffers();

就这样!我们已经绘制了我们的第一个多边形!

示例代码

如果你下载本章的代码包,你将找到三个项目,它们展示了本章学到的 Marmalade 功能的使用。

ITX 项目

ITX 项目演示了 ITX 文本解析器和 CIwManaged 类的使用。

示例首先通过解析 ITX 文件创建我们自己的类的自定义实例,然后将这些实例序列化到文件中。然后,所有实例都被销毁并重新创建,通过重新加载序列化的文件。

示例还演示了 IwUtil API 的两个更多部分的用法,这些部分我们没有深入探讨,但了解它们是非常有用的。首先是 CIwManagedList 类,它用于维护从 CIwManaged 继承的对象列表,其次是 IwTrace 系统,它允许我们将信息记录到文件(以及标准输出),以帮助调试。

Graphics2D 项目

Graphics2D 项目将本章学到的所有内容结合起来,在屏幕上渲染一个旋转的、带纹理的多边形。以下截图展示了这个项目的实际运行情况:

Graphics2D 项目

滑雪项目

在整本书中,我们将构建一个完整的游戏示例,将我们学到的知识应用到实际实践中。这个游戏将是那个老牌的简单滑雪游戏的一个版本,玩家引导滑雪者沿着山下滑,试图通过尽可能多的旗帜,同时避开障碍物。

在本章中,我们从屏幕顶部的一个滑雪图形开始,该图形从一侧移动到另一侧,同时一些随机的树木在屏幕边缘向上滚动。

以下截图显示了项目当前的状态:

滑雪项目

虽然这本书的目的不是教你游戏的编程细节(假设你已经知道如何做),但提供一些关于如何组装示例游戏的笔记可能仍然是有价值的。

GameObject 类

GameObject 类是构成游戏世界任何一部分的基础类。目前有两个类从这个类继承,SkierTree。猜猜它们代表什么!

GameObject 提供了两个虚拟方法,这些方法可以被子类覆盖以实现游戏对象的行为。GameObject::Update 方法通过向对象应用速度来支持改变对象的位置,而 GameObject::Render 方法允许定义大小和材质,并且它将使用这些信息在对象当前位置绘制一个纹理多边形。

ModeManager 和 Mode 类

大多数游戏的主要流程通常在内部表示为某种状态机。即使是最简单的游戏通常也至少有一个标题屏幕和主游戏屏幕,但加上暂停模式、高分榜、选项屏幕等,你很快就会拥有大量你的游戏可能处于的状态。

通常,这些状态是完全互斥的,但有时我们可能希望同时有几个状态处于活动状态,或者至少是可见的。例如,暂停模式通常会出现在游戏屏幕上方。只有暂停模式会接受输入,但暂停模式和游戏屏幕都需要被绘制。

一种方法(这纯粹是我个人的偏好;你的可能大相径庭)是创建一个单独的类来处理游戏的一个部分。为了更好地描述,我使用一个名为 Mode 的基类来表示这些类。

Mode 类与 GameObject 类相似,因为它提供了两个虚拟方法,称为 UpdateRender。一个模式可以被激活,这意味着它的 Update 方法将在每一帧执行,并且它可以被可见,这意味着它的 Render 方法将被调用。这两个状态完全独立于彼此。

当创建一个 Mode 实例时,它将自动添加到一个由名为 ModeManager 的单例类维护的列表中。ModeManager 类使用 Mode 实例的列表来更新所有活动模式,并在主游戏循环的每次迭代中渲染所有可见模式。

目前,项目只包含一个名为 ModeGame 的单个模式,它负责加载和释放所需资源,并初始化、更新和渲染构成游戏世界的所有 GameObject

摘要

在本章中,我们学习了 Marmalade 强大的资源管理系统。我们知道如何在其简单层面上使用它来加载和释放资源,例如位图图像或我们自己的自定义类;我们还对资源管理系统是如何构建的,以及我们如何通过自己的功能来扩展它有一个很好的了解。

我们还介绍了屏幕上渲染图像的选项,并看到了如何使用其中之一,IwGx,来在屏幕上渲染多边形。

在下一章中,我们将学习如何开始使用现代移动设备上可用的各种输入选项,因为游戏的核心就是它们的交互性。

第三章。用户输入

如果用户无法控制发生的事件,那么一款视频游戏将不会很有趣去玩,所以在本章中,我们将探讨使用 Marmalade 可以通过哪些方式增加程序的交互性。到本章结束时,你将知道如何检测以下类型的输入:

  • 键盘按键

  • 触摸屏和指针输入

  • 检测如滑动和捏合等手势

  • 使用加速度计改变设备方向

检测关键输入

我们将从最简单的玩家输入方法开始我们的旅程——按键,我们通过使用 s3eKeyboard API 来检测按键。为了在我们的代码中使用这些函数,我们只需要包含 s3eKeyboard.h 文件。

虽然触摸屏现在可能是与许多现代设备交互的主要方法,但了解如何检测按键仍然很有价值。特别是,Android 设备具有旨在用于快速访问菜单和程序导航的按键。这些按键通常甚至不是物理按钮,只是触摸屏底部的区域,但它们仍然被报告为按键。

在 Windows 模拟器中调试代码时,按键检测也非常有用,因为 Marmalade 允许完全访问您的计算机键盘。这使得通过按键触发调试功能变得非常容易。

s3eKeyboard API 允许我们通过按键状态或字符输入来检测键盘输入。它还提供了允许我们确定我们运行的设备具有哪些键盘支持的功能。

键信息的初始化和更新

有一个名为 s3eKeyboardGetInt 的函数,可以让我们找出我们的设备具有哪种类型的键盘。如果我们想的话,我们可以使用这些信息为我们的程序提供不同的输入方法。例如,在一个高分榜上输入用户名时,如果设备具有全字母键盘,用户可以直接输入他们的名字,但如果设备没有全键盘,则可能回退到使用箭头键循环字符的方法。

s3eKeyboardGetInt 函数调用需要一个来自 s3eKeyboardProperty 枚举的单个参数,并返回一个整数值。以下表格提供了可用属性的具体信息:

属性名称 描述
S3E_KEYBOARD_HAS_NUMPAD 如果设备具有数字键盘,则返回 1,否则返回 0
S3E_KEYBOARD_HAS_ALPHA 如果设备具有字母键盘,则返回 1,否则返回 0
S3E_KEYBOARD_HAS_DIRECTION 如果设备具有方向控制(上、下、左、右以及选择确定按钮),则返回 1,否则返回 0
S3E_KEYBOARD_NUMPAD_ORIENTATION 如果设备有数字键盘,则此属性将返回键盘相对于用户持握设备的方式(如果可能检测到)的朝向。
S3E_KEYBOARD_GET_CHAR 如果设备支持字符码输入方法,则返回1;如果不支持,则返回0

此表中的最终值也可以与函数s3eKeyboardSetInt一起使用,在 Android 和 iOS 设备上显示和隐藏虚拟键盘,这将允许我们在这些类型的设备上使用字符码输入方法。以下函数调用将显示虚拟键盘:

s3eKeyboardSetInt(S3E_KEYBOARD_GET_CHAR, 1);

要隐藏虚拟键盘,请传入0而不是1

由于此功能仅限于 Android 和 iOS,且无法确定在运行时是否支持该功能,如果您打算支持广泛的设备,则此方法可能最好避免。

为了让我们的程序持续接收按键更新的信息,我们必须在代码中调用函数s3eKeyboardUpdate,每次游戏帧调用一次。s3eKeyboard API 维护自己的内部缓存,用于当前按键状态,该缓存在调用此函数时更新;因此,如果我们不频繁调用s3eKeyboardUpdate,我们可能会错过按键事件。

检测按键状态

对于大多数街机风格的游戏,最有用的按键检测方法是可以发现设备上任何按键的上或下状态。s3eKeyboard API 提供了两种方法,我们可以通过轮询当前按键状态和注册回调函数来实现这一点。

使用轮询检测按键状态变化

我们将从最简单的方法开始,即轮询按键的当前状态。这可能是最简单的方法,但就游戏编程而言,在大多数情况下,这也是最佳方法,因为我们通常只想知道按键是否当前被按下或释放,以便我们可以相应地更新我们的游戏状态。

要检测设备上任何按键的当前状态,我们调用s3eKeyboardGetState,该函数从s3eKey枚举中取值(查看s3eKeyboard.h文件以获取完整列表,但您通常可以很容易地猜测枚举的名称——例如,s3eKeyUp是向上箭头键,s3eKey4是数字4键,等等)以识别我们感兴趣的按键。该函数返回一个整数值,它是一个表示该按键当前状态的位掩码。通过执行位与操作,可以检测以下按键状态:

位掩码名称 描述
S3E_KEY_STATE_DOWN 按键当前被按下。
S3E_KEY_STATE_PRESSED 在上一次调用s3eKeyboardUpdate时,按键从释放变为按下。
S3E_KEY_STATE_RELEASED 在上一次调用s3eKeyboardUpdate时,按键从按下变为释放。

如果函数返回的值是零,则可以假设该键当前处于上位置(即,没有被按下)并且也没有刚刚被释放。

以下代码片段展示了我们如何检测数字 3 键是否刚刚被按下:

if ((s3eKeyboardGetState(s3eKey3) & S3E_KEY_STATE_PRESSED) != 0)
{
  // Number 3 key has just been pressed!
}

使用回调检测键状态变化

使用回调函数,我们也可以随时得知按键被按下或释放。许多程序员更喜欢回调,因为它们迫使我们编写更小、更易于管理的函数,这通常会产生更简洁、可重用的解决方案。从表面上看,轮询方法可能更容易,但很容易导致代码库中散布着键状态检查逻辑,遍布多个源文件。使用回调方法通常会确保键处理代码以更结构化的方式实现。

要设置一个检测键状态变化的回调函数,我们使用 s3eKeyboardRegister 函数。我们向这个函数提供枚举值 S3E_KEYBOARD_KEY_EVENT 以标识我们设置的回调类型,一个指向将作为回调的函数的指针,以及一个 void 指针,可以用来向回调函数传递我们自己的自定义数据。

当按键被按下或释放时,我们将指定的函数将被调用。回调函数传递一个指向 s3eKeyboardEvent 结构的指针,该结构详细说明了按键或释放事件,并且还提供了我们在注册回调时指定的自定义数据指针。

当我们不再希望接收键状态通知时,我们可以调用 s3eKeyboardUnRegister 来禁用回调机制。我们只需传递 S3E_KEYBOARD_KEY_EVENT 枚举和指向我们回调方法的指针,以停止回调的发生。

这里有一个代码片段,用于说明我们如何检测数字 3 键的状态变化:

// Callback function that will receive key state notifications
int32 KeyStateCallback(s3eKeyboardEvent* apKeyEvent,
                       void* apUserData)
{
  if (apKeyEvent->m_Key == s3eKey3)
  {
    if (apKeyEvent->m_Pressed)
    {
      // Number 3 key has just been pressed
    }
    else
    {
      // Number 3 key has just been released
    }
  }
}

// We use this to register the callback function…
s3eKeyboardRegister(S3E_KEYBOARD_KEY_EVENT,
                    (s3eCallback) KeyStateCallback, NULL);

// …and this to cancel notifications
s3eKeyboardUnRegister(S3E_KEYBOARD_KEY_EVENT,
                      (s3eCallback) KeyStateCallback);

要使用的按键检测方法实际上取决于项目需求和个人偏好。由于调用 s3eKeyboardUpdate 会为我们缓存每个键的状态,如果我们需要在任何时间检测多个键的当前状态,轮询方法可能最佳。如果我们只想立即响应按键,而不太关心键的状态跟踪,回调方法可能更好。

检测字符代码输入

s3eKeyboard API 还提供了从键盘读取字符代码的支持。使用这种方法,我们不会收到按键或释放的通知。相反,我们收到一系列字符代码,这些代码会自动考虑任何特殊修饰键;因此,如果用户先按下 Shift 键,然后按下 A 键,最后释放这两个键,我们只会收到大写字母 A 的字符代码。

由于这不是一种即时通知的形式,而且现在越来越少设备具有可按的物理按键,这种方法对于大多数游戏来说可能不太有用。

并非所有设备都支持这种输入方法,因此你应该使用调用 s3eKeyboardGetInt(S3E_KEYBOARD_GET_CHAR)来确定它是否可以使用。

尽管为了完整性,让我们看看我们如何可以通过轮询或回调来接收字符代码。

使用轮询检测字符代码输入

要确定生成字符代码的键是否被按下,我们只需调用以下函数:

s3eWChar lCharCode = s3eKeyboardGetChar();

s3eWChar类型只是标准 C++类型wchar_t的另一种类型定义,是一种宽字符。虽然此类型的大小可能不同,但在 Marmalade 中假定它是一个 16 位值。当按键被按下时,其字符代码将被添加到队列的末尾。调用此函数将返回队列前面的字符,或者如果队列为空,则返回S3E_WEOF。我们通常在循环中调用此函数,以尝试保持队列为空,避免丢失按键。

返回的字符代码将取决于你正在运行的设备,但在大多数情况下,标准字母表 A 到 Z、数字和标点符号将是 ASCII 代码,只是存储在一个 16 位值中。

使用回调检测字符代码输入

使用接收字符代码的回调方法与接收按键状态变化的回调方法采用相同的方法。

我们再次使用s3eKeyboardRegisters3eKeyboardUnRegister来开始和停止通知的发生,但我们使用枚举值S3E_KEYBOARD_CHAR_EVENT来指示我们想要接收的是字符代码事件。

我们提供的回调函数现在将接收到一个指向s3eKeyboardCharEvent结构的指针,该结构包含一个名为m_Char的单个成员,其类型为s3eWChar。此成员将包含用户生成的字符代码。

注意

如果你在具有物理键盘的设备上运行,字符代码输入才真正推荐使用,因为在使用触摸屏设备的虚拟键盘时,许多按键可能无法被可靠地检测到,尤其是在输入正常 ASCII 字符集之外的字符时(例如,中文或日文输入)。

输入字符串

我们已经看到如何使用s3eKeyboard功能来读取字符代码,但如果我们想要允许用户输入字符串,并且我们不在乎我们的程序放弃自己的用户界面而选择标准模式字符串输入对话框,那么我们有一个可用的快捷方式。

s3eOSReadString API 使得字符串输入变得非常简单;但实际上并非每个平台都支持它。为了使用此 API,我们需要包含文件s3eOSReadString.h,然后调用函数 s3eOSReadStringAvailable来查看是否可以使用字符串输入功能。

如果我们能够使用 API,那么我们就有两个函数可供使用。第一个是s3eOSReadStringUTF8,它将显示一个字符串输入对话框,并以const char指针的形式返回 UTF-8 编码的字符串。第二个方法是s3eOSReadStringUTF8WithDefault,它允许我们指定一个 UTF-8 字符串,当对话框出现时,将使用此字符串填充字符串对话框。

注意

UTF-8 是一种广泛使用的字符格式,它允许完全的多语言字符支持。当内存问题最为关键时,它经常被使用,因为单字节字符,如标准 ASCII 字符集,仍然可以用一个字节表示。ASCII 集之外的字符(例如,日文汉字)使用两个、三个或更多字节的信息进行编码。UTF-8 的一个大优点是,您仍然可以使用空终止字符串,因为可以保证零字节永远不会成为有效字符代码的一部分。

这两个函数在其他方面的工作方式相同。它们都返回用户输入的字符串的指针(API 将负责释放此内存),或者在用户取消对话框时返回 NULL。

它们还都接受一个可选的最后一个参数,可以自定义字符串输入对话框的布局。如果省略参数或传递零值,则不应用任何限制。以下表格显示了可以使用的其他值:

描述
S3E_OSREADSTRING_FLAG_EMAIL 表示我们期望输入一个电子邮件地址。
S3E_OSREADSTRING_FLAG_NUMBER 表示我们期望输入一个数值。
S3E_OSREADSTRING_FLAG_PASSWORD 表示应用程序将使用操作系统方法输入密码,可能隐藏输入时的字符。
S3E_OSREADSTRING_FLAG_URL 表示我们期望输入一个 URL。

当在应用程序中使用这些函数时,用户可能输入我们无法处理或显示的字符;这一点应予以注意,因为通用的字符串输入并不总是最佳选择(例如,您可能无法使用游戏字体显示所有可能的字符!)。

使用此 API 也可能破坏游戏的视觉效果,因为其超级炫酷的用户界面突然被单调乏味的系统对话框覆盖或替换。

这些原因,加上它并非所有平台都支持的事实,可能意味着实现我们自己的游戏内字符串输入例程是一个更好的决定。尽管如此,如果只是为了调试目的,了解这个 API 仍然是有用的。

检测触摸屏和指针输入

现在发布的设备中,没有多少不配备触摸屏。大多数新设备都将其作为主要输入方法,并且几乎完全放弃了物理按钮。

在 Marmalade 中,我们使用s3ePointer API 检测触摸屏事件,我必须承认,对于处理触摸屏输入的 API 来说,这个名字可能不是最明显的。为了在我们的程序中使用此 API,我们只需包含s3ePointer.h文件。

这种略微奇怪的命名原因是,当这个 API 最初开发时,触摸屏并不常见。相反,一些设备有类似摇杆式的凸起,能够将指针在屏幕上移动,就像电脑上的鼠标一样。

由于触摸屏输入主要关注屏幕坐标,并且不太可能有一种设备同时具有触摸屏和指针输入,Marmalade SDK 只是简单地调整了现有的 s3ePointer API 以适应触摸屏,因为你的手指或笔实际上就是一个指针。

为了本章的目的,当我们谈论一个位置被“指向”时,我们指的是屏幕上的光标被移动到该位置,或者触摸屏在该位置有接触。位置总是以相对于屏幕左上角的像素位置返回,如下面的图示所示,它显示了在具有竖屏 HVGA 屏幕尺寸的设备上可以期待的内容,例如非视网膜显示屏的 iPhone:

检测触摸屏和指针输入

在接下来的章节中,我们将学习如何发现我们正在运行的设备上可用的功能,以及如何处理单点和多点触摸屏。

确定可用的指针功能

我们使用s3ePointerGetInt函数来确定我们正在运行的硬件的特性。我们传入以下表格中的一个值,然后我们可以使用结果相应地调整我们的输入方法。

属性 描述
S3E_POINTER_AVAILABLE 如果我们可以在设备上使用 s3ePointer API,则返回1,否则返回0
S3E_POINTER_HIDE_CURSOR 如果系统在屏幕上显示某种类似鼠标指针的光标,此属性将返回 1,如果指针当前可见,否则返回0。此属性也可以在s3ePointerSetInt函数中使用来显示和隐藏光标。
S3E_POINTER_TYPE 这将返回我们可用的指针类型。有关更多信息,请参阅下一小节。
S3E_POINTER_STYLUS_TYPE 这将返回我们设备使用的笔的类型。有关更多信息,请参阅下一小节。
S3E_POINTER_MULTI_TOUCH_AVAILABLE 如果设备支持多点触摸(能够在触摸屏上同时检测多个触摸),将返回值1。单点触摸设备将返回0

对于大多数游戏代码,通常首先使用 S3E_POINTER_AVAILABLE 属性来查看我们是否有指针能力可用,并使用 S3E_POINTER_MULTI_TOUCH_AVAILABLE 属性来适当地配置我们的输入方法。

确定指针输入类型

当向 s3ePointerGetInt 传递属性类型 S3E_POINTER_TYPE 时,返回值是来自 s3ePointerType 枚举中的一个值。

返回值 描述
S3E_POINTER_TYPE_INVALID 无效请求。最可能的原因是 s3ePointer API 在此设备上不可用。
S3E_POINTER_TYPE_MOUSE 指针输入来自具有屏幕光标以指示位置的设备。光标可能由鼠标或其他输入设备,如游戏手柄控制。
S3E_POINTER_TYPE_STYLUS 指针输入来自基于笔输入的方法,很可能是某种触摸屏。

在大多数情况下,这种区别通常并不重要,但如果您需要跟踪指针的移动,它可能是有意义的。

使用鼠标时,我们的代码会在指针在屏幕上移动时接收事件,无论鼠标按钮是否被按下。在触摸屏上,我们显然只有在屏幕被触摸时才会接收到移动事件。

注意

这在模拟器上运行时尤为明显,因为只要鼠标指针在模拟器窗口的边界内移动,我们就会接收到指针事件。

确定笔输入类型

如果我们使用 s3ePointerGetInt 与属性 S3E_POINTER_TYPE 并得到返回类型 S3E_POINTER_TYPE_STYLUS,我们可以通过再次调用 s3ePointerGetInt 并使用属性 S3E_POINTER_STYLUS_TYPE 来进一步调查我们将使用哪种类型的笔。可能的返回值如下表所示:

返回值 描述
S3E_STYLUS_TYPE_INVALID 调用无效;最可能的原因是我们没有在使用笔的硬件上运行。
S3E_STYLUS_TYPE_STYLUS 输入是通过用笔触摸输入表面来进行的。
S3E_STYLUS_TYPE_FINGER 输入是通过用手指触摸输入表面来进行的。

这可能不是我们需要担心的大部分情况中的区别,但了解这一点可能对游戏在用手指进行输入时更加宽容是有用的,因为笔的接触面积要小得多,因此应该允许更精确的输入。

更新当前指针输入状态

为了使 s3ePointer API 与当前的触摸屏输入保持最新,有必要在每一帧中调用一次 s3ePointerUpdate 函数。这将更新 s3ePointer API 内维护的当前指针状态缓存。

检测单点触控输入

如果我们的设备上提供了 s3ePointer API,我们就有保证能够检测并响应用户触摸屏幕、移动他们的笔或手指,或者移动屏幕上的光标并按下某种选择按钮。

即使我们的硬件支持多点触摸,如果我们的游戏不需要知道多个同时触摸点,我们仍然可以利用单点触摸输入。这可能使得编写我们的游戏代码变得稍微简单一些,因为我们不需要担心用户界面上的两个按钮同时被按下的这类问题。

就像键输入一样,我们可以选择使用轮询或回调方法。

使用轮询检测单点触摸输入

我们可以通过使用 s3ePointerGetXs3ePointerGetY 函数来确定当前被指向的屏幕上的位置(无论是屏幕上的光标还是屏幕上的触摸),这两个函数将返回当前指向的水平像素位置和垂直像素位置。

在触摸屏的情况下,如果用户当前没有进行输入,这些函数返回的当前位置将是用户最后已知的位置。在触摸之前,默认值将是 (0,0)——屏幕的左上角。

要确定当前是否有输入正在进行,我们可以使用 s3ePointerGetState 函数,它从一个 s3ePointerButton 枚举元素中获取一个元素,并返回一个 s3ePointerState 枚举的值。以下表格显示了组成 s3ePointerButton 枚举的值:

描述
S3E_POINTER_BUTTON_SELECT 返回左鼠标按钮或触摸屏点击的状态。
S3E_POINTER_BUTTON_LEFTMOUSE S3E_POINTER_BUTTON_SELECT 的一个备选名称,如果您还需要检测其他鼠标按钮,可能会更愿意使用它。
S3E_POINTER_BUTTON_RIGHTMOUSE 返回右鼠标按钮的状态。
S3E_POINTER_BUTTON_MIDDLEMOUSE 返回中鼠标按钮的状态。
S3E_POINTER_BUTTON_MOUSEWHEELUP 用于确定用户是否向上滚动鼠标滚轮。
S3E_POINTER_BUTTON_MOUSEWHEELDOWN 用于确定用户是否向下滚动鼠标滚轮。

下一个表格显示了 s3ePointerState 枚举的成员,它们指示请求的指针按钮或触摸屏点击的当前状态:

描述
S3E_POINTER_STATE_UP 按钮没有被按下或当前没有与触摸屏建立接触。
S3E_POINTER_STATE_DOWN 按钮正在被按下或与触摸屏建立了接触。
S3E_POINTER_STATE_PRESSED 按钮或触摸屏刚刚被按下。
S3E_POINTER_STATE_RELEASED 按钮或触摸屏刚刚被释放。
S3E_POINTER_STATE_UNKNOWN 当前按钮的状态未知。例如,请求了中间鼠标按钮的状态,但在硬件上没有中间鼠标按钮。

通过这些信息,我们现在能够跟踪指针或触摸屏位置,并确定用户何时触摸或释放触摸屏或按下鼠标按钮。

使用回调检测单个触摸输入

使用基于回调的系统跟踪指针事件也是可能的。对于单点触摸输入,我们可以为两种事件类型注册回调函数;这些是按钮和移动事件。

我们可以通过调用s3ePointerRegister函数开始接收指针事件,并且可以通过调用s3ePointerUnRegister函数停止它们。这两个函数都接受一个值来标识我们关心的事件类型,以及一个指向回调函数的指针。

在注册回调函数时,我们还可以提供一个指向我们自己的自定义数据结构的指针,该数据结构将在事件发生时传递给回调函数。

以下代码片段显示了我们可以如何注册一个回调函数,该函数将在触摸屏或鼠标按钮被按下或释放时执行:

// Callback function that will receive pointer button notifications
int32 ButtonEventCallback(s3ePointerEvent* apButtonEvent,
                          void* apUserData)
{
  if (apButtonEvent->m_Button == S3E_POINTER_BUTTON_SELECT)
  {
    if (apButtonEvent->m_Pressed)
    {
      // Left mouse button or touch screen pressed
    }
    else
    {
      // Left mouse button or touch screen released
    }
  }
  return 0;
}

// We use this to register the callback function…
s3ePointerRegister(S3E_POINTER_BUTTON_EVENT,
                   (s3eCallback) ButtonEventCallback, NULL);

// …and this to cancel notifications
s3ePointerUnRegister(S3E_POINTER_BUTTON_EVENT,
                     (s3eCallback) ButtonEventCallback);

按钮事件回调的第一个参数是一个指向s3ePointerEvent结构的指针,该结构包含四个成员。被按下的按钮存储在一个名为m_Button的成员中,其类型为s3ePointerButton(有关此枚举类型的更多详细信息,请参阅本章前面的使用轮询检测单个触摸输入部分)。

如果按钮被释放,则m_Pressed成员将为0,如果被按下,则为1。您可能期望这将是bool类型而不是整数,但事实并非如此,因为这是一个基于 C 的 API,而不是基于 C++的,并且bool不是标准 C 语言的一部分。

我们还可以通过使用结构的m_xm_y成员来发现事件发生的屏幕位置。

我们还可以注册一个回调,当用户执行指针移动操作时,它会通知我们。我们再次使用s3ePointerRegister/s3ePointerUnRegister函数,但这次使用S3E_POINTER_MOTION_EVENT作为回调类型。

我们注册的回调函数将传递一个指向s3ePointerMotionEvent结构的指针,该结构仅包含m_xm_y成员,这些成员包含当前被指向的屏幕坐标。

检测多点触摸输入

一款支持多点触控的显示屏使我们能够同时检测屏幕上的多个触摸点。每次触摸屏幕时,设备的操作系统都会为该触摸点分配一个 ID 号码。当用户在屏幕上移动手指时,与该 ID 号码关联的坐标将会更新,直到用户将手指从屏幕上移开,此时该触摸将变为不活跃状态,ID 号码也变得无效。

虽然 Marmalade 提供了基于轮询的多点触控事件处理方法,但回调方法可能是更好的选择,因为它会导致代码更加优雅,并且效率略高。

使用轮询检测多点触控输入

Marmalade 提供了一组函数,允许多点触控检测。函数 s3ePointerGetTouchStates3ePointerGetTouchXs3ePointerGetTouchY 与单点触控函数 s3ePointerGetStates3ePointerGetXs3ePointerGetY 相当,但多点触控版本接受一个单一参数——触摸 ID 号。

s3ePointer API 还声明了一个预处理定义 S3E_POINTER_TOUCH_MAX,它表示触摸 ID 号可能的最大值(加一!)。当用户触摸和释放显示时,触摸 ID 号将被重用。这一点非常重要。

以下代码片段展示了循环,它将允许我们处理当前活跃的触摸点:

for (uint32 i = 0; i < S3E_POINTER_TOUCH_MAX; i++)
{
  // Find position of this touch id.  Position is only valid if the
  // state for the touch ID is not S3E_POINTER_STATE_UNKNOWN or
  // S3E_POINTER_STATE_UP
  int32 x = s3ePointerGetTouchX(i);
  int32 y = s3ePointerGetTouchY(i);

  switch(s3ePointerGetTouchState(i))
  {
    case S3E_POINTER_STATE_RELEASED:
     // User just released the screen at x,y
     break;
    case S3E_POINTER_STATE_DOWN:
     // User just pressed or moved their finger to x,y
     // We need to know if we've already been tracking this
     // touch ID to tell whether this is a new press or a move
     break;
    default:
     // This touch ID is not currently active
     break;
  }
}

这种方法最大的问题是 Marmalade 从未向我们发送一个明确的通知,表明一个触摸事件刚刚发生。s3ePointerGetTouchState 函数永远不会返回 S3E_POINTER_STATE_PRESSED,因此我们需要在处理 S3E_POINTER_STATE_DOWN 时跟踪迄今为止所有已看到的活跃触摸 ID。如果看到一个新触摸 ID,我们就检测到了刚刚按下的条件。

虽然这段代码可以工作,但我希望你会觉得我们即将考虑的基于回调的方法会带来一个稍微更优雅的解决方案。

使用回调的多点触控输入

与轮询方法一样,使用回调的多点触控检测几乎与单点触控回调方法完全相同。我们仍然使用 s3ePointerRegisters3ePointerUnRegister 来开始和停止事件发送到我们的代码,但这次我们使用 S3E_POINTER_TOUCH_EVENT 来接收用户按下或释放屏幕的通知,以及使用 S3E_POINTER_TOUCH_MOTION_EVENT 来找出用户何时在屏幕上拖动手指。

注册到 S3E_POINTER_TOUCH_EVENT 的回调函数将接收到一个指向 s3ePointerTouchEvent 结构体的指针。这个结构体包含了事件发生的屏幕坐标(m_xm_y 成员),屏幕是否被触摸或释放(m_Pressed 成员,如果屏幕被触摸,则设置为 1),最重要的是,这个触摸事件的 ID 号(m_TouchID 成员),我们可以使用它来跟踪用户在显示上移动手指时的触摸情况。

S3E_POINTER_TOUCH_MOTION_EVENT 回调将接收一个指向 s3ePointerTouchMotionEvent 结构体的指针。这个结构体包含了被更新的触摸事件的 ID 号以及新的屏幕坐标值。这些结构体成员的名称与 s3ePointerTouchEvent 结构体中对应的成员名称相同。

Marmalade 并没有提供调整触摸事件频率的方法。相反,它实际上完全依赖于底层操作系统代码调度这些事件的多频繁。

希望你能看出基于回调的方法比轮询方法更为整洁。首先,我们可以告别轮询方法中用于检测所有当前活动触摸点的真正讨厌的循环。

其次,通过仔细的编码,我们可以使用相同的代码路径来处理单点和多点触摸输入。如果我们首先为多点触摸输入编写代码,那么让单点触摸工作就简单地将一个假的触摸 ID 添加到传入的单点触摸事件中,并将它们传递到多点触摸代码中。

识别手势输入

触摸屏的出现给移动设备带来了与向我们的程序输入相关的一系列新术语。多年来,我们一直使用鼠标,点击和拖动来与程序交互,而现在随着触摸屏的出现,我们很快就习惯了滑动和捏合的概念。

这些交互方法被称为手势,并且用户已经习惯了它们,现在如果你的应用程序没有按照他们的预期响应,他们可能会很快对你的应用程序感到沮丧。

不幸的是,Marmalade 并不提供检测这些手势的支持,因此我们不得不自己编写代码来实现。以下章节旨在提供一些指导,说明如何轻松地检测滑动和捏合手势。

检测滑动手势

当用户触摸屏幕,然后快速滑动触摸点并在释放屏幕之前将其移动到屏幕上时,就会发生滑动。

要检测滑动,我们首先必须跟踪用户触摸屏幕的屏幕坐标和触摸发生的时间。当这个触摸事件因为用户释放屏幕而结束时,我们首先检查它持续了多长时间。如果持续时间不长(比如说不到四分之一秒),我们检查起点和终点之间的距离。如果这个距离足够大(可能是一百像素的长度,或者屏幕显示尺寸的一部分),那么我们就检测到了滑动。

通常我们只想对特定方向上的滑动做出响应。我们可以通过点积来确定这一点,其公式如下所示:

检测滑动手势

点积是通过将两个向量的 x 和 y 分量相乘并将结果相加来计算的,或者通过将两个向量的长度相乘,然后乘以两个向量之间角度的余弦值来计算。

要检查用户的滑动是否在特定方向,我们首先将滑动方向转换为单位向量,然后将它与期望滑动方向的单位向量进行点积。通过使用单位向量,我们将前一个图中左侧的公式简化为两个向量之间角度的余弦值,因此现在很容易看出我们的滑动是否沿着期望的方向。

如果点积值非常接近 1,那么我们的两个方向向量几乎是平行的,因为cos(0°) = 1,我们已经检测到沿所需方向的滑动。同样,如果点积接近-1,我们检测到沿相反方向的滑动,因为cos(180°) = -1

检测捏合手势

捏合手势只能在具有多点触控显示的设备上使用,因为它们需要两个同时存在的触摸点。捏合手势通常用于允许放大和缩小,是通过在屏幕上放置两个手指然后移动它们靠近或分开来实现的。这最容易被拇指和食指完成。

在代码中检测捏合手势实际上相当简单。一旦我们在屏幕上检测到两个触摸点,我们就计算从一个点到另一个点的向量,并找到这个向量的距离。这个距离被存储为初始距离,并将表示无缩放。

当用户在屏幕上移动手指时,我们只需不断计算两个触摸点之间的新距离,然后将这个距离除以原始距离。这个计算的结果是缩放比例因子。如果用户将手指靠在一起,缩放因子将小于 1;如果他们分开手指,缩放值将大于 1。

用户从显示中移除至少一个手指后,捏合手势才算完成。

检测加速度计输入

本章我们将考虑的最后一个输入方法是加速度计,它允许我们检测用户当前持握设备时的方向。加速度计是一种可以测量作用于设备的力的传感器,无论是静态力,如重力,还是由在设备周围挥动产生的动态力。

大多数设备将有三只加速度计垂直排列,如下面的图所示。这种配置使我们能够确切地发现用户在任何时候如何持握设备,因此为我们提供了控制游戏的方法。

检测加速度计输入

前一个图中箭头的方向表示加速度将产生正值的方向。这意味着如果你将设备水平放置,显示向上,在你面前倾斜,则 x 轴加速度计将产生正值;如果你将它远离你,则 y 轴将产生正值;将设备垂直向上移动将产生 z 轴的正值。

Marmalade SDK 通过s3eAccelerometer API 为我们提供了访问设备加速度计的权限,我们可以在代码中通过包含s3eAccelerometer.h文件来使用它。

开始和停止加速度计输入

在尝试在我们的程序中使用加速度计之前,我们必须首先检查我们的设备上是否提供了加速度计输入。如果支持可用,那么我们可以开始接收加速度计输入。我们通过以下检查来完成:

if (s3eAccelerometerGetInt(S3E_ACCELEROMETER_AVAILABLE) != 0)
{
  // Accelerometer is available!  Start receiving input.
  s3eAccelerometerStart();
}

当我们完成使用加速度计时,我们只需调用s3eAccelerometerStop,我们就不会再收到进一步的输入。

注意

在移动设备上,确保我们只在实际使用时启用硬件的部分是一种良好的做法,因为这有助于节省电池电量。在加速度计的情况下,功耗可能非常小,以至于可以忽略不计,但这是移动游戏编程中始终值得牢记的领域。

读取加速度计输入

查找当前的加速度计输入值非常简单。Marmalade 提供了三个函数,这些函数返回每个轴的当前加速度计值。这些函数分别称为s3eAccelerometerGetXs3eAccelerometerGetYs3eAccelerometerGetZ。不出所料,它们返回指定轴的加速度计当前值。

这些函数返回的值使用 1000(尽管我们应该使用方便的定义S3E_ACCELEROMETER_1G以避免代码中的魔法数字!)来表示相当于正常地球重力的加速度。

当对设备进行尖锐、快速的运动时,作用在设备上的力将大于正常重力。在这种情况下,由加速度计值形成的向量的大小将大于S3E_ACCELEROMETER_1G。这可以是一种检测用户是否在摇晃设备的有效方法。

如果设备在桌面上水平放置,我们应该为 X 和 Y 轴返回0的值,为 z 轴返回-1000,因为重力是向下的!当我们旋转设备时,返回的值将形成一个向量,显示重力作用的方向,然后我们可以使用这个向量来确定设备的方向。

通过一些三角函数,我们可以计算出围绕 x 轴(向前/向后)和 Y 轴(左/右)的尖端角度。X 轴周围的角可以通过计算 Y 加速度计值除以 Z 值的反正切得到。

Y 轴周围的角稍微有点复杂。首先,我们必须找到加速度计向量在 YZ 平面上的投影长度,然后我们可以找到 X 加速度计值除以投影长度的反正切。

如果这一切听起来像太多的可怕数学,以下代码片段为我们完成了所有工作。注意,当使用 IwGeomAtan2 函数计算绕 x 轴的旋转时,我们会对 Y-和 Z-加速度计值取反,以获得更可用的结果范围,当设备水平时返回 0 度,当倾斜远离用户时返回增加的值。

iwangle xAngle = IwGeomAtan2(-accY, -accZ);
int32 lYZProjection = (int32) sqrtf((float) ((accY * accY) +
                                             (accZ * accZ)));
iwangle yAngle = IwGeomAtan2(accX, lYZProjection);

平滑加速度计输入

当使用加速度计作为输入时,我们会遇到的一个问题是它返回的值往往有点“跳跃”。即使是最稳定的手也无法将设备保持静止足够长的时间,以便从加速度计获得稳定的值。这可能导致你的游戏在你不想的时候注册动作。

解决这个问题的常见方法是通过将当前读数与之前的读数相结合来平滑加速度计值。以下代码展示了最简单的方法:

int32 accX = 0, accY = 0, accZ = 0;
int32 lSmoothFactor = IW_GEOM_ONE / 4;
// The following loop shows how we generate the smoothed accelerometer
// inputs.  In a real application the code within the loop would be called once
// per game frame.
while (TRUE)
{
  int32 deltaX = s3eAccelerometerGetX() - accX;
  int32 deltaY = s3eAccelerometerGetY() - accY;
  int32 deltaZ = s3eAccelerometerGetZ() - accZ;
  accX += IW_FIXED_MUL(lSmoothFactor, deltaX);
  accY += IW_FIXED_MUL(lSmoothFactor, deltaY);
  accZ += IW_FIXED_MUL(lSmoothFactor, deltaZ);
}

变量 accXaccYaccZ 是我们将用于程序输入的平滑加速度计值。lSmoothFactor 值决定了我们将应用多少平滑到输入中。如果它设置为 IW_GEOM_ONE,则不会应用任何平滑,结果将直接来自加速度计。

平滑因子值越低,生成的输入值抖动越小,但这将以增加输入的延迟为代价。延迟的程度取决于平滑代码执行的频率,这又取决于你游戏的帧率。

确定平滑因子的合适值实际上只是试错的过程。你只需要不断调整值,直到达到满意的结果。

注意

IW_FIXED_MUL 是 Marmalade 提供的一个有用的函数,用于执行定点乘法,其中 IW_GEOM_ONE(4096)相当于一。它将两个参数相乘,然后将结果移回正确的范围。

在 Windows 模拟器上测试加速度计输入

由于计算机通常不提供任何类型的加速度计输入,因此在 Windows 模拟器中测试这种输入形式似乎是不可能的。幸运的是,Marmalade 为我们提供了一种方法来实现这一点。

在模拟器中运行应用程序时,选择菜单项 配置 | 加速度计…,将显示一个显示小型 3D 表示的移动设备的窗口:

在 Windows 模拟器上测试加速度计输入

通过点击并拖动这个虚拟设备,我们可以改变输入到模拟器的加速度计值。对于玩游戏来说有点棘手,但通常足够了,因此你可以至少测试那些仅依赖于加速度计输入的应用程序。

窗口还提供了一些编辑框,显示当你围绕 3D 设备旋转时加速度计输入的当前值。如果你需要输入精确值,也可以使用这些编辑框。

示例代码

本章的代码包包含三个项目,展示了我们在本章中学到的内容。

手势项目

该项目通过显示用户指向的屏幕坐标来演示 s3ePointer API 的使用。如果有多点触控显示屏可用,它将显示多个触摸点。

该项目还演示了一种检测滑动和捏合的简单方法,以及如何使用相同的手势检测代码与单点和多点触控显示屏一起使用。

Slide 项目

Slide 项目展示了如何使用 s3eAccelerometer API 读取当前的加速度计值,对它们应用平滑算法,并生成围绕 X 轴和 Y 轴的倾斜角度。

它还通过允许你通过倾斜设备来在屏幕上移动一个小红盒子,展示了一些更类似游戏的功能。

滑雪项目

在本章中,我们的滑雪游戏变得互动起来,允许你左右旋转滑雪者,让他穿越屏幕并影响滚动速度。滑雪者可以通过按键、触摸屏或加速度计输入来控制。

我们还通过添加一个标题屏幕模式来获得更多的游戏流程,该模式允许选择输入方法,以及一个游戏结束模式,当玩家进入游戏世界边缘的树木时触发。

下面的章节突出了项目中添加的一些新类。

玩家旋转

玩家旋转是通过包含多个不同的动画帧来实现的,每个帧都显示了玩家以不同的旋转角度。这使得它很容易插入我们现有的GameObject代码中,该代码期望绘制一个未旋转的方形图像。

虽然这个解决方案非常简单,但可能不是最佳选择。我们本可以扩展GameObject以支持旋转图像,这样既可以节省内存(我们就不需要存储所有额外的动画帧),还可以产生更平滑的旋转效果,因为滑雪者目前以 10 度旋转的增量在帧之间移动。

ModeTitle 和 ModeGameOver 类

这些类实现了游戏的标题屏幕和游戏结束模式。这些是为了使项目看起来更像一个游戏而添加的,尽管它们看起来非常基础。

更重要的是,这些类展示了我们如何通过使它们活跃和可见来在游戏模式之间切换。特别注意的是ModeGameOver类,它阻止了正常游戏模式的更新,但仍然允许它渲染,这样我们就可以看到游戏世界以及游戏结束的消息。

摄像机类

为了在世界上指定一个观察点,项目已添加了 Camera 类。在渲染时,我们现在使用相机位置作为屏幕上的原点位置。因此,当我们移动相机时,整个屏幕显示将相对于它移动。这使得在不更新游戏世界中所有对象的 x 坐标的情况下实现水平滚动效果成为可能。

进行这种更改的另一个原因是,在下一章将游戏升级为使用 3D 模型时,会使我们的生活更加轻松,因为这与 3D 图形的渲染方式更接近。

输入管理类

项目中还添加了三个新的单例类,以使访问按键、触摸屏和加速度计输入更加整洁。它们分别称为 KeyManagerTouchManagerAccelerometerManager

这些类将 Marmalade 提供的功能封装到一个更简单的接口中,这使得我们的游戏代码更容易阅读。这也意味着我们可以在以后更改输入而无需更改游戏代码。例如,KeyManager 类提供了指示左或右箭头键是否被按住的方法。如果我们想要重新映射这些键或提供其他可能的键,我们可以在 KeyManager 代码中这样做,而我们的游戏代码将正常工作。

SkierController 类

为了在 Skier 类和各个输入管理器之间添加一层抽象,添加了 SkierController 类。这个类提供了一个“转向”值,它是一个从 -IW_GEOM_ONE 到 +IW_GEOM_ONE 的整数,表示用户尝试向左(负值)或向右(正值)转向的程度。Skier 类可以直接使用这个值来旋转滑雪者,而无需考虑这个值是如何得到的。

在内部,SkierController 类使用标题屏幕上选择的输入方法生成转向值。

对于键盘输入,左右箭头键在每个帧中稍微修改当前的转向值。

触摸屏输入使用玩家在屏幕上的水平位置来生成值;因此,当玩家触摸屏幕的左侧时为 -1,当他们触摸右侧时为 +1。

最后,加速度计输入只是将 x 轴加速度计值缩放到所需的范围内。

摘要

在本章中,我们介绍了如何通过检测按键和触摸屏点击以及使用现代移动设备的加速度计来使我们的程序具有交互性。我们还看到了如何在此基础上构建基本功能以检测滑动和捏合手势。

在下一章中,我们将通过展示如何使用 Marmalade 在我们的游戏中渲染 3D 图形来回到更图形化的内容。

第四章:3D 图形渲染

普通智能手机内部的图形硬件现在能够渲染出令人惊讶的高质量 3D 图形,这对于一个足够小可以放入口袋的设备来说是非常了不起的。

Marmalade SDK 使得在你的游戏中使用 3D 图形变得极其简单,正如我们在本章接下来的主题中将要了解的那样:

  • 3D 图形渲染的基本原理——投影、裁剪、光照等

  • 完全通过代码创建和渲染一个简单的 3D 模型

  • 从建模软件中导出 3D 模型数据

  • 将导出的 3D 模型加载到内存中并渲染它们

3D 图形快速入门

在我们开始编写渲染代码之前,让我们简要地了解一下如何实现 3D 渲染的基本原理。如果你已经很好地掌握了 3D 渲染技术,那么你可以自由地跳过这一部分。

描述 3D 模型

在计算机图形学中,一个对象的 3D 表示通常被称为模型。当我们为视频游戏构建一个三维模型时,我们创建了一组定义模型形状的三角形。我们也可以使用四边形来简化建模过程,但在渲染时这些最终会被转换成两个三角形。

因此,3D 模型的最简单表示仅仅是一个大列表的顶点,这些顶点定义了渲染模型所需的三角形,但我们经常指定大量额外的信息,以便我们可以精确控制模型在屏幕上的显示方式。

指定模型的顶点流

每个 3D 模型都有一个旋转中心,也称为原点,这是模型围绕其旋转和缩放的点。在 3D 建模软件中,这个点可以放置在你想要的位置,但在游戏中为了简化数学计算,我们通常将点(0,0,0)视为旋转中心。

模型中的每个三角形都是由三个顶点定义的,每个顶点由一个 x、y 和 z 组件组成,这些组件声明了顶点在所谓的模型空间(有时也称为对象空间)中的位置。这仅仅意味着每个顶点的组件相对于模型的旋转中心。

下图展示了一个立方体的示例。旋转中心位于立方体的中心,因此是模型空间的原点。角点使用正负值,但每个组件的绝对值都是100,这使得立方体的边长为 200 单位。为了清晰起见,立方体的三个正面也显示了它们是如何由两个三角形构建而成的。

指定模型的顶点流

为了向 Marmalade 提供立方体的顶点,我们只需使用 Marmalade 的三分量浮点指针向量类CIwFVec3来提供一个顶点数组。与 2D 渲染一样,我们已经看到这被称为顶点流,只不过这次流由三个分量向量组成。

指定模型的索引流

你会注意到,在前面的图中,立方体的角已经被标注了一个数字以及它们在模型空间中的坐标。如果我们回顾一下我们在 2D 图形中的工作,我们会记得 Marmalade 通过接受一个顶点流作为输入以及一个定义这些顶点应如何处理的顺序的索引流来渲染多边形。

当渲染 3D 图形时,同样的方法也适用。我们将索引流指定为一个无符号 16 位整数数组(uint16),这决定了从流中读取顶点的顺序,以便进行渲染。

使用索引流的一个优点是,我们可以潜在地多次引用同一个点,而无需在顶点流中重复它,从而节省一些内存。由于索引流只是告诉 GPU 它必须按照什么顺序处理顶点、颜色、UV 和法线流中包含的数据,因此它可以是我们想要的任意长度或短。索引流甚至不需要引用其他流的每个元素,这意味着我们可以潜在地创建一组可以由多个不同的索引流引用的流。

索引流的另一个优点是,我们可以使用它们来加速渲染。你会记得我们使用IwGxDrawPrims函数调用来渲染 2D 多边形。要渲染 3D 多边形,我们使用完全相同的调用。对这个函数的每次调用都会导致渲染引擎必须执行一些初始化,因此如果我们能找到一种方法来最小化我们必须做出的绘制调用次数,我们就可以更快地渲染游戏世界。

我们可以通过在多边形渲染列表中插入退化多边形来实现这一点。一个退化多边形是指绘制时不会修改任何像素的多边形,这是通过确保构成多边形的所有顶点都位于同一直线上来实现的。大多数图形硬件足够聪明,能够识别退化多边形,并且不会浪费时间尝试渲染它。

例如,假设我们正在渲染一些三角形带。我们可以通过调用IwGxDrawPrims两次来渲染它们,或者我们可以用一些退化多边形将两个带子连接起来,并通过一次调用IwGxDrawPrims来渲染它们。我们可以继续这样做,将我们想要的任意数量的三角形带连接起来。

我们如何指定退化三角形?以下图示中显示的最简单方法是复制第一条带的第一点以及第二条带的第一点。这产生了四个退化三角形(A3A4A4、A4A4B0、A4B0B0、B0B0B1),但比多次绘制调用更可取。以下图示中的虚线显示了连接条带的额外退化三角形(这些三角形会塌陷形成一条线!):

指定模型的索引流

指定模型的颜色、UV 和法向量流

就像 2D 渲染一样,我们还可以提供许多其他流类型,使渲染的多边形看起来更有趣。我们可以以与二维渲染相同的方式提供颜色和纹理 UV 流,但我们还可以指定一种称为法向量流的第三种流类型。

在 3D 数学中,法向量被定义为垂直于两个其他非平行向量的向量,换句话说,就是指向多边形面向方向的向量。以下图示说明了这一点:

指定模型的颜色、UV 和法向量流

为什么法向量流有用?嗯,它允许我们模拟光线对我们 3D 模型的影响。通过为我们的模型每个顶点提供一个单位法向量(即指向多边形法线方向的向量,长度为一单位),我们可以计算从该顶点反射的光量,并相应地调整渲染的颜色。

3D 模型的实时光照可能是一个耗时的工作,所以在编写游戏时,我们尽量在可能的情况下避免这样做,以加快渲染速度。如果我们不想对 3D 模型进行光照,就没有必要指定法向量流;因此,通过不光照模型,我们也能节省内存。

在指定这些额外流时,有几个要点需要注意。

首先,Marmalade 期望提供的颜色、UV 和法向量的数量与提供的顶点数量相匹配。虽然你可以指定不同长度的流,但这通常会导致断言被触发,并且在渲染时可能会产生意外的结果。

其次,也许最重要的是,这些额外的流可能需要我们在顶点流中添加额外的顶点副本,因为我们只能提供一个索引流。

以一个立方体为例,其中每个顶点都是立方体三个不同面的角点。由于每个面指向不同的方向,我们需要将每个顶点重复三次,以便在索引流中引用,同时包含三个不同的法向量。

指定模型的颜色、UV 和法向量流

当顶点的 UV 或颜色跨越它所形成的每个多边形时,我们也会遇到相同的问题。

对于我们遇到的每种不同的颜色、UV 和法线组合,我们需要提供每个顶点的额外副本,因此也需要额外的颜色、UV 和法线值,以确保所有流长度相同。

执行 3D 到 2D 投影

当我们将我们的 3D 世界渲染到显示设备上时,我们必须以某种方式将我们的 3D 模型顶点数据转换为 2D 屏幕坐标,然后我们才能绘制任何东西。这个过程被称为投影,通常使用矩阵数学在坐标系之间转换顶点,直到我们得到屏幕坐标,使得构成 3D 模型的三角形可以在屏幕上渲染。

执行 3D 到 2D 投影

以下部分提供了将点投影到屏幕上的步骤概述,以确保你熟悉涉及的关键概念。本书不涉及 3D 图形数学的详细解释,因此预期你会熟悉矩阵是什么,以及几何操作如旋转、缩放和平移。

理解 3D 图形中的矩阵

回想一下学校数学课程,你可能会记得矩阵被描述为在尝试对向量执行旋转、平移和缩放等操作时是一个有用的工具。

我个人对学习矩阵的记忆是,当时它们似乎有点神奇。这里有一个数字网格,可以用来执行一系列非常有用的几何操作,更重要的是,你可以通过相乘几个矩阵来一次执行多个操作。这个概念本身是合理的,但涉及到的数字太多,看起来有点令人困惑。

在 3D 几何中,我们通常使用一个 4x4 矩阵,其中左上角的 3x3 数字网格代表矩阵的旋转和缩放部分,而底部第一行的前三个数字代表所需的平移。

虽然翻译部分对我来说完全合理,但矩阵中的 3x3 旋转和缩放部分是我从未真正掌握的部分,直到我发现矩阵的这一部分实际上代表的是 x、y 和 z 轴的大小和方向。

看一下以下图像,它显示了 4x4 矩阵的单位矩阵。这意味着矩阵中的每个元素都是0,除了从左上角到底右角的对角线上的元素,它们都是1

理解 3D 图形中的矩阵

注意到顶行前三个数字是(1,0,0),这恰好是一个沿着 x 轴的单位向量。同样,第二行是(0,1,0),它代表一个沿着 y 轴的单位向量,而第三行(0,0,1)是一个沿着 z 轴的单位向量。

一旦我意识到这一点,创建矩阵以执行不同类型的几何操作就变得更为明显。

想要围绕 y 轴旋转?只需计算出 x 轴和 z 轴需要位于的方向向量,并将这些向量放入矩阵的相关部分。同样,缩放操作只是意味着我们为每个要缩放的轴提供一个非单位大小的向量。

一些人在阅读这篇文档时可能会想“这很明显”,但如果这能帮助至少一个人更好地理解如何理解矩阵数学,我的工作就完成了!

坐标系之间的转换

当我们讨论 3D 模型在数据表示中的表示时,我们提到模型的顶点位于模型空间中。因此,为了使用这些顶点进行渲染,我们必须将这些模型空间顶点转换为屏幕坐标。

这个过程的第一个步骤是使用模型矩阵将顶点从模型空间转换为世界空间。模型中的每个顶点都乘以模型矩阵,这将首先旋转和缩放顶点,使模型正确定位,然后平移每个点,使模型的重心现在位于矩阵中提供的平移位置。

现在我们所有的顶点都正确地定位在我们的虚拟世界中,下一步是将它们转换为视图空间,这是由我们的视点的位置和方向定义的坐标系,由于显而易见的原因,通常被称为我们的相机。我们通过提供另一个称为视图矩阵(如果你更喜欢,也可以称为相机矩阵)的矩阵来完成这项工作,这个矩阵将旋转、缩放和转换世界空间顶点,使它们现在相对于我们的相机视图。

现在顶点已经位于视图空间中,最后的操作是将顶点转换为 2D 屏幕坐标。我们有两种方法来做这件事,这包括正射投影透视投影

正射投影将视图空间坐标仅缩放并平移每个顶点的 x 和 y 分量,将它们放置到屏幕上。顶点的 z 分量在计算实际屏幕坐标时不起作用,但它用于确定多边形的绘制顺序,因为它用作深度值。

然而,在大多数情况下,我们使用透视投影。同样,每个视图空间顶点的 x 和 y 分量用于生成 x 和 y 屏幕坐标,但这次它们被顶点的 z 分量除以,这会使远离物体的物体看起来更小。

这些分量还乘以一个称为透视乘数的常数。这个值实际上是视图平面距离相机的距离。视图平面是包含屏幕显示矩形区域的平面。

通常,当我们思考相机视图时,更方便的是考虑视野,即我们观察锥体的水平角度。以下图示显示了如何将这个角度转换为正确的透视乘数值:

坐标系统之间的转换

投影透视的下一部分是将投影点进行转换。通常我们希望位于相机正前方的点显示在屏幕中心,因此我们会将屏幕宽度的一半加到 x 分量上,将屏幕高度的一半加到 y 分量上。可以指定不同的偏移位置,这在我们需要在游戏用户界面中显示 3D 模型时特别有用。假设你想要在屏幕右上角绘制一个玩家刚刚捡起的可收集对象的 3D 模型。指定这个屏幕位置的偏移比尝试计算相对于相机位置的 3D 空间中的位置要容易得多,这个位置等同于所需的屏幕区域。

裁剪平面

我们已经讨论了视图平面作为包含最终屏幕显示的平面,但还有一些其他平面用于帮助加速渲染并避免一些奇怪的图形错误发生。

首先,我们有远裁剪平面近裁剪平面,它们与视图平面平行。我们通过提供这些平面与相机视点的垂直距离来告诉 Marmalade 这些平面应该位于何处。

远裁剪平面阻止距离相机太远的多边形被渲染,而近裁剪平面,不出所料,阻止距离相机太近的多边形被渲染。近裁剪平面尤其重要,因为我们如果不使用它,就会开始看到位于相机后面的模型在屏幕上被渲染。

你通常应该尽量使远裁剪平面和近裁剪平面尽可能靠近,因为这些值也用于计算深度缓冲区值。如果平面相隔太远,你可能会开始遇到被称为闪烁Z 冲突的渲染问题。这些问题可能发生在深度缓冲区值分辨率不足时,导致远距离多边形渲染时边缘参差不齐,或者更糟糕的是,当它们或相机移动时,会随机穿透彼此。以下图像显示了尝试渲染两个重叠的共面多边形时可能发生的另一种 Z 冲突示例:

裁剪平面

还有四个名为左、右、上和下的裁剪平面。这些是通过相机位置和屏幕显示区域在视平面上的左、右、上或下边界的平面。它们共同形成一个金字塔形体积,从相机发出,定义了 3D 空间中可见的部分,因此可以出现在屏幕上。

Marmalade 自动为我们管理裁剪平面,它们非常有用,因为它们允许我们快速拒绝整个模型提交渲染,如果该模型完全在屏幕之外。离屏检查是通过我们正在渲染的模型的边界球体来执行的,这只是一个以模型的重心为中心,包含模型所有顶点的球体。边界球体可以快速与所有六个裁剪平面进行测试,如果边界球体完全在裁剪体积之外,则可以跳过该模型。

光照

为了完成我们的 3D 入门,让我们快速看一下实时光照是如何实现的。我们不会过多关注其数学原理,因为 Marmalade 主要为我们处理这些,所以我们将只解释我们可以利用的不同类型的光照。

我们将要讨论的每种光照类型都可以随时启用或禁用。禁用不同的光照类型可以缩短渲染时间。

发光光照

Marmalade 提供的最简单类型的光照是发光光照,它只是渲染多边形自然具有的颜色量。发光光照颜色由设置在渲染多边形时的CIwMaterial实例提供。

发光光照对于想要用单一纯色绘制多边形很有用,但通常我们希望有更多的灵活性,因此我们可能会设置一个颜色流,或者使用其他光照形式之一。

环境光照

环境光照为我们场景中的背景光水平提供光源,例如可能由太阳提供的光。

没有环境光照,任何不是直接面向光源的多边形将几乎没有光照,因此会显得是黑色的。通常这不是非常理想,因此我们可以使用环境光照为我们的多边形提供一个基本的颜色和亮度水平。

在 Marmalade 中,我们将全局环境光照项设置为 RGB 颜色。在渲染时使用的CIwMaterial实例也具有一个与环境光照结合的环境光照值。如果材质的环境光照设置为亮白色,多边形将以全局环境光照的全部量进行渲染。

如果全局环境光照被禁用,则直接使用材质的环境颜色来控制渲染多边形的颜色。这提供了一种在渲染时轻松调整模型亮度和暗度的简单方法。

漫反射光照

为了使用漫射照明,我们的模型数据必须提供一个法线流。漫射光由颜色和光指向的方向组成。光的方向向量通过点积运算与模型中每个顶点的法线向量相结合。

点积运算的结果乘以全局漫射照明颜色和当前的CIwMaterial漫射颜色或颜色流中的 RGB 值(如果提供了的话)。这将产生用于将多边形渲染到屏幕上的最终颜色值。

镜面照明

与漫射照明一样,镜面照明只能在我们提供了法线流的情况下工作。它还需要指定漫射光,因为它依赖于漫射光的方向。

这种类型的照明使我们能够通过使模型在面向漫射光的方向上暂时变亮,使其看起来更亮。

我们可以为CIwMaterial指定全局和镜面光颜色,并且材料还提供了一个镜面功率的设置。此值允许我们缩小镜面照明的响应。数值越高,顶点法线必须几乎与照明方向平行,镜面照明才会生效。

使用 IwGx 渲染 3D 图形

记得我们在第二章“资源管理和二维图形渲染”中查看二维图形渲染时,我说我们将使用 IwGx,因为它会使过渡到渲染三维图形变得容易得多。现在是时候看看我的说法是否正确了!

在本节中,我们将探讨如何实现“Hello World”程序的 3D 等效——一个旋转的立方体。

为 3D 渲染准备 IwGx

与二维渲染一样,我们首先需要做的事情是通过调用IwGxInit初始化 IwGx API,当然,我们应在程序结束时调用IwGxTerminate

当 IwGx 准备就绪后,我们接下来需要设置投影。我们将使用透视投影,因此我们需要能够指定我们想要使用的透视乘数值。执行此操作的代码如下:

IwGxSetPerspMul((float) IwGxGetScreenWidth() * 0.5f);

这行代码设置了透视乘数,以提供 90 度的视野。请参阅本章前面的坐标系统转换部分,了解更多关于如何计算所需的透视乘数值的信息。

接下来,我们必须设置远裁剪面和近裁剪面的距离。为了我们的演示目的,我们将近平面设置为10,远平面设置为1000;这些值设置如下:

IwGxSetFarZNearZ(1000.0f, 10.0f);

这些值在视图空间单位中,可以设置为任何大于零的值(远值应大于近值!)以适应我们游戏的需求。通常,远裁剪距离是最重要的,因为它需要设置得足够远,以便我们的世界能够满意地渲染,但又不能太远,以免帧率下降,因为我们渲染了太多的内容。

注意

你可能想知道为什么这些数字被写成 10.0f 而不是只是 10 或 10.0?原因是确保编译器将这些值视为单精度 float 值。后两种形式都将被解释为 double,这可能导致从 doublefloat 的耗时转换。

设置照明信息

为了让我们的旋转立方体看起来更有吸引力,我们将设置一些光源,以便当立方体旋转时,其表面颜色相应地改变。Marmalade 提供的照明支持可能看起来有点有限,但通常足以满足大多数移动游戏的需求。

Marmalade 只允许我们定义一个环境光和一个漫反射光。让我们首先设置全局环境光照值。

我们首先调用的第一个函数是 IwGxSetLightType,它接受一个 ID 数字来标识我们想要修改的光,以及一个描述我们指定光类型的定义。这个 API 可能被选择是为了让 Marmalade 能够轻松地支持更多的光源,但到目前为止,ID 数字只能是零或一,光类型必须是 IW_GX_LIGHT_AMBIENTIW_GX_LIGHT_DIFFUSEIW_GX_LIGHT_UNUSED 之一。后者值可以用来禁用光源。

在处理完光类型后,我们使用函数调用 IwGxSetLightCol 来设置光的颜色。这个函数有两个版本。两者都接受我们想要修改的光的 ID,但光的 RGB 颜色可以是三个 uint8 值(红色、绿色和蓝色),或者可以提供一个指向 CIwColour 实例的指针。

以下代码将 ID 为零的光源设置为具有中等灰色颜色的环境光:

IwGxSetLightType(0, IW_GX_LIGHT_AMBIENT);
IwGxSetLightCol(0, 128, 128, 128);

现在我们来创建一个具有漫反射光和镜面高光的光源。为此,我们需要两个额外的函数,IwGxSetLightSpecularCol 用于设置镜面高光的颜色,以及 IwGxSetLightDirn 用于设置光的方向。方向是以世界空间坐标为单位指定的单位向量。以下是一些示例代码:

CIwFVec3 lLightDir(1000.0f, 0.0f, 1000.0f);
lLightDir.Normalise();

IwGxSetLightType(1, IW_GX_LIGHT_DIFFUSE);
IwGxSetLightCol(1, 128, 128, 128);
IwGxSetLightSpecularCol(1, 200, 200, 200, 255);
IwGxSetLightDirn(1, &lLightDir);

这段代码片段设置了 ID 为 1 的光源,使其成为一个具有中等灰色强度和更亮灰色镜面高光的漫反射光。光源指向世界坐标系的 x 和 z 轴之间的 45 度角。

我们的光源现在已经初始化完成,所以剩下的就是通知 Marmalade 我们要打开它们!为此,我们有多个函数可供选择。我们可以使用IwGxLightingOnIwGxLightingOff来启用或禁用所有已初始化的光源,或者我们可以独立地启用照明模型的每个部分。以下示例代码禁用了发射性照明,但启用了环境、漫反射和镜面照明:

IwGxLightingAmbient(true);
IwGxLightingDiffuse(true);
IwGxLightingEmissive(false);
IwGxLightingSpecular(true);

由于我们使用的是镜面照明,还有一件事要做。用于渲染我们的多边形的材质必须指定一个镜面颜色和功率。材质的镜面颜色用于调制全局镜面颜色,而功率值表示顶点法线必须接近光方向的程度,以便启动镜面高光。功率值是一个uint8值,只有非常低的值(即小于 8)才能在渲染效果中产生明显的差异。以下是说明这一点的代码:

CIwMaterial* lpMaterial = new CIwMaterial;
lpMaterial->SetColSpecular(255, 255, 255);
lpMaterial->SetSpecularPower(3);

注意

之前的例子只是利用了 Marmalade 的内置照明模型,因为它易于使用,并且对于大多数需求来说效果足够好。然而,我们完全没有必要使用这个照明模型,因为我们没有任何阻止我们使用我们想要的任何照明算法来生成自己的颜色流。或者,我们也可以使用 OpenGL ES 2.0 着色器,尽管关于这个特定主题的讨论超出了本书的范围。

立方体的模型数据

我们将渲染一个带有每个面不同颜色的光照立方体,因此我们需要提供顶点、颜色和法线的数据流,以及一个索引流来显示这些数据应该如何被渲染引擎解释。由于在这个例子中我们没有使用纹理,因此不需要提供 UV 流。

我们还希望我们的绘图尽可能高效,因此我们的目标是只用一次IwGxDrawPrims调用来绘制整个立方体。要做到这一点,我们需要每个顶点有三个副本(每个顶点所属的每个面一个),这样我们就可以为它分配不同的颜色和法线,我们还需要在我们的索引流中指定一些退化三角形,以便将所有面连接成一个大的三角形带。

让我们从顶点流开始。我们分配一个CIwFVec3数组,并用顶点数据初始化它。立方体的中心点将是正中心,因此所有顶点坐标将具有相同的幅度。

const uint32 lVertexCount = 24;
CIwFVec3* v = new CIwFVec3[lVertexCount];
v[0].x = 100.0f;    v[0].y = -100.0f;  v[0].z = -100.0f;
v[1].x = -100.0f;   v[1].y = -100.0f;  v[1].z = -100.0f;
v[2].x = 100.0f;    v[2].y = 100.0f;   v[2].z = -100.0f;
v[3].x = -100.0f;   v[3].y = 100.0f;   v[3].z = -100.0f;
v[4].x = 100.0f;    v[4].y = -100.0f;   v[4].z = 100.0f;
v[5].x = 100.0f;    v[5].y = -100.0f;   v[5].z = -100.0f;
v[6].x = 100.0f;    v[6].y = 100.0f;    v[6].z = 100.0f;
v[7].x = 100.0f;    v[7].y = 100.0f;    v[7].z = -100.0f;
v[8].x = 100.0f;    v[8].y = -100.0f;   v[8].z = 100.0f;
v[9].x = 100.0f;    v[9].y = 100.0f;    v[9].z = 100.0f;
v[10].x = -100.0f;  v[10].y = -100.0f;  v[10].z = 100.0f;
v[11].x = -100.0f;  v[11].y = 100.0f;   v[11].z = 100.0f;
v[12].x = -100.0f;  v[12].y = -100.0f;  v[12].z = 100.0f;
v[13].x = -100.0f;  v[13].y = 100.0f;   v[13].z = 100.0f;
v[14].x = -100.0f;  v[14].y = -100.0f;  v[14].z = -100.0f;
v[15].x = -100.0f;  v[15].y = 100.0f;   v[15].z = -100.0f;
v[16].x = -100.0f;  v[16].y = 100.0f;   v[16].z = -100.0f;
v[17].x = -100.0f;  v[17].y = 100.0f;   v[17].z = 100.0f;
v[18].x = 100.0f;   v[18].y = 100.0f;   v[18].z = -100.0f;
v[19].x = 100.0f;   v[19].y = 100.0f;   v[19].z = 100.0f;
v[20].x = -100.0f;  v[20].y = -100.0f;  v[20].z = -100.0f;
v[21].x = -100.0f;  v[21].y = -100.0f;  v[21].z = 100.0f;
v[22].x = 100.0f;   v[22].y = -100.0f;  v[22].z = -100.0f;
v[23].x = 100.0f;   v[23].y = -100.0f;  v[23].z = 100.0f;

顶点是按面顺序排列的,所以前四个顶点形成立方体的前面,接下来的四个形成右手面,依此类推。你可以自由地指定顺序,因为最终是索引流将决定单个三角形的渲染方式。

现在我们将创建法线流。在 Marmalade 中,法线也被指定为CIwFVec3实例,并且它们应该具有单位长度。这意味着向量的模长应该是一。下面是一个执行此任务的代码片段:

CIwFVec3* n = new CIwFVec3[lVertexCount];
n[0].x = 0.0f;      n[0].y = 0.0f;    n[0].z = -1.0f;
n[1].x = 0.0f;      n[1].y = 0.0f;    n[1].z = -1.0f;
n[2].x = 0.0f;      n[2].y = 0.0f;    n[2].z = -1.0f;
n[3].x = 0.0f;      n[3].y = 0.0f;    n[3].z = -1.0f;
n[4].x = 1.0f;      n[4].y = 0.0f;    n[4].z = 0.0f;
n[5].x = 1.0f;      n[5].y = 0.0f;    n[5].z = 0.0f;
n[6].x = 1.0f;      n[6].y = 0.0f;    n[6].z = 0.0f;
n[7].x = 1.0f;      n[7].y = 0.0f;    n[7].z = 0.0f;
n[8].x = 0.0f;      n[8].y = 0.0f;    n[8].z = 1.0f;
n[9].x = 0.0f;      n[9].y = 0.0f;    n[9].z = 1.0f;
n[10].x = 0.0f;     n[10].y = 0.0f;   n[10].z = 1.0f;
n[11].x = 0.0f;     n[11].y = 0.0f;   n[11].z = 1.0f;
n[12].x = -1.0f;    n[12].y = 0.0f;   n[12].z = 0.0f;
n[13].x = -1.0f;    n[13].y = 0.0f;    n[13].z = 0.0f;
n[14].x = -1.0f;    n[14].y = 0.0f;    n[14].z = 0.0f;
n[15].x = -1.0f;    n[15].y = 0.0f;    n[15].z = 0.0f;
n[16].x = 0.0f;     n[16].y = 1.0f;    n[16].z = 0.0f;
n[17].x = 0.0f;     n[17].y = 1.0f;    n[17].z = 0.0f;
n[18].x = 0.0f;     n[18].y = 1.0f;    n[18].z = 0.0f;
n[19].x = 0.0f;     n[19].y = 1.0f;    n[19].z = 0.0f;
n[20].x = 0.0f;     n[20].y = -1.0f;   n[20].z = 0.0f;
n[21].x = 0.0f;     n[21].y = -1.0f;   n[21].z = 0.0f;
n[22].x = 0.0f;     n[22].y = -1.0f;   n[22].z = 0.0f;
n[23].x = 0.0f;     n[23].y = -1.0f;   n[23].z = 0.0f;

现在我们需要一个颜色流。就像在 2D 渲染中一样,这需要一个CIwColour实例的数组。下面是代码片段!

CIwColour* c = new CIwColour[lVertexCount];
c[0].Set(255, 0, 0, 255);
c[1].Set(255, 0, 0, 255);
c[2].Set(255, 0, 0, 255);
c[3].Set(255, 0, 0, 255);
c[4].Set(255, 255, 0, 255);
c[5].Set(255, 255, 0, 255);
c[6].Set(255, 255, 0, 255);
c[7].Set(255, 255, 0, 255);
c[8].Set(0, 255, 0, 255);
c[9].Set(0, 255, 0, 255);
c[10].Set(0, 255, 0, 255);
c[11].Set(0, 255, 0, 255);
c[12].Set(0, 0, 255, 255);
c[13].Set(0, 0, 255, 255);
c[14].Set(0, 0, 255, 255);
c[15].Set(0, 0, 255, 255);
c[16].Set(0, 255, 255, 255);
c[17].Set(0, 255, 255, 255);
c[18].Set(0, 255, 255, 255);
c[19].Set(0, 255, 255, 255);
c[20].Set(255, 128, 0, 255);
c[21].Set(255, 128, 0, 255);
c[22].Set(255, 128, 0, 255);
c[23].Set(255, 128, 0, 255);

最后,是创建索引流的时候了。同样,就像在 2D 渲染中一样,这只是一个uint16值的数组,它指示了流中元素应该被访问的顺序。下面是代码:

const uint32 lIndexCount = 34;
uint16* i = new uint16[lIndexCount];

// Front face (red)
i[0] = 0;  i[1] = 1;  i[2] = 2;  i[3] = 3;
// Degenerate
i[4] = 3;  i[5] = 7;
// Right face (yellow)
i[6] = 7;  i[7] = 6;  i[8] = 5;  i[9] = 4;
// Degenerate
i[10] = 4;  i[11] = 9;
// Back face (green)
i[12] = 9;  i[13] = 11;  i[14] = 8;  i[15] = 10;
// Degenerate
i[16] = 10;  i[17] = 12;
// Left face (blue)
i[18] = 12;  i[19] = 13;  i[20] = 14;  i[21] = 15;
// Degenerate
i[22] = 15;  i[23] = 16;
// Bottom face (cyan)
i[24] = 16;  i[25] = 17;  i[26] = 18;  i[27] = 19;
// Degenerate
i[28] = 19;  i[29] = 23;
// Top face (orange)
i[30] = 23;  i[31] = 21;  i[32] = 22;  i[33] = 20;

注意,流中的前四个值定义了立方体的第一个完整面。接下来的两个值形成一个退化三角形,它允许我们将第一个面与第二个面连接起来,而实际上并不渲染任何内容。正如我们在本章前面看到的,将两个三角形带连接起来的最简单方法是将第一个带的最后一个索引重复,并以它的第一个索引的两个副本开始下一个带。这种模式一直持续到我们绘制了立方体的最后一个面。

规定顶点的顺序是最重要的考虑因素,因为我们必须确保我们得到正确的裁剪模式。对于背面裁剪(即远离相机的面不会被渲染),我们需要第一个指定的三角形顶点的顺序为逆时针。

由于我们使用三角形带,顶点的顺序实际上在逆时针和顺时针之间交替。通常我们不必太担心这一点,因为带中顶点的自然顺序会处理它,但当你尝试将包含奇数个顶点的三角形带连接起来时,这可能会引起问题。

注意

将三角形带与退化三角形连接的一般规则是,具有奇数个点的带需要反转下一个带中点的顺序。例如,如果你的第一个三角形带包含奇数个点,下一个带的第一个三角形需要按顺时针而不是逆时针顺序指定;否则它将不会被正确裁剪。

视图矩阵

在渲染 3D 图形时,我们需要能够提供我们想要从哪个位置和方向查看游戏世界的信息。我们通过提供视图或相机矩阵来实现这一点;在 Marmalade 中,可以使用CIwFMat类的实例来完成。

CIwFMat 类使用一个 3 x 3 的 float 数组来表示旋转部分,以及 CIwFVec3 来表示平移部分,来表示一个 4 x 4 矩阵。4 x 4 矩阵的其余元素(即最右侧的数字列)被固定为与单位矩阵相同(从列的顶部到底部依次为 0, 0, 0, 和 1)。这些值对正常的 3D 变换没有任何影响;因此,通过省略它们,我们可以节省内存,并且可以通过不执行这些矩阵部分的乘法来使矩阵乘法代码稍微更高效。

是时候创建一个合适的视图矩阵了。为了我们旋转立方体的目的,如果我们能够指定摄像机的位置,然后计算矩阵的正确旋转来查看我们的立方体,那将很好。幸运的是,矩阵类有一个名为 LookAt 的方法,这使得这变得容易实现:

CIwFMat vm;
vm.t.x = 0.0f;  vm.t.y = 0.0f;  vm.t.z = -400.0f;
vm.LookAt(vm.t, CIwFVec3::g_Zero, CIwFVec3::g_AxisY);

之前的代码声明了一个新的 CIwMat 实例,并将其平移设置为 (0, 0, -400)。然后我们调用 LookAt 方法,该方法传递我们想要放置摄像机的位置,我们想要它朝向的空间中的点,以及垂直向上的单位向量。

Marmalade 在 3D 渲染时的默认坐标系中,x 轴的正方向从屏幕的左侧到右侧运行,而 z 轴的正方向进入屏幕。然而,正 y 轴的方向是从屏幕顶部到底部,这可能不是你最初预期的。我们习惯于将地面以上的高度视为正数,但在 Marmalade 中,它将是负数。

一旦我们有了视图矩阵,我们就可以使用一个指向矩阵的 const 指针调用函数 IwGxSetViewMatrix

模型矩阵

模型矩阵用于在世界上定位我们的 3D 模型,并允许它按需旋转或缩放。与视图矩阵一样,可以使用 CIwFMat 实例指定模型矩阵。

对于我们的旋转立方体,我们将创建一个矩阵,使立方体围绕 x 和 y 轴旋转。我们通过创建两个矩阵来实现这一点,一个用于 x 轴旋转,另一个用于 y 轴旋转,然后将它们相乘。我们将立方体定位在世界的原点。

CIwFMat lModelMatrix;
lModelMatrix.SetRotY(lRotationY);
CIwFMat lRotX;
lRotX.SetRotX(lRotationX);
lModelMatrix.PreMult(lRotX);

显示的代码声明了两个 CIwFMat 实例,并使用 SetRotYSetRotX 方法生成围绕 y 和 x 轴的旋转矩阵。旋转角度由两个变量 lRotationYlRotationX 提供,这两个变量都是 float 类型,表示旋转的角度(以弧度为单位)。如果我们增加这两个变量的值,每次主游戏循环迭代,它将改变立方体的方向,使其在渲染时看起来在旋转。

注意

在使用矩阵类的 SetRotXSetRotYSetRotZ 方法时要小心。这些方法接受两个额外的 bool 参数,允许将矩阵的平移部分以及矩阵 3x3 旋转部分中未使用的任何元素置零。这两个参数默认为 true;因此,特别是如果你在调用这些方法之前在矩阵中设置了平移,除非你指定第二个参数为 false,否则它将会丢失。

一旦我们有了两个旋转矩阵,我们就可以使用 PreMult 方法将它们相乘以生成最终的模型矩阵。矩阵相乘的顺序非常重要,因为最终的旋转将取决于使用的顺序。Marmalade 提供了 PreMultPostMult 方法,使我们能够确定调用矩阵是乘法中的第一个矩阵还是第二个。

当我们准备好模型矩阵后,只需调用 IwGxSetModelMatrix 函数来用于渲染。

模型渲染

所有艰苦的工作现在都完成了,我们最终可以提交我们的立方体进行渲染。以下代码将提交所有流,我们的立方体将被渲染。希望你会看到它与我们用于 2D 渲染的代码是多么接近:

IwGxSetColClear(128, 190, 220, 255);
IwGxClear();

IwGxSetMaterial(lpMaterial);
IwGxSetVertStreamModelSpace(v, lVertexCount);
IwGxSetNormStream(n, lVertexCount);
IwGxSetColStream(c);
IwGxDrawPrims(IW_GX_TRI_STRIP, i, lIndexCount)

IwGxFlush();
IwGxSwapBuffers();

使用 3D 建模包创建模型数据

我们已经看到了如何在代码中创建立方体的数据流,坦白说它并不美观!即使是像立方体这样简单的形状也需要如此多的数据,以至于我们很难跟踪所有这些数据,几乎不可能创建一个更复杂的 3D 形状。

幸运的是,有一个更简单的方法。我们可以使用 3D 建模包来创建、着色和纹理 3D 模型,并将所有所需数据导出为 Marmalade 可以加载和使用的格式。

Marmalade 3D 导出插件

Marmalade 随带提供了用于大多数专业游戏开发工作室中使用的两个建模包——Maya 和 3DS Max 的导出插件。以下各节中的详细信息同样适用于这两个建模包的导出插件。

安装插件

当你安装主 SDK 时,导出插件会被安装到你的电脑上,但它们不会自动安装到建模包中以供使用。为了使用导出插件,我们必须使用 Marmalade Launch Pad 程序来设置,如下步骤所示:

  1. 启动 Marmalade LaunchPad 程序。在 Windows 上,它可以在 开始 菜单中的 Marmalade 文件夹内找到。你应该会看到一个窗口出现,其中包含一个标签页视图。

  2. 点击标签页上标记为 安装导出插件 的选项。以下屏幕应显示:安装插件

  3. 使用此屏幕上的选项卡选择您想要安装的 3D 建模软件版本。您必须选择您软件的正确版本,以及它是 32 位还是 64 位安装。Maya 7.0 和 3DS Max 8.0 是最早支持的版本。旧版本的导出器包含在标记为Maya 32bit LegacyMax 32bit legacy的选项卡中。

  4. 点击所需建模软件版本旁边的安装…按钮,导出器将被安装。Windows 用户账户控制可能会首先弹出一个请求,以确保您想要继续,所以只需在此对话框中点击按钮。

导出模型

安装插件后,启动您的 3D 建模软件并创建或加载您想要导出的模型。由于这是一本编程书籍,我们不会详细介绍如何创建 3D 模型。

注意

如果您是从事编程工作且对如何使用 3D 建模软件毫无头绪,请不要感到难过。多年来,我见过一些真正糟糕的“程序员艺术”作品;所以,将这种知识的缺乏视为一件好事,并让真正的艺术家参与到您游戏的艺术作品中来。您会为您的决定感到高兴的!

假设您有一个准备导出的 3D 模型,让我们启动 Marmalade 导出器插件。导出器窗口本身如图所示:

导出模型

导出器窗口的显示方式取决于您使用的建模软件。

  • 在 Maya 中,您可以通过使用菜单选项Marmalade Tools | Marmalade Studio: Maya Exporter或从Marmalade Studio标签中的图标来访问导出器。

  • 在 3DS Max 中,可以通过点击实用工具选项卡,然后点击Marmalade Studio Exporter按钮来打开导出器部分。在展开部分中,还有一个标记为Marmalade Studio Exporter的按钮,它将显示导出器窗口。

导出器窗口现在应该显示在屏幕上,如您所见,有许多可用的选项。现在我们可以不用大多数这些选项,所以我们只会介绍我们需要用来导出非动画 3D 模型的那些选项。

我们需要设置的第一件事是当前项目字段。导出器维护一个项目列表,在其最简单的层面上,这是一个快速选择导出模型文件将创建的目录的方法。

由于我们尚未创建导出器项目,让我们通过点击标记为设置项目的按钮来创建一个。以下对话框将出现:

导出模型

要创建一个新项目,请点击对话框底部的添加…按钮,您将被提示输入项目的名称。一旦您接受名称,项目将出现在项目:列表中。

点击浏览…按钮,定位到我们的代码项目中的data目录。所有资源文件都需要位于我们的代码项目的data目录中;这将使得将模型文件导出到正确的位置变得容易得多。

目前我们将忽略此对话框中的其他设置,因为它们超出了本章的范围;因此,点击确定按钮返回主导出窗口,此时应该在新创建的项目在当前项目下拉列表中选中,并且我们在项目数据目录字段中设置的data目录。

设置好项目后,我们现在可以按照以下步骤导出模型:

  1. 首先定位到导出类型字段。此字段旁边有一个带有大于符号的按钮。点击此按钮,从出现的弹出菜单中选择模型导出类型字段应更改为场景(模型)。

  2. 现在看看第一组标记为启用导出的复选框。我们只需要勾选几何形状导出组选项来导出正确的文件集。

  3. 下一个标记为导出标志的复选框组。我们不需要勾选任何这些设置。

  4. 现在我们到达了资产名称字段。这个字段指定了导出器生成各种输出文件时将使用的基准文件名。

  5. 保存位置字段允许提供相对于项目数据目录字段的目录路径。所有生成的文件都将创建在这个目录中,如果该目录不存在,它将被创建。

  6. 缩放因子允许我们提供一个数值缩放因子,该因子将应用于每个导出顶点的 x、y 和 z 分量。这使得艺术家可以使用熟悉的单位(如建模包中的米)来创建他们的模型,然后将这些单位转换为不同的比例用于游戏,例如我们程序员类型所喜爱的“2 的幂”这样的常用比例。然而,请注意,确保所有参与项目的艺术家使用相同的单位和缩放因子至关重要,否则在尝试让所有这些模型在游戏中正确协同工作时会遇到真正的问题!

  7. 接下来,我们可以通过标签为导出的下拉列表选择导出当前场景中的所有内容、仅选定的对象,或仅可见对象。

  8. 变换类型下拉框让我们选择导出的顶点是在模型空间还是世界空间中。在大多数情况下,当我们导出单个模型时,我们会选择局部选项(这也是模型空间的一种说法!)。

  9. 我们可能感兴趣的最后设置是纹理目录字段。这个字段允许指定一个目录,模型上要使用的任何纹理都将从这个目录导出。它可以是绝对路径,也可以是相对于项目数据目录的相对路径。

  10. 这就完成了我们目前需要关注的全部字段。接下来要做的就是点击 导出!按钮,这将生成必要的文件,并显示一个窗口,列出导出过程中创建的所有文件。

Blender 插件

毫无疑问,Maya 和 3DS Max 都是出色的产品,但它们的价格也相当昂贵。不幸的是,Marmalade 似乎依赖于使用这两个重量级软件包之一。

承认,Marmalade 也附带了一个用于 Collada 的转换器,这是一种创建用于在不同软件包之间交换 3D 模型的文件格式。然而,我犹豫是否推荐这种方法,因为在撰写本文时,Marmalade 附带的 Collada 转换器已知存在一些问题,尤其是在导出动画时。

幸运的是,有一个更便宜的替代方案。有一个名为 Blender 的 3D 建模软件包,可以免费下载和使用;然而,Blender 团队总是乐于接受捐赠以持续改进产品,所以如果您觉得它有用,请考虑帮助他们。

Marmalade SDK 并不自带对 Blender 的支持,但幸亏有 Benoit Muller 的努力,有一个相当酷的导出插件,它能很好地替代 3DS Max 和 Maya 的导出器。

安装 Blender 和导出插件

如果您还没有安装 Blender,请访问 Blender 网站,下载一个副本。URL 如下:

www.blender.org/

安装 Blender 只需执行下载的安装程序并遵循屏幕上的说明。

安装 Blender 后,我们现在需要获取导出插件,该插件可以在以下 URL 中找到:

wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/Marmalade_Exporter

该插件是一个 Python 脚本,可以使用以下步骤安装到 Blender 中:

  1. 将下载的插件文件 io_export_marmalade.py 复制到 Blender 插件目录中。在 Windows 上,这通常类似于 C:\Program Files\Blender Foundation\Blender\2.63\scripts\addons

  2. 启动 Blender 并转到 文件 | 用户首选项…

  3. 点击首选项窗口顶部的 插件 选项卡。

  4. 在窗口左侧的 类别 列表中,点击 导入-导出。您应该看到一个类似于以下图所示的屏幕:安装 Blender 和导出插件

  5. 找到 导入-导出:Marmalade 跨平台应用 (.group) 条目,并点击其右侧的复选框以启用插件。

导出模型

要使用 Blender 导出器导出 3D 模型,请按照以下步骤操作:

  1. 在 Blender 中创建或加载您希望导出的模型。

  2. 转到文件 | 导出 | Marmalade 跨平台应用(.group)。主 3D 视图将被文件名请求器替换。窗口的左下角应包含导出选项,并看起来像以下图像:导出模型

  3. 首先使用文件请求器选择要导出数据文件的位置。屏幕顶部有两个文本编辑框;最上面的用于保存目录,下面的指定了导出时想要使用的文件名。这个文件名应该是 GROUP 文件,因此其扩展名始终是.group

  4. 在导出设置中,首先使用导出下拉列表选择是要导出所选模型还是当前场景中的所有模型。

  5. 合并选项控制当场景中有多个模型要导出时会发生什么。默认选项将每个模型单独导出,其坐标在模型空间中,这是我们目前需要的选项。其他两个选项允许将多个模型合并为一个大的多边形网格或作为多个单独的网格导出,所有顶点都指定在世界空间坐标中。

  6. 缩放百分比值允许模型顶点进行放大或缩小,以便艺术家可以使用 Blender 中最方便的测量单位来构建模型,同时仍然允许导出的模型具有顶点缩放到一组可能对游戏更有用的单位。

  7. 翻转法线复选框将反转所有导出法线的方向。通常情况下可以不勾选,但有时可以用来修复被错误照明且法线指向错误方向的模型。

  8. 应用 修改器复选框会在创建导出数据之前评估 Blender 中应用于模型的任何网格修改器。默认情况下是关闭的。

  9. 如果模型已应用顶点颜色,则只有在选择导出顶点颜色复选框时才会导出。

  10. 下一个复选框,导出材质颜色,确定在导出过程中创建的材质是否带有其定义的颜色导出。

  11. 如果您的模型中有纹理映射的多边形,则应勾选导出纹理和 UVs复选框。

  12. 由于 Marmalade 需要访问用于模型纹理化的任何图像,因此可以选择复制纹理文件复选框以确保图像文件也被复制到导出目录中。

  13. 剩余的设置大多与导出动画有关,因此现在可以忽略它们;然而,值得提及的是详细复选框,它将导出过程的信息记录到 Blender 的控制台窗口中。这可能会帮助您解决导出过程不按预期工作时的模型问题。

  14. 当所有设置都完成后,点击位于 Blender 窗口右上角的导出 Marmalade按钮。或者,如果您想取消导出过程,导出按钮下方有一个取消按钮。

Marmalade 3D 模型数据文件格式

我们已经看到了如何从建模软件包中导出 3D 模型数据,但我们还没有查看作为导出过程一部分生成的文件。

虽然我们可能不需要手动修改这些文件,但了解一些关于它们的信息是有用的,因为它可以帮助我们发现为什么模型没有像预期的那样导出。

让我们看看与我们在本章早期代码中创建的类似立方体模型将生成的文件。

GROUP 文件

生成的第一个文件是一个 GROUP 文件,它将在导出设置中指定的目录中创建。GROUP 文件包含导出的单个模型文件(称为GEO 文件)的列表。以下是示例立方体模型的 GROUP 文件可能的样子:

// Source file: C:/Work/MarmaladeBook/Maya/Cube.mb
// Exported By: Sean on 05/30/12 16:30:55

CIwResGroup
{
  name "Cube"
  "./models/Cube.geo"
}

导出器有帮助地包括了用于导出的源建模软件包文件的名称,以及导出时间和导出者信息。

然后它只是声明了一个新的CIwResGroup实例,其名称基于导出时指定的资产名称。资源组由需要加载的 GEO 文件列表填充。

MTL 文件

当我们处理 2D 图形时,我们已经手动创建了一个 MTL 文件,所以它应该已经很熟悉了。以下是对于立方体的文件可能的样子:

// Source file: C:/Work/MarmaladeBook/Maya/Cube.mb
CIwMaterial
{
  name "Cube/phong1"
  colAmbient {127,127,127}
  colDiffuse {127,127,127}
  colSpecular {255,255,255}
  specularPower 3
}

同样,导出器包括了用于生成 MTL 文件的源建模软件包文件的名称。在此文件中定义的CIwMaterial实例都是从建模软件包中使用的材料生成的,因此艺术家可以在他们最喜欢的建模工具中轻松更改颜色和其他材料属性。

导出器在指定的导出目录中创建一个名为models的子目录,并将 MTL 文件写入此目录。

GEO 文件

要导出的最重要的文件类型是 GEO 文件,因为这个文件实际上包含了描述我们的 3D 模型所需的所有数据。与所有 Marmalade 资源一样,此文件是 ITX 文件格式的另一种用途。

GEO 文件通过一个名为CIwResHandlerGEO的资源处理类进行处理。这个类负责从 GEO 文件中加载所有数据并将其提交给一个名为CIwModelBuilder的单例类。这个类处理模型数据并生成用于快速渲染的数据优化版本,然后将其序列化到文件中。

CIwModelBuilder类仅在调试构建中可用,因此您只能通过加载引用 GEO 文件的 GROUP 文件的序列化版本来在发布构建中加载模型数据。

导出器将以与 MTL 文件相同的方式将 GEO 文件写入模型的子目录中。

注意

你可能已经注意到,之前显示的 GROUP 文件只引用了 GEO 文件,而没有引用 MTL 文件。GEO 资源处理程序会自动处理 MTL 文件的加载,通过检查是否存在与 GEO 文件具有相同基本名称的 MTL 文件。

让我们来看看我们的立方体模型的 GEO 文件的内部结构。

// Source file: C:/Work/MarmaladeBook/Maya/Cube.mb
CIwModel
{
  name "Cube"
  CMesh
  {
    name "Cube"
    scale 100.0
    CVerts
    {
      numVerts 8
      v {-100,-100,100}
      v {100,-100,100}
      v {-100,100,100}
      v {100,100,100}
      v {-100,100,-100}
      v {100,100,-100}
      v {-100,-100,-100}
      v {100,-100,-100}
    }
    CVertNorms
    {
      numVertNorms 6
      vn {0,0,1}
      vn {0,1,0}
      vn {0,0,-1}
      vn {0,-1,0}
      vn {1,0,0}
      vn {-1,0,0}
    }
    CVertCols
    {
      numVertCols 6
      col {1,0,0,1}
      col {0,1,1,1}
      col {0,0,1,1}
      col {1,1,0,1}
      col {1,0.50000,0,1}
      col {0,1,0,1}
    }
    CSurface
    {
      material "phong1"
      CQuads
      {
        numQuads 6
        q {2,0,-1,-1,0} {3,0,-1,-1,0} {1,0,-1,-1,0}
{0,0,-1,-1,0}
        q {4,1,-1,-1,4} {5,1,-1,-1,4} {3,1,-1,-1,4}
{2,1,-1,-1,4}
        q {6,2,-1,-1,5} {7,2,-1,-1,5} {5,2,-1,-1,5}
{4,2,-1,-1,5}
        q {0,3,-1,-1,1} {1,3,-1,-1,1} {7,3,-1,-1,1}
{6,3,-1,-1,1}
        q {3,4,-1,-1,3} {5,4,-1,-1,3} {7,4,-1,-1,3}
{1,4,-1,-1,3}
        q {4,5,-1,-1,2} {2,5,-1,-1,2} {0,5,-1,-1,2}
{6,5,-1,-1,2}
      }
    }
  }
}

再次,导出器将在开始定义CIwModel实例之前包含一个注释,引用源建模包文件。CIwModel是 Marmalade 用来表示完整 3D 模型数据集合的类。

首先给CIwModel实例起一个名字。这个名字实际上来自建模包中给出的模型名称,并且是我们代码中访问模型时使用的名称,因此对于艺术家来说,合理命名是很重要的。

接下来声明一个CMesh实例,这是一个将所有各种模型数据组合在一起的类。这个类以及我们即将看到的包含在其内的所有其他类,仅用于模型构建器内部。一旦模型被处理,这些类将不再存在于内存中,因此我们无法在代码中使用它们来访问原始模型数据。

用于导出顶点数据的scale值首先列在CMesh实例中,随后是声明各种模型数据类型的类。在立方体示例中,我们可以看到CVertsCVertNormsCVertCols,它们分别只是包含顶点、法线和颜色数据的大列表。还有一个类似的类CUVs,它用于提供纹理信息。

接下来我们看到一个名为CSurface的类。这个类为模型提供多边形信息,并且对于模型中使用的每种材质都会存在一个实例。使用的材质首先被指定,然后是多边形信息。CQuads实例用于提供使用该材质的所有四边形多边形的列表,而CTris实例列出三角形多边形。

多边形通过为多边形中的每个顶点提供一组数据来定义。多边形以一组五个数字的形式提供,这些数字是文件中之前指定的数据块的索引,并按以下顺序出现:

{Vertex index, Normal index, UV 0 index, UV 1 index, Color index}

由于材质可以指定两个将在渲染时混合在一起的纹理,因此存在两个 UV 值。每个纹理都可以有自己的 UV 流。

一旦所有这些数据都被加载,模型构建类将分析它们并创建一个针对实时渲染目的远更优化的数据版本。

加载和渲染导出的 3D 模型

好的,现在我们已经导出了模型数据,那么我们如何将其加载到我们的程序中并进行渲染呢?实际上,这出奇地简单,接下来的几节将会展示这一点。

将 IwGraphics API 添加到项目中

Marmalade 的 3D 模型渲染代码是 IwGraphics API 的一部分,因此在我们能够绘制任何东西之前,我们需要将这个库添加到我们的项目中。这是通过在 MKB 文件的 subprojects 部分添加 iwgraphics 来实现的。

我们需要在程序的开始处添加对 IwGraphicsInit 的调用,并在结束时添加 IwGraphicsTerminate。这个 API 依赖于 IwGx 和 IwResManager,因此我们必须在调用 IwGraphics 之前调用这两个 API 的初始化函数。

加载和访问导出的 3D 模型

你可能已经猜到了,这几乎是微不足道的简单。导出器生成了一个 GROUP 文件,所以我们只需要将其加载到内存中,然后从资源组中提取模型。以下是一段执行此操作的代码块:

CIwResGroup* lpCubeGroup = IwGetResManager()->
LoadGroup("Cube/Cube.group");
CIwModel* lpCube = static_cast<CIwModel*>(lpCubeGroup->
GetResNamed("Cube", "CIwModel"));

或者,如果你不想保留资源组实例的指针,你可以这样做:

CIwModel* lpCube = static_cast<CIwModel*>(IwGetResManager()->
GetResNamed("Cube", "CIwModel"));

就这样。模型现在已加载到内存中,并准备好渲染。

渲染导出的 3D 模型

现在是时候在屏幕上渲染模型了,这同样非常简单。我们只需要使用 IwGxSetViewMatrixIwGxSetModelMatrix 设置我们的视图和模型矩阵,然后执行以下操作:

lpCube->Render();

变量 lpCube 是指向我们从上一节资源管理器中检索到的 CIwModel 实例的指针。

实际上,Render 方法可以接受两个可选参数。第一个参数是一个 bool 值,它告诉 Marmalade 检查模型的边界球体是否与裁剪平面相交,以确定它是否实际上需要被渲染。此参数默认为 true,因此默认情况下会进行此检查。边界球体是由模型构建器代码自动为我们生成的。

第二个参数是标志字段。除了一个与 2D 屏幕旋转有关(我说“有关”是因为我在尝试时并没有发现它做了很多)的标志之外,其他标志仅在与包含法线数据的动画 3D 模型处理时相关,所以我们现在不必担心这些。

释放 3D 模型数据

由于我们的 3D 模型数据已经使用资源组系统加载到内存中,我们可以利用相同的销毁组机制来释放不再需要的模型数据。作为一个回顾,如果我们有一个指向包含 3D 数据的 CIwResGroup 的指针,我们只需做以下操作:

IwGetResManager()->DestroyGroup(lpCubeGroup);

或者,我们可以通过按名称销毁它来从内存中释放一个组,如下所示:

IwGetResManager()->DestroyGroup("Cube");

示例代码

这里有一些关于本章附带示例项目的详细信息。

The Cube project

这是本章讨论的第一个旋转立方体项目的完整示例,其中我们在代码中生成模型数据,并使用 IwGxDrawPrims 将其提交给 IwGx 进行渲染。请参见以下截图:

The Cube project

Cube2 项目

这个项目几乎与上一个项目完全相同,只是将立方体的模型数据从 3D 建模软件导出为 GEO 文件。

滑雪项目

对于本章,滑雪游戏告别了其旧的位图图形,转而迎来了一些新的 3D 模型。以下图中可以看到带有新 3D 外观的游戏截图:

滑雪项目

以下几节描述了本章游戏代码的一些其他更有趣的更改。

迁移到 3D

第一步是将所有位置和速度信息从二维向量更改为三维向量,这意味着将CIwVec2实例更改为CIwFVec3,并确保向量的额外分量被初始化。

由于我们倾向于将 y 轴视为地面以上的高度,所以在游戏中 y 分量也被用来表示高度。然而,由于滑雪者和树木都粘在了地面上,这意味着所有位置向量的 y 分量始终为零。

因此,游戏沿着 z 轴滚动树木,并将摄像机放置在空中并朝向滑雪者。这仍然提供了树木向上移动屏幕的效果。

第二步是去除所有旧的 2D 纹理,并用 3D 模型替换它们。由于GameObject类处理所有渲染,所以只需将这个类更改为使用CIwModel实例而不是CIwMaterial实例。子类随后只需提供一个指向模型的指针而不是指向材质的指针。

GameObject类还添加了一个 y 轴旋转。这用于旋转滑雪者模型,这比我们之前的方法运动更加平滑。

树木也使用了旋转功能。游戏只包含一个树模型,但通过在随机角度旋转它,可以在不增加更多游戏资源的情况下使游戏看起来更加有趣。

添加碰撞检测系统

代码现在具有一个非常简单的碰撞检测系统。GameObject类现在允许设置碰撞半径,然后用于执行球体相交测试。

ModeGame::Update方法现在遍历游戏世界中的每个游戏对象(目前当然是树木),并找出它距离滑雪者的距离。如果距离小于滑雪者和另一个游戏对象的碰撞半径之和,则发生了碰撞。

因此,为了处理这些碰撞,在GameObject类中添加了一个名为OnCollide的虚方法。子对象可以重写这个类,并在与另一个对象发生碰撞时相应地做出反应。Skier类实现了这个方法,因此每当滑雪者与树木碰撞时,游戏就结束了。

摘要

我们现在知道如何渲染由代码生成或从 3D 建模软件导出的 3D 图形。我们最终使用哪种方法取决于我们想要做什么。

如果是在游戏角色或场景中进行渲染,那么导出模型路径无疑是最好的选择;但通过代码创建自己的多边形数据是一种更好、更高效的方法来创建如粒子系统等效果,因为它更容易将大量单独的多边形批量合并成一个单独的绘制调用。

我们还学会了如何从三个不同的建模软件——Maya、3DS Max 和 Blender——导出 3D 模型数据,并将这些导出的数据加载到我们的程序中并对其进行渲染。

我们将暂时继续使用 3D 渲染,因为下一章全部是关于使我们的模型动画化的内容。

第五章:3D 图形动画

我们现在已经看到了如何创建 3D 模型并在屏幕上显示它,但我们目前限于非动画模型。当然,我们可以随意旋转或缩放,但当需要动画化比立方体更复杂的物体时,比如一个人物模型,这真的不够。

在本章中,我们将探讨以下主题:

  • 3D 动画涉及概念快速概述

  • 从 3D 建模软件包中导出动画

  • 在 Marmalade 项目中加载和渲染导出的 3D 动画

3D 动画快速入门

让我们先看看 3D 模型动画的实现方式。

使用模型矩阵进行动画

到目前为止,最简单、最明显地动画化 3D 模型的方法是改变其位置、方向和大小。这三个属性都可以使用在渲染模型时设置的模型矩阵来指定。

我们可以在游戏类中存储一个矩阵,并为每一帧乘以一个表示位置、旋转和缩放变化的第二个矩阵;但这种方法通常不可靠。随着时间的推移,矩阵开始退化,这是由于涉及乘法和加法中的精度误差的累积效应。矩阵通常会变得非正交(即其三个轴不再相互垂直),这会对 3D 模型产生剪切效应。缩放也可能受到这些精度误差的影响,导致 3D 模型逐渐缩小!

一种更可靠的方法是将平移、旋转和缩放分别存储,并为每一帧计算一个新的矩阵。如何实现将在以下章节中描述。

通过平移进行动画

我们的游戏类只需要维护一个包含对象当前世界位置的位矢量的位置向量。我们可以通过添加一个速度向量来在游戏中移动对象,该向量指示游戏对象在本帧移动了多远以及朝哪个方向移动,相对于存储的位置向量。

要生成最终的模型矩阵,我们只需要将位置向量复制到矩阵的平移部分。我们通常在最后一步这样做,因为在生成旋转和缩放时乘以矩阵会影响矩阵的平移。

// lTimeStep is the time elapsed since the last frame (here we're
// setting it to the time interval required to run at 30 frames
// per second).
float lTimeStep = 1.0f / 30.0f;

// Calculate how far we've moved this frame and update position
CIwFVec3 lVelocityStep = mVelocity * lTimeStep;
mPosition += lVelocityStep;

// Copy the position into the matrix used to render the model
mModelMatrix.t = mPosition;

通过旋转进行动画

模型矩阵左上角的 3x3 部分指定了模型要绘制的旋转。我们的游戏对象存储所需的旋转并在每一帧更新它。当渲染时间到来时,我们只需使用存储的旋转来计算旋转矩阵。

存储对象旋转的方式有很多种。以下章节将展示三种最常见的方式。

使用欧拉角进行旋转

欧拉角由 x、y 和 z 轴上所需的旋转角度组成,我们通常使用向量来存储这些角度。如果不需要围绕每个轴旋转,你可以选择只存储所需的旋转值。

欧拉角很容易可视化并实现,这也是为什么它们被广泛使用的原因。要将一组欧拉角转换为旋转矩阵,我们只需要为每个轴的旋转生成三个矩阵,然后将它们相乘。

然而,这正是欧拉角的问题所在。矩阵乘法的结果取决于矩阵乘法的顺序;因此,在使用欧拉角时,你必须仔细选择乘法顺序,这取决于你想要实现的目标。以下图表展示了如何说明这一点:

使用欧拉角的旋转

在图表中,我们正在旋转一个指向正 y 轴的箭头。在第一行中,我们首先围绕 z 轴旋转 90 度,然后围绕 y 轴旋转 90 度。箭头最终指向 z 轴。

在图表的第二行中,我们使用相同的原始箭头,但以相反的顺序应用旋转。正如你所看到的,这次箭头最终指向 x 轴的方向。

以下代码片段展示了如何构建用于应用 XYZ 顺序的欧拉角的完整旋转矩阵:

CIwFMat lMatXYZ;
lMatXYZ.SetRotX(xAngle);
lMatXYZ.PostRotateY(yAngle);
lMatXYZ.PostRotateZ(zAngle);

注意

Marmalade 中使用的所有角度都是以弧度指定的,而不是度。

使用轴角对进行旋转

表示旋转的轴角方法需要存储方向向量和旋转角度。该向量表示我们希望对象朝向的方向,而角度允许对象绕该轴旋转。

当处理玩家角色时,我们可能会发现这种方式指定旋转很有用。例如,为了定位一个人类角色,我们可能会指定方向向量为正 y 轴,这样就可以使用旋转角度来改变角色的航向。

Marmalade 允许我们将轴角对转换为用于渲染的矩阵,如下所示:

CIwFVec3 lDir(0.0f, 1.0f, 0.0f);
float lAngle = PI / 2.0f;
CIwFMat lMat;
lMat.SetAxisAngle(lDir, lAngle);

使用四元数的旋转

四元数是表示三维旋转的另一种方法,当你第一次遇到它时,可能会觉得有点令人震惊。我不会继续谈论四维超球体和让你的大脑部分融化,我只会提供一个快速指南,告诉你如何使用四元数。如果你想了解更多,我建议你在 Google 上搜索“四元数”!

四元数由四个分量组成:x、y、z 和 w。3D 旋转可以用单位四元数来表示,这与向量的表示方式类似,只是意味着所有四个分量的平方和的模长为 1。

两个单位四元数的乘法类似于两个旋转矩阵的乘法。结果表示第一个方向被第二个方向旋转,并且结果取决于你执行乘法的顺序。

四元数的大问题是它们几乎无法可视化。如果给出一个欧拉角或轴角对,大多数人可以在脑海中形成一个关于该旋转外观的图像,但对于四元数来说则不然。

四元数可以从旋转矩阵(以及因此欧拉角)和轴角对中相当容易地创建。以下图表显示了轴角对与四元数之间的关系:

使用四元数进行旋转

四元数在骨骼角色 3D 动画中真正发挥其作用,这是我们将在本章后面讨论的主题。这是一种需要每次更新动画帧时计算大量旋转的技术,幸运的是,四元数在内存使用和执行速度方面都使这一过程更加高效。

虽然四元数的理论对于我们这些凡人可能有点可怕,但实际上我们几乎没有必要担心数学问题,因为 Marmalade 为我们提供了一个四元数类,CIwFQuat,我们可以使用。例如,可以从轴角对创建四元数,然后从中生成旋转矩阵,如下所示:

CIwFQuat lQuat;
lQuat.SetAxisAngle(1.0f, 0.0f, 0.0f, PI / 2.0f);
CIwFMat lMat(lQuat);

通过缩放动画

缩放因子通常存储为包含 x、y 和 z 轴所需大小的向量,或者作为应用于每个轴的单个缩放值。通常后者就足够了,因为当模型在每个轴上不均匀缩放时,它们看起来会很奇怪。

缩放矩阵的创建非常简单,因为你只需将 x、y 和 z 轴所需的缩放因子放置在矩阵 3x3 旋转部分的从左上角到右下角的对角线上。所有其他单元格都保留为零。

由于创建缩放矩阵非常简单,CIwFMat类不包含创建通用缩放矩阵的方法。然而,它确实提供了一些快捷方法,使得通过相同的缩放因子在每个轴上缩放矩阵变得容易。以下代码片段提供了一个示例:

CIwFMat lMat;
lMat.SetRotX(PI / 2.0f);
lMat.ScaleRot(2.0f);

此代码将创建一个围绕 x 轴旋转 90 度的旋转矩阵,然后仅将矩阵的旋转部分按因子二放大。你也可以选择仅放大矩阵的平移部分,或者使用ScaleTransScale方法分别放大旋转和平移。

3D 模型动画

模型矩阵动画当然非常重要,因为没有它我们就无法在游戏世界中定位和移动我们的 3D 模型;但仅凭这一点并不能使游戏看起来最吸引人。

大多数游戏需要更多。例如,我们可能希望一个人类或动物角色行走、奔跑、跳跃或执行其他类型的动作。理想情况下,我们需要一种方法使 3D 模型的整体形状随时间变化。

以下章节将解释我们如何实现这一点。

使用形态目标

3D 模型动画的一个简单方法是使用形态目标。为此,我们改变 3D 模型的顶点位置以生成动画的关键帧。关键帧只是模型的一组特定顶点位置,它是整体动画的重要组成部分,例如,角色行走时腿部移动的各种位置。关键帧还与时间相关联。

下图展示了一个非常简单的例子,一个木偶人举起手臂。关键帧 1在时间索引0秒时手臂处于下垂位置,而关键帧 2在时间索引2秒时手臂被举起。这些关键帧可以被视为独立导出的 3D 模型。

使用形态目标

如果我们想要回放这个动画,我们可以在正确的时间绘制相关的 3D 模型,但这会产生非常生硬的结果,类似于 2D 位图动画。相反,我们可以在02秒之间的任何时间索引计算插值帧,以获得更平滑的结果。

计算插值帧的过程足够简单。我们从第一个关键帧中的每个顶点计算出与第二个关键帧中相应顶点的delta 向量。然后,我们将 delta 向量按我们想要计算的索引时间与两个关键帧之间总时间的比例进行缩放,并将缩放后的结果加到第一个关键帧中顶点的位置上。

在图中,我们想要计算时间索引1秒的插值帧,因此我们将 delta 向量按一半的比例缩放。最终结果将是手臂半举起时的帧。

这种方法可能易于实现,但最终我们发现它存在一些问题,如下所述:

  • 结果动画的准确性:仔细观察前图中木偶人的插值帧,你会发现木偶人的手臂实际上变短了。这是因为我们正在以直线方式插值顶点位置,而实际上我们需要末端顶点围绕肩部点旋转。

  • 所需的关键帧数量:为了制作高质量的动画,我们需要存储大量关键帧。在我们的木偶人动画示例中,我们可以提供额外的关键帧,从而最小化手臂缩短效果。然而,由于我们需要存储模型中每个顶点的位置,无论其是否移动,这很快就会变成大量数据。

  • 确保顶点顺序在关键帧之间不发生变化的必要性:我们能够可靠地实现形变目标动画的唯一方法是在每个关键帧中,模型中的每个顶点在顶点流中都处于相同的位置。当从建模软件导出 3D 模型时,顶点流顺序可能会在帧之间发生变化,这会导致我们的动画在顶点在完全错误的位置之间插值时表现不正确。

由于上述原因,Marmalade 不支持形变目标动画,尽管如果你愿意,实现这种方法相当简单。形变目标对于面部动画等任务仍然非常有用,随着移动设备性能的不断提高,这可能会很快成为移动游戏中的更常见功能。

使用骨骼动画

大多数 3D 视频游戏都会使用骨骼动画系统来实现 3D 模型的动画。这种方法通过允许动画师设置虚拟骨骼的骨骼结构,然后可以使用这些骨骼来变形 3D 模型的顶点。3D 模型本身通常被称为动画的皮肤

要设置骨骼动画,第一步是使用 3D 建模软件创建你想要动画化的 3D 模型,并使其处于绑定姿态。绑定姿态通常选择为便于访问模型中每个多边形进行纹理和着色,以及布置骨骼的位置。对于人类角色来说,这通常意味着手臂从身体水平伸展出去的姿态,脚部相隔一段短距离。

在创建绑定姿态后,动画师开始进行绑定过程。这涉及到通过放置骨骼将骨骼添加到模型中。骨骼被链接在一起形成层次结构;因此,每当移动一个骨骼时,所有与其链接的子骨骼也会移动。最终,在层次结构中会有一个顶级父骨骼,这被称为根骨骼

为了性能考虑,最好将骨骼数量保持在最小,但这也必须与保证足够数量以实现高质量动画的需求相平衡。以下图表显示了在我们示例游戏项目中使用的 3D 滑雪角色在被绑定后的样子:

使用骨骼动画

骨骼布局完成后,下一步是将皮肤(换句话说,多边形的网格)绑定到骨骼上。这是通过允许 3D 模型的每个顶点被一个或多个骨骼修改来实现的。

如果一个顶点映射到多个骨骼,也会为每个骨骼定义一个权重,以确定它对顶点的影响程度。权重范围从零到一,特定顶点的所有权重的总和应该等于一。

大多数 3D 建模软件都会在自动执行绑定过程时有一个良好的尝试,但通常动画师需要对绑定进行一些调整,以确保当骨骼移动时,皮肤能够正确地动画化。

完成所有这些后,动画师就可以通过旋转和移动骨骼来定义所需的关键帧位置,从而让角色做他们想做的任何事情,就像使用变形目标一样。骨骼系统将产生最终动画的更好质量,并且存储关键帧所需的内存量通常不会太大,因为需要存储的只是每个骨骼的朝向和位置。

Marmalade SDK 配备了一个骨骼动画系统,我们将在本章的剩余部分学习它。该系统非常灵活,几乎没有限制。

需要注意的主要事项是,你只能有一个根骨骼,总共最多 256 个骨骼,并且每个顶点最多只能受到四个骨骼的影响。在大多数情况下,这些限制不太可能给你带来任何问题。

使用 3D 建模软件创建动画数据

已经有整本书籍专门讲解如何最好地创建 3D 动画角色;因此,不出所料,我们在这里不会探讨如何实际制作动画 3D 模型。实际上,我在上一章关于“程序员艺术”的警告可能对“程序员动画”来说加倍适用。为了支持这个说法,请看看本书示例程序中的图形,这些都是我亲自制作的“程序员艺术”的例子。我真的应该听从自己的建议。

无论如何,希望这个提示现在已经深入人心,让我们看看如何从 3D 建模软件中导出动画数据。

导出动画需要导出多种新文件类型。这些将在稍后详细讨论,但简而言之,它们是代表骨骼、皮肤以及实际动画本身的文件。以下各节将展示如何导出这些数据。

使用 Marmalade 3D 导出插件导出动画

如果你使用 3DS Max 或 Maya 来创建你的动画,所需的动画文件将通过 Marmalade 导出插件导出。为了刷新你的记忆,以下截图显示了导出插件窗口:

使用 Marmalade 3D 导出插件导出动画

要导出动画,只需将其加载到你的建模软件中,并按照以下步骤操作:

  1. 设置导出选项的方式与导出静态模型时相同。如果你忘记了各种选项是什么,请查看第四章中的步骤列表,“3D 图形渲染”。现在,我们将查看额外的动画特定选项。

  2. 确保在标记为启用导出的组中仅勾选几何骨骼exportgroup复选框。

  3. 您现在可以点击导出!按钮以写入模型的 GEO、MTL 和 GROUP 文件。还将导出两种新文件类型,即 SKEL 和 SKIN 文件,正如您可能猜到的,它们代表模型的骨骼和皮肤信息。

这些步骤中导出的文件对于动画模型是必要的,但它们实际上并不包含任何动画数据。以下是获取描述模型实际动画方式的数据的方法:

  1. 返回导出插件窗口,并点击导出类型组合框右侧的按钮。应该出现一个弹出菜单,您应从中选择anim选项。

  2. 启用导出部分中的复选框应更改,以便仅勾选动画复选框。

  3. 导出标志部分,如果您场景中有多个动画要导出,可以选择multianim复选框。请注意,每个动画应为相同的 3D 模型。

  4. 动画范围类型选项可以取三个可能值之一。默认值为可见范围,它将仅导出在建模包中动画轨道栏上当前可见的帧的范围。下一个选项是单个动画范围,它将仅导出每个动画的第一个和最后一个关键帧之间的动画数据。最后一个选项完整范围仅在 Maya 中可用。它将导出整个动画,无论是否在动画轨道栏上设置了帧范围。

  5. 动画范围选项允许您将一个大的动画序列拆分成几个较小的动画。如果您点击编辑…按钮,将显示刚刚看到的对话框。使用添加按钮创建一个新的动画范围,使用名称文本框命名动画,然后拖动滑块设置动画的开始结束帧。使用删除按钮从列表中删除一个动画范围。完成按钮将关闭对话框并接受所做的任何更改,而取消按钮将在关闭对话框之前丢弃所做的任何更改。

  6. 影响动画导出的最后一个选项是子动画根文本框。您可以输入骨骼中一个骨骼的名称,并且动画数据将仅为此骨骼及其子项导出。我们将在本章后面了解更多关于子动画的内容。

  7. 在导出插件中设置好所有与动画相关的选项后,只需点击导出!按钮即可输出一个或多个 ANIM 文件。导出的文件数量取决于场景中动画的数量、multianim复选框的状态以及是否使用了动画范围选项。

使用 Blender 插件导出动画

您还可以使用 Blender 插件导出动画。Blender 中用于动画的术语有些不寻常,因为 Blender 将骨骼称为骨架,但除此之外,动画的方法是相同的。

使用 Blender 插件导出动画

以下是您应遵循的步骤以从 Blender 导出动画模型:

  1. 将您希望导出的动画加载到 Blender 中,然后转到文件 | 导出 | Marmalade 跨平台应用程序 (.group)以显示导出选项屏幕。提醒一下,导出选项在之前的屏幕截图中已显示,但请参考第四章中列出的步骤,3D 图形渲染,以获取有关标准模型导出设置的更多信息。

  2. 要告诉导出器写入所有不同的动画文件类型,请确保选中导出骨架复选框。

  3. 动画帧组合框包含三个选项。将不导出任何动画数据,仅关键帧将仅导出动画的关键帧数据(这是您通常会选择的选项),而完整动画将导出每帧的数据,无论它是否是关键帧(这通常被称为“烘焙”动画,意味着您将获得建模软件包中看到的精确动画,但代价是动画内存占用增加)。

  4. 动画动作组合框包含两个设置。默认动画将仅导出已选为骨架默认值的动画。另一个选项是所有动画,这将导出为骨架定义的所有动画。

  5. 最后的设置是动画帧率值。这决定了动画每秒的播放速度,因此可以通过更改此值来加快或减慢动画的播放速度,而无需更改所有关键帧的时间。

  6. 要导出数据,请确保您已在屏幕顶部的框中输入了文件位置和名称,然后单击导出 Marmalade按钮。

Marmalade 3D 动画文件格式

我们现在可以从我们选择的 3D 建模软件包中导出动画数据,但在我们实际使用它们之前,让我们快速查看我们刚刚生成的新文件类型。

SKEL 文件

SKEL 文件包含有关我们动画骨骼的所有信息。文件首先定义了一个CIwAnimSkel类的实例,这是一个多个CIwAnimBone实例的包装器。

CIwAnimSkel实例是从CIwResource类派生出来的,因此它有一个与之关联的名称,以便可以在资源管理器中查找。实例的名称取自 SKEL 文件的文件名,而 SKEL 文件的名称又来自骨骼的根骨骼名称。

每个CIwAnimBone实例都有一个与之关联的名称、位置和旋转,这定义了动画的绑定姿势。位置只是一个模型空间中的向量,而旋转则以四元数的形式存储。除了根骨骼之外,每个骨骼还会列出其父骨骼,从而构建骨骼层次结构。

SKEL 文件与 GEO 和 MTL 文件一起导出到models子目录中。以下是一个 SKEL 文件的示例:

CIwAnimSkel
{
  numBones 12
  CIwAnimBone
  {
    name "FlagPoleBase"
    pos {-0.38309,-1.27709,0}
    rot {0.70711,0,0,0.70711}
  }
  CIwAnimBone
  {
    name "PoleA"
    parent "FlagPoleBase"
    pos {258.20248,0.00000,0}
    rot {1,0,0,0}
  }
  CIwAnimBone
  {
    name "PoleB"
    parent "PoleA"
    pos {255.88675,0.00000,0}
    rot {1.00000,0,0,-0.00200}
  }
  CIwAnimBone
  {
    name "PoleC"
    parent "PoleB"
    pos {257.60138,-0.00000,0}
    rot {0.00000,0.99998,0.00615,0.00000}
  }
  CIwAnimBone
  {
    name "PoleD"
    parent "PoleC"
    pos {255.08751,-0.00000,-0.00000}
    rot {0.00000,0.99998,0.00621,0.00000}
  }
  CIwAnimBone
  {
    name "PoleE"
    parent "PoleD"
    pos {257.19775,0.00000,-0.00000}
    rot {1.00000,0,0,0.00206}
  }
  CIwAnimBone
  {
    name "FlagStart"
    parent "PoleE"
    pos {152.41219,0.00000,-0.00000}
    rot {0.61894,0,0,-0.78544}
  }
  CIwAnimBone
  {
    name "FlagA"
    parent "FlagStart"
    pos {85.99931,-0.00000,-0.00000}
    rot {0.99505,0,0,0.09934}
  }
  CIwAnimBone
  {
    name "FlagB"
    parent "FlagA"
    pos {57.19376,0.00000,-0.00000}
    rot {1.00000,0,0,0.00128}
  }
  CIwAnimBone
  {
    name "FlagC"
    parent "FlagB"
    pos {61.42473,0.00000,-0.00000}
    rot {0.99996,0,0,0.00846}
  }
  CIwAnimBone
  {
    name "FlagD"
    parent "FlagC"
    pos {60.33911,0,-0.00000}
    rot {0.99996,0,0,-0.00877}
  }
  CIwAnimBone
  {
    name "FlagE"
    parent "FlagD"
    pos {60.36695,-0.00000,-0.00000}
    rot {0.99985,0,0,0.01754}
  }
}

SKIN 文件

SKIN 文件是骨骼和 3D 模型绑定姿势中顶点之间的桥梁。它包含所有表示哪些顶点受哪些骨骼影响的数据。

文件首先定义了CIwAnimSkin类的一个实例。此实例包含对定义所需骨骼的CIwAnimSkel实例的引用,以及用于在计算新顶点位置后渲染模型的CIwModel实例。与 SKEL 文件一样,CIwAnimSkin实例的名称是从 SKIN 文件的文件名派生的。

文件随后包含多个CIwAnimSkinSet类的实例,这表明哪些顶点被哪些骨骼修改。这是通过首先列出骨骼(最多四个),然后列出集合中的顶点数量来实现的。然后,通过提供模型顶点流中顶点的索引,随后为每个骨骼提供一个权重值,为每个顶点指定骨骼权重。每个顶点的权重值之和必须总计为 1。

SKIN 文件也导出到models子目录,以下代码提供了一个部分示例。这些文件由于需要大量数据,即使是简单的动画也会相当大,因此提取应该足以提供这些文件的外观。

CIwAnimSkin
{
  skeleton "FlagPoleBase"
  model "Flag"
  CIwAnimSkinSet
  {
    useBones { FlagPoleBase }
    numVerts 16
    vertWeights {0,1}
    vertWeights {1,1}
    vertWeights {2,1}
    vertWeights {3,1}
    vertWeights {4,1}
    vertWeights {35,1}
    vertWeights {58,1}
    vertWeights {60,1}
    vertWeights {62,1}
    vertWeights {64,1}
    vertWeights {65,1}
    vertWeights {90,1}
    vertWeights {91,1}
    vertWeights {92,1}
    vertWeights {93,1}
    vertWeights {94,1}
  }
  CIwAnimSkinSet
  {
    useBones { FlagPoleBase PoleA }
    numVerts 10
    vertWeights {5,0.50170,0.49830}
    vertWeights {6,0.50251,0.49749}
    vertWeights {7,0.50177,0.49823}
    vertWeights {8,0.50116,0.49884}
    vertWeights {9,0.50114,0.49886}
    vertWeights {57,0.50251,0.49749}
    vertWeights {59,0.50177,0.49823}
    vertWeights {61,0.50116,0.49884}
    vertWeights {63,0.50114,0.49886}
    vertWeights {66,0.50170,0.49830}
  }

// Several more CIwAnimSkinSet instances would have been
// defined here but they have been left out to avoid
// filling the book with boring numbers!

  CIwAnimSkinSet
  {
    useBones { FlagA FlagB FlagC FlagD }
    numVerts 8
    vertWeights {39,0.15992,0.33285,0.33292,0.17431}
    vertWeights {44,0.15632,0.33444,0.33462,0.17462}
    vertWeights {49,0.17817,0.32895,0.32812,0.16476}
    vertWeights {54,0.18163,0.32749,0.32638,0.16450}
    vertWeights {106,0.15632,0.33444,0.33462,0.17462}
    vertWeights {112,0.17817,0.32895,0.32812,0.16476}
    vertWeights {120,0.18163,0.32749,0.32638,0.16450}
    vertWeights {121,0.15992,0.33285,0.33292,0.17431}
  }
  CIwAnimSkinSet
  {
    useBones { FlagB FlagC FlagD FlagE }
    numVerts 20
    vertWeights {40,0.14751,0.34532,0.34603,0.16114}
    vertWeights {41,0.02763,0.13480,0.41879,0.41879}
    vertWeights {45,0.14368,0.34743,0.34842,0.16046}
    vertWeights {46,0.02625,0.13072,0.42151,0.42151}
    vertWeights {50,0.16232,0.34459,0.34444,0.14865}
    vertWeights {51,0.03730,0.16777,0.39776,0.39717}
    vertWeights {55,0.16581,0.34255,0.34229,0.14936}
    vertWeights {56,0.03875,0.17110,0.39544,0.39471}
    vertWeights {102,0.02763,0.13480,0.41879,0.41879}
    vertWeights {103,0.02625,0.13072,0.42151,0.42151}
    vertWeights {107,0.14368,0.34743,0.34842,0.16046}
    vertWeights {108,0.02625,0.13072,0.42151,0.42151}
    vertWeights {109,0.03730,0.16777,0.39776,0.39717}
    vertWeights {113,0.16232,0.34459,0.34444,0.14865}
    vertWeights {114,0.03730,0.16777,0.39776,0.39717}
    vertWeights {115,0.03875,0.17110,0.39544,0.39471}
    vertWeights {122,0.16581,0.34255,0.34229,0.14936}
    vertWeights {123,0.14751,0.34532,0.34603,0.16114}
    vertWeights {124,0.03875,0.17110,0.39544,0.39471}
    vertWeights {125,0.02763,0.13480,0.41879,0.41879}
  }
}

ANIM 文件

我们需要考虑的最后一个文件类型是ANIM 文件,正如其名称所暗示的,这是定义特定动画的实际文件。

文件首先声明了CIwAnim类的一个实例,与其他动画类类型一样,将给出一个从文件名派生的资源名称。

这个动画将要应用到的骨骼是CIwAnim实例首先指定的内容。然后是多个CIwAnimKeyFrame声明,详细说明了在特定时间索引下每个受影响骨骼的位置和方向。

关键帧不需要列出骨骼中每个骨骼的朝向和位置。如果一个骨骼相对于其父骨骼没有移动,其位置将保持与上一个关键帧相同。

导出器将创建一个anims子目录来保存所有 ANIM 文件。以下代码提供了一个 ANIM 文件的示例;但与 SKIN 文件一样,这只是一个部分示例,以免使这本书的页面充满大量数字:

CIwAnim
{
  skeleton "FlagPoleBase"
  // Keyframe# 1
  CIwAnimKeyFrame
  {
    time 0
    bone "FlagPoleBase"
    pos {-0.38309,-1.27709,0}
    rot {0.70711,0,0,0.70711}

    bone "PoleA"
    pos {258.20248,0.00000,0}
    rot {1,0,0,0}

    bone "PoleB"
    pos {255.88675,0.00000,0}
    rot {1.00000,0,0,-0.00200}

    bone "PoleC"
    pos {257.60138,-0.00000,0}
    rot {0.00000,0.99998,0.00615,0.00000}

    bone "PoleD"
    pos {255.08751,-0.00000,-0.00000}
    rot {0.00000,0.99998,0.00621,0.00000}

    bone "PoleE"
    pos {257.19775,0.00000,-0.00000}
    rot {1.00000,0,0,0.00206}

    bone "FlagStart"
    pos {152.41219,0.00000,-0.00000}
    rot {0.61894,0,0,-0.78544}

    bone "FlagA"
    pos {85.99931,-0.00000,-0.00000}
    rot {0.99505,0,0,0.09934}

    bone "FlagB"
    pos {57.19376,0.00000,-0.00000}
    rot {1.00000,0,0,0.00128}

    bone "FlagC"
    pos {61.42473,0.00000,-0.00000}
    rot {0.99996,0,0,0.00846}

    bone "FlagD"
    pos {60.33911,0,-0.00000}
    rot {0.99996,0,0,-0.00877}

    bone "FlagE"
    pos {60.36695,-0.00000,-0.00000}
    rot {0.99985,0,0,0.01754}

  }
  // Keyframe# 5
  CIwAnimKeyFrame
  {
    time 0.16667
    bone "FlagPoleBase"
    pos {-0.38309,-1.27709,0}
    rot {0.73026,0,0,0.68317}

    bone "PoleA"
    pos {258.20248,0.00000,0}
    rot {0.99889,0,0,-0.04716}

    bone "PoleB"
    pos {255.88675,0.00000,0}
    rot {0.99864,0,0,-0.05222}

    bone "PoleC"
    pos {257.60138,-0.00000,0}
    rot {0.00000,0.99857,-0.05338,0.00000}

    bone "PoleD"
    pos {255.08751,-0.00000,-0.00000}
    rot {0.00000,0.99624,0.08662,-0.00000}

    bone "PoleE"
    pos {257.19775,0.00000,-0.00000}
    rot {0.99483,0,0,-0.10158}

  }
  // Keyframe# 15
  CIwAnimKeyFrame
  {
    time 0.58333
    bone "FlagPoleBase"
    pos {-0.38309,-1.27709,0}
    rot {0.70668,0,0,0.70754}

    bone "PoleA"
    pos {258.20248,0.00000,0}
    rot {0.99873,0,0,0.05033}

    bone "PoleB"
    pos {255.88675,0.00000,0}
    rot {0.99775,0,0,0.06711}

    bone "PoleC"
    pos {257.60138,-0.00000,0}
    rot {0.00000,0.99951,0.03144,-0.00000}

    bone "PoleD"
    pos {255.08751,-0.00000,-0.00000}
    rot {0.00000,0.99996,0.00868,0.00000}

    bone "PoleE"
    pos {257.19775,0.00000,-0.00000}
    rot {0.99853,0,0,0.05420}

  }
  // Further key frames follow to define the remainder of the
  // animation but these have been removed to avoid including
  // large amounts of datafile in the pages of this book
}

加载和渲染导出的 3D 动画

我们现在可以开始渲染 3D 动画了,就像渲染静态 3D 模型一样,这出奇地简单。

将 IwAnim API 添加到项目中

在我们能够使用 Marmalade 的动画功能之前,我们首先需要将 IwAnim API 添加到我们的项目中。这个 API 建立在渲染静态 3D 模型所需的 IwGraphics API 之上。

与所有此类 Marmalade API 一样,我们通过在 MKB 文件的subprojects部分列出iwanim来为一个项目添加对 IwAnim 的支持。然后我们必须在调用IwGraphicsInit之后调用IwAnimInit,在关闭时需要调用IwAnimTerminate

加载和访问 3D 动画

GROUP 文件格式再次帮助我们,以便将动画数据加载到内存中。导出过程已经为我们创建了一个包含 GEO、MTL、SKEL 和 SKIN 文件的 GROUP 文件,因此我们只需为我们要使用的 ANIM 文件添加条目。

在 GROUP 文件中引用所有内容后,我们只需使用资源管理器将其加载到内存中,然后以与我们处理任何其他资源相同的方式访问资源。

以下代码片段说明了我们如何加载 GROUP 文件,然后访问渲染动画 3D 模型所需的资源:

CIwResGroup* lpFlagGroup = IwGetResManager()->
  LoadGroup("Flag/Flag.group");
CIwModel* lpFlag = static_cast<CIwModel*>(lpFlagGroup->
  GetResNamed("Flag", "CIwModel"));
CIwAnimSkel* lpSkel = static_cast<CIwAnimSkel*>(lpFlagGroup->
  GetResNamed("FlagPoleBase", "CIwAnimSkel"));
CIwAnimSkin* lpSkin = static_cast<CIwAnimSkin*>(lpFlagGroup->
  GetResNamed("Flag", "CIwAnimSkin"));
CIwAnim* lpFlagWobble = static_cast<CIwAnim*>(lpFlagGroup->
  GetResNamed("FlagWobble", "CIwAnim"));

好的,现在我们已经将资源加载到内存中,我们需要对它们做些什么。

播放 3D 动画

为了播放动画,我们需要让 Marmalade 知道我们想要播放哪个动画,播放速度应该是多少,以及我们是否想要它是一次性播放还是循环播放。所有这些以及更多都是由CIwAnimPlayer类提供的。

在创建CIwAnimPlayer的新实例后,我们必须提供动画所需的骨骼实例的指针。这如下所示:

CIwAnimPlayer* lpAnimPlayer = new CIwAnimPlayer;
lpAnimPlayer->SetSkel(lpSkel);

玩家对象现在已准备好开始动画,所以我们只需传递我们想要播放的动画的详细信息。这只需一行代码即可完成:

lpAnimPlayer->PlayAnim(lpFlagWobble, 1.0f,
                       CIwAnimBlendSource::LOOPING_F, 0.0f);

PlayAnim方法首先接受我们想要播放的CIwAnim实例的指针。然后它期望看到播放速度、一些控制标志和一个混合间隔。

播放速度被指定,使得值为1产生正常导出的动画速度。将此值加倍将使动画以两倍速度播放,依此类推。

函数的第三个参数是一组控制标志,主要用于指示动画在达到最后一个关键帧时是否应该循环。如果需要循环,应使用标志CIwAnimBlendSource::LOOPING_F

CIwAnimBlendSource定义了多个其他值,但其中大部分是用于只读状态标志的,CIwAnimPlayer类提供了其他方法,应该用来确定当前状态。因此,在这个方法中唯一将使用的其他标志是CIwAnimBlendSource::RESET_IF_SAME_F,这将强制动画播放器重新启动指定的动画,如果它已经是当前动画。如果将正在播放的动画传递给PlayAnim方法,除非使用此标志,否则请求将被忽略。

动画播放器现在已初始化,所以最后必须做的事情是指示它计算所需的动画帧。这是通过在主游戏循环的每次迭代中调用CIwAnimPlayer实例的Update方法来完成的,如下代码所示:

lpAnimPlayer->Update(lTimeStep);

lTimeStep参数是一个float类型的值,表示当前动画状态应该前进的时间量(以秒为单位)。当这个调用完成后,将创建一个骨骼副本,所有骨骼都将正确定位和旋转,以便渲染当前动画帧。

渲染 3D 动画

随着动画播放器现在愉快地更新,最后一步是将动画模型渲染出来。这可能整个过程中最简单的一部分,如下代码所示:

IwGxSetViewMatrix(&lViewMatrix);
IwGxSetModelMatrix(&lModelMatrix);

IwAnimSetSkelContext(lpAnim->GetSkel());
IwAnimSetSkinContext(lpSkin);

lpFlag->Render();

IwAnimSetSkelContext(NULL);
IwAnimSetSkinContext(NULL);

希望其中大部分对你来说已经很熟悉了。第一步是设置我们想要用于渲染的视图和模型矩阵。然后我们需要提供一些关于动画帧的信息,即动画骨骼和皮肤数据。

骨骼信息由CIwAnimPlayer实例维护,可以使用GetSkel方法检索。皮肤只是由资源管理器加载的CIwAnimSkin实例。我们使用IwAnimSetSkelContextIwAnimSetSkinContext函数将此数据提供给渲染引擎。

要将动画模型渲染到屏幕上,我们只需在lpFlag上调用Render方法,lpFlag是一个指向CIwModel实例的指针,就像我们渲染没有动画的模型一样。

渲染完成后,我们清除皮肤和骨骼上下文,这样未来的模型渲染调用就不会在渲染过程中尝试使用错误的数据。养成这个习惯是好的,因为确定一个未动画模型突然开始剧烈变形的原因可能是一个棘手的错误,难以追踪。

进一步探索 3D 动画

恭喜!你现在能够渲染全动画的 3D 模型了!虽然这是一个相当酷的成就,但我们迄今为止看到的功能只是 IwAnim API 允许我们做的表面功夫。以下章节描述了一些我们可用的其他功能。

反向播放动画

有时候能够播放动画倒放是有用的。例如,想象一个角色跪下来检查一个物体。而不是创建一个全新的动画来让他们再次站起来,我们只需播放跪下的动画倒放即可。

通过将负动画速度传递给PlayAnim调用,可以简单地实现动画倒放,因此-1的值将以正常速度倒放动画。

动画之间的混合

在两个动画之间转换时,我们通常不希望直接跳转到新序列的开始,因为这可能导致当前动画帧和新动画第一帧之间出现明显的跳跃。我们可以通过在动画之间混合来解决这个问题。

我们在首次介绍PlayAnim方法时提到了如何实现这一点。此方法中的最后一个参数是混合时间,它以秒为单位使用浮点数指定。

通过指定非零混合间隔,动画播放器将计算旧动画和新动画所需的动画帧,然后在指定的时间内通过在这两个帧之间插值来生成第三个过渡帧。过渡帧是随后用于绘制 3D 模型的帧。一旦混合间隔结束,原始动画将停止计算,因为它不再需要。

检测动画播放事件

能够检测动画是否循环或播放完成非常重要,因为我们可以开始将动画链接在一起或防止用户在动画完成之前执行任务。例如,想象一个玩家需要重新装填武器,并播放一个动画来展示这个过程。我们需要知道动画何时完成,以便允许玩家再次开始攻击。

CIwAnimPlayer类允许我们通过调用IsCurrentAnimComplete方法来检测一次性动画是否完成,当动画播放完毕时,该方法将返回true

此外,还有IsCurrentBlendComplete方法,当动画播放器完成两个动画之间的混合时,将返回true

检测动画是否循环也是可能的,尽管CIwAnimPlayer没有提供快速检测此事件的快捷方式。相反,我们必须进行一些手动标志测试。

在任何时刻,动画播放器都可以更新两个主要动画:当前动画(定义为最后使用PlayAnim方法指定的动画)和上一个动画(在PlayAnim上次调用时带有混合间隔正在播放的动画)。

这两个动画的当前状态存储在CIwAnimBlendSource类的实例中,我们可以通过名为GetSourceCurrGetSourcePrevCIwAnimPlayer类方法来访问它们。CIwAnimBlendSource类有一个名为GetFlags的方法,它返回以位掩码形式表示的播放状态信息。要检测动画是否循环,我们只需查看标志CIwAnimBlendSource::LOOPED_F是否设置。以下源代码展示了这一操作:

if (lpAnimPlayer->GetSourceCurr()->GetFlags() &
                      CIwAnimBlendSource::LOOPED_F)
{
  // Animation has looped!
}

如果您喜欢这种方法,您还可以使用标志CIwAnimBlendSource::COMPLETE_F来检测单次动画何时完成。

优化动画播放

你还记得我们是通过调用CIwAnimPlayerUpdate方法来计算当前动画帧的吗?这个方法必须做很多事情,其中一些我们可能实际上不需要在每一帧都做。例如,如果一个游戏中的角色当前不在屏幕上可见,我们可能想要确保我们仍然可以遍历其动画;但是,计算当前动画帧的骨骼位置是浪费处理器时间,因为我们不会渲染动画。

Update方法实际上是通过调用CIwAnimPlayer的另外三个方法来实现的,如果我们愿意,可以独立调用这些方法。

第一个方法是UpdateParameters,它将我们需要更新动画的时间增量作为其唯一的参数。此方法将更新动画播放器当前正在使用的所有动画的时间索引,并设置标志以指示那些动画是否已完成或循环。

UpdateSources方法不接受任何参数,用于计算每个动画的当前骨骼方向,并根据需要应用动画之间的混合。

最后是UpdateMatrices方法,它同样不需要任何参数。这个方法执行将每个骨骼的位置和方向转换为用于在渲染过程中更新 3D 模型顶点流的矩阵的最终步骤。

这些方法需要按照之前呈现的顺序调用,但如果不需要计算该方法的输出结果,则不需要在每一帧都调用所有三个方法。

播放子动画

子动画允许我们仅对整个骨骼的一部分进行动画处理,这在我们需要游戏中的角色能够同时执行两个不同的动作时非常有用。例如,一个角色可能在游戏世界中移动时能够挥舞几种不同的武器。应用于角色的主要动画将是一个行走、跑步或仅静止不动的动画。然后,子动画可以叠加在主要动画之上,以显示玩家持有、射击或重新装填不同类型的武器。

为了导出子动画,我们只需在 Marmalade 导出插件中的子动画根字段中指定子动画的根骨骼名称。在之前给出的示例情况下,你可能会选择从有两个手臂骨骼作为子节点的骨骼开始导出子动画。

不幸的是,Blender 插件目前不支持此功能,尽管您可能可以导出整个动画,然后在 ANIM 文件中手动删除对子动画根骨骼以上层次骨骼的任何引用。

子动画导出后,要播放它,我们只需调用CIwAnimPlayerPlaySubAnim方法。此函数的使用示例如下:

lpAnimPlayer->PlaySubAnim(0, lpFlagWave, 1.0f,
CIwAnimBlendSource::LOOPING_F, 0.0f);

如您所见,它在结构上几乎与PlayAnim方法相同。唯一的区别是额外的初始参数,即子动画索引号。动画播放器可以同时支持两个不同的子动画,索引号应为01,以指示您希望更改哪个子动画。

要检测子动画的当前播放状态,我们可以通过CIwAnimPlayerGetSourceSub方法获取CIwAnimBlendSource实例。此方法接受一个参数,即所需子动画的索引号。

偏移动画

当处理导致游戏角色移动的动画,如行走、奔跑或进行攻击动作时,我们希望根据正在播放的动画更新角色的位置,以便角色的脚看起来不会在地面上滑动。

Marmalade 提供了一种通过偏移动画来实现此功能的方法,这是一种由单个骨骼组成的动画,其位置和旋转可以用来在游戏世界中移动对象。偏移动画使用与其他动画相同的导出过程进行导出。

要使用偏移动画,我们使用CIwAnimPlayerPlayOfsAnim方法,如下面的代码所示:

lpAnimPlayer->PlayOfsAnim(lpMovementAnim, 1.0f, 0);

此函数的参数是指向偏移动画实例的指针、播放速度(再次,值为1将以正常速度播放),以及所需的动画标志;因此,可以一次性或循环播放偏移动画。

要找到偏移动画的当前状态,我们可以使用CIwAnimPlayer上的GetSourceOfs方法来检索维护它的CIwAnimBlendSource实例。

我们还可以使用CIwAnimPlayerGetMatOfsInitialGetMatOfsFinalGetMatOfs方法来获取起始、结束和当前偏移的位置和旋转信息。这些方法中的每一个都允许访问一个表示偏移当前方向的CIwFMat对象。然后我们可以使用这些信息来更新游戏角色的位置,以便其他游戏功能,如碰撞检测,可以继续正常工作。

获取骨骼位置和旋转

在之前讨论子动画时,我们展示了角色能够携带各种不同武器的例子。子动画当然只能提供这个问题的部分解决方案,因为它们会将角色的手臂移动到正确的姿势;但是,由于武器不是源 3D 模型的一部分,角色看起来就像是在空中抓取。

我们需要一种方法来绘制进一步描述武器的模型,但我们如何将其定位在正确的位置?

答案是要求动画播放器提供位于武器模型需要绘制点的骨骼的当前朝向和位置。我们可以通过调用CIwAnimPlayerGetBoneNamed方法来实现,该方法将返回指向表示请求骨骼当前朝向的CIwAnimBone实例的指针。

可以使用CIwAnimBoneGetPosGetRot方法找到骨骼的位置和旋转,这些方法允许我们在模型空间中生成一个矩阵,或者如果它已经在CIwAnimPlayer实例的更新过程中计算过,GetMat方法将返回一个表示骨骼位置和旋转的模型空间矩阵。

使用骨骼信息,我们可以轻松地计算出用于在正确位置渲染武器模型的模型矩阵。首先,我们使用骨骼信息在模型空间中生成一个矩阵,然后乘以任何旋转矩阵以将角色定位在游戏世界中。最后,加上角色的世界位置,武器模型就可以在角色的手中渲染出来。

示例代码

以下几节概述了本章附带的示例项目。

旗帜项目

此示例演示了播放主动画和子动画。一面旗帜在虚拟的风中飘扬。每隔几秒钟,旗杆会摇摆,但杆顶的旗帜会继续飘动。以下图可以看到屏幕截图:

旗帜项目

旗帜飘扬的动画是通过循环的子动画实现的,而旗杆的摇摆是主动画,每五秒作为一次性的动画启动。

这种方法的一个问题是子动画只有在当前有主动画进行时才会播放。而不是创建一个旗杆静止的一帧动画,这里使用了一个巧妙的技巧。

旗杆摇摆动画实际上是连续播放的,但速度为零。由于摇摆动画的第一帧是杆子直立的位置,我们已经实现了静态动画帧的目标。

每隔五秒,摇摆动画作为一次性的动画重新启动。当摇摆动画完成后,我们将其速度设置为零以再次保持旗杆稳定。

滑雪项目

本章对滑雪项目的修改真的让它看起来更像一个游戏。以下截图和以下部分突出了添加的新功能:

滑雪项目

新的游戏玩法功能

在此之前,游戏中并没有太多实际的游戏玩法。我们可以左右操控这个小滑雪者,但除了撞到树上之外,实际上并没有太多可以做的事情。

为了应对这种情况,添加了由几面旗帜组成的门。玩家必须引导滑雪者通过这些门来增加他们的分数,现在分数显示在屏幕底部。

为了实现门,创建了一个名为Flag的新类。ModeGame类在赛道上选择一个随机位置,并在该位置左右稍远处生成一个旗帜。旗帜向上滚动,当它们从顶部滚动出去时,在游戏世界的底部选择一个新的随机位置。

ModeGame类维护对两个Flag实例的指针,以便在它们滚动出屏幕并需要重新定位时检测到,同时我们也可以使用这些指针来确定玩家是否在它们之间移动。

还引入了随机放置的岩石,必须避开它们,因为撞到它们会导致游戏结束,就像撞到树一样。这些由另一个新类Rock表示。这个类与现有的Tree类非常相似,主要区别在于当岩石从屏幕顶部滚动出去时,它会在底部以新的水平位置替换。

添加了动画

考虑到本章的主题,很明显,向游戏中添加动画将是做出的更改之一。

我们的小滑雪角色现在被赋予了一个循环动画,所以这个小家伙现在用他的滑雪杖推动自己前进。如果玩家撞到树或岩石,这个可怜的小家伙现在也会摔倒,最终在地上堆成一团。哎呦!

游戏中添加的另一个动画是为旗帜设计的。它与本章其他示例项目中的动画结构相同。一个子动画播放,使旗帜在风中飘扬,而主动画则是旗帜杆摇摆。

动画不是在固定的时间间隔摇摆,而是在玩家滑雪太靠近旗帜时触发。

概述

我们在本章中广泛地介绍了 3D 动画。现在我们可以在游戏世界中移动、旋转和缩放 3D 模型,并且我们可以使用骨骼动画来改变模型的实际形状,使角色行走、奔跑、跳跃、跳舞或完成我们需要的任何动作。

我们还探讨了更多高级主题,例如动画之间的混合、在主动画之上应用子动画,以及从进行中的动画中检索骨骼位置和方向信息,以便我们找到模型特定部分的位置。

在下一章中,我们将回到二维空间,探讨我们如何使游戏的用户界面比仅使用布局糟糕的调试字体看起来更漂亮。

第六章:实现字体、用户界面和本地化

现在我们有了创建充满动画角色的 3D 世界的知识,我们真的需要开始思考如何改善我们向玩家展示的用户界面的外观。

在本章中,我们将涵盖以下内容:

  • 创建可用于 Marmalade 项目的字体

  • 文本的绘制和格式化

  • 讨论实现您游戏用户界面的方法

  • 将您的游戏本地化为多种语言

实现字体

改善我们游戏外观的第一步是告别调试字体,用一些更时尚的东西来替换它。Marmalade SDK 附带了一个名为 IwGxFont 的 API,专门用于字体渲染,让我们好好利用它。

将 IwGxFont API 添加到项目中

到现在为止,我敢肯定您一定能猜到这是如何完成的。没错,只需在 MKB 文件的子项目部分添加iwgxfont,然后调用IwGxFontInit来初始化 API,并在关闭时调用IwGxFontTerminate来释放它。

如此 API 的名称所暗示的,它需要 IwGx 才能工作。我们还需要 IwResManager,以便将字体数据加载到内存中,因此 IwGxFont 的初始化调用必须在初始化这两个模块之后进行,如下面的代码片段所示:

IwGxInit();
IwResManagerInit();
IwGxFontInit();

创建字体资源

我们需要做的第一件事是创建一个描述我们想要使用的字体的CIwGxFont资源。这很容易做到,多亏了作为 Marmalade SDK 一部分安装的Marmalade Studio - Font Builder实用程序。以下图片显示了该程序运行时的截图:

创建字体资源

IwGxFont API 通过使用包含所有需要渲染的字符图像的大位图的片段来逐个绘制字符来渲染文本。生成位图本身相当简单,可以使用任何艺术包完成,但我们需要以某种方式指定位图的哪一部分代表哪个字符。这就是 IwGxFont 和字体构建实用程序对我们有所帮助的地方。

生成字体资源需要以下基本步骤:

  1. 启动Marmalade Studio - Font Builder实用程序。您可以在Marmalade | x.x | Tools下找到它的 Windows 开始按钮菜单快捷方式,其中x.x代表您安装的 Marmalade SDK 的版本号。

  2. 在标记为输入的左上角面板中,首先点击选择…按钮以显示字体选择对话框。选择所需的字体、大小和样式,然后点击确定。您可以选择任何已安装的 Windows 字体,无论是可缩放的 TrueType 字体还是固定大小的位图字体。

  3. 字符文本框允许您指定您在字体中需要的字符列表。默认选择覆盖了大多数欧洲语言,但您可以添加或删除您想要的任意数量的字符。显然,字符越少越好,因为它将减少生成的位图大小,从而占用更少的内存。您还可以通过使用文件 | 加载字符映射来加载文本文件来填充此文本框。此时,字符文本框将包含文本文件中出现的每个唯一字符,不包括格式化字符,如制表符或换行符。

  4. 接下来是标记为输出选项的部分,您可以使用数字输入框或单击选择…按钮来选择字体颜色,该按钮将显示颜色选择对话框。建议您将颜色设置为默认的亮白色,因为文本颜色可以在运行时轻松设置。

  5. 此处还有一些其他设置,允许您调整字体的外观。有复选框可以强制所有字符为大写,并启用向字体添加阴影的功能。如果此复选框被勾选,则有一个文本框允许指定阴影的像素偏移量。

  6. 接下来,查看右上角的输出面板。在这里,我们可以看到生成的字体字符的预览。只需单击重绘按钮,稍后字符将出现在视图区域。然后,您可以使用两组上一页下一页按钮来循环查看每个字符。

  7. 我们几乎准备好导出字体了;但在我们能够这样做之前,我们需要指定字体文件要创建的位置,这可以在保存和加载面板中完成。单击浏览…按钮以显示文件选择器以选择所需的目录或直接将其输入到保存路径文本框中。此文件名的最后一部分是用于导出字体数据的基文件名。

  8. 要创建字体文件,请确保勾选了标记为保存 .TGA保存 .gxfont的两个复选框,然后单击创建按钮。

在这次演示中,我们还没有探索一些选项,因为在大多数情况下,您不需要担心它们。特别是,我们完全跳过了标记为输入(按范围)选项的面板。

此面板提供了对字体特征的控制,例如字距,这是字体字符之间的偏移量。有时,字距调整可以使某些字符组合更靠近。例如,考虑大写字母 A 和 V。这些字符的形状意味着您可能希望将它们稍微靠近一些,以便在它们并排显示时提供更自然的外观。

您还可以声明字符子范围,这允许您将不同的全局设置应用于某些字符范围。您可以使用此功能使用完全不同的字符大小,甚至为不同的字符范围使用完全不同的源字体。如果我们想创建一个包含标准 ASCII 集字符和另一种语言字符的字体,这尤其有用。我们用于 ASCII 字符的字体可能不包含其他语言的字符,因此我们可以创建一个子范围,允许我们为这些字符选择一个完全不同的字体。

GXFONT 文件格式

字体构建器实用程序创建两种类型的文件来定义字体资源。其中第一种是实际的字体位图,它以 Targa 文件格式导出,这是一种通常通过文件扩展名.tga识别的图像文件格式。

导出的第二个文件是一个GXFONT 文件,它既允许字体被重新加载到字体构建器中进行进一步编辑,也是将字体加载到我们自己的程序中的方式。

以下是一个示例 GXFONT 文件,它包含仅使用标准 Windows 字体 Arial Black 以 20 磅绘制的数字字符:

//Temp file created by AS Font Builder (User: Sean At: 06/29/12 18:00:06)
//Command Line:
//: -fontdesc "0;-27;0;0;0;900;0;0;0;0;3;2;1;34;Arial Black" 0 4 0 -pad 0 0 
//: -col #FFFFFF -shadow 0 -spacing 4 -force16 0

CIwGxFont
{
  utf8 1
  image numbers.tga
  charmap "0123456789"
}

如您所见,这是相当直观的。文件开头的注释主要用于字体构建器实用程序了解创建字体时使用的设置,因此如果您想稍后编辑字体,应保留这些值。

注意

我们可以在字体构建器实用程序中稍后编辑字体,方法是使用文件 | 加载字体或按加载…按钮。将出现一个文件请求器,允许我们选择要编辑的 GXFONT 文件。

我们真正感兴趣的 GXFONT 文件部分是CIwGxFont实例的定义。这里我们看到的三项参数表明,与该字体一起使用的字符编码应该是 UTF-8,要使用的位图图像称为numbers.tga,而字体中包含的字符是从零到九的数字。

注意

默认情况下,选择 UTF-8 字符编码,因为这个格式通常提供了文本字符串最紧凑的内存表示,至少就欧洲语言而言。

如果我们在字体构建器实用程序中指定了任何其他字体设置,例如字符子范围或字距调整信息,这也会在文件顶部的注释和CIwGxFont结构中表示。不过,这里我们不会涉及这一点,因为字体构建器会为我们处理所有困难的工作。

加载和访问字体资源

与我们看到的所有资源类型一样,我们通过将对其的引用添加到 GROUP 文件中,使用资源管理器加载 GROUP 文件,然后搜索字体资源来将字体资源加载到我们的程序中。

为了完整性,这里是一个代码片段,展示如何做到这一点:

CIwResGroup* lpResGroup = IwGetResManager()->
LoadGroup("fonts/fonts.group");
CIwGxFont* lpSmallFont = static_cast<CIwGxFont*>(IwGetResManager()->
GetResNamed("small", "CIwGxFont"));

使用字体资源绘制文本

加载了字体资源并获取了其指针后,我们可以开始使用它来在屏幕上绘制一些文本。首先,我们将探讨绘制文本字符串的基本方法,然后我们将看看如何对文本进行对齐、更改其大小,以及如何使绘制过程更加优化。

在屏幕上绘制文本

当我们查看如何创建字体资源时,提到可以为字体选择颜色。建议选择白色,这样我们就可以在运行时将文本颜色更改为我们想要的任何颜色。我们通过调制字体位图与所选颜色来更改字体的颜色,因此如果字体位图不是白色,这将不会产生预期的颜色变化。

我们将在稍后看到如何更改字体颜色,但为了使字体着色工作,首先必须提到 IwGxFont 的一个特性。

注意

当尝试在运行时重新着色字体时,我们必须确保使用函数调用 IwGxLightingEmissive(true) 启用发射光照。IwGxFont 通过使用发射光照组件来影响字体的颜色,如果它被禁用,则不会应用此颜色。

在处理完关于光照的注意事项后,渲染文本的第一步是指出我们想要使用哪个字体来绘制它。这是通过将相关 CIwGxFont 实例的指针传递到函数 IwGxFontSetFont 中来完成的。

接下来,我们可以使用 IwGxFontSetCol 设置我们想要使用的颜色。这个函数有两个版本,一个接受对 const CIwColour 实例的引用,另一个接受颜色值作为 uint32 值。当使用后者时,请注意颜色是按 ABGR 格式指定的——即,最高有效字节是 alpha,然后是蓝色、绿色和红色在最低有效字节中。

现在,我们需要指定文本在屏幕上的位置,这通过定义一个矩形区域来完成,其中文本应该出现。这是通过使用包含矩形左上角的 x 和 y 值以及宽度和高度值的 CIwRect 实例来指定的。我们使用的函数调用是 IwGxFontSetRect

使用 IwGxFontDrawText 函数现在可以绘制文本。第一个参数是要打印的文本字符串,指定为一个 const CIwChar 指针。CIwChar 只是标准 C char 类型的 typedef 类型。

文本的默认编码为 UTF-8。对于由 ASCII 集合中的字符组成的文本,这意味着我们根本不需要对文本数据进行任何操作。

该函数还接受第二个参数,即要绘制的文本的长度。其默认参数值为 -1,表示应绘制整个字符串。任何其他值将绘制指定的字符数。如果你想要实现许多游戏中常见的系统,其中文本逐个字符出现在屏幕上,这将非常有用。

将所有这些结合起来,以下是一个示例,它在屏幕上以黄色绘制“Hello World”:

IwGxLightingEmissve(true);
IwGxFontSetFont(lpSmallFont);
IwGxFontSetCol(0xFF00FFFF);
IwGxFontSetRect(CIwRect(0, 0,IwGxGetScreenWidth(), 100));
IwGxFontDrawText("Hello World");

文本换行和调整

想知道为什么我们为文本指定了一个矩形区域而不是仅仅一个屏幕位置吗?原因是这样,IwGxFont 可以为我们自动换行和调整文本。

注意

虽然 Marmalade 允许我们在代码中包含换行符以强制在文本中换行,但它不提供对其他格式化字符(如制表符或退格符)的支持。最好让 Marmalade 自动换行文本,而不是手动在文本中插入换行符,因为如果我们更改字体大小或矩形绘制区域的尺寸,我们就不必以任何方式更改文本本身。

渲染文本时的默认行为是在文本行超出使用 IwGxFontSetRect 设置的矩形区域边界时进行换行。我们可以使用 IwGxFontSetFlags 函数来改变这种行为,该函数可以组合以下值进行 OR 操作:

定义
IW_GX_FONT_DEFAULT_F      使用默认字体设置。
IW_GX_FONT_NOWRAP_F 不在矩形边界的边缘换行。
IW_GX_FONT_NOWORDWRAP_F 不对文本执行完整的单词换行。
IW_GX_FONT_ONELINE_F 只渲染单行文本。渲染在遇到换行符 ('\n') 时停止。
IW_GX_FONT_NUMBER_ALIGN_F 强制所有数字以相同的宽度显示。
IW_GX_FONT_UNDERLINE_F 以带下划线的方式绘制文本。
IW_GX_FONT_ITALIC_F 以斜体形式绘制文本。
IW_GX_FONT_RIGHTTOLEFT_F 从右到左绘制字符。对于阿拉伯语等语言很有用。
IW_GX_FONT_NOWORDSPLIT_F 在单词末尾换行。一个单词可以跨越矩形边界的末尾,但下一个单词将开始在新的一行上。

可以使用 IwGxFontClearFlags 再次清除标志。

我们还可以使用 IwGxFontSetAlignmentHor 函数指定文本是否在矩形边界区域内左对齐、右对齐或居中,该函数接受以下值之一:

定义
IW_GX_FONT_ALIGN_LEFT 将文本对齐到边界框的左侧。
IW_GX_FONT_ALIGN_CENTRE 在边界框中水平居中文本。
IW_GX_FONT_ALIGN_RIGHT 将文本对齐到边界框的右侧边缘。
IW_GX_FONT_ALIGN_PARAGRAPH 根据设备的本地化设置执行左对齐或右对齐。

我们也可以使用 IwGxFontSelAlignmentVer 和以下这些值之一来进行类似的垂直对齐:

定义
IW_GX_FONT_ALIGN_TOP 绘制文本,使文本的顶部行触及边界框的顶部。
IW_GX_FONT_ALIGN_MIDDLE 在边界框中垂直居中文本。
IW_GX_FONT_ALIGN_BOTTOM 绘制文本,使最后一行的底部触及边界框的底部。

在运行时更改字体大小

有时我们希望能够通过改变文本大小来使文本动起来。例如,在射击游戏中,击杀敌人所获得的分数可能会出现在敌人的位置,然后逐渐变大并淡出。

函数IwGxFontSetScale使我们能够做到这一点。它接受两个参数,因此字体可以按不同的量进行缩放,包括水平和垂直。缩放因子以定点值的形式传递,其中IW_GEOM_ONE表示缩放因子为1,因此不改变大小。

IwGxFont 通过为文本中的每个字符渲染一个矩形多边形来绘制文本,并将字体图像的相关部分映射到它上面。通过指定缩放因子,我们可以改变用于渲染单个字符的多边形的大小,但如果缩放因子过大(例如,超过字体原始大小的两倍),则可能会得到较差的结果。

通过准备文本优化绘制

文本渲染的一个问题是在进行对齐、换行等操作时,需要逐个字符地格式化文本,以查看下一个字符是否跨越了矩形边界框区域。

如果我们需要绘制一段固定文本,例如说明屏幕,我们可以通过一次准备文本用于渲染并使用一些缓存数据来绘制它,从而避免在每一帧都计算格式化信息。

要做到这一点,我们使用函数IwGxFontPrepareText。这个函数接受一个指向CIwGxFontPreparedData类实例的引用,要准备的文本字符串,以及可选地,我们想要考虑的字符串中的字符数。如果省略此参数,则处理整个字符串。

在文本准备完成后,我们可以使用IwGxFontDrawText函数的另一个版本来绘制它。这个版本接受一个指向CIwGxFontPreparedData实例的引用,以及两个可选参数,分别指示从准备数据中绘制的第一个字符和要绘制的字符数。以下是一个代码示例:

CIwGxFontPreparedData lFontData;
IwGxFontSetRect(CIwRect(100, 100, 200, 100));
IwGxFontPrepareText(lFontData, "This is the text to be prepared!");
IwGxFontDrawText(lFontData);

注意,文本将在屏幕上绘制在IwGxFontSetRect调用中设置的格式化矩形所指示的位置。

实现用户界面

每个游戏都需要某种类型用户界面,即使只是一个可以按下以开始新游戏的按钮。在本节中,我们将探讨如何为您的游戏实现用户界面。

IwUI API

Marmalade SDK 附带了一个名为 IwUI 的 API,它允许我们为我们的项目创建由按钮、标签和其他常见控件组成用户界面。

此 API 功能丰富,不仅允许创建游戏界面,还可以为更严肃的应用程序创建界面。Marmalade 曾经附带一个名为 Marmalade Studio UI Builder 的工具,但遗憾的是,这不再是 SDK 支持的一部分。然而,仍然可以通过安装 Marmalade 的旧版本(v5.2.x 系列中的一个版本可能最好)或从github.com/marmalade/UI-Builder下载其源代码来访问此工具。

也可以在不使用 UI 创建工具的情况下使用 IwUI,通过手动构建描述我们界面布局的 ITX 文件来实现。这些布局文件可能会变得相当冗长,因此难以维护,所以 Marmalade Studio UI Builder 使得编辑布局变得更加容易管理。

Marmalade 文档中提到,从 SDK 中删除 UI Builder 的原因是为了允许使用一个标准化的 UI 标记系统,该系统被许多其他第三方工具支持。在撰写本书时,尚未就这一形式的具体内容做出进一步公告。

毫无疑问,IwUI API 将在可预见的未来仍然是 Marmalade 的一部分。然而,我们不会在本书中深入探讨 API 本身,因为它似乎很可能很快会有一个新的 UI 系统加入 Marmalade。如果您对 IwUI 能做什么感兴趣,请查看 Marmalade 文档和随 SDK 提供的众多示例代码。

IwNUI API

Marmalade 提供了一个名为 IwNUI 的第二个用户界面 API。"N"代表 Native,因为这个 API 允许您使用应用程序运行的平台的标准 UI 控件来构建用户界面。

这听起来像是一个好主意,但主要的缺点是它只支持 iOS 和 Android。所有其他平台将使用使用之前提到的 IwUI API 实现的默认样式。

无论如何,大多数游戏倾向于实现符合游戏风格的 UI,这通常意味着我们不想使用标准的操作系统用户界面控件,但如果您想开发实用程序或其他应用程序类型,IwNUI 是一个不错的选择。

实现我们自己的用户界面解决方案

由于我们在用户界面实现上实际上是从零开始的,让我们考虑一下我们如何创建自己的解决方案。

以下部分强调了在开发用户界面代码时需要注意的一些问题。本章附带的示例项目实现了一个用户界面库,试图考虑以下大部分内容。

使用通用方法

当处理用户界面代码时,真正值得花时间开发尽可能通用的解决方案。虽然实现游戏的界面前端在编码上并不特别困难,但很容易发现自己为每个项目从头开始编写 UI 代码。

通过投资于一种通用方法,你可以快速为所有项目构建一个功能性的 UI。前端菜单系统实际上往往只是一个按钮和标签的集合;那么为什么需要多次编写这段代码呢?实现这些类型的控件一次,然后当你需要为游戏创建定制控件时,你可以有更多的时间来创建。

建议你通过创建一个单独的子项目来实现你的 UI 代码,因为这将有助于确保你的解决方案尽可能通用和自包含。

注意

Marmalade 使我们能够通过使用 SDK 用于包含其组件部分的相同系统来轻松创建自己的库模块。只需创建一个包含库中所有源文件的 MKB 文件,但保存时使用.mkf扩展名而不是.mkb。然后,你可以通过将 MKF 文件的名称(不包括扩展名)添加到主项目 MKB 文件的subprojects部分来引用此模块。库模块目录应放置在与主项目目录相同的位置,以便在从 MKB 文件创建项目时可以找到它们。

充分利用类继承

一个良好的类层次结构可以使实现你的 UI 体验变得更加愉快,并且查看现有系统是如何构建的非常值得。

大多数现代 UI 实现通常从基类开始,所有其他控件类型都由此派生,在本章的讨论中,我们将称之为元素。元素将负责诸如控件定位和内部命名等问题,以便可以标准化 UI 事件的处理。

当实现代表元素的类时,我们应该利用虚拟方法,这些方法可以被子类覆盖以改变默认行为。至少这通常意味着我们应该有可以调用的方法来更新和渲染控件。

另一个非常有用的概念是框架,它能够将多个元素组合在一起,以便可以同时移动、启用或隐藏它们。

当更新或渲染用户界面时,框架负责决定是否更新或渲染其包含的子元素。

框架内所有元素的定位和大小也应相对于框架本身的定位和大小来计算。

实现了代表元素和框架的类之后,可以非常简单地实现大多数常见的 UI 控件。以下是一些示例来说明这一点:

  • 标签控件简单地显示屏幕上的文本行。它可以派生自元素类,在其最简单的情况下,我们只需要定义存储要绘制的文本的成员变量,以及一些字体和颜色信息。然后我们可以重写虚拟渲染方法,以便在元素类指示的位置绘制文本。

  • 位图控件与标签非常相似,但显示的是图像而不是文本。我们只需要存储指向我们想要绘制的图像的指针(可能是一个 CIwTextureCIwMaterial 指针),然后实现渲染方法来在屏幕上绘制它。

  • 按钮控件可以派生自框架。大多数 UI 系统允许在按钮上显示图像或文本字符串(或两者),因此我们可以通过将标签或位图控件添加到框架内包含的元素列表中来实现这一点。

  • 滑块控件也可以以框架为基础,并可以包括两个位图控件,一个用于滑块的背景,另一个用于选择旋钮。如果你想要显示滑块的当前位置作为数值,也可以包括一个标签。

希望这能给你一个想法,即通过一点初步规划,实现各种用户界面控件实际上变得非常容易。

实现数据驱动系统

在良好的类层次结构到位之后,下一步是确保你的 UI 可以轻松地从配置数据文件中创建。虽然完全有可能在代码中创建所有控件,但这很难维护,最重要的是,这只能由程序员编辑。

允许你的 UI 从数据文件中构建意味着你的团队的其他成员可以帮助设计 UI。拥有数据文件格式也使得开发用户界面布局工具变得更加容易,如果你想让这个过程对人们来说更容易使用的话。

我们已经看到如何使用 ITX 文件格式在运行时从文件中构建我们自己的自定义类,因此将这种方法应用于我们的 UI 代码是有意义的(如果你想要刷新对这个问题的记忆,请回顾第二章,资源管理和 2D 图形渲染)。没有必要编写比必需的更多代码!

响应用户输入事件

游戏的用户界面必须解决两个主要问题。第一个是向玩家传达信息,我们已经在前面讨论了如何做到这一点。第二个是响应用户输入。

正如本书前面所讨论的,现代移动设备提供了许多让玩家与游戏互动的方式。你支持哪一种取决于你试图针对的设备,但最流行的选择无疑是触摸屏。按下屏幕按钮只是与应用程序互动的一种非常自然的方式,所以几乎可以肯定你最终会支持你的 UI 中的触摸屏。

显然,并非所有控件都需要响应触摸。例如,标签不太可能执行任何操作,因此提供一种机制来指示哪些控件应该响应触摸,哪些不应该是有意义的。

虽然我们可以在检测到元素类边界区域内触摸时被调用的元素类中添加一些虚拟方法,但这可能不是最好的解决方案,因为它开始使元素类变得有些杂乱。

我们真的希望以某种方式封装这种功能,一个好的方法是通过使用事件系统。这样一个系统通过拥有一个中心事件管理器来实现,其唯一任务是接收来自代码一部分的事件消息,并将这些消息传递给任何已向事件管理器注册自身以通知特定事件的类实例。

为了实现这样一个系统,我们可以引入两个新的基类。一个事件类,它是所有事件消息类型的基类,以及一个EventHandler类,它包含一个名为Execute的单个虚拟方法,该方法将被调用来响应事件。

在最基本层面上,事件类将只包含一个成员,用作特定类型事件的唯一标识符,例如,枚举类型。我们可以通过从事件派生它们并添加任何我们可能想要与消息一起传递的信息的成员来声明我们自己的事件类型。例如,触摸屏事件可能包含触摸发生的屏幕坐标。

任何想要响应特定事件的类都可以从EventHandler类派生,并为虚拟方法提供一个实现。当创建一个类的新的实例时,它会通过传递事件的唯一标识符和指向自身(转换为EventHandler指针)的指针到事件处理器来注册对任何事件的兴趣。

现在,每当发生事件时,我们都会创建一个相关事件类型的实例,用关于该事件的信息填充其成员,并将其传递给事件管理器。事件管理器会将事件的唯一标识符与其已注册实例列表进行比较,然后调用任何已注册实例的EventHandlerExecute方法,这些实例希望被通知刚刚发生的事件类型。事件消息将被传递到实例的Execute方法中,以便其数据可以相应地被处理。

屏幕分辨率和方向

很可能你的游戏会在具有不同屏幕分辨率和宽高比的多个不同设备上运行,这可能会使得创建一个看起来不错的用户界面变得非常繁琐。

因此,提供一种非常灵活的方式来指定 UI 控件的位置和大小是非常重要的。

当指定控件屏幕坐标、宽度和高度时,考虑允许使用精确的像素大小以及包含框架宽度和高度的比率。

当使用比率定义大小时,允许控件适应特定的宽高比也是一个很好的做法。能够确保控件具有特定的宽高比使得保持任何子控件的统一布局变得容易得多,这在绘制最终会显得拉伸的位图图像时尤为重要。当将控件固定到特定的宽高比时,您将希望能够指示是宽度还是高度应该改变以保持控件的正确形状。

能够相对彼此布局控件也是一个非常有用的能力。实现这一目标的一种方法是通过指定一个控件应该通过向另一个控件的位置添加偏移量来获取其位置。

当用户旋转设备且屏幕在纵向和横幅模式之间切换时,这可能会打乱工作流程。对于大多数游戏,我们希望忽略屏幕方向的变化,因为大多数游戏设计为在纵向或横幅模式下进行,而不是两者都进行。

通过将DispFixRot设置添加到应用程序的 ICF 文件中,可以简单地忽略屏幕方向的变化,如下所示:

[S3E]
DispFixRot=Landscape

此设置可以采用以下值:

描述
Free 当用户旋转设备时,屏幕将旋转。如果未使用DispFixRot,则这是默认值。
Portrait 屏幕将始终保持在纵向模式,但可以在设备以任何可能的纵向方向持有时旋转。由于手机可以倒置持有,因此很容易忽略存在两种可能的纵向方向这一事实!
Landscape 屏幕将始终保持在横幅模式,但可以在设备以任何可能的横幅方向持有时旋转。再次提醒,根据您从正常纵向位置旋转手机的方向,存在两种可能的横幅方向。
FixedPortrait 屏幕将固定在设备的默认纵向方向,并且不会旋转。
FixedLandscape 屏幕将固定在设备的默认横幅方向,并且不会旋转。

如果我们选择支持屏幕方向的变化,我们需要一种方法来检测方向是否已更改。我们可以通过设置以下回调函数来实现:

// This is the callback function
int32 OnOrientationChanged(s3eSurfaceOrientation* apOrientation,
void* apUserData)
{
  if (apOrientation->m_OrientationChanged)
  {
    if (apOrientation->m_Width > apOrientation->m_Height)
    {
      // Switch to landscape
    }
    else
    {
      // Switch to portrait
    }
  }
  return 0;
}

// Call this somewhere in our set up code
s3eSurfaceRegister(S3E_SURFACE_SCREENSIZE, (s3eCallback)
   OnOrientationChanged, NULL);

如果你支持你的游戏中的纵向和横向模式,强烈建议你为每个方向定义你控件的具体布局,并在设备旋转时在这些布局之间切换。尝试用一个布局来适应两种方向是可能的,但往往在两种方向上都会产生令人失望的结果,所以通过为每个方向提供自定义布局,最大限度地利用可用的屏幕空间。

添加模板功能

一致性是用户界面设计的重要组成部分。我们期望类似类型的控件看起来相同。如果它们看起来不一样,设计就会开始显得杂乱无章,不够专业。因此,能够提供一种方法来一次性定义我们 UI 的某些方面是非常有用的,模板定义正允许我们做到这一点。

实现模板的一个相对简单的方法是将一个 UI 控件设置复制到另一个控件。我们可以创建一个实际上永远不会显示的控件,但会作为其他控件的模板。在创建新控件时,我们可以从模板复制所有成员设置,然后继续修改设置,以便控件显示我们需要的任何内容。

实现这一点的其中一种方法是在元素类中添加一个虚拟方法,该方法提供了一个指向模板控件的指针。每个类都可以覆盖此方法,根据模板中包含的值设置其成员变量。通过在父类中调用虚拟方法,我们可以从模板复制所有成员变量设置,直到基元素类。

本地化你的项目

随着技术的进步,世界似乎变得越来越小,你的游戏可能会在全球各地的设备上被玩。因此,考虑本地化你的游戏,以便全球玩家都能用他们自己的语言体验你的游戏,是非常值得的。

虽然支持所有已知语言是不切实际的,但现在许多畅销游戏至少提供了对EFIGS语言(英语、法语、意大利语、德语和西班牙语)的支持,你通常还可以将葡萄牙语、俄语、波兰语、日语、韩语以及简体中文和繁体中文添加到列表中。

支持除你自己的母语之外的其他语言是非常值得的,因为玩家更愿意玩他们能阅读和理解的游戏,而不是那些他们无法理解的游戏。

无论你是否决定支持其他语言,按照我即将描述的方式实现你的游戏文本仍然有好处,因为它允许你将所有文本从源代码中移除并集中在一个地方,这使得更改文本变得容易得多。

创建文本电子表格

在您的游戏中本地化文本的第一步是使用像 Microsoft Excel 或 OpenOffice Calc 这样的程序创建一个包含所有游戏文本的电子表格。通过使用电子表格,很容易添加或插入新的文本字符串,并且电子表格的列可以用来为要支持的语言提供字符串的翻译。以下截图显示了这样一个电子表格的示例:

创建文本电子表格

在这个电子表格中,第一列用作文本标识字段。这只是一个字符串,我们可以在源代码和数据文件中使用它来表示特定的文本字符串。

第一行用于指示电子表格中每一列代表的语言。在示例中,我们使用了标准的两位字母 ISO 国家代码来表示支持的语言,即英语(EN)和法语(FR)。

电子表格的其余部分就是我们要在游戏中显示的实际文本。

将文本放入游戏

现在我们将游戏文本放入电子表格中,如何在游戏代码中访问它?答案是处理电子表格,使其格式易于我们加载并在游戏代码中使用。

逗号分隔值文件

一个选择是将文本作为逗号分隔值CSV)文件从我们的电子表格程序中导出。这是一个简单的纯文本格式,将数据库的每一行输出为文件中的单独一行,每个单元格的内容由逗号分隔。

这种方法的麻烦在于它可能会出错。如果您的字符串文本中有一个逗号,可能会对输出造成混乱,因为逗号已经被用来表示一个字符串的结束和下一个字符串的开始。通常通过在每个字符串周围加上引号来解决这个问题,但如果字符串本身需要包含引号,这可能会引起更多问题!

记住,IwGxFont 默认期望以 UTF-8 格式提供文本。如果您支持像日语或韩语这样的语言,这变得非常重要,并且一些电子表格程序不支持以 UTF-8 格式导出 CSV 文件。

使用 Python 脚本处理

从电子表格中提取文本并将其放入我们的游戏中的更好方法是将其处理成简单的数据文件,这样就可以轻松地将其加载到我们的游戏中。为了演示这一点,我们将使用 Python 脚本语言。

Python 可能对代码布局有相当奇怪的方法(您的代码的作用域级别是通过缩进来表示的,而不是使用大括号等符号来表示代码段的开始和结束),但不可否认的是,它在处理这类任务方面非常出色。

您可以从以下 URL 获取 Python 的安装程序:

www.python.org/download/

我们将采用直接访问文本电子表格中的数据的方法。如果我们以 Excel 97 格式(文件扩展名.xls,大多数电子表格程序都支持)保存电子表格,那么有一个名为xlrd的优秀的 Python 库可以在此处下载:

pypi.python.org/pypi/xlrd/

首先安装 Python,然后安装xlrd库。确保 Python 可执行文件可以被轻松找到是一个好主意,可以将 Python 安装目录添加到你的路径环境变量中。检查 Python 目录是否已经在你的路径变量中的一种简单方法是打开一个命令提示符窗口,并输入path来显示当前将被搜索的目录列表。

为了说明使用 Python 和 xlrd 从电子表格文件中访问数据是多么简单,以下脚本将打开一个电子表格文件并输出它包含的所有行和列:

import xlrd

lXLS = xlrd.open_workbook("StringList.xls")
lSheet = lXLS.sheet_by_index(0)
for lRow in range(lSheet.nrows):
  lCells = lSheet.row_values(lRow, 0, lSheet.ncols)
  print lCells

即使你以前从未见过 Python 脚本,这也应该相当容易理解,但这里有一个简要的解释。

import xlrd行与 C/C++中的#include指令等效。它只是声明我们想要使用xlrd库。

接下来,我们通过调用xlrd.open_workbook方法打开电子表格文件,传递我们想要使用的电子表格的文件名。这返回一个由xlrd定义的 Python 类的实例,代表电子表格文件。请注意,Python 是一种弱类型语言,不需要声明变量的类型。

我们在电子表格对象上调用sheet_by_index方法来检索电子表格的第一个工作表。这会产生另一个代表工作表的 Python 对象。

然后,我们进入一个for循环,使lRow变量在0和电子表格中填充的行数之间迭代。在循环中,我们使用工作表对象通过row_values方法一次访问整个行的电子表格单元格。

Python 有一个内置的列表类型,这就是我们用来一次性访问行上所有单元格的原因。lCells变量将包含一个列表,其元素是行中的每个单元格。

最后,我们使用 Python 的print命令将整个列表显示到标准输出。在 Python 中,你可以使用print来显示几乎任何类型,包括列表,并以人类可读的形式显示。

伴随本章的 UI 示例项目包括一个 Python 脚本,该脚本将接受电子表格作为输入,并将其转换为电子表格中包含的每种语言的简单数据文件。

数据文件列出了文件中的字符串数量,接着是文本标识符字段(电子表格的第一列)生成的哈希值以及字符串本身。编写 C++代码将此文件加载到内存中相当简单。

注意

在这里使用哈希函数意味着两个字符串可能最终得到相同的哈希值,导致字符串表中发生冲突,这意味着可能会返回错误的字符串。在实践中,一个好的哈希函数意味着这种情况几乎不会发生,但如果开始返回错误的字符串,这可能是原因之一。纠正这种问题的最简单方法就是只是重命名冲突中的一个文本标识符!

要访问特定的文本字符串,我们在代码中使用标识符字段。从标识符字段生成一个哈希值,然后在字符串数据列表中搜索该哈希值。如果找到匹配项,则返回相应的文本字符串;否则,可以引发断言并返回默认的文本字符串。默认文本可以是“缺失字符串!”这样的内容,这使得追踪诸如在代码中标识符字段错误或字符串在文本数据文件中应该存在却不存在等问题变得更容易。

选择运行时使用的正确语言

现在我们有能力为我们的游戏提供多种语言的文本字符串,但我们如何决定实际使用哪种语言呢?一种方法当然是在我们游戏的启动期间实现一个语言选择屏幕,然后根据用户的输入加载相关的字符串表。然而,我们还有更好的方法可用。

s3eDevice API 允许我们找出我们正在运行的设备上当前使用的语言。只需在游戏代码的启动部分插入以下代码行:

int32 lLanguage = s3eDeviceGetInt(S3E_DEVICE_LANGUAGE);

返回值将是s3eDeviceLanguage枚举的一个成员,例如S3E_DEVICE_LANGUAGE_ENGLISHS3E_DEVICE_LANGUAGE_GERMAN。所有可能的语言代码的完整列表可以在s3eDevice.h中找到。

通过这个调用确定的语种类型,我们可以然后加载正确的字符串表,并且用户将神奇地看到他们自己的语言版本的游戏,当然前提是你已经支持它!

示例代码

与本章相关的有三个示例项目,将在以下各节中描述。

字体项目

第一个示例项目演示了 IwGxFont API 的使用,可以在下面的屏幕截图中看到。此示例演示了如何在项目中使用多个字体,准备打印文本,以及调整字体的大小:

字体项目

UI 项目

UI 示例实现了一个遵循本章前面讨论的如何实现 UI 代码的用户界面库。它还提供了一个完整的本地化库,包括一个可以将 XLS 电子表格转换为单独的语言数据文件的 Python 脚本。脚本还为每种语言生成一个文件,详细说明所有字符串使用的字符。这在生成用于显示文本的字体资源时非常有用。

UI 和本地化库已作为 Marmalade 子项目(称为 GUI 和 Localise)实现,这使得它们在其他项目中重用变得非常容易。如果你觉得它们有用,请随意在你自己的项目中使用它们。

文本字符串包含在 data/text 目录中的一个 XLS 文件中。已包含英语和法语字符串,尽管对于任何法语读者来说,如果这些字符串不是 100% 正确,我们表示歉意,因为它们是使用在线翻译引擎生成的。

在 Windows 模拟器中,你可以看到使用提供的两种语言文件运行的程序。程序运行时,转到配置 | 设备…。在出现的对话框中,有一个标记为报告的设备语言的下拉框。在此列表中选择英语法语,然后点击确定按钮。退出程序并再次运行,将使用所选语言。

此示例动作的截图如下:

UI 项目

滑雪项目

最后,我们来到了我们不断发展的滑雪!游戏,现在由于为 UI 示例创建的 GUI 和 Localise 模块,它拥有一个简单但看起来更好的用户界面。以下截图显示了新的主菜单屏幕:

滑雪项目

摘要

在本章中,我们看到了如何使用 IwGxFont API 将任何样式和大小的字体添加到我们的项目中。我们还学习了如何使用 Marmalade Studio 字体构建器将 TrueType 字体转换为位图字体,这些字体可以被 IwGxFont 加载。

我们还讨论了如何实现我们自己的用户界面库,以及我们如何通过添加对多种语言的支持来本地化我们的游戏。

在下一章中,我们将探讨如何通过添加音效和音乐来停止我们的游戏成为无声事件。我们还将简要地看看如何添加视频文件播放。

第七章:添加声音和视频

你的游戏可能看起来很惊艳,但如果它是静音的,观众可能会觉得体验很无聊。幸运的是,Marmalade 通过支持声音和视频播放来解决这个问题。在本章中,我们将学习以下内容:

  • 播放记录在 MP3 等格式的音频文件

  • 使用声音样本添加多个同时音效

  • 播放全动作视频剪辑

Marmalade 的多媒体支持

现代智能手机和平板设备现在能够播放高质量的音乐和视频,因此 Marmalade 提供让我们利用这些能力的方法是有意义的。

Marmalade 提供了三个不同的 API 层,适用于多媒体支持。这些是 s3eSound、s3eAudio 和 s3eVideo。不出所料,后者与视频文件的播放有关,但您可能想知道为什么提供了两个与声音相关的 API。

s3eSound 和 s3eAudio 之间的区别在于,前者通常用于音效,而后者通常用于音乐。s3eSound API 允许我们同时播放多个不同的音效,但默认情况下只提供对 16 位单声道 PCM 声音样本的支持。另一方面,s3eAudio API 允许我们播放压缩格式,如 MP3,但(在大多数设备上)我们只能播放单个音频轨道。

好消息是,大多数现代设备允许我们同时使用 s3eSound 和 s3eAudio,从而让我们享受到两者的最佳效果。

在接下来的章节中,我们将探讨如何使用这三个 API,并也会看看另一个名为SoundEngine的模块,它使得使用 s3eSound API 变得更容易。

s3eAudio API

让我们从最快捷、最简单的方法开始,让我们的游戏不再只是沉默的强者。

s3eAudio API 允许我们播放压缩的音乐格式,如 MP3 和 AAC。某些设备也可能允许我们播放其他格式,如 MIDI 文件。Marmalade 使用特定设备可能内置的任何音频编解码器,而不是解码音频本身,因此请确保你选择的音频格式支持你希望针对的所有设备。

注意

由于其普遍性,建议您使用 MP3 作为首选格式。很少有设备(如果有的话)不能播放 MP3 文件,而且该格式本身提供了广泛的比特率,这样您可以在音频质量和文件大小之间进行权衡。

现在,让我们看看如何播放音频轨道,以及 s3eAudio API 为我们提供了哪些其他功能。为了使用 s3eAudio,我们不需要在 MKB 文件中添加任何内容,因为它是 Marmalade 的低级 API 之一,始终可用。我们只需要在需要访问 s3eAudio 函数的任何源文件中包含头文件s3eAudio.h

开始音频播放

有两种方法可以开始播放音频轨道。第一种允许我们指定要播放的音频轨道的文件名和轨道重复的次数,看起来像这样:

s3eAudioPlay("music.mp3", aRepeatCount);

文件名只是一个标准的 C 语言,以 null 结尾的字符串,当从 Windows 运行时相对于 data目录,或者相对于设备上的应用程序安装目录。指定重复次数的数字将使音频轨道播放指定次数,而将其设置为零将使轨道连续循环。

另一种方法是按照以下方式从内存区域播放音频轨道:

s3eAudioPlayFromBuffer(apBuffer, aBufferLength, aRepeatCount);

参数 apBuffer aBufferLength提供了音频轨道所在的内存位置和音频数据字节数的长度。重复次数的指定方式与 s3eAudioPlay相同。

在大多数情况下,我们会发现第一种方法就足够好了,因为它易于使用,不需要我们分配内存块并填充数据。如果你已经预加载了音频数据,可能会发现缓冲区方法提供了稍微快一点的初始播放,但在大多数最新设备上,这种差异是可以忽略不计的。

如果你在一个音频轨道正在播放时调用这些函数中的任何一个,该轨道将被停止,并开始播放新轨道。

暂停、恢复和停止播放

一旦音频轨道开始播放,我们可以通过调用 s3eAudioPause函数来暂停播放。可以通过调用 s3eAudioResume从暂停点重新开始播放音频。最后,要完全停止播放,只需调用 s3eAudioStop

这三个函数都不需要参数,在没有错误发生的情况下将返回 S3E_RESULT_SUCCESS。如果在没有意义的情况下调用这些函数,例如在没有音频播放时调用 s3eAudioPause,则会引发错误。

改变音量

与 Marmalade 中的大多数低级 API 一样,s3eAudio 具有一对函数 s3eAudioGetInt s3eAudioSetInt,用于更改与该 API 相关的属性。在 s3eAudio 中,我们使用这些函数之一来更改音频播放的音量。

要设置播放音量,我们可以进行以下调用:

s3eAudioSetInt(S3E_AUDIO_VOLUME, S3E_AUDIO_MAX_VOLUME / 2);

在上述示例中,我们将音量设置为 S3E_AUDIO_MAX_VOLUME的一半,这是允许的最大音量。

要确定当前音量,我们使用以下代码:

int32 lVolume = s3eAudioGetInt(S3E_AUDIO_VOLUME);

我们还可以通过传入值 S3E_AUDIO_VOLUME_DEFAULT来请求音频的默认音量。这是播放音频的默认音量级别,由 Marmalade SDK 选择,以便在所有设备上提供相当一致的音量级别。

其他音频查询

s3eAudioGetInt函数允许我们进行关于音频播放的几个其他查询。以下表格显示了可以指定的属性:

属性 描述
S3E_AUDIO_STATUS 返回当前音频状态——S3E_AUDIO_STOPPEDS3E_AUDIO_PLAYINGS3E_AUDIO_PAUSEDS3E_AUDIO_FAILED之一。
S3E_AUDIO_POSITION 返回音频轨道中的当前位置,单位为毫秒,如果没有轨道正在播放,则返回0。请注意,并非所有平台都支持此功能。
S3E_AUDIO_CHANNEL 返回当前选定的音频通道。此属性也可以在s3eAudioSetInt中使用,以选择将应用于未来音频命令的音频通道。有关音频通道的更多信息,请参阅以下属性。
S3E_AUDIO_NUM_CHANNELS 返回可用的音频通道数。在大多数平台上,这将返回1,因为大多数设备在任何时候只允许播放一个音频轨道。一些设备提供多个通道,这意味着可以同时播放多个音频轨道。
S3E_AUDIO_MUTES_S3ESOUND 如果硬件无法同时通过 s3eAudio 和 s3eSound 输出声音,则返回1。在这种情况下,播放音频轨道将继续 s3eSound 处理,但不会实际产生任何输出。
S3E_AUDIO_DURATION 返回当前播放的轨道长度,单位为毫秒。
S3E_AUDIO_PLAYBACK_FROM_HTTP_AVAILABLE 如果硬件能够通过从 URL 流式传输播放音频轨道,则返回1

轨道结束通知

我们可以使用两种方法来确定音频轨道何时完成。一种方法是使用轮询方法,另一种方法是使用回调。

要轮询音频轨道是否已完成,我们可以执行以下操作:

if (s3eAudioIsPlaying() == S3E_FALSE)
{
  // Audio is not playing!
}

此函数返回S3E_TRUE,如果音频正在播放,或者返回S3E_FALSE,如果它已停止或暂停。实际上,这个函数只是调用s3eAudioGetInt并带有属性S3E_AUDIO_STATUS的一个快捷方式。

回调方法也非常简单易用,如下面的代码片段所示:

int32 AudioFinished(s3eAudioCallbackData* apAudioData,
void* apUserData)
{
  // apAudioData->m_ChannelID identifies the audio channel that
  // has completed.
  // s3eCallback functions must return a value, but in case of
  // audio callback the value returned does not matter.
  return 0;
}

// Use the following line to set up the audio callback
s3eAudioRegister(S3E_AUDIO_STOP, (s3eCallback) AudioFinished, NULL);

// And this line to remove the callback function
s3eAudioUnRegister(S3E_AUDIO_STOP, (s3eCallback) AudioFinished);

当音频轨道完成时,回调函数将被调用,并将通过apUserData参数传递的用户数据指针传递。如果我们要求音频轨道循环播放,除非是最后一次重复,否则不会调用该函数。如果由于错误(如损坏的轨道)而停止音频,该函数也会被调用。我们可以通过调用返回枚举类型s3eAudioError的错误代码的s3eAudioGetError函数来确定完成是否由错误引起。错误代码的完整列表可以在s3eAudio.h中找到。

是否使用轮询或基于回调的方法取决于你的应用程序,实际上在游戏中我们通常并不特别关心音频轨道何时结束,因为我们通常只是希望同一轨道无限循环,直到需要新的音频片段。如果你只是在启动画面期间等待铃声结束,轮询方法可能就足够了,但如果你想要将几个轨道依次连接起来,回调方法可能带来一个干净的解决方案。

s3eSound API

如果你想要在你的游戏中添加点音效,比如激光束和爆炸声,你需要使用 s3eSound API。这个 API 允许同时以不同的音量和音调播放多个声音样本,通过将它们混合成一个单一的输出。

要使用 s3eSound API,只需在你的源代码中包含文件s3eSound.h

API 期望所有音效都作为未压缩的 16 位有符号 PCM 提供。API 不支持如 WAV 这样的文件格式,因此你必须编写自己的代码来加载和从这些文件中提取样本数据。

随着你阅读这一部分,你可能会开始认为要播放一些音效有很多事情要做。虽然这看起来可能是这样,但 s3eSound 实际上是一个非常底层的 API,提供了足够的灵活性,允许你编写自己的复杂音效程序。

在本章的后面部分,我们将介绍 Marmalade 提供的SoundEngine模块,它为 s3eSound API 提供了一个包装器。SoundEngine模块为我们处理了使用 s3eSound API 的大部分繁重工作,并且还包括直接从 GROUP 文件中加载 WAV 文件的能力。

开始播放声音

为了使用 s3eSound 播放声音样本,我们首先必须分配一个空闲的声音通道。s3eSound API 提供了一定数量的通道(我们稍后会看到如何确定确切的数量),允许我们指定声音样本、音量和播放速率。然后,所有当前活动通道的声音数据在 s3eSound 的内部工作中被混合成一个单一的波形,这就是通过设备的音频硬件播放的内容。为了分配一个空闲通道,我们进行以下函数调用:

int32 lChannel = s3eSoundGetFreeChannel();

这将返回一个空闲通道的 ID 号,如果没有可用通道,则返回-1。大多数情况下,不太可能没有空闲通道,但如果我们正在播放很多音效,我们可能想要考虑给每个音效标记一个优先级值,并维护一个当前活动音效的列表。当我们用完通道时,我们可以检查音效列表,并回收被优先级最低的音效使用的通道,当然前提是它比我们希望启动的音效优先级低!

假设有一个可用的通道,我们必须设置样本数据的播放速率,这可以通过以下方式完成:

s3eSoundChannelSetInt(lChannel, S3E_CHANNEL_RATE, lFrequency);

第一个参数是我们刚刚分配的声音通道 ID。第二个参数表示我们想要设置该通道的播放速率,第三个参数是实际期望的播放速率,单位为赫兹(Hz)。可以设置的最大频率由定义S3E_SOUND_MAX_FREQ指定。

我们还应该设置我们想要播放的声音的音量,这同样是通过使用s3eSoundChannelSetInt函数来完成的:

s3eSoundChannelSetInt(lChannel, S3E_CHANNEL_VOLUME, lVolume);

lVolume参数的有效值从0到定义S3E_SOUND_MAX_VOLUME

注意

一旦声音开始播放,就可以随时更改音量和播放速率。这使得可以实现音量淡入淡出或音调转换等效果。

现在我们可以开始播放我们的声音样本。我们通过以下调用来完成:

s3eSoundChannelPlay(lChannel, lSampleData, lNumSamples, lRepeatCount,
lLoopIndex);

毫不奇怪,我们首先传入我们正在使用的通道 ID,然后是lSampleData参数中内存地址,可以在其中找到 16 位 PCM 样本数据。lNumSamples参数是我们波形中的实际声音样本数量(不是字节数),而lRepeatCount表示我们希望声音重复的次数。值为0将使声音永远播放。最后,lLoopIndex参数允许我们指定如果声音重复,则从哪个样本开始。这使得可以使用只需要重复部分样本数据的声音。

暂停、恢复和停止播放

一旦声音通道开始播放声音样本,我们可能想要暂时暂停其播放或完全停止它。要暂停声音通道,我们使用s3eSoundChannelPause函数,并且我们可以从暂停位置重新开始播放,使用s3eSoundChannelResume。要完全停止声音通道,我们调用s3eSoundChannelStop。每个这些函数都接受一个单一参数,即我们想要影响的声音通道 ID。

要确定特定声音通道的当前播放状态,我们可以使用s3eSoundChannelGetInt函数,如下所示:

if (s3eSoundChannelGetInt(lChannel, S3E_CHANNEL_STATUS) == 1)
{
  // Sound channel is currently playing
}

if (s3eSoundChannelGetInt(lChannel, S3E_CHANNEL_PAUSED) == 1)
{
  // Sound channel is currently active, but paused
}

注意,此函数也可以与S3E_CHANNEL_RATES3E_CHANNEL_VOLUME属性一起使用,以发现特定通道的当前采样率和音量。

最后,还可以使用s3eSoundPauseAllChannelss3eSoundResumeAllChannelss3eSoundStopAllChannels函数一次性影响所有当前活动的声音通道。这些函数不接受任何输入,对于处理进入和退出暂停模式,或从游戏的一个部分切换到另一个部分(例如,退出标题屏幕并进入主游戏)等情况非常有用。

全局声音设置

除了能够按通道读写设置外,我们还可以设置影响全局声音支持的设置。为此,我们使用s3eSoundSetInts3eSoundGetInt函数,如下所示:

// To read a global sound setting
int32 lValue = s3eSoundGetInt(lProperty);

// To change a global sound setting
s3eSoundSetInt(lProperty, lValue);

这里是一些 lProperty 参数的更有用的值:

属性 描述
S3E_SOUND_VOLUME 可以用来读取或写入当前主音量。这将适当地缩放每个独立通道的音量。最大值由定义 S3E_SOUND_MAX_VOLUME 决定。
S3E_SOUND_DEFAULT_FREQ 这是启动声音通道播放时将使用的默认频率。如果我们的所有声音波形具有相同的采样率,则可以写入此属性一次,而无需在播放每个单独的声音时显式设置采样率。
S3E_SOUND_NUM_CHANNELS 一个只读值,指示可以同时播放的最大声音数量。
S3E_SOUND_USED_CHANNELS 一个只读值,显示当前正在使用的声音通道。这作为一个位掩码返回,最低有效位与声音通道 0 相关。此值可以用来确定一个可用的声音通道,但为了未来的兼容性,建议使用 s3eSoundGetFreeChannel 来执行此操作。
S3E_SOUND_AVAILABLE 一个只读值,如果设备上可用 s3eSound,则返回 1
S3E_SOUND_VOLUME_DEFAULT 一个只读值,用作全局音量的默认值。它可能因设备而异,旨在允许所有设备上的声音输出具有相似的音量。

Marmalade 文档中描述了其他值,但在这里我们不会介绍它们,因为它们用于诸如自定义声音流生成等目的,这些目的超出了本书的范围。

声音通知

我们已经看到了如何使用轮询方法来检测声音通道是否正在播放,但有时知道声音样本何时播放完毕也是有用的,例如,这样我们就可以立即开始播放新的声音效果。

s3eSound API 允许我们在每个通道上设置几个不同的回调函数,我们使用 s3eSoundChannelRegisters3eSoundChannelUnRegister 函数来启用和禁用它们,如下所示:

// To set up a sound channel callback
s3eSoundChannelRegister(lChannel, lCallbackType, (s3eCallback)
              CallbackFunction, lpUserData);

// To disable a sound channel callback
s3eSoundChannelUnRegister(lChannel, lCallbackType);

与所有其他 Marmalade 回调一样,我们通过传递函数本身的指针来指定回调函数的代码,我们还可以注册一个用户数据块,当它被触发时,此数据块将被传递到该函数中。有四种不同的回调类型,称为 S3E_CHANNEL_END_SAMPLES3E_CHANNEL_STOP_AUDIOS3E_CHANNEL_GEN_AUDIOS3E_CHANNEL_GEN_AUDIO_STEREO。在这里,我们只会查看前两种,因为后两种与生成自定义音频流有关,超出了本书的范围。有关如何使用这些回调类型的示例,请查看 SoundEngine 模块的源代码,我们将在下一节中介绍。

首先,让我们看看S3E_CHANNEL_END_SAMPLE回调,它允许我们循环声音并将不同的声音作为一个序列连接起来。注册的回调函数将其第一个参数传递给一个指向s3eSoundEndSampleInfo结构的指针。该结构通过其m_Channel成员指示哪个声音通道已结束。

如果我们想在通道上开始播放全新的声音,我们可以将s3eSoundEndSampleInfo结构的m_NewData成员设置为新的样本数据的起始地址,将m_NumSamples成员设置为新波形中的样本数量。

该结构还包含一个名为m_RepsRemaining的成员,它允许我们更改我们希望在声音通道上重复的样本数据的次数。请注意,尽管如此,每次达到样本数据的末尾时,此回调仍然会被触发。

如果我们希望通道继续播放样本数据,无论是原始数据还是使用s3eSoundEndSampleInfo结构的m_NewDatam_NumSamples成员指定的新样本,我们必须从回调函数返回一个非零值。如果返回零,则声音通道将停止播放。

以下代码示例将之前描述的功能付诸实践:

// Simple structure used to indicate the next sound sample to play
typedef struct
{
  void* mSampleData;
  uint32 mSampleCount;
} NewSoundData;

// Sample callback function that will start a new sound effect
// playing if one has been specified when registering the
// callback function
int32 SoundEndCallback(s3eSoundEndSampleInfo* apInfo,
   NewSoundData* apSound)
{
  if (apSound)
  {
     apInfo->m_NewData = apSound->mSampleData;
     apInfo->m_NumSamples = apSound->mSampleCount;
     apInfo->m_RepsRemaining = 1;
  }
  return apInfo->m_RepsRemaining;
}

// Register the callback function to play a new sound when
// current sound completes
s3eSoundChannelRegister(lChannel, S3E_CHANNEL_END_SAMPLE,
   (s3eCallback) SoundEndCallback,
   &lNewSoundDataInstance);

我们将要考虑的第二种回调类型是S3E_CHANNEL_STOP_AUDIO。当声音通道完全播放完声音时(例如,如果我们设置了S3E_CHANNEL_END_SAMPLE回调并从中返回零以结束所有播放)将发生此回调。它传递一个指向s3eSoundEndSampleInfo结构的指针,但唯一有效的字段是m_Channel成员。

SoundEngine模块

如本章前面的部分所示,使用 s3eSound 的基本方法实际上相当简单。作为开发者,我们必须处理的主要问题是 s3eSound 只能支持原始未压缩的 16 位 PCM 样本,这意味着将声音数据放入内存以便播放是我们的责任。

存储声音样本最常见的文件格式之一是 WAV 文件,所以如果我们能够使用这种格式来存储我们的声音效果,那岂不是很好?如果我们能够使用与纹理和 3D 模型相同的资源管理器代码将这些文件加载到内存中,岂不是也很好?

我们所祈求的答案是SoundEngine模块,这是一个位于 s3eSound 之上的层,它允许我们通过资源管理器轻松地加载和访问声音效果。

SoundEngine模块不仅仅如此。它还封装了我们在本章中学到的 s3eSound 调用,并允许我们支持一种可以存储在 WAV 文件中的进一步的声音格式——即压缩的 IMA ADPCM 类型。鉴于声音样本数据可能相当大,这种格式特别有用;因此,这种格式可以帮助我们在略微降低音质的情况下回收一些内存空间。

以下部分简要介绍了如何使用此模块,但为了获取完整细节,你应该参考源代码和头文件,以查看 SoundEngine 提供的所有功能。本章附带的音频示例项目也使用了此模块,因此请查看该部分以了解更多信息。

将 SoundEngine 模块添加到项目中

SoundEngine 模块实际上随 Marmalade SDK 一起提供,但它尴尬地位于 examples 目录中。解决这个问题的最简单方法是将整个 SoundEngine 目录复制到你的项目所在的目录,然后通过将 SoundEngine 添加到你的 MKB 文件子项目中来引用它。这与我们在上一章的示例代码中引入的 GUI 和 Localise 模块所采用的方法相同。

注意

SoundEngine 模块位于 examples 文件夹中,这意味着它实际上并不被认为是 Marmalade SDK 的主要部分。实际上,SoundEngine 代码突然从 SDK 中消失的可能性非常低,因为 s3eSound API 很可能不会发生剧烈变化;所以你不需要担心直接在自己的项目中使用它。如果你更喜欢编写自己的代码,SoundEngine 至少提供了一个很好的示例,说明了如何使用 s3eSound API。

在将模块添加到我们的项目中后,我们可以在代码中包含 IwSound.h 文件以使用它。需要调用 IwSoundInit 来设置一切,并在程序结束时调用 IwSoundTerminate 来清理。

我们还必须添加一个自定义资源处理程序,以便资源管理器能够加载 WAV 文件。以下代码片段将完成这项工作:

IwGetResManager()->AddHandler(new CIwResHandlerWAV);

最后,有一个管理类负责处理所有与声音相关的事件,我们必须确保在主游戏循环的某个地方调用这个类的 Update 方法。我们通过以下代码行来实现:

IwGetSoundManager()->Update();

加载和访问声音资源

要加载一个 WAV 文件,我们只需在 GROUP 文件中添加对其文件名的引用即可,尽管我们还需要做更多的工作才能播放声音。我们需要做的是声明 CIwSoundSpec 类的一个实例。

这个类允许我们通过名称引用特定的声音样本,并让我们设置音量和音调来播放声音。我们还可以指定是否希望声音循环(请注意,SoundEngine 目前没有提供指定循环次数的方法;我们只能表示连续循环)。以下是一个示例定义:

CIwSoundSpec
{
  name gun1
  data gun_shot1

  // Play at the default pitch for the sample
  pitch 1.0

  // Play at half volume
  vol 0.5

  // Do we want this sound to loop?
  looping false
}

pitchvol(音量)参数被指定为分数比例,其中 1.0 表示声音的默认音调或音量级别。我们还可以为这两个参数指定一个范围,以便在开始声音时选择一个随机值。为音调指定一个范围可以非常有用,可以在不添加大量略有不同声音样本的情况下,为你的游戏中的音效添加一些变化。

下面的示例显示了如何指定音量和音调的范围:

CIwSoundSpec
{
  name gun2
  data gun_shot2

  // Choose a random pitch when playing this sound
  pitchMin 0.9
  pitchMax 1.1

  // Choose a random volume when playing this sound
  volMin 0.9
  volMax 1.1

  // Do we want this sound to loop?
  looping false
}

另一个我们可以访问的有用类是 CIwSoundGroup。这个类允许我们将多个不同的音效组合在一起,并且可以同时暂停、恢复、停止,或者改变正在播放的任何音效的音量或音调。请注意,音效组只允许指定一个音量或音调值,而不是一个随机范围:

CIwSoundGroup
{
   name guns

   // Reduce volume of all gun sounds by a half
   vol 0.5

   // Include the gun1 sound in this group
   addSpec gun1
}

可以使用 addSpec 关键字将声音添加到组中,或者你也可以在定义时使用 group 关键字后跟组名,将 CIwSoundSpec 添加到组中。我们可以使用这两种方法中的任何一种,但组或声音必须在引用它之前被声明。

要访问声音规范或组,我们只需加载 GROUP 文件,并使用资源管理器以正常方式检索它们。以下是一个示例:

IwGetResManager()->LoadGroup("sounds.group");
CIwSoundSpec* lpGunSpec = static_cast<CIwSoundSpec*>(
  IwGetResManager()->GetResNamed("gun1", "CIwSoundSpec"));
CIwSoundGroup* lpGunsGroup = static_cast<CIwSoundGroup*>(
  IwGetResManager()->GetResNamed("guns", "CIwSoundGroup"));

播放、停止和改变声音参数

一旦我们获得了指向 CIwSoundSpec 的指针,我们就可以通过调用 Play 方法开始播放它,这将执行所有幕后操作,如分配一个空闲通道和设置音量和播放速度。Play 方法可以传递一个可选参数,它是一个 CIwSoundParams 类的实例,允许在开始声音时修改音量和音调。

Play 方法返回一个指向 CIwSoundInst 类的指针,该类提供了修改单个声音实例的音量或音调的方法,并且还提供了名为 PauseResumeStop 的方法,这些方法应该很容易理解!如果没有可用的空闲音效通道,Play 方法将返回 NULL

如果我们有一个指向 CIwSoundGroup 的指针,我们可以影响其中所有当前正在播放的声音实例。同样,有 PauseResumeStop 方法,它们会执行你预期的操作,此外还有 SetVolSetPitch 方法,这些方法将调整声音的当前音量和音调。这些方法使用值 IW_GEOM_ONE(4096)来表示一个比例为一的缩放。

s3eVideo API

我们将通过快速浏览 Marmalade 的多媒体支持来结束对 Marmalade 的多媒体支持的探讨,我们将简要了解使用 s3eVideo API 播放视频剪辑的支持。为了使用它提供的功能,我们只需要将 s3eVideo.h 文件包含到我们的源代码中。

在我们开始之前,使用游戏中的视频片段时需要考虑两件事。首先,虽然可以指定视频片段在屏幕上的显示位置,但它总是会覆盖在所有其他图形之上。第二个问题是,由于许多移动设备硬件的限制,s3eVideo API 不能与 s3eAudio 和 s3eSound API 同时使用。在 s3eAudio 的情况下,任何当前正在播放的曲目将被停止(这也适用于相反的情况——开始音频曲目将停止当前正在播放的视频片段)。当视频片段播放时,s3eSound API 将继续处理其事件,但其声音输出将被静音,直到视频片段播放完毕。对于大多数游戏,我们可能会决定在开始视频片段之前明确停止所有 s3eSound 播放,尤其是如果我们正在执行任何高级操作,比如使用回调系统将声音样本合并在一起。

开始视频播放

s3eVideo API 的工作方式与 s3eAudio API 类似。要开始播放视频片段,我们使用 s3eVideoPlay 函数,指定视频片段的文件名、我们希望它循环的次数、屏幕位置以及我们想要显示的大小,如下所示:

s3eVideoPlay(lFileName, lRepeatCount, lX, lY, lWidth, lHeight);

视频片段将自动调整大小以适应矩形,但不会尝试保持正确的宽高比。

在可能的情况下,通常最好尝试使视频片段的分辨率与您想要显示它们的矩形区域相同。这将避免不必要的图像拉伸(这可能会看起来相当难看!)并可能导致性能略有提升,尽管在大多数现代设备上,缩放将在硬件中完成,并且不会有明显的差异。

视频文件本身的实际大小也值得考虑,因为我们通常希望最小化最终安装包的大小。最终,我们需要进行一些尝试和错误,直到我们得到一个在可接受的质量、性能和文件大小方面都符合要求的结果。

确定视频编解码器支持

s3eVideo API 利用设备的内置视频解码功能,因此并非所有视频格式都能在所有设备上播放。为了确定是否支持特定的编解码器,有一个名为 s3eVideoIsCodecSupported 的函数,它接受来自 s3eVideoCodec 枚举的值。查看 s3eVideo.h 文件或 Marmalade 文档以获取可能的值的完整列表。

暂停、恢复和停止视频播放

当涉及到控制视频播放时,与 s3eAudio API 的相似之处显而易见。函数 s3eVideoPauses3eVideoResumes3eVideoStop 都不接受任何参数,并分别用于暂停、恢复和结束视频片段播放。

视频结束通知

我们再次有选择轮询或回调来检测视频播放结束。让我们从涉及调用函数 s3eVideoIsPlaying 的轮询方法开始,该函数如果视频正在播放,则返回 S3E_TRUE,如果视频已暂停或停止,则返回 S3E_FALSE。实际上非常简单!

如果我们想使用回调方法,以下代码片段说明了应该做什么:

int32 VideoFinished(void* apSystemData, void* apUserData)
{
  // apSystemData will always be NULL as there is no data associated
  // with this callback.
  // Return value is unimportant.
  return 0;
}

// To set up the callback function
s3eVideoRegister(S3E_VIDEO_STOP, (s3eCallback) VideoFinished, NULL);

// And to cancel it again...
s3eVideoUnRegister(S3E_VIDEO_STOP, (s3eCallback) VideoFinished);

当视频播放停止时,回调将被触发,无论是我们明确调用 s3eVideoStop,播放过程中发生错误(如损坏的视频文件)发生,还是使用 s3eAudioPlay 开始音频轨道。请注意,如果我们正在循环视频剪辑,则回调在视频剪辑重复之间不会被触发。

对于大多数游戏,视频剪辑可能仅在开场序列或教程中使用,因为在使用游戏本身中的视频可能并不实用。考虑到这一点,检测视频剪辑何时完成的轮询方法通常足够。

其他视频查询

s3eVideo API,就像 s3eSound 和 s3eAudio API 一样,也有一对用于读取和写入全局视频参数的函数。它们被称为 s3eVideoGetInts3eVideoSetInt。它们如下所示:

int32 lValue = s3eVideoGetInt(lProperty);
s3eVideoSetInt(lProperty, lValue);

以下表格显示了可用于 lProperty 参数的值:

属性 描述
S3E_VIDEO_VOLUME 此属性用于查找与视频剪辑相关的声音的当前音量级别,也可以设置新的音量。最大音量级别由 S3E_VIDEO_MAX_VOLUME 的值定义。
S3E_VIDEO_DEFAULT_VOLUME 这是一个只读属性,显示用于在视频剪辑中播放声音的默认音量。其值旨在为所有设备类型提供类似的音量水平。
S3E_VIDEO_STATUS 这是一个只读参数,显示视频播放的当前状态。它将返回以下值之一:S3E_VIDEO_STOPPEDS3E_VIDEO_PLAYINGS3E_VIDEO_PAUSEDS3E_VIDEO_FAILED
S3E_VIDEO_POSITION 此属性返回视频当前播放位置(以毫秒为单位),如果没有视频正在播放,则返回 0。此参数不能写入,因此无法跳转到视频剪辑的特定点。

示例代码

本章与三个示例项目相关联,它们将在以下部分中描述。这些项目中使用的声音、音频和视频剪辑是从几个提供大量免费库存媒体的优秀网站上获取的!以下提供了这些网站的链接:

www.royalty-free-music-room.com

www.partnersinrhyme.com

声音项目

此项目演示了使用 s3eAudio API 和 SoundEngine 模块(它反过来又使用 s3eSound)。

运行示例时,你会看到三个可点击的按钮,这些按钮是使用上一章中引入的 GUI 模块实现的。第一个按钮使用 s3eAudio 切换 MP3 轨道的开启和关闭,而其他两个按钮则使用SoundEngine播放一些音效。

视频项目

这是一个简单的例子,展示了如何使用 s3eVideo API 来启动和停止视频片段。屏幕底部的按钮可以启动和停止一个视频片段,该片段会连续循环播放。

滑雪项目

最后,我们再次回到滑雪项目,它通过添加一些音乐和音效得到了增强,这并不会让人感到惊讶。

现在主菜单在等待玩家按按钮时会播放 MP3 音频轨道。按下按钮时,会播放一个确认音效。

在游戏本身中,增加了几个声音。当滑雪者移动时,会通过循环样本产生呼啸声,随着玩家转向,这个样本的音调会降低,使声音听起来更加动态。

新增的其他声音包括当玩家通过一个门时播放的庆祝声音,当玩家与障碍物碰撞时发出的痛苦呼喊声,以及当玩家与旗帜杆碰撞并使其晃动时播放的弹簧声。

摘要

随着本章的结束,我们对 Marmalade 的多媒体支持的研究现在使我们能够播放音效、音乐轨道,以及播放视频片段。

几乎没有游戏不包含某种形式的声音或音乐,添加一些音效可以使你的游戏产生巨大的变化。虽然并非所有游戏都需要使用视频,但知道我们随时可以使用它,这感觉很好。

在下一章中,我们将探讨 Marmalade 如何帮助我们尽可能多地针对各种设备,从入门级手机到高端手机。

第八章。支持广泛的设备

真的很棒,Marmalade SDK 允许我们针对如此多的不同设备和平台。然而,为了完全优化您的应用程序以适应所有这些不同的设备类型,需要一定的关注和意识。

在本章中,我们将涵盖以下主题:

  • 在尝试支持广泛不同设备时需要警惕的一些内容的概述

  • 对我们在本书第一章中遇到的 ICF 文件系统的更深入探讨

  • 使用 Marmalade 的内置系统允许使用多个不同的数据集,并以不同的方式处理这些数据集(例如,允许指定设备上使用的最终纹理格式)

  • 配置部署系统以创建不同类型的构建

  • 使用 Derbh 归档器减小安装包中资源的大小

适应广泛的设备类型

移动操作系统,如 iOS 或 Android,能够在广泛不同的设备上运行。在我们讨论 Marmalade 如何使我们轻松针对多种设备类型之前,我们首先强调在开发游戏时应注意的一些事项,以确保它在尽可能多的不同设备上看起来和运行得最好。

Marmalade 还附带了一份白皮书,涵盖了在开发旨在在多个设备规范上运行的游戏时需要注意的一些事项。您可以在 Marmalade 文档的白皮书 | 设备无关代码部分找到它。

处理不同的屏幕分辨率

不同设备之间最明显的一个差异可能是屏幕分辨率。以 iOS 为例,你可能需要支持从低端 320 x 480 到两个不同的 iPhone Retina 屏幕分辨率(640 x 960 和 640 x 1136)以及 iPad 的 1024 x 768,一直到最新 iPad 的疯狂分辨率 2048 x 1536(你很难找到能够显示该分辨率的 PC 显示器!)。

我们已经在第六章中提到了这个主题,实现字体、用户界面和本地化,当时我们讨论了实现用户界面的最佳方式。我们永远不应该将游戏硬编码为在固定屏幕分辨率下运行,因为这将在以后将其移植到其他屏幕分辨率时变得更加困难。

我们应该查询 Marmalade 的屏幕尺寸,然后使用这些值来定位和调整我们想要绘制的所有内容的位置和大小,无论是通过使用屏幕大小的百分比,通过将对象固定在屏幕边缘,还是确实使用你自己的选择的其他方法。我们可以如下找到屏幕宽度和高度:

uint32 lScreenWidth = IwGxGetScreenWidth();
uint32 lScreenHeight = IwGxGetScreenHeight();

这些函数也会自动处理设备方向。当玩家旋转设备时,返回的值会改变,除非我们使用DispFixRot ICF 文件设置禁用了此功能(稍后我们将详细介绍此设置)。

使用不同资源针对不同屏幕分辨率

使用屏幕尺寸来定位和调整我们想要绘制的元素大小效果很好,但这确实会导致另一个问题。我们可能会发现,如果屏幕上的图像需要被放大到很大尺寸,它们开始看起来模糊或块状。

同样,在低分辨率下表现良好的字体,当在更高分辨率的设备上使用时可能变得难以阅读,因为它们太小了。虽然我们可以在渲染时对字体应用缩放,但一个更美观的解决方案是使用在更大点大小创建的不同版本的字体。

幸运的是,正如我们将在本章后面看到的那样,Marmalade 为这个问题提供了一个非常易于使用的解决方案,允许我们提供用于针对不同屏幕分辨率的替代资源集。

检查设备功能

当针对大量不同设备时,还需要警惕的另一件事是,某些设备可能不支持某些 Marmalade SDK 功能。

一些设备可能具有多点触控显示屏,而另一些设备可能只有单点触控或根本没有触摸屏。一些设备可能没有加速度计输入或键盘。因此,确保我们调用各种 Marmalade 函数来查询这些和其他功能是否可用以及提供了哪些功能,这样我们就可以为用户提供适合他们设备的选项,这是一个好主意。

使用 ICF 文件设置配置您的游戏

如果你回想起本书第一章中的“Hello World”项目,你会记得我们使用 ICF 文件来根据代码在哪个平台上执行来显示不同的欢迎信息。如果你已经忘记了所有这些是如何工作的,不要担心,我们很快就会再次介绍。

当我们试图针对尽可能多的不同设备时,这个功能证明极其有用,因为内置的参数允许我们为包括内存使用、OpenGL ES 图形性能、启动画面等在内的各种事物应用不同的设置。

内置 ICF 设置

ICF 文件设置被分配给一个由方括号中放置的章节名称定义的章节标识符。当指定一个 ICF 设置的值时,你必须确保它出现在正确的章节标识符之后,否则在运行时将找不到它,并且会引发断言。以下是一个示例:

[S3E]
MemSize=10000000
SysAppVersion="1.0.0"

由于 ICF 设置太多,无法在本书中全部涵盖,因此我们将查看一些更实用的设置。如果您想查看完整列表,请查阅 Marmalade 文档,通过访问 Marmalade | Marmalade 开发工具参考 | ICF 文件设置。|

下表显示了控制 Marmalade 在其最低级别的几个设置。这些设置的标识符为 [S3E]

设置 值类型 描述
MemSize 整数 可供应用程序使用的内存堆大小,以字节为单位。实际上,Marmalade 应用程序可以有最多十个内存堆可用,因此还有 MemSize0MemSize9 等设置,这些设置允许声明这些堆的大小。MemSize0 实际上等同于使用 MemSize。有关内存堆的更多信息,请参阅 Marmalade 文档中的 s3eMemory API。
MemSizeDebug 整数 当执行 Windows 调试构建时,调试内存堆的大小,以字节为单位。这是一个用于在资源构建过程中处理 3D 模型和将纹理转换为不同格式的特殊内存块。
SysAppVersion 字符串 允许应用程序访问其版本号。虽然此值可以在 ICF 文件中设置,但也可以使用 MKB 部署的 version 设置来设置。
SysGlesVersion 整数 识别应用程序是否应尝试初始化 OpenGL ES 1.x 或 2.x 接口。只能指定主版本号(即 1 或 2)。
SysStackSize 整数 程序可用的堆栈大小,以字节为单位。例如,当应用程序需要额外的堆栈空间(由于高度递归的算法)时,这很有用。
SplashScreenFile 字符串 在应用程序加载期间显示的图像文件的名称。文件名相对于 data 目录。
SplashScreenBkR ,``SplashScreenBkG , andSplashScreenBkB 字节 一个从 0255 的值,用于指定启动画面背景颜色的红色、绿色和蓝色分量值。这是在显示指定的启动画面图像之前用于清除屏幕的颜色,假设图像小于屏幕尺寸。
SplashScreenWidthSplashScreenHeight 整数 启动画面图像应该绘制的宽度和高度。如果小于屏幕尺寸,图像将居中显示。
AudioAllowBackground 01 当设置为 1 时,允许用户可能已启动的任何音频轨道(例如,通过 iOS 设备上的 iPod 应用程序)在我们应用程序启动时继续播放。
DispFixRot 字符串 允许屏幕锁定到特定方向,而不是在用户旋转设备时旋转。可以设置为以下值之一:FreePortraitLandscapeFixedPortraitFixedLandscapeFree 设置允许任何设备方向,而 FixedPortraitFixedLandscape 将屏幕方向锁定到默认的纵向或横向比例,这在使用加速度计控制游戏时防止不希望的屏幕旋转非常重要!

以下表格列出了用于更改 OpenGL ES 初始化的一些有用参数。这些设置必须在 [GL] 部分标识符之后发生:

设置 值类型 描述
AlphaInFrameBuffer 01 当设置为 1 时,此设置表示帧缓冲区也包括目标 alpha 通道。
EGL_RED_SIZEEGL_GREEN_SIZEEGL_BLUE_SIZEEGL_ALPHA_SIZE 整数 指示在帧缓冲区中存储红色、绿色、蓝色和 alpha 通道时使用的位数。为了获得最佳的渲染质量,所有这些设置通常都应设置为 8,从而产生 RGBA8888 显示。大多数硬件也可以支持 RGBA5551 和 RGB565 等格式,这将使用更少的视频内存,并且可能在牺牲视觉质量的情况下渲染得更快。
EGL_DEPTH_SIZE 整数 用于深度缓冲区的位数。有效值是 162432,其中 32 提供最高的精度,因此在渲染时最不可能发生 Z 缓冲区冲突,但代价是渲染速度较慢和内存使用量较大。

我们将以一些与资源管理相关的设置结束,这些设置我们将在本章后面更深入地探讨。它们已被包含在此处以便于参考。这些设置位于 ICF 部分 [RESMANAGER]

设置 值类型 描述
ResBuild 01 当设置为 1 时,Windows 调试构建将通过解析原始 GROUP 文件并加载源模型、纹理和其他资源来加载资源。一旦数据被处理,它将以二进制格式保存到 data-ram 目录。如果此设置设置为 0,则不会加载源资产,并且将直接加载任何现有的二进制格式数据。这可以在游戏数据没有更改时加快测试速度。
ResBuildStyle 字符串 指定在 Windows 调试构建处理原始源资产时使用的资源构建样式。正如我们将在本章后面学到的那样,此参数允许我们提供不同集合的资源,以适应不同能力的设备。

定义新的 ICF 设置

ICF 文件最好的事情之一是我们能够通过创建自己的自定义设置来利用它们。为了定义新的设置,我们只需要将它们添加到 app.config.txt 文件中,当我们使用 MKB 文件创建新项目时,该文件会自动为我们生成。

当定义新的设置时,我们还可以提供一个文本字符串来解释这个设置的作用。虽然这个描述实际上并不被 Marmalade SDK 使用或需要,但它是一种很好的记录设置预期功能的方法!

注意

然而,将所有设置的说明添加到 app.config.txt 文件中是非常重要的,因为它将防止应用程序在执行时生成大量的断言。在 Windows 调试构建中,Marmalade 检查在执行开始时加载 ICF 文件以及每次我们尝试从自己的代码中访问设置时是否已声明 ICF 设置。

我们也可以通过在 app.config.txt 文件中列出方括号中的节名称,然后跟上新设置的定义来定义我们自己的节标识符。以下是一个说明如何创建新的节标识符和设置的示例:

[GAME_DEBUG]
SkipToLevel      Skip to a level at game start

[GAME]
FrameRate        The frame rate we want the game to run at
MaximumHealth      Amount of energy the player has at game start

在创建库模块时,定义自己的节标识符可以非常有用,例如在 第六章 中创建的 GUI 和 Localise 模块,实现字体、用户界面和本地化。创建模块时的唯一区别是 app.config.txt 文件将更改为 modulename.config.txt,并且它应该位于模块主目录中的 docs 子目录中。例如,如果我们想向 GUI 模块添加自己的设置,我们会创建一个名为 GUI\docs 的目录,列出设置的文件将被称为 GUI.config.txt

在代码中访问 ICF 设置

如果没有某种方式访问它们,仅仅能够在 ICF 文件中提供设置几乎没有任何用处。这就是 s3eConfig API 发挥作用的地方,我们可以通过只包含 s3eConfig.h 头文件来使用它。

我们将要查看的第一个函数是 s3eConfigGetString,它接受我们想要访问的节标识符和设置名称,以及一个指向 char 数组的指针,该数组将在函数完成后用于返回设置的值。由于 app.icf 文件实际上不过是一个 ASCII 文本文件,这个函数所做的只是返回指定 ICF 设置等于号后面的文本字符串。

传递给 s3eConfigGetStringchar 数组应该至少有 S3E_CONFIG_STRING_MAX 的长度,因为这是该函数可以返回的最大字符串大小。如果请求的设置在 ICF 文件中找不到,则此缓冲区将不会改变,这非常有用,因为它允许我们在代码中为参数设置默认值。

// Set default first level
char lLevelName[S3E_CONFIG_STRING_MAX];
strcpy(lLevelName, "level1");

s3eConfigGetString("GAME_DEBUG", "SkipToLevel", &lLevelName);
// lLevelName will still contain "level1" if the SkipToLevel setting 
// could not be found in the ICF file

很频繁地,我们可能需要指定 ICF 设置,这只需要一个数值。为了使我们更容易做到这一点,Marmalade 提供了另一个名为 s3eConfigGetInt 的函数,它不是指向一个 char 数组的指针,而是指向一个 int 变量的指针。

此函数将读取 ICF 文件中的设置字符串,然后尝试将其转换为整数值。如果失败(例如,字符串包含非数字字符或超出 int 的范围)或设置在 ICF 文件中不存在,则不会更改变量的当前值,从而允许在代码中指定默认值。

如果设置值可以检索到,这两个函数都将返回 S3E_RESULT_SUCCESS,如果存在问题,则返回 S3E_RESULT_ERROR。函数 s3eConfigGetError 将通过返回以下值之一让我们发现问题:

描述
S3E_CONFIG_ERR_NONE 没有发生错误。
S3E_CONFIG_ERR_PARAM s3eConfigGetInts3eConfigGetString 的一个参数无效。例如,传递了一个 NULL 值。
S3E_CONFIG_ERR_NOT_FOUND 请求的 ICF 设置找不到。
S3E_CONFIG_ERR_PARSE 在使用 s3eConfigGetInt 将 ICF 设置值转换为整数值时出现了问题。

通过平台和设备限制 ICF 设置

当针对大量不同的设备时,我们想要根据应用程序运行的设备执行不同的事情,这种情况并不少见。

ICF 文件系统通过允许我们根据设备的操作系统甚至根据单个设备类型提供不同的参数值,使处理这一点变得非常容易。

首先,我们可以在平台范围内提供不同的设置。"Hello World" 项目来自 第一章,Marmalade 入门,已经展示了这一点,但为了回顾,我们使用 OS 条件将设置限制在特定的操作系统上。这最好通过一个示例来说明:

[GAME]
FrameRate=20

{OS=BADA}
FrameRate=15

{OS=IPHONE}
FrameRate=30
{}

此示例为 FrameRate 设置设置了默认值 20。然后,它将此值覆盖为 Bada 设备的 15 和 iOS 设备的 30。请注意,出于历史原因,值 IPHONE 指的是所有 iOS 设备(所有版本的 iPad 和 iPod touch 以及所有 iPhone)。

注意

早期示例以开闭大括号结束。这会将此点之后所做的所有设置返回为全局设置,适用于所有设备和平台。

还可以创建仅适用于特定平台上的特定设备子集的设置。这是通过使用 ID 条件来完成的,它首先指定平台类型,然后有一个以逗号分隔的设备标识符列表,该设置应应用于这些标识符。以下是一个示例:

[GAME]
FrameRate=30

{ID=ANDROID "HTC Hero", "T-Mobile G1"}
FrameRate=20
{}

在这里,我们为FrameRate设置设置默认值30,然后如果游戏运行在列出的任何 Android 设备上,则将值限制为20。仅当设备名称包含空格时才需要引号。

想要知道如何发现设备名称?通常它是设备的名称,但并不总是如此。发现特定设备名称的最简单方法是为该设备创建一个简短的测试程序,调用s3eDeviceGetString函数,如下所示:

const char* lpDeviceID = s3eDeviceGetString(S3E_DEVICE_ID);

注意

s3eDeviceGetString函数及其兄弟函数s3eDeviceGetInt允许我们确定运行在设备上的大量信息,包括操作系统、处理器类型、电话号码、当前语言设置等等。请参阅s3eDevice.h头文件或 Marmalade 文档以获取更多详细信息。

创建多个资源集

由于 Marmalade 允许我们针对如此多的不同设备,因此限制自己仅使用其中的一部分,仅仅因为我们的图形分辨率过高或过低,或者某些设备内存较少,因此无法处理大量高分辨率纹理,这似乎有些遗憾。

我们可能还会遇到另一个问题,即不同的设备支持不同的音频或视频剪辑文件格式。为了提高渲染速度和内存使用效率,我们还可以考虑使用硬件纹理压缩,当然,这取决于特定设备所使用的图形处理器的类型。

Marmalade 提供了一些解决方案来解决这个问题。第一种,更全局的方法是利用构建风格,它允许我们在加载 GROUP 文件时加载不同的资源文件集,并指定要应用的硬件纹理压缩类型。

构建风格通过资源模板的概念得到增强,这允许我们更精细地控制资源的配置。资源模板可以用来影响纹理的最终格式,或者修改 3D 模型以用于游戏中的方式,以及其他方面。

使用构建风格

Marmalade 附带了一些内置的构建风格,允许我们为移动设备上使用的所有常见 GPU 格式构建资源。以下表格显示了可用的构建风格:

构建风格 描述
sw 为与 Marmalade 的遗留软件渲染器一起使用而优化的构建资源。以这种方式构建的资源不能使用硬件加速进行渲染。此格式现在仅在我们使用 MKB 文件中的IW_USE_LEGACY_MODULES定义以使软件渲染器可用时才有用。
gles1 不使用任何形式的纹理压缩构建资源。如果没有指定构建风格,这是默认设置。
gles1-pvrtc gles1 相同,但在图像上使用 PVRTC 格式进行纹理压缩,这种压缩类型效果良好。通常这仅仅意味着没有 alpha 通道的图像,因为 PVRTC 在这种纹理上通常表现不佳。
gles-atitc gles1 相同,但在可能的情况下使用 ATITC 纹理压缩格式。
gles1-dxt gles1 相同,但在可能的情况下使用 DXT 格式进行纹理压缩。
gles2-etc 适用于使用 OpenGL ES 2.x 并支持 ETC 纹理压缩格式的设备。

如果默认的构建样式不足以满足需求,我们也可以定义自己的自定义构建样式。为此,我们在 data 目录中创建一个名为 resbuildstyles.itx 的文件。当在调用 IwResManagerInit 时初始化资源管理器时,该文件会自动被加载,并包含一个或多个 CIwResBuildStyle 类的实例。

为了声明一个构建样式实例,我们必须给它一个名称,以便它可以被选中使用,一个可选的目录列表,资源文件可以驻留在其中,以及一个指示此构建样式针对的平台。请注意,在构建样式的案例中,平台并不指代任何特定的操作系统;相反,它指的是样式针对的 GPU 类型,这在很大程度上意味着将要使用的硬件纹理压缩类型。

这里是一个用于以下部分讨论的 resbuildstyles.itx 文件示例:

CIwResBuildStyle
{
  name             "default"
  platform         "GLES1"
}
CIwResBuildStyle
{
  name             "pvrtc"
  addReadPrefix    "data-pvrtc"
  platform         "IMG_MBX"
}
CIwResBuildStyle
{
  name             "atitc"
  addReadPrefix    "data-atitc"
  platform         "ATI_IMAGEON"
}

添加额外的资源目录

addReadPrefix 参数允许我们在尝试加载任何类型的文件时添加一个新的搜索路径。指定一个目录名称;这必须是项目 data 目录下的一个子目录。如果您想添加多个额外的搜索目录,只需包含更多的 addReadPrefix 条目。

每当我们尝试打开一个文件时,Marmalade 会首先按照指定的顺序在由构建样式指定的额外目录列表中查找。如果请求的文件在这些目录中的任何一个中找到,它将从那里加载;否则,资源管理器将回退到在 data 目录中查找。

支持的构建样式平台

CIwResBuildStyle 实例的 platform 字段可以取以下值之一:

平台值 描述
SW 为与 Marmalade 的传统软件渲染器一起使用而优化的构建资源。再次强调,我们必须在我们的 MKB 中使用 IW_USE_LEGACY_MODULES 定义才能使用此功能。
GLES1 如果未指定,这是默认选项,并构建可以使用 OpenGL ES 高效渲染的资源。
IMG_MBX GLES1 相同,但在图像上使用 PVRTC 格式进行纹理压缩,这种压缩类型效果良好。
IMG_MBX_VGP 目前与 IMG_MBX 相同。
ATI_IMAGEON GLES1 相同,但在可能的情况下使用 ATITC 格式进行纹理压缩。
NVIDIA_GOFORCE 目前与GLES1执行相同。
ARM_MALI 目前与GLES1执行相同。

虽然平台标识符使得为不同类型的 GPU 创建资源变得容易,但也可以更具体地指定要使用的纹理压缩类型。这可以通过指定平台为GLES1并添加textureFormat设置来实现。例如,前面示例中的atitc条目可以写成以下形式:

CIwResBuildStyle
{
  name             "atitc"
  addReadPrefix    "data-atitc"
  platform         "GLES1"
  textureFormat    "ATITC"
}

可以使用以下值作为textureFormat参数:

描述
PVRTC_2 使用 2 位 PVR 纹理压缩。通常不推荐使用,因为它往往会产生低质量的结果。可以在具有 Imagination 生产的芯片组的设备上使用,例如 iOS 设备。
PVRTC_4 使用 4 位 PVR 纹理压缩。这种类型通常对没有 alpha 通道的纹理产生良好的结果,但在压缩透明纹理时可能会相当差。默认情况下,Marmalade 不会对任何具有 alpha 组件的源纹理执行此类型压缩。这种压缩类型由使用 Imagination GPU 的设备支持,例如 iOS 设备。
ATITC 将使用 ATI 压缩纹理。自动在无 alpha 通道的纹理上使用 4 位压缩,或在具有透明度的纹理上使用 8 位压缩。支持在许多 Android 设备中使用的 ATI/Qualcomm 芯片组。
ETC 使用 4 位爱立信纹理压缩对没有 alpha 通道的纹理进行压缩。透明纹理无法压缩。支持在 ATI/Qualcomm 芯片组和大多数支持 OpenGL ES 2.x 的芯片组上使用。
DXT1,DXT3, 和DXT5 DXT1压缩是一种用于非透明纹理的 4 位格式。DXT3是一种允许压缩透明纹理的 8 位格式。DXT5是另一种具有更好的 alpha 通道渐变支持的 8 位格式。如果指定了DXT3DXT5,并且遇到不透明纹理,Marmalade 将自动使用DXT1压缩。在 NVIDIA Tegra2 芯片组设备上可用。

指定要使用的构建样式

在声明了构建样式之后,我们现在只需让 Marmalade 知道在加载资源时要使用哪个。最简单的方法是使用ResBuildStyle ICF 设置,我们通过在 ICF 文件中添加以下内容来实现:

[RESMANAGER]
ResBuildStyle=pvrtc

我们还可以在运行时切换构建样式,因为资源管理器为我们提供了设置和获取当前构建样式的功能。以下代码片段说明了这一点:

// Discover the currently selected build style
CIwStringL lCurrentStyle = IwGetResManager()->
   GetBuildStyleCurrName();

// To change to a different build style
IwGetResManager()->SetBuildStyle("atitc");

然而,请注意,虽然切换构建样式很容易,但这种行为仅在 Windows 调试构建中受支持。当我们为设备创建发布构建时,我们通常会只为该设备类型提供所需的资源,以减少安装包的大小。我们将在本章后面讨论如何实现这一点。

使用资源模板

构建样式允许我们在全局层面上对游戏资源进行处理做出决策;但有时我们希望有更精细的控制,以便以不同的方式处理不同类型的资源。

这就是资源模板发挥作用的地方。简单来说,资源模板允许我们更改在处理纹理、材质、3D 模型、动画和 GROUP 文件时应用的默认设置。

资源模板可以在一个 ITX 文件中定义,我们在尝试加载任何资源之前会解析这个文件。由于这些模板仅在 Windows 调试构建中需要,所以如果我们不会构建资源,我们不需要加载这个文件。

Marmalade 提供了一个方便的宏定义,IW_BUILD_RESOURCES,它仅在 Windows 调试构建中定义。使用这个宏定义,我们可以通过排除任何资源处理代码来减小编译代码的大小。例如,如果我们的资源模板定义包含在一个名为 restemplates.itx 的文件中,我们可以使用以下代码片段来加载该文件:

#ifdef IW_BUILD_RESOURCES
IwGetTextParserITX()->ParseFile("restemplates.itx");
#endif

以下代码提供了一个 restemplates.itx 文件可能的样子。我们将在接下来的章节中更详细地讨论不同的资源模板类型;但请注意,每种类型都有一个名为 default 的模板被定义。这样,如果我们想恢复正常的加载行为,就可以这样做。

CIwResTemplateImage
{
  name        "default"

  formatHW    FORMAT_UNDEFINED
  formatSW    FORMAT_UNDEFINED
}

CIwResTemplateImage
{
  name        "rgba4444_nomipmap"

  formatHW    RGBA_4444
  mipMapping  false
}

CIwResTemplateMTL
{
  name        "default"
}

CIwResTemplateMTL
{
  name         "clamped_unfiltered"
  clampUV      true
  filtering    false
}

一旦定义了资源模板,就可以通过在 GROUP 文件中使用 useTemplate 参数从内部调用它。此参数接受资源模板的类型和名称,搜索它,如果找到,则将模板中定义的任何设置应用于从那时起加载的任何类型的资源。以下是一个示例:

CIwResGroup
{
  name "images"

  useTemplate "image" "rgba4444_nomipmap"
  useTemplate "mtl" "clamped_unfiltered"

  "./materials.mtl"

  useTemplate "image" "default"
  useTemplate "mtl" "default"
}

定义材质模板

材质资源模板通过 CIwResTemplateMTL 类的实例声明,并用于为在模板使用期间创建的所有 CIwMaterial 实例提供一个起始配置。

我们可以在材质模板中指定任何可以应用于从 ITX 文件处理时的 CIwMaterial 实例的参数。在以下表中,列出了一些对模板有用的参数,但完整的列表请参阅 Marmalade 文档中的 CIwMaterial

参数 描述
colAmbientcolDiffusecolEmissivecolSpecular 允许为环境、漫反射、发射和镜面反射照明组件指定默认的 RGBA 颜色。例如:colAmbient { 255, 255, 255, 255 }
cullMode 指定材质使用的背面剔除方法。可以是 BACKFRONTNONE 之一。
alphaMode 指定默认的不透明度模式。可以是 NONEADDSUBHALFBLEND 之一。
blendMode 指定绘制时使用的混合类型。可能的值有 MODULATEMODULATE_2XMODULATE_4XDECALADDREPLACEBLEND
alphaTest 指定绘制像素时使用的 alpha 测试类型。由一个测试类型后跟一个 alpha 值组成。有效的测试类型有 DISABLEDNEVERLESSEQUALLEQUALGREATERGEQUALNOTEQUALALWAYS。例如:alphaTest GEQUAL 128
zDepthOfszDepthOfsHW 允许在渲染时将偏移量添加到顶点的 z 分量,以强制绘制前后。这对于绘制发光效果非常有用,可以强制它们出现在 3D 模型之后或之前。zDepthOfs 用于软件渲染器,而 zDepthOfsHW 用于使用 OpenGL ES 进行渲染。
filtering 设置为 true 以在渲染时使用双线性过滤。
clampUV 如果设置为 true,则 UV 坐标将在纹理的边界内夹紧。这有助于避免在渲染纹理边缘时由双线性过滤引起的问题,因为双线性过滤将尝试在图像的左和右或上和下之间的 texel 之间进行混合,因为它假定纹理可以被平铺。

定义图像模板

我们还可以使用资源模板系统来指定我们希望图像如何处理,这包括指定使用的纹理格式。为了定义图像的资源模板,我们必须声明一个 CIwResTemplateImage 实例,可以使用以下参数进行配置:

参数 描述
formatSWformatHW 将任何图像转换为请求的格式。此参数的两个版本允许为软件渲染器定义一个格式,并为 OpenGL ES 渲染定义另一个格式。有关纹理格式的完整列表,请参阅 Marmalade 文档中的 CIwImage 类,但请注意,其中一些格式仅适用于软件或硬件渲染。例如,OpenGL ES 不支持任何基于调色板的格式,而软件渲染器不支持压缩格式,如 PVRTC 或 ATITC。
compressForDiskSpace 当设置为 true 时,使用 formatSWformatHW 参数转换纹理时,如果转换后的二进制版本 GROUP 文件(在内存大小上)小于原始格式的图像,则只存储转换后的版本。默认为 false
mipMapping 当设置为 true 时,图像将自动生成米柏图。对于将形成 UI 一部分的图像,将其设置为 false 可能非常有用,因为这些图像通常希望以原生大小绘制,而米柏图将不再需要。
allowLowQualityCompression 如果使用硬件压缩格式,当结果纹理可能质量较低时,例如在使用具有 alpha 通道的图像上使用 PVRTC 时,Marmalade 不会使用请求的压缩。将此参数设置为 true 允许您强制 Marmalade 执行请求的压缩。
ignoreImages 如果设置为 true,将忽略图像,并使用 2 x 2 的棋盘纹理代替。这在调试时可以加快加载时间。

定义模型模板

当从 GEO 文件加载 3D 模型时,我们可以使用 CIwResTemplateGEO 资源模板的一个实例来控制模型的处理方式。许多可用的选项允许我们在知道特定模型将在某些条件下使用时提高渲染性能;例如,它将仅使用 OpenGL ES 进行渲染,或者它可能已经导出带有法线,因为模型将永远不会启用光照进行渲染。

下表显示了一些更有用的设置,但还有很多其他设置,因此请查阅 Marmalade 文档中的 CIwResTemplateGEO 以获取更多详细信息:

参数 描述
scale 允许指定一个浮点值,该值将用于缩放模型的全部顶点。这在允许使用建模软件中的一个比例创建 3D 模型并在游戏中以不同的比例使用时很有用。
buildColsbuildNormsbuildUVsbuildUV1s 如果设置为 true,处理后的模型数据将包括顶点颜色、法线和 UV 信息(假设在导出的模型中存在)。这在游戏中对模型不需要光照或纹理时可以节省内存。
triStrip 如果设置为 true,模型将准备使用三角形带进行渲染。默认值为 false,这将导致生成三角形列表。仅在模型正在为使用 OpenGL ES 进行渲染而准备时生效。
calculateNorms 如果设置为 true,模型构建器将尝试为光照目的生成顶点法线。如果源模型由于任何原因导出时没有法线,则很有用。
chunked 如果设置为 true,模型将被细分为更小的 "chunk" 以使用二进制空间划分进行渲染。这在渲染比屏幕尺寸大的模型时很有用,因为它允许忽略模型中不在屏幕上的整个部分。
maxPrimsPerChunk chunked 参数一起使用,用于指定模型每个 chunk 应包含的最大多边形数量。

定义动画模板

CIwResTemplateANIM 类允许在处理过程中调整 ANIM 文件数据。它只提供了一两个选项,如下表所示:

参数 描述
zeroMotionTolerance 允许指定一个浮点值,该值将用于过滤任何关键帧数据的平移部分。在动画模型时,艺术家可能会不小心将一些微小的动作包含到骨骼位置中,这会导致输出数据集更大。此值允许忽略达到指定值以内的动作,这意味着可能需要输出的关键帧更少。
transformPrecision 另一个浮点值,用于指定动画时的精度。默认值是 4.0,这意味着动画数学计算是在世界空间分辨率的四倍下进行的。如果您有一个包含许多微妙动作的动画,您可能需要考虑增加此值,以防止这些动作丢失。

定义 GROUP 文件模板

最后,有一个 CIwResTemplateGROUP 类,用于创建纹理图集。纹理图集简单地说就是一组较小的纹理,这些纹理被排列在一个更大的纹理中。这可以提高渲染速度,因为渲染时需要的纹理交换更少。

在这本书中,我们不会详细探讨纹理图集,所以如果您想了解更多信息,请查看 Marmalade 文档页面中的 CIwResTemplateGROUP 类。

生成资源的二进制版本

在这本书的之前部分,我们已经看到有关 Marmalade 生成我们资源二进制版本的引用,这些二进制版本通常比源资产更小,加载速度更快。

到目前为止,我们对此有点轻描淡写,但现在我们知道了构建样式,值得仔细看看。

每当我们加载 GROUP 文件时,资源二进制版本会自动为我们生成,前提是我们将 ICF 设置 ResBuild 设置为 1,并且正在运行游戏的 Windows 调试构建。这些文件以 .group.bin 文件扩展名写入到名为 data-ram 的目录中,该目录位于我们的源资产所在的常规 data 目录旁边。

如果我们在任何项目的 data-ram 目录中查看,我们会发现另一组子目录,这些子目录包含我们资源的二进制版本。这些子目录对应于我们在构建样式中指定的额外前缀目录。

.group.bin 文件被写入时,它们将始终写入由当前活动构建样式指定的前缀目录,无论源文件是从标准 data 目录还是从额外前缀目录读取的。

data 目录到相对目录路径也会在输出目录中创建,当写入文件的二进制版本时。

这使得我们可以非常容易地将不同的资源集部署到不同的平台,因为我们只需要包含 data-ram 的子目录之一中的所有 .group.bin 文件。

让我们用一个简单的例子来说明这一点。假设我们有一个名为data/images/images.group的文件,它加载了多个纹理。如果没有指定构建样式,默认是 Marmalade 定义的GLES1样式,它指定了一个名为data-gles1的前缀目录。文件的二进制版本将被写入文件路径data-ram/data-gles1/images/images.group.bin

如果我们现在再次运行程序,选择pvrtc构建样式(如本章前面关于构建样式的部分所述),图像将被转换为 PVRTC 格式,并写入文件路径data-ram/data-pvrtc/images/images.group.bin

事实上,Marmalade 不仅会输出 GROUP 文件的二进制版本,还会创建一些其他文件,这些文件在调试过程中可能很有用。在这本书中,我们不会详细讨论这些文件,但如果你在处理某些资源时遇到问题,你可能需要查看这些文件。特别是,有一个扩展名为.group.bin.txt的文件,它详细说明了在处理特定 GROUP 文件时遇到的所有类。

注意

这种方法有一个缺点,那就是你必须加载游戏中引用的每一个 GROUP 文件,以便生成它们的所有二进制版本。如果你的游戏有大量关卡,并且每个关卡都有一个 GROUP 文件,这尤其可能成为一个问题。解决这个问题的好方法是为你的游戏创建一个特殊模式,它可以接受一个包含所有必需 GROUP 文件的列表(以及它们之间可能存在的任何依赖关系),然后依次加载每个文件以生成二进制版本。

使用 Derbh 归档程序压缩资源

游戏资源很快就会变得非常大,所以如果我们能以某种方式压缩这些文件,使它们在安装包中占用的空间更少,那就太好了,尤其是如果安装包的最大大小有限制的话。

Marmalade 提供了类似 Derbh 归档这样的功能,这与你可能熟悉的 ZIP 等压缩系统非常相似。Derbh 支持多种压缩算法,包括标准的 LZMA 以及它自己的专有算法,通过同时操作多个文件,可以实现更好的压缩效果。

Marmalade SDK 提供了一个 API,允许我们以与提供单个未压缩文件一样容易的方式加载压缩文件。还提供了一个名为 DZip 的命令行实用程序,用于最初生成归档。

创建 Derbh 归档

要创建 Derbh 归档,我们首先必须创建一个DZip 配置文件DCL)。这个文件被传递给 DZip 实用程序,以指定源文件以及它们应该如何被压缩。以下是一个简单的 DCL 文件示例,它来自本章的滑雪示例项目:

archive data-ram\data-gles1\skiing.dz
basedir data
basedir data-ram\data-gles1

file text\EN.str 0 dz
file models.group.bin 0 dz
file flag\flag.group.bin 0 dz
file rock\rock.group.bin 0 dz
file skier\skierskiing.group.bin 0 dz
file sound\sound.group.bin 0 dz
file tree\tree.group.bin 0 dz
file ui\ui.group.bin 0 dz

第一行使用archive关键字来指定要创建的 Derbh 存档的名称,通常会给它一个.dz扩展名。可以通过简单地添加更多的archive条目来一次创建多个存档。

basedir关键字允许我们指定一个目录,用于搜索将构成存档的文件。在先前的例子中,我们指定了datadata-ram\data-gles1目录。

接下来,我们使用file关键字列出所有将被添加到存档中的文件。第一个参数是要包含的文件名,它应该是相对于由basedir关键字指定的目录的相对路径。之后是一个数字和一个压缩类型。这个数字指的是文件应该添加到哪个存档中,其中零是指 DCL 文件中指定的第一个存档。

有多种压缩类型可供选择,尽管请注意,并非所有这些类型实际上都会压缩源文件!如果我们愿意,可以为每个文件使用不同的压缩类型。以下表格显示了可用的类型:

类型 描述
lzma 使用 lzma 压缩,通常提供最佳的压缩比,并且具有合理的解压速度。
dz Marmalade 的自身压缩格式,提供了良好的压缩比和解压速度。
zlib 使用 zlib 压缩,提供了不太理想的压缩比,但具有非常好的解压速度。
zero 将添加一个与文件大小相同的零块到存档中。可以用于调试目的,例如,如果我们需要检测损坏的文件。
copy 文件以未压缩的形式包含在存档中。对于已经压缩的文件类型,这可以产生比尝试压缩文件更小的存档最终文件大小。

构建了 DCL 文件后,我们可以使用 DZip 实用程序来构建存档文件。这个实用程序可以在 Marmalade SDK 安装目录中的tools\dzip\dzip.exe文件中找到。

要创建存档,只需将 DCL 文件的名称传递给 DZip 实用程序,确保您在可以找到archivebasedir条目的目录中运行命令。

在代码中使用 Derbh 存档

创建了 Derbh 存档后,在游戏中使用它就变得非常简单。首先,我们需要通过在 MKB 文件中将derbh添加到subprojects列表中来添加对 Derbh API 的支持。我们还需要包含derbh.h文件,以便访问 API 函数。

要使用我们的存档文件,我们只需要添加一个对函数dzArchiveAttach的调用,该函数接受单个参数——Derbh 存档本身的文件名。从那时起,任何打开文件的调用都将首先检查它是否存在于 Derbh 存档中,如果存在,数据将在我们尝试从文件中读取时自动解压并返回。这真的非常简单!

我们还可以通过简单地调用每个我们希望使用的存档的 dzArchiveAttach 来一次附加多个存档。

如果请求的文件不在存档中,Marmalade 将会查找 datadata-ram 目录。

如果我们出于任何原因想要停止使用 Derbh 存档,我们可以通过调用 dzArchiveDetach 来移除最后附加的存档,或者我们可以使用 dzArchiveDetachNamed 函数指定要分离的存档。

注意

重要的是要注意,只有从应用程序代码中加载的文件才能从附加的 Derbh 存档中访问。如果你试图使用 s3eAudio 播放音乐轨道或使用 s3eVideo 播放视频剪辑,这些文件必须作为单独的文件存在,因为它们是通过操作系统原生方法加载的,显然将无法访问 Derbh 文件的内容。

自动 Derbh 方法

对于大多数项目来说,实际上还有一种更简单的方法来利用 Derbh 存档,这不需要我们创建 DCL 文件或自己构建 Derbh 文件。我们甚至不需要在我们的代码中附加存档!要使用此功能,我们只需要将以下内容添加到我们的 MKB 文件 deployments 部分即可(我们将在稍后更详细地介绍 MKB 文件的这一部分)。

deployments
{
  auto-derbh
}

在此基础上,Marmalade 部署工具将自动从 MKB 文件 assets 部分的相关文件中构建一个 Derbh 存档(再次提醒,assets 部分将在稍后讨论),并在我们的应用程序代码开始执行之前将其附加。

注意

如果你部署的文件需要在安装后由你的代码修改,请小心使用自动 Derbh 功能。一旦文件包含在存档中,你将无法修改该文件,因此你需要在应用程序第一次运行时在新的位置创建任何此类文件的副本。

创建不同的部署类型

现在是时候更深入地了解 Marmalade 如何处理部署过程了。如果你一直在跟踪示例代码,你可能想知道我们是如何制作包含所有必要资源文件以便运行的部署包的。或者,如果我们正在创建多个资源集,我们在创建安装包时如何选择与我们的代码配对的哪一个?

我们还需要一种方法来包含图标和标题,这些图标和标题将用于在设备上安装我们的应用程序时表示我们的应用程序。

所有这些魔法都在 MKB 文件中发生,接下来的部分旨在解释你确切需要做什么。

指定图标、应用程序名称和其他详细信息

MKB 文件的 deployments 部分是我们可以设置所有将应用于我们应用程序最终安装包的属性的地方。有大量的部署选项可以指定,其中一些适用于所有支持的平台,而另一些则是特定于操作系统的。

以下表格列出了几个更直接有用的属性,但你应该查阅 Marmalade 文档中的 Marmalade | Marmalade 开发工具参考 | MKB 文件设置 | 部署选项,以获取完整详情。

属性 描述
assets 指定在部署中使用的资产组。这将在以下部分中详细解释。
name 指定部署的名称。此名称将用于安装目录、可执行文件和安装包文件的名称。如果没有指定此值,则将使用 MKB 文件的文件名。
caption 这是用于识别设备上已安装的应用程序的名字——例如,出现在程序图标下方的文本。如果没有指定标题,则将使用 name 值。
app-icf 允许指定一个替代文件,而不是默认的 app.icf 文件。
version 指定应用程序的版本号。它应以 major.minor.revision 的形式提供。
version-major,version-minor, 和 version-revision 指定版本号的另一种方式。每个属性后应跟一个数字,代表版本号的相应部分。
iphone-icon,iphone-icon-ipad,iphone-icon-high-res, 和 iphone-icon-ipad-high-res 设置用于 iOS 部署的图标。这些设置指定了一个图标文件的名称,该文件具有合适的格式和尺寸,用作指定的图标类型。
android-icon,android-icon-hdpi, 和 android-icon-ldpi 设置用于 Android 部署的图标文件名。
bada-icon 指定用于 Bada 部署的图标文件。

如你所见,大多数平台都有指定图标文件的选项,实际上还有更多针对特定平台的属性,用于指定诸如应用程序签名密钥等信息。

你应该查看 Marmalade 文档中提到的上述页面,以获取更多关于此方面的详细信息,因为没有这些信息,你将无法生成用于提交的最终部署包。

指定资产列表

我们需要一种方式来列出所有必须包含在部署包中的资源文件,以便我们的游戏可以运行。Marmalade 允许我们通过 MKB 文件的 assets 部分来实现这一点。以下是本章 Skiing 项目的示例:

assets
{
  [common]
  (data)
  sound/music.mp3

  [normal]
  <include common>
  (data-ram/data-gles1)
  skiing.dz

  [highres]
  <include common>
  (data-ram/data-highres)
  skiing.dz
}

这个小示例演示了在 assets 部分中大部分可用的功能。首先,你会注意到使用方括号创建资产命名单元组。在示例中,我们有名为 commonnormalhighres 的资产组。

正常的括号用于指定一个目录,相对于包含 MKB 文件的目录,需要包含在部署包中的文件可以位于该目录。然后是文件本身。在资产组中可以有任意数量的这些文件块。

在资产组中指定目录和文件时需要记住的重要事项是,括号中的目录成为设备上应用程序安装目录的根路径。让我们通过查看一个示例来阐述这一点。

首先,我们有 common 资产组,它指定名为 sound/music.mp3 的文件可以在 data 目录中找到。当安装在设备上时,music.mp3 文件将被写入应用程序安装目录下的一个名为 sound 的子目录中。

现在让我们考虑名为 normal 的资产组。在这里,文件的路径完全被括号包围,只指定了文件名,skiing.dz。这将导致 skiing.dz 文件被写入应用程序的安装目录中。

示例中展示了资产部分的最后一个特性,即能够在另一个资产组中包含一个资产组。这是通过使用 include 关键字来完成的,该关键字用尖括号括起来,并包含要包含的资产组的名称。

通过查看示例,我们可以看到 normalhighres 资产组都包括了 common 资产组。

创建和使用部署类型

我们现在可以查看为不同设备创建不同的配置。MKB 文件的 deployments 部分还允许我们通过指定方括号中的名称来创建不同的部署类型。在此之后所做的所有设置将仅适用于该部署类型。可以通过在定义部署类型之前使用方括号来指定设置,以将设置应用于所有部署类型。

可以通过在方括号后的名称后跟一个平台标识符或用引号分隔的平台列表来限制部署类型到一组特定的移动平台。

在本文撰写时,Marmalade 支持的所有平台的全列表如下表所示:

平台 备注
android 指定 Android 操作系统。
iphone 任何基于 iOS 的设备——iPhone、iPod touch 或 iPad。
bada 针对三星 Bada 平台。
lgtv 指定 LG 智能电视系统。
playbook 用于针对 Blackberry Playbook 平板。
symbian9 构建在 Symbian 9 S60 或 Symbian ³ 设备上运行的应用程序。
webos 针对 webOS 平台,最知名的设备是现已停产的 HP TouchPad。
winmobile 允许支持 Windows Mobile 6 设备。注意,Marmalade 无法针对 Windows Phone 7 进行目标定位。
win32 用于 x86 Windows 构建版本。
osx 用于 x86 Apple Mac 构建(当使用 Marmalade 的 Mac 版本时)。

在部署类型中指定平台列表不是强制性的。如果没有给出列表,则假定任何平台都是有效的目标。

一旦指定了部署类型,任何属性都将仅适用于该部署类型。这对于我们来说非常有用,因为我们能够指定不同的资源集。通过使用 assets 属性,我们可以指定我们希望在最终部署包中包含的资产组。以下 deployments 部分的示例取自本章的滑雪项目。

deployments
{
  name="SkiingC8"
  caption="SkiingC8"

  [normal]
  assets=normal

  [highres]
  assets=highres
}

要为特定的部署类型创建安装包,我们只需遵循本书第一章(ch01.html "第一章. Marmalade 入门")中提供的相同部署说明来启动 Marmalade 系统部署工具。此应用程序的第二页允许我们通过点击复选框来选择我们想要创建的部署类型,如下面的截图所示:

创建和使用部署类型

此页面允许你通过 添加 复制 删除 按钮创建和修改部署类型,但我个人更喜欢在 MKB 文件中手动指定它们。使用这些按钮会相应地修改 MKB 文件。

一旦你完成了部署工具的所有页面并制作了部署包,它们可以在 build_projectname_vcxx\deployments 文件夹中找到,其中 projectname 是 MKB 文件的名字,vcxx 指的是你用于开发的 Microsoft Visual C++ 版本。

示例代码

本章附带两个示例项目,它们将在以下部分中描述。

构建样式项目

这是一个非常简单的示例,展示了构建样式、资源模板和部署类型的用法。它基于第二章的 Graphics2D 示例,资源管理和 2D 图形渲染

resbuildstyles.itx 文件定义了一个名为 highres 的构建样式,该样式指定了一个名为 data-highres 的前缀目录。如果你查看 data 目录,你会看到 data\images\textures\marmalade.png 中的橘皮果图像的 jar 文件大小为 256 x 256 像素。还添加了一个新的 highres 构建样式的目录,其中包含该图像的 512 x 512 版本。此文件名为 data\data-highres\images\textures\marmalade.png

如果你现在查看 app.icf 文件,你会看到新的条目 ResBuildStyle=highres。如果你用这一行运行程序,将会加载 512 x 512 版本的图像。取消注释或删除此行,将加载 256 x 256 像素的图像。

restemplates.itx 文件显示了一个简单的资源模板示例,该模板将强制图像转换为 RGBA4444 格式,并禁用 mipmap。此资源模板在 data\images\images.group 文件中使用,以减小 images.group.bin 文件的大小,因为不需要在其中存储 mipmap 图像。

最后,BuildStyles.mkb 文件声明了两个名为 normalhighres 的部署类型。当使用 Marmalade 系统部署工具 制作安装包时,我们可以选择这两个选项之一来包含低分辨率或高分辨率图像。请注意,部署工具还将列出默认部署类型,因为这总是由部署工具自动定义。使用默认类型将不会包含任何资源,因此不会在设备上工作。

滑雪项目

对于本章,滑雪项目已被更新以使用构建样式、资源模板和部署类型。它还利用 Derbh 存档来减小安装包的大小。

在这种情况下,构建样式系统已被用于允许在屏幕分辨率更高的设备上使用更大的字体。data\data-highres\ui\fonts 目录包含字体文件 skiing.gxfontskiing.tga 的替代版本,当在 app.icf 文件中选择 highres 构建样式时,这些版本将被加载。

由于我们使用了基于设备屏幕尺寸来调整控件大小的方法,因此不需要对任何 UI 布局配置进行更改。我们只需要一个稍微大一点的字体来更好地填充更大的屏幕区域。

为了使部署更容易并减小安装包的整体内存大小,还使用了 Derbh API。如果您查看根项目目录,您将看到两个名为 skiing.dclskiing-highres.dcl 的新文件。这些文件列出了游戏所需的所有资源,并用作 DZip 工具的输入以创建存档文件。还包含了一个名为 MakeDerbh.bat 的批处理文件,以演示 DZip 工具的使用。

注意,Derbh 存档显然不能在生成各种 .group.bin 文件之前创建。为了做到这一点,您需要运行游戏两次,一次是在 app.icf 文件中将 ResBuildStyle=highres 设置设置为高分辨率,再次是将此行注释掉。

两个 DCL 文件在 data-ram\data-gles1data-ram\data-highres 目录内创建目标存档,但两者都生成一个名为 skiing.dz 的存档。Skiing.mkb 文件中的部署类型包括此文件的相关版本,因此我们的代码与部署类型无关。在程序开始时,我们只需使用 dzArchiveAttach 函数附加 skiing.dz 存档,以便访问正确的资源文件。

摘要

在本章中,我们学习了 Marmalade 如何简化我们组织资源文件的方式,以便我们可以为不同规格的设备创建它们的多个版本。我们只需要提供必须不同的资源替代版本,例如更高分辨率的纹理。任何常见的文件,如配置和 GROUP 文件,通常可以保持不变。

我们还介绍了资源模板的使用,这使我们能够更精细地控制资源在游戏中的使用方式(例如,指定要使用的特定类型的纹理压缩)以及我们如何创建包含相同核心代码但不同资源文件的不同的部署类型。

最后,我们还研究了 Derbh API,它允许我们压缩资源文件以节省安装包中的空间。

在下一章中,我们将探讨如何利用社交媒体让我们的玩家与他们的 Facebook 朋友分享有关我们游戏的信息。

第九章。添加社交媒体和其他在线服务

现代移动设备在图形和声音方面非常强大,但它们与其他专用手持式电子游戏系统之间最大的区别可能是,大多数设备都能连接到互联网。

虽然其他游戏系统可能可以通过 WiFi 上网,但许多现代设备也可以使用 3G 或其他此类数据连接,无论用户身处何地都能连接到互联网。因此,许多游戏现在都具备连接社交媒体网站(如 Facebook)或使用苹果的 Game Center 等服务分享分数的功能。

在本章中,我们将探讨如何使用 Marmalade 将以下在线功能添加到我们的游戏中:

  • 启动网页浏览器以显示网页

  • 在 iOS 和 Android 上与 Facebook 集成

  • 熟悉其他在线功能的可能性,包括广告和在应用内购买

在设备浏览器中启动网页

让我们通过查看向我们的游戏中添加在线功能的最简单方法——在设备浏览器中启动网页——来开始我们对连接世界的探索。

能够将用户引导到网站对于诸如说明书、提示和技巧或技术支持访问等事项非常有用。它对于通过提供一个简单的“获取更多游戏”按钮来促进跨推广也非常好,这个按钮突出显示了你创建的其他游戏。

我们是如何实现这个魔法的?这真的很简单!只需包含头文件s3eOSExec.h,然后调用s3eOSExecAvailable来查看我们正在运行的平台上是否支持此功能。Marmalade 支持的大多数平台都允许此功能,但最好总是检查一下!

如果支持可用,我们只需调用函数s3eOSExecExecute,并传入网页的 URL 和一个布尔值,表示我们的应用程序是否将退出。在不支持多任务处理的平台上,此参数将不起作用,因此通常可以将此标志设置为false以确保我们的应用程序不会被关闭。

这里有一个代码片段来演示:

if (s3eOSExecAvailable())
{
  s3eOSExecExecute("http://www.google.com", false);
}

这种方法的主要缺点是,通过在设备的内部网页浏览器中启动应用程序,它会将用户从我们的游戏中带走;但在前面提到的案例中,鉴于实现起来非常简单,这可能是一个可以接受的权衡。

与社交媒体集成

社交媒体网站如 Facebook 通过让玩家为我们传播信息,为我们的游戏提供了一种很好的广告方式。有无数的游戏例子允许玩家在他们的 Facebook 墙或 Twitter 上发布消息,以展示他们最新的高分或吹嘘在游戏中达到的某个目标。

在本节中,我们将详细探讨如何实现与 Facebook 的集成,同时也会简要地讨论 Twitter。

使用 Facebook

Marmalade 附带一个名为 s3eFacebook 的 API,它封装了与 Facebook 服务器通信的大部分复杂操作。不幸的是,这种易用性是有代价的,那就是它只支持 iOS 和 Android。

如果需要在所有平台上支持 Facebook,我们需要从头开始使用 Marmalade 提供的 IwHTTP API 通过 HTTP 请求来实现一切。这是一个具有挑战性的任务,所以在这部分书中我们将使用 s3eFacebook API。

创建 Facebook 应用

将 Marmalade 项目与 Facebook 集成的第一步是在 Facebook 网站上创建一个 Facebook 应用,这实际上只是验证任何 Facebook API 请求来源的一种方式。

当我们创建一个 Facebook 应用时,我们会得到两个十六进制值。其中一个被称为App Id(有时也称为API Key),另一个是App Secret。当我们向 Facebook 发送请求以在 Facebook 服务器上识别我们的应用时,这些值将是必需的。

要创建 Facebook 应用,请按照以下步骤操作:

  1. 通过访问www.facebook.com并输入用户名和密码来登录 Facebook。如果您还没有 Facebook 账户,您也可以在此地址注册一个。创建 Facebook 应用

  2. 登录到 Facebook 后,访问 URL www.facebook.com/developers。如果您之前从未创建过 Facebook 应用,您将看到一个类似于前面截图的对话框。这个屏幕有一个下拉框,允许您指定是否让所有人或只是您的朋友能看到应用创建的帖子。现在,请将此设置为默认值所有人,然后点击转到应用按钮。

  3. 现在,您将看到一个屏幕,详细列出您创建的所有 Facebook 应用,如果这是您第一次创建应用,这个屏幕将是空的!点击+ 创建新应用按钮开始创建 Facebook 应用。创建 Facebook 应用

  4. 应该现在出现的上一个对话框应该没有包含关于验证账户文本的粉红色框(更多关于这一点稍后讨论)。为了本章的目的,我们只需要提供应用名称值,这是一个字符串,当我们的 Marmalade 项目首次尝试访问 Facebook 时将显示给用户。因此,使用游戏名称或公司名称作为此字段是有意义的。

  5. 点击 Continue 按钮创建 Facebook 应用程序。请注意,现在可以忽略剩余的字段。App Namespace 的值用于在 Facebook 上作为 URL 或 HTTP 请求的一部分引用应用程序,并且是为了更高级的 Facebook 集成。LocaleWeb Hosting 控制也可以在本章中忽略。

  6. 现在,您将看到那些令人烦恼的 Captcha 对话框之一,以证明您是人类,而不是某种垃圾邮件发送的网虫。输入图片中显示的单词以继续。

  7. 在这一点上,您可能会再次看到步骤 4 中的 Create New App 对话框,这次带有小粉色框中的文本。这是 Facebook 为了阻止数百个恶意 Facebook 应用程序被创建而设置的另一个安全检查。在您创建 Facebook 应用程序之前,您需要授权您的 Facebook 账户。我建议您点击标有 mobile phone 的链接来验证您的账户,因为这无疑是迄今为止最简单的方法。您将被要求输入您的手机号码,以便发送包含授权代码的短信给您,然后您将输入该代码以验证自己。

  8. 一旦您验证了您的账户,您将再次返回到 Create New App 对话框。请确保 App Name 的值是正确的,并再次点击 Continue 按钮。Captcha 屏幕可能会再次出现,所以请填写它。创建 Facebook 应用程序

  9. 在这一点上,Facebook 应用程序已经创建,您现在应该看到与之前显示的屏幕相似的屏幕,该屏幕显示了有关 Facebook 应用程序的各种信息。最重要的是 App Id/App KeyApp Secret 值,您稍后需要它们;所以请记下它们。

创建 Facebook 测试用户

显然,我们希望在应用程序实施后测试其 Facebook 集成,但如果我们不需要向所有朋友发送测试墙贴等内容,那就更好了。因此,创建一个测试用户是个好主意。

由于可理解的原因,Facebook 并不想让我们为测试用户创建完整的 Facebook 账户,因此他们允许我们使用我们的 Facebook 应用程序创建测试用户。按照以下步骤创建测试用户:

  1. 登录 Facebook 并访问 www.facebook.com/developers 页面。

  2. 在左侧面板中点击相关的 Facebook 应用程序,然后点击页面右侧 Roles 部分的 Edit Roles 标签链接。创建 Facebook 测试用户

  3. Facebook 应用的 Roles 页面将显示(见前面的截图)。底部有一个标有 Test Users 的部分,其中有一个标有 Add 的链接,您应该点击以创建新的测试用户。

  4. 将会弹出一个包含三个选项的小对话框。第一个选项标有添加数量,是一个下拉框,允许生成一到十个测试用户。

  5. 授权此应用复选框使我们能够确定创建的用户是否已经授权 Facebook 应用使用他们的账户。值得创建两种类型的用户来全面测试我们的应用程序,但最终是否现在授权还是在我们首次尝试使用此用户账户登录时授权,取决于您。

  6. 最后,启用 Ticker复选框让您决定用户是否将使用 Facebook Ticker 界面(这是一个实时墙帖子和其他事件的时间线)或较老的标准界面。并非所有用户都有权访问较新的 Ticker 界面,因此值得使用这两种方法测试您的项目。

  7. 点击添加按钮以创建新用户。您将返回到第 3 步中首次显示的屏幕,但现在新用户将显示在页面底部。

  8. 每个测试用户旁边都会有一些链接。您应该首先点击设置密码链接,以便为该用户设置密码。将出现一个文本框,允许您输入密码。

  9. 接下来,点击其中一个用户旁边的切换到链接,以登录为该用户并显示他们的 Facebook 墙。

  10. 在测试用户墙的右上角,应该有一个标有编辑个人资料的按钮。点击它。

  11. 编辑个人资料屏幕上,点击左侧面板中的联系信息链接。

  12. 屏幕顶部应该有两个与个人资料关联的电子邮件地址。其中之一应该是<username@tfbnw.net>的形式,这是我们稍后作为测试用户登录时需要使用的电子邮件地址。记下这个电子邮件地址以及您在第 8 步中设置的密码。

将 s3eFacebook API 添加到 Marmalade 项目中

在配置了 Facebook 应用和测试用户后,让我们开始将 Facebook 支持添加到 Marmalade 项目中。首先要做的是打开项目 MKB 文件,并将s3eFacebook添加到subprojects列表中。然后,我们可以在需要使用 s3eFacebook API 函数时包含s3eFacebook.h文件。

我们还需要在deployments部分的 MKB 文件中添加另一个配置设置。问题所在行看起来像这样,并且仅适用于 iOS 构建。在 iOS 上,当我们登录到 Facebook 时,我们的应用程序会暂时失去焦点,这个值确保我们在登录过程完成后恢复控制:

iphone-bundle-url-schemes="fb0123456789abcdef"

在初始fb之后的十六进制值应替换为 Facebook 应用生成的 16 位 App Id。

检查 s3eFacebook 支持

如前所述,s3eFacebook API 仅支持 iOS 和 Android,因此能够在运行时检查我们是否能够支持 Facebook 是很好的。这可以通过使用s3eFacebookAvailable函数轻松完成,如果 API 可用,则返回S3E_TRUE,如果不可用,则返回S3E_FALSE

初始化和终止

在我们能够调用任何 s3eFacebook API 之前,我们必须首先初始化一个 Facebook 会话。我们通过调用s3eFBInit函数来完成此操作,该函数接受一个参数,即包含我们想要使用的 Facebook 应用程序 App Id 的以 null 结尾的字符串。

该函数将返回一个指向s3eFBSession实例的指针,我们将需要使用它来访问 Facebook API 并向其发出请求。

我们可以通过调用s3eFBTerminate函数来释放 Facebook 会话,该函数接受从s3eFBInit返回的会话指针作为其唯一参数。

在我们想要发出任何 Facebook 请求的第一次调用s3eFBInit函数后,我们可以使用相同的会话信息来执行应用程序的生命周期。s3eFBTerminate函数只需要在关闭时调用。

登录和退出 Facebook

在我们能够发出任何 Facebook 请求之前,我们必须首先登录到 Facebook。这是通过s3eFBSession_LogIn函数完成的,该函数接受五个参数。第一个是from s3eFBInit返回的s3eFBSession指针。然后我们可以指定一个回调函数,该函数将在成功登录后触发。还可以指定一个指向用户数据块的指针,当它被触发时,该指针将被传递到回调函数中。

回调函数可以指定为NULL,在这种情况下,我们需要通过调用s3eFBSession_LoggedIn函数来检查登录是否完成。该函数接受会话指针作为参数,当会话登录时将返回S3E_TRUE

s3eFBSession_LogIn函数的最后两个参数是一个以 null 结尾的字符串数组,列出了我们想要使用的 Facebook API 权限以及该数组中的权限数量。权限允许我们的应用程序通知用户,我们的应用程序想要在他们的账户上执行某些操作,例如在他们的墙上发布或访问他们的照片收藏。完整的权限列表可以在网页developers.facebook.com/docs/authentication/permissions/中找到。

以下示例代码展示了示例回调函数以及如何使用s3eFBSession_Login函数:

// Login callback
void LoginCallback(struct s3eFBSession* apSession,
s3eResult* apLoginResult, void* apUserData)
{
  if (*apLoginResult == S3E_RESULT_SUCCESS)
  {
    // Logged in OK
  }
  else
  {
    // Login failed
  }
}

// Log in to Facebook using the session returned from s3eFBInit.
const char* permissions[] = { "publish_stream" };

s3eFBSession_Login(lpSession, LoginCallback, NULL,
  permissions, 1);

此代码尝试登录 Facebook,请求publish_stream权限,该权限允许应用程序在用户的墙上发布内容。

当尝试进行 Facebook 登录时,我们的应用程序将失去焦点,并启动设备的 Facebook 应用程序。如果用户没有安装 Facebook 应用程序,设备上的网络浏览器将被启动。

您将被要求提供您的 Facebook 账户的登录详情,因此为了测试目的,请输入我们之前生成的测试用户账户的详情。登录后,另一个屏幕将出现,详细说明我们的应用程序想要如何使用用户的 Facebook 账户。在上一个示例中,这将是向用户的墙发布消息。如果 Facebook 应用程序尚未为 Facebook 账户授权,屏幕上还将有两个按钮,分别标有允许不允许,用户可以使用这些按钮分别授予或拒绝访问权限。

注意

Facebook 登录过程首先会检查用户是否已经通过浏览器 cookie 登录到 Facebook,如果是这种情况,则不会要求用户提供用户名和密码详情。如果测试设备也是您自己的个人设备,您在测试应用程序之前可能希望从 Facebook 登出,以避免打扰您的朋友列表中的其他人!

在授权(或实际上拒绝授权)Facebook 应用程序后,我们的应用程序将重新获得焦点,并将触发登录回调函数以说明过程是否成功。如果未授权 Facebook 应用程序或没有可用的互联网连接,登录将失败。

再次从 Facebook 登出也很简单。我们只需调用s3eFBSession_Logout函数,并将s3eFBSession指针作为其唯一参数传递。实际上,我们只有在关闭应用程序或您明确希望登出以允许使用不同的用户凭据时才需要从 Facebook 登出。只要我们的应用程序正在执行,会话就不会过期或失效。

向用户的墙发布消息

我们现在将查看游戏最常使用 Facebook 做的一件事之一:向用户的墙发布消息,以提醒他们的朋友新的高分或游戏中的成就。

为了做到这一点,我们将使用 Facebook Graph API。还有其他方法,但 Graph API 是目前最最新潮的方法,并且看起来在不久的将来不太可能被取代。

注意

有关 Facebook Graph API 的更多信息,请查看网页developers.facebook.com/docs/reference/api/,有关墙帖子详情,请查看developers.facebook.com/docs/reference/api/post/

要开始发送 Facebook Graph API 请求,我们使用函数s3eFBRequest_WithGraphPath。此函数将会话指针、所需的 Facebook Graph 路径和要使用的 HTTP 方法(GET 或 POST)作为参数。Graph 路径和 HTTP 方法都指定为以 null 终止的字符串。

如果成功,该函数将返回一个指向s3eFBRequest结构的指针,表示新的请求;如果失败,则返回NULL

在创建请求结构后,我们可以使用 s3eFBRequest_AddParamNumber s3eFBRequest_AddParamString函数向其中添加所需的各个参数。这两个函数都接受 s3eFBRequest结构指针和一个以 null 终止的字符串作为参数名称的前两个参数。对于前一个函数调用,第三个参数是一个 64 位整数值(Marmalade 定义了一个名为int64的类型),对于后一个函数,是一个指向以 null 终止的字符串的const char指针。

大多数 Graph API 值都需要你指定一个访问令牌来证明你的应用程序有权发出请求。访问令牌作为登录过程的一部分提供给我们的应用程序,我们可以使用 s3eFBSession_AccessToken函数检索它,该函数再次以会话指针作为其唯一输入。访问令牌作为const char指针返回。

可以使用 s3eFBRequest_AddParamString函数通过指定 access_token作为参数名称,并使用 s3eFBSession_AccessToken函数的返回值作为参数值,将访问令牌添加到 Graph 请求中。

一旦将所有参数添加到请求中,我们就可以使用 s3eFBRequest_Send函数将其发送到 Facebook 服务器。该函数接受请求指针作为其第一个输入,然后是一个回调函数,以及一个指向可选数据块的指针,当触发回调函数时,该数据块将被传递给回调函数。

如果请求已发送,该函数将立即返回 S3E_RESULT_SUCCESS,如果传输过程中出现问题,则返回 S3E_RESULT_ERROR。s3eFacebook API 将等待 Facebook 的请求到达,并在请求到达时调用指定的回调函数。

当请求完成时,我们应该调用 s3eFBRequest_Delete来释放与其相关的任何资源。

让我们通过一个示例来了解如何将所有这些点应用于向用户的墙发布一条简单消息:

// Sample callback function for s3eFBRequest_Send function
void RequestCallback(struct s3eFBRequest* apRequest,
   s3eResult* apRequestResult, void* apUserData)
{
   if (*apRequestResult == S3E_RESULT_SUCCESS)
   {
      // Request successful
   }
   else
   {
      // Request failed
   }

   // Free the request resources
   s3eFBRequest_Delete(apRequest);
}

// The following code snippet illustrates how we can send a request
// to Facebook using the Graph API to post a wall message

s3eFBRequest* lpRequest = s3eFBRequest_WithGraphPath(lpSession,
   "me/feed", "POST");
if (lpRequest)
{
   // Add the required parameters
   const char* lpAccessToken = s3eFBSession_AccessToken(lpSession);
   s3eFBRequest_AddParamString(lpRequest, "access_token",
      lpAccessToken);
   s3eFBRequest_AddParamString(lpRequest, "message",
      "Hello Facebook!");

   // Send the request to Facebook
   if (s3eFBRequest_Send(lpRequest, RequestCallback, NULL) ==
      S3E_RESULT_SUCCESS)
   {
      // Wait for the callback to be triggered now!
   }
   else
   {
      // Error occurred sending request, so free it
      s3eFBRequest_Delete(lpRequest);
   }
}

进一步的 s3eFacebook 功能

前几节实际上只是触及了使用 s3eFacebook 可能实现的 Facebook 集成的表面。例如,我们没有提到处理由 Facebook API 发送回我们应用程序的任何结果。有一系列以s3eFBRequest_Response为前缀的函数,允许分析 Facebook 请求的返回值。

关于整个 s3eFacebook API 的更多信息,请访问 Marmalade 文档中的Marmalade API 参考 | 扩展 API 文档 | Facebook 扩展 | Facebook API 参考

使用 Twitter

很遗憾,Marmalade 没有提供针对 Twitter 的专用内置支持;所以如果你认为 Twitter 很重要,你需要提供自己的实现。

实现这一功能的一种方法是通过直接使用 Twitter API,通过 IwHTTP API 向 Twitter 的服务器发送 HTTP 请求。这将允许创建一个应该能在所有操作系统上正常工作的解决方案;但可能需要编写大量代码来处理在线工作时可能出现的所有可能问题(例如,网络连接中断、服务器超时等)。

另一种可能性,尽管它将仅限于 iOS 和 Android,将是使用 Marmalade 扩展开发工具包EDK)来访问这两个平台上现有的 Twitter 解决方案。这可能更容易实现,因为低级别的 Twitter API HTTP 请求已经被处理;但 EDK 目前仅支持 iOS 和 Android。第十章,使用扩展开发工具包(EDK)扩展 Marmalade,本书将更详细地探讨 EDK。

如果你感兴趣在 Marmalade 中支持 Twitter,以下网页可能对你有所帮助:

dev.twitter.com/docs/twitter-libraries#cplusplus

它提到了一些现有的基于 C++的库,用于访问 Twitter,这些库可能为 Marmalade 解决方案提供一个良好的起点。

连接到其他类型的在线服务

现在我们快速浏览一下移动设备上游戏通常连接到的其他类型在线服务。虽然我们不会对这些服务进行深入探讨,但提及它们仍然有助于形成一个更全面的了解,了解可能实现的功能。

支持社交游戏网络

社交游戏网络,如苹果的 Game Center 或跨平台解决方案,如 Scoreloop 或 OpenFeint,在许多移动游戏中已成为常见。在接下来的章节中,我们将探讨 Marmalade 项目中可用于这些类型服务的可能性。

使用苹果的 Game Center

在移动游戏世界中,最知名的社会游戏系统之一无疑是苹果的 Game Center(www.apple.com/game-center/)。不出所料,这个系统完全致力于基于 iOS 的设备,所以如果你正在为 iOS 开发游戏,这可能是你支持的首选。

由于它是一个 Objective-C 库,我们无法直接访问苹果的 API,因此 Marmalade 提供了一个名为 s3eIOSGameCenter 的服务包装 API。

s3eIOSGameCenter API 对于我们来说太大,无法在这里深入探讨,但它是对标准苹果提供的 API 的相当紧密封装,因此理解如何将你在互联网上遇到的任何示例代码转换为使用 Marmalade 封装相当简单。一个演示其使用的示例项目包含在 Marmalade 安装文件夹examples\s3eIOSGameCenter中,Marmalade 文档中也有大量信息,包括Marmalade API 参考 | S3E API 文档 | S3E: 仅限 iOS | S3E iOS 游戏中心

支持 Game Center 的所有主要功能,包括排行榜和成就、多人匹配和甚至语音聊天!

使用 Scoreloop

Scoreloop 系统是一个极受欢迎的跨平台解决方案,截至写作时,它支持 iOS、Android、BlackBerry PlayBook 和 Windows Phone 7。鉴于 Marmalade 支持这三种平台中的前三种,再加上 Scoreloop 的友好人士还提供了一个可以直接在 Marmalade 项目中使用的 API 版本,如果您想在跨平台项目中支持社交游戏,这是一个非常好的选择。

Marmalade 版本的 Scoreloop 提供了对排行榜、成就和 Scoreloop 的离线多人游戏挑战系统的支持。

更多关于 Scoreloop 的信息可以在www.scoreloop.com找到,在那里您可以注册一个免费的开发者账户并下载 SDK 的最新版本。

支持内购

所谓的免费增值游戏(Freemium games)的当前流行是因为现在除了单次购买成本外,还有其他方式为游戏收费。内购IAP)的出现使我们能够免费赠送我们的游戏,同时通过向已经玩过并享受过我们游戏的用户销售额外的游戏模式或关卡包来获利。

在接下来的章节中,我们将探讨 Marmalade 如何支持 iOS 和 Android 上的内购。

为 iOS 设备添加内购功能

与 Game Center 一样,苹果提供的内购 SDK 是用 Objective-C 编写的,因此我们无法直接在 Marmalade 项目中使用它。

同样,Marmalade 通过将苹果库封装成一个名为 s3eIOSAppStoreBilling 的 API 来解决此问题。

此 API 允许我们获取可购买的产品列表及其成本。然后我们可以请求购买特定产品,当苹果的服务器处理完所有幕后工作以处理付款时,我们将收到成功或失败的通知。

就像原始的 Apple 实现一样,我们不支持在购买完成后自动下载额外数据。相反,我们必须在收到购买确认后自行实现这一功能,这可能涉及将所有“可解锁”数据与原始应用程序下载一起发送,或者从我们自己的服务器下载。

如需了解有关此 API 的更多信息,请访问 Marmalade 文档,通过访问Marmalade API 参考 | S3E API 文档 | S3E: 仅限 iOS | S3E iOS 应用商店计费,并在 Marmalade 安装目录中的examples\s3e\s3eIOSAppStoreBilling可以找到示例代码。

为 Android 设备添加内购功能

Marmalade 还为在 Android 上实现内购提供了一个名为 s3eAndroidMarketBilling 的包装 API。这个 API 的命名仍然基于原始的 Android 商店名称(Android Marketplace),但它与重命名的 Google Play 系统兼容。

很遗憾,Marmalade 无法提供一个可以针对多个平台的单一 API,这仅仅是因为 iOS 和 Android 系统的工作方式如此不同。一个很好的例子是,Google Play 系统不允许我们查询应用程序可用的产品列表。这是 Google 方面的一个真正奇怪的遗漏(尤其是考虑到您仍然需要在 Google Play 服务器上设置产品列表),这意味着我们要么必须在应用程序中硬编码产品标识符,要么提供自己的服务器来镜像这些信息。

在文档中,您可以通过访问Marmalade API 参考 | S3E API 文档 | S3E: 仅限 Android | S3E Android 市场计费来找到有关此 API 的信息,并且examples\s3e\s3eAndroidMarketBilling目录中包含一些示例代码。

使用广告

我们刚刚探讨了从您的游戏中生成收入的一种方式,即内购,但另一种方式是利用许多可用的广告解决方案之一。就像大多数网站常见的可点击广告一样,我们可以将游戏屏幕显示的一小部分空间留给广告,从而提供另一个潜在的收益来源。

以下部分将探讨我们可用的选项。

实现 iOS 设备的 iAd 支持

如您所知,Apple 为其 iOS 设备提供自己的广告解决方案,称为iAd。同样,这需要使用 Objective-C API,因此 Marmalade SDK 提供了一个名为s3eIOSIAD的 C 包装器。

这是一个非常简单的 API,允许您从 iAd 服务器请求广告。如果广告可用,您可以选择何时显示它,因此如果需要,广告只需在游戏中的特定点可见。

Marmalade API 参考 | S3E API 文档 | S3E:仅限 iOS | S3E iOS iAd中可以找到此 API 的文档,示例代码位于 Marmalade 安装目录中的examples\s3e\s3eIOSIAd

使用其他广告解决方案

由于 iAd 只能在 iOS 平台上使用,因此当我们针对其他平台时,我们被迫考虑其他可能的解决方案(尽管大多数这些其他解决方案仍然可以在 iOS 上使用!)。

Marmalade 不直接提供对任何其他广告系统的支持,但其他开发人员在这里接受了挑战,并在 Marmalade Code Community 页面上提供了自己的解决方案供使用。

在撰写本文时,有两个有用的项目称为s3eAdWhirls3eAdNinja,至少为 Android 提供了支持。这些解决方案非常巧妙,因为它们实际上针对多个移动广告源,以确保广告尽可能频繁地在您的应用程序中显示,以最大化您的收入。

IwGameAds模块是另一个开源社区项目,展示了如何与多个广告服务集成,并且可以在比您能挥动一根非常长的棍子还要多的平台上工作。其完整源代码和文档可以在以下网址找到:

www.drmop.com/index.php/iwgameads-sdk/

在不太可能的情况下,如果这些不符合您的需求,并且您想使用特定的移动广告系统,另一种可能性是使用下一章中更详细描述的扩展开发工具包来实现对该系统的支持。

示例代码

现在让我们看看本章相关联的示例代码。

Facebook 项目

Facebook 项目将本章关于发布到用户 Facebook 墙上的所有信息汇集在一个地方,这样您可以轻松地看到如何在更真实的应用程序中实现代码。

运行示例后,我们看到了两个菜单按钮。第一个按钮允许我们登录和退出 Facebook,而第二个按钮在我们成功登录后允许我们向我们的墙发布消息。状态消息将在屏幕底部显示。

s3eFacebook API 已被进一步封装到一个名为Facebook的小类中,该类处理 Facebook 的登录和注销以及构建 Graph API 请求。这是一个很好的方法,因为它提供了进一步的抽象层,并将所有 s3eFacebook API 的使用集中在一个地方。如果 Facebook 的核心 API 因任何原因(考虑到 Facebook 可能随时更改做事的方式,这是可能的)发生变化,所有需要更新的代码都很容易找到。

使用s3eOSReadStringUTF8WithDefault函数请求要发布到墙上的消息;因此,这个示例也充当了使用此 API 的指南。

如果你想构建和运行此示例代码,你需要创建自己的 Facebook 应用,并为其提供由它生成的 App Id 和 App Secret 值。app.icf文件包含两个设置,允许指定这些值(尽管目前代码中实际只使用了 App Id!)。

还需要修改项目 MKB 文件中的iphone-bundle-url-schemes行。如果此设置未更改,则应用程序在 iOS 设备上完成 Facebook 登录过程后不会重新获得焦点。

如本章前面讨论 s3eFacebook API 时提到的,此示例代码只能在 iOS 和 Android 设备上运行。

滑雪项目

本章介绍了 Facebook 支持被添加到滑雪项目中。为 Facebook 项目创建的Facebook.cppFacebook.h文件未经修改地添加到滑雪项目中,以支持向用户的墙发布消息。

当玩家达到“游戏结束”屏幕时,会检查是否提供了 Facebook 支持。如果没有,将显示正常的“游戏结束”消息,并在短暂的延迟后,用户将返回到标题屏幕。

如果 Facebook 功能可用,将显示一个略有不同的“游戏结束”屏幕。这个版本会告知玩家他们的得分,并询问他们是否想在墙上发布一条消息来向朋友炫耀。提供了标记为的按钮,让玩家选择要做什么。

如果他们点击按钮,游戏将尝试登录 Facebook,然后发布一条详细说明玩家得分的消息。请求还引用了一个图像文件和一个网页链接,这些链接也将与墙上的消息一起显示。

与之前的 Facebook 项目一样,需要创建自己的 Facebook 应用,并为其提供正确的 App Id、App Secret 以及iphone-bundle-url-schemes设置值。

摘要

在本章中,我们简要地探讨了如何将各种在线服务添加到我们的游戏中。具体来说,我们看到了如何将 Facebook 支持添加到我们的游戏中,并且现在知道了如果想要包含社交游戏、广告或应用内购买,我们应该从哪里开始着手。

这些主题中的每一个都足以填满整整一章,但遗憾的是,这本书中并没有足够的空间进行更深入的探讨。不过,希望你现在已经对可用的选项有了很好的了解。

在本章的几个地方提到了扩展开发工具包(EDK)作为实现当前 Marmalade SDK 基础部分不支持在线功能的一种可能方式。在下一章中,我们将探讨 EDK,看看我们如何访问构成 iOS 和 Android 操作系统的 API。

第十章. 使用扩展开发工具包(EDK)扩展 Marmalade

在上一章中,我们提到了 Marmalade 的扩展开发工具包EDK)是如何通过使用标准的 Marmalade API 来向 Marmalade 应用程序添加未在其他方面公开的功能的一种可能方式。

在本章中,我们将探讨以下主题:

  • EDK 概述及其必要性

  • 如何通过创建 Windows、iOS 和 Android 的 EDK 扩展来扩展 Marmalade 以支持读取陀螺仪信息

为什么 EDK 是必要的?

Marmalade SDK 通过提供一套 API,这些 API 位于每个平台特定 API 之上,实现了能够将一个代码库部署到多个平台上的神奇功能。

部署的应用程序可执行文件实际上由两个独立的文件组成。我们的应用程序代码被编译成S3E 文件,这是 Marmalade 的 Windows动态链接库DLL)等价物。该文件在所有平台上都是相同的。

为了执行我们的 S3E 文件,我们使用一个加载程序。这个程序是我们运行的平台和我们的代码之间的粘合剂。加载程序首先启动,将 S3E 文件加载到内存中,然后将控制权传递给其中的代码。如果我们的代码需要执行平台相关的调用,它实际上会向加载程序中的一个函数发出请求,然后该函数会调用正确的操作系统函数。

加载程序是一个固定的实体,我们无法更改它,因此 Marmalade 为我们提供了 EDK 系统,使我们能够进行平台特定的函数调用。Marmalade SDK 的某些部分实际上就是这样实现的;例如,s3eFacebook API 实际上是一个扩展!

EDK 的唯一问题是它不是一个完全跨平台的解决方案。在撰写本文时,只能为 iOS、Android、Windows 和 Mac OSX 编写扩展。

注意

由于本书主要关注使用 Marmalade 的 Windows 版本进行开发,因此我们不会探讨如何构建 Mac 扩展,然而,由于我们不可避免地需要使用 Apple iOS SDK,因此我们需要访问 Mac 计算机来构建 iOS 扩展,因为 iOS SDK 作为 Windows 下载不可用。有关创建 Mac 扩展的详细信息,请参阅 Marmalade 文档,前往Marmalade (C++) | 扩展开发工具包 (EDK) | 按平台划分的 EDK 指南 | OS X EDK 指南

创建用于陀螺仪输入的扩展

为了说明创建 Marmalade 扩展的过程,我们将查看如何添加对陀螺仪输入的支持。这是一个有用的添加,因为它让我们能够向我们的游戏添加全新的输入方法,同时也展示了扩展 Marmalade 功能是多么容易。

我们扩展将包含以下函数:

函数 描述
GyroscopeAvailable 这个函数由 EDK 构建过程自动为我们生成。如果当前平台支持陀螺仪扩展,则返回 S3E_TRUE;如果不支持,则返回 S3E_FALSE
GyroscopeSupported 并非所有移动设备实际上都包含陀螺仪硬件,因此这个函数提供了确定我们是否可以在游戏中使用陀螺仪的方法。该函数返回一个正常的 C++ bool 值,指示是否存在陀螺仪。
GyroscopeStartGyroscopeStop 这两个函数用于启动和停止生成陀螺仪输入数据的硬件。
GyroscopeGetXGyroscopeGetYGyroscopeGetZ 返回 X、Y 和 Z 轴的当前陀螺仪数据值。这些值以每秒弧度(float 值)的形式返回。

之前详细说明的 API 提供了提供陀螺仪支持所需的最小功能,并且故意保持简单,以便更清楚地展示构建扩展的过程。

声明扩展 API

创建扩展的第一步是指定它将包含的函数,我们将使用 S4E 文件 来完成这项工作。这个文件用于定义我们扩展的 API,最好通过一个例子来说明。如果您想跟上来,创建一个名为 Gyroscope 的新目录,并在其中创建一个名为 Gyroscope.s4e 的文件,其内容如下:

include:
#include <s3eTypes.h>

functions:
bool GyroscopeSupported() false
void GyroscopeStart() void
void GyroscopeStop() void
float GyroscopeGetX() 0.0f
float GyroscopeGetY() 0.0f
float GyroscopeGetZ() 0.0f

示例从 include: 行开始,随后跟随着任意数量的 C 预处理器命令、包含文件、结构定义和类定义,这些将成为扩展的主要头文件的一部分。在我们的例子中,我们只是包含了 s3eTypes.h 文件;但如果我们需要在扩展和调用代码之间传递大量数据,我们可能还想在这里添加结构或类、枚举和定义。

接下来是文件的 functions: 部分,这基本上是包含我们扩展将包含并可以在使用扩展的 Marmalade 项目中调用的函数的列表。

注意

我们不必在函数列表中明确列出 GyroscopeAvailable 函数。EDK 构建过程会自动为我们生成这个函数,通过取 S4E 文件名并在其末尾添加 "Available" 来实现。

如您所见,函数的列表几乎就像它们是正常的 C 函数原型一样。每个函数都单独列出,首先声明返回类型,然后是其名称和参数列表(在这个例子中,所有这些都恰好是空的!)。

此外,S4E 文件中函数列表中的每个函数还指定了它将返回的默认值,并且可以跟随着一系列可选指令,这些指令控制函数的行为、如何将其添加到扩展中以及如何调用它。我们的例子没有使用这些指令,但以下表格显示了可以指定的内容:

指令 描述
run_on_osthread 指定扩展函数应在应用程序的主要 OS 线程上执行。如果该函数执行任何类型的用户界面交互,这尤其重要,因为许多平台只允许在主线程上调用 UI 调用。
no_lock 在调用此函数时禁用线程安全锁定。默认情况下,所有扩展函数只能在任何特定时间由单个线程调用,并且会自动生成锁定代码以确保这一点。
fast 启用快速栈切换。这是一个优化选项,意味着在通过使用与加载模块相同的栈来调用扩展函数时,我们应用程序和加载器之间需要传递的数据更少。通常,加载模块和我们的应用程序代码有独立的栈。
no_assert 如果在尚未为该平台构建扩展的情况下调用扩展函数,将阻止抛出断言。函数的默认值将被返回。
order 默认情况下,S4E 文件中列出的每个函数都将按列表顺序添加到扩展中,并且此顺序在内部用于定位要调用的正确函数指针。随着我们的扩展随时间发展,我们可能想要添加或弃用函数,但仍希望将相关函数在 S4E 文件中保持在一起。通过在函数声明后添加order=x,我们表示此函数将占据函数顺序中的位置x,其中x=1表示紧接在未指定顺序值的最后一个函数之后。如果这听起来很复杂,请不要担心;对于我们的项目,我们可能永远不会需要使用此功能,因为它只有在我们将扩展提供给其他人使用时才真正成为问题!

在 S4E 文件中还可以指定一些全局指令,并且这些指令应列在文件的最开始处,在include:行之前。同样,我们的示例没有使用这些指令,但为了您的信息,它们列在以下表中:

指令 描述
no_init_term 指定扩展不需要自动生成初始化或终止函数。由于这些函数通常用于设置扩展与我们的项目代码之间的接口,因此您不太可能使用此指令。
errors 允许访问一些宏,这些宏通过自动生成函数(如许多 S3E API 中存在的GetError)使错误通信更容易实现。这些 API 构成了低级 Marmalade API。
globals 声明扩展将需要为其内部使用分配一个全局结构块,并使一些宏可用,以便支持在此结构中获取和设置值。
callbacks 表示此扩展希望使用回调,并将自动定义回调 ID 以支持此功能,使用与其他内置 S3E API 相同的方法。

为 Windows 制作扩展

我们将首先创建用于 Windows 的扩展。显然,Windows PC 很可能没有陀螺仪硬件(尽管我想不是不可能的!),但以 Windows 版本开始是最容易的,因为它不需要我们安装任何额外的软件或 SDK 来构建它。

创建 Windows 扩展

由于我们实际上不会在 Windows 上支持陀螺仪输入,我们的 API 只需要在 GyroscopeSupported 函数中返回 false,并且访问当前陀螺仪值的函数应该始终返回一个 0 值。显然,开始和停止函数需要做的是绝对没有任何操作!

我们已经创建了 S4E 文件,现在我们将开始使用它。打开 Windows 资源管理器,导航到 Gyroscope 目录,然后右键单击 Gyroscope.s4e 文件。选择菜单选项 构建 Windows 扩展,这将运行一个 Python 脚本,生成多个新文件和目录。

在主 Gyroscope 目录中创建了三个新文件:

  • Gyroscope_build.mkf 是扩展的 MKF 文件,允许我们指定构建它所需的附加通用或平台相关源文件。

  • Gyroscope.mkf 是任何使用我们扩展的 Marmalade 项目需要包含为子项目以访问扩展函数的 MKF 文件

  • Gyroscope_windows.mkb 是创建一个 Visual Studio 项目的 MKB 文件,我们可以使用它来编译扩展代码

还创建了四个子目录。我们可以安全地忽略 stamp 目录,它包含一个由 EDK 构建脚本内部使用的文件,用于跟踪扩展 API 的更改。我们还可以忽略 interface 目录中的文件,这些文件是自动生成的,不应被修改。

h 目录包含一个名为 Gyroscope.h 的单个文件,我们不应修改它,因为我们所做的任何更改都将被扩展创建脚本覆盖。然而,这个文件非常有用,因为它是我们将包含在我们的项目源文件中以便访问扩展中函数的文件。

最后是 source 目录,它反过来又包含三个更多的子目录。generic 子目录包含定义扩展默认行为的源文件,如果未提供平台特定的源文件。h 目录还包含用于构建扩展代码的所有平台的文件。虽然我们可以修改这些文件,但我们不太可能需要这样做。

这使得我们拥有一个名为 Gyroscope_platform.cpp 的单个文件的 windows 子目录。此文件包含从 S4E 文件的功能列表中提供的数据生成的我们每个扩展函数的存根。

然而,请注意,所有模拟函数都以_platform后缀结尾。EDK 系统实际上生成了一组具有 S4E 文件中指定名称的通用函数,如果存在,则调用以_platform结尾的等效函数。这是必要的,以便使用扩展的代码仍然可以在没有或无法创建扩展的平台上进行编译和执行。

实现 Windows 扩展

通常,我们需要修改Gyroscope_platform.cpp文件来实现扩展;但出于我们的目的,实际上并不需要任何更改,因为生成的模拟提供了在 Windows 上所需的函数。

显然,在这种情况下,一个 Windows 扩展有点多余,但请记住,我们始终可以创建一个更复杂的扩展,它以某种方式模拟陀螺仪行为,也许使用游戏手柄或其他输入设备。

构建 Windows 扩展

要构建扩展,我们只需双击Gyroscope\Gyroscope_windows.mkb文件来创建一个 Visual Studio 项目。一旦 Visual Studio 启动,从 Visual Studio IDE 顶部的下拉菜单中选择(x86)发布构建类型,转到菜单选项构建 | 构建解决方案(或直接按F7键),扩展的 Windows 版本将被创建。很简单!

创建 Android 扩展

现在,我们将注意力转向 Android。然而,在我们开始之前,我们需要安装一些软件,因为构建过程需要能够访问 Java 开发工具和 Android SDK。

安装 Android 开发所需的软件

首先,您需要安装 Java JDK,它可在以下地址下载:

www.oracle.com/technetwork/java/javase/downloads/index.html

注意

下载 JDK 时,请确保下载的是版本 6,而不是更新的版本 7。Android SDK 与版本 7 可能无法正确工作。

下载安装包后,执行它并按照说明将 Java 开发工具安装到您的 PC 上。

接下来,您需要下载 Android SDK 和 NDK。Android SDK 是通常用于开发 Android 应用程序的 Java 库,而 NDK 是一组额外的库,它允许 Java Android 代码与编译的 C++代码接口。

Android SDK 可在以下网址获取:

developer.android.com/sdk/index.html

它以 Windows 安装程序文件的形式提供;所以只需执行它,接受所有默认安装选项,等待安装完成。

注意

一旦安装了 Android SDK,将环境变量 ANDROID_ROOT 设置为安装目录是有用的。这可以让 Marmalade 部署工具知道 Android 平台工具的位置,以便它可以自动安装和运行连接到你的 PC 的 Android 设备上的生成的包文件。

接下来,你可以访问以下 URL 下载 Android NDK:

developer.android.com/tools/sdk/ndk/index.html

注意

根据你使用的 Marmalade 版本,你可能需要不同版本的 NDK。如果你使用的是 Marmalade 6.1 或更高版本,正如本书所预期的那样,你需要 NDK 版本 8。对于 Marmalade 的早期版本,你需要 NDK 版本 7。

NDK 以 ZIP 存档的形式提供,因此你需要使用合适的归档程序(例如,WinZip)将其解压缩。NDK 应该包含在一个名为 android-ndk-xxx 的目录中,其中 xxx 指的是 NDK 的版本号。你可以将其复制到 C: 的根目录,或者你可以设置环境变量 NDK_ROOT 以指向安装路径。

创建 Android 扩展

现在我们已经安装了必要的开发工具,我们可以通过再次使用 Windows 资源管理器来定位 Gyroscope.s4e 文件来创建 Android 扩展文件。右键单击文件并选择 构建 Android 扩展 菜单选项。

Gyroscope_android.mkbGyroscope_android_java.mkb 文件将在主 Gyroscope 目录中创建。这些文件将在稍后用于构建扩展代码。

source 目录现在将包含一个名为 android 的新目录,其中包含两个文件 Gyroscope.javaGyroscope_platform.cpp。前者是我们可以添加使用 Android SDK 代码来实现我们的扩展 API 的 Java 代码的地方。后者是我们 Marmalade 项目将调用的 C++ 代码,它反过来调用 Java 实现代码。

有可能通过使用 Java Native InterfaceJNI)来访问和调用编译后的 Java 代码,在 Gyroscope_platform.cpp 文件中实现整个扩展;但这增加了一个额外的复杂性层,通常在 Java 中实现扩展要简单得多!

实现 Android 扩展

要为 Android 实现陀螺仪代码,我们需要编辑 source\android\Gyroscope.java 文件。首先,我们需要引用我们将要使用的 Java 类;因此,将文件顶部的导入声明列表更改为如下所示:

import com.ideaworks3d.marmalade.LoaderAPI;
import com.ideaworks3d.marmalade.LoaderActivity;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

前两个导入允许我们访问一些辅助函数,这些函数提供了对诸如应用程序的主要 Activity 类(所有 Android 应用程序都需要从这个基类派生)等事物的访问。我们需要这个来访问一些系统资源。

剩余的导入是为了使用 Android SDK 的某些部分,我们将使用它们来访问陀螺仪数据。

EDK 系统已生成一个名为Gyroscope的 Java 类,其中包含我们需要实现的所有方法的占位符。不过,我们需要稍微修改类定义,因为我们需要实现一些将接收陀螺仪更新的方法。按如下方式更改类定义:

class Gyroscope implements SensorEventListener

SensorEventListener是一个 Java 接口,我们的类必须实现它以接收传感器事件(在我们的案例中,是陀螺仪数据)。

我们还将添加一些成员变量来缓存陀螺仪值,以及一个标志,我们将用它来处理某些 Android 设备返回每秒度数而不是每秒弧度值的事实。将以下代码添加到类定义的底部:

// Cached gyroscope values
private float x;
private float y;
private float z;

// Are the results in degrees/s or radians/s
private boolean mUsesDegrees;

在开始实现 EDK 本身之前,我们将添加几个私有辅助函数,以便我们可以访问 Android 的SensorManager和陀螺仪Sensor实例,这将允许我们检索当前的陀螺仪数据。在类定义的开始处添加以下两个方法:

// Helper function for accessing the Android SensorManager
private SensorManager GetSensorManager()
{
   Context lContext = (Context) LoaderActivity.m_Activity;
   SensorManager lSensorManager = (SensorManager)
      lContext.getSystemService(Context.SENSOR_SERVICE);
   return lSensorManager;
}

// Helper function for accessing the Android Gyroscope Sensor
private Sensor GetGyroscopeSensor()
{
   SensorManager lSensorManager = GetSensorManager();
   if (lSensorManager == null)
      return null;

   Sensor lGyroscope =
      lSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
   return lGyroscope;
}

GetSensorManager方法通过使用 Marmalade 应用程序的主Context类来访问全局SensorManager实例。我们使用 Marmalade 的LoaderActivity类来完成此操作,该类包含一个成员变量,它是主 Android SDK Activity类实例的引用。然后,由于Activity类继承自Context,这个引用可以被转换为Context实例的引用。

一旦我们有了Context引用,我们就用它来获取负责控制输入设备(包括陀螺仪)的 Android SensorManager类的引用。如果没有可用的引用,将返回一个null引用。

GetGyroscopeSensor方法允许我们通过请求SensorManager类的默认陀螺仪处理程序来检查是否存在陀螺仪。如果没有找到合适的处理程序(即返回值为null),则表示此设备上没有陀螺仪硬件。

现在我们可以通过查看GyroscopeSupported方法来开始实现 API。这个函数只有在设备具有陀螺仪硬件时才需要返回true。我们可以这样做:

public boolean GyroscopeSupported()
{
   Sensor lSensor = GetGyroscopeSensor();
   return lSensor != null;
}

现在是时候实现一个函数,使我们能够开始接收陀螺仪数据。找到GyroscopeStart方法,并将其更改为以下代码片段:

public void GyroscopeStart()
{
  x = 0.0f;
  y = 0.0f;
  z = 0.0f;
  mUsesDegrees = false;

  Sensor lGyroscope = GetGyroscopeSensor();
  if (lGyroscope != null)
  {
    mUsesDegrees = lGyroscope.getMaximumRange() > 100;
    GetSensorManager().registerListener(this, lGyroscope,
                  SensorManager.SENSOR_DELAY_FASTEST);
  }
}

在这个方法中,我们首先确保缓存的陀螺仪值为零,并假设设备将返回每秒弧度值。然后,我们使用我们的私有GetGyroscopeSensor方法获取陀螺仪的Sensor类实例。

为了确定这个设备返回的是角度还是弧度值,我们查看陀螺仪传感器的最大范围值。如果最大范围大于 100,我们将mUsesDegrees成员变量设置为true,因为没有更可靠的方法来确定这一点。

然后我们将我们的类实例设置为陀螺仪数据的监听器。定期地,onSensorChanged 方法(我们尚未实现)将使用新的陀螺仪值被调用。

接下来我们将实现 GyroscopeStop 函数,它应该看起来像这样:

public void GyroscopeStop()
{
  SensorManager lSensorManager = GetSensorManager();
  if (lSensorManager != null)
  {
    lSensorManager.unregisterListener(this);
  }

  x = 0.0f;
  y = 0.0f;
  z = 0.0f;
}

再次获取 SensorManager 类引用,并告诉它我们不再希望接收陀螺仪数据。我们还清除缓存的陀螺仪值,以防我们的代码在陀螺仪硬件不活跃时尝试访问它们。

我们需要实现的下一个三个方法是返回缓存的陀螺仪值的方法。这些方法很容易实现,应该看起来像这样:

public float GyroscopeGetX()
{
  return x;
}

public float GyroscopeGetY()
{
  return y;
}

public float GyroscopeGetZ()
{
  return z;
}

我们现在几乎完成了。剩下要做的就是实现 SensorEventListener 接口中我们从中派生 Gyroscope 类的监听器方法。在 GyroscopeGetZ 方法之后添加以下代码。

public void onAccuracyChanged(Sensor aSensor, int aAccuracy) 
{
}

public void onSensorChanged(SensorEvent aEvent) 
{
  if (aEvent.accuracy != SensorManager.SENSOR_STATUS_UNRELIABLE)
  {
    x = aEvent.values[0];
    y = aEvent.values[1];
    z = aEvent.values[2];

    if (mUsesDegrees)
    {
      x = (x * 3.14159267f) / 180.0f;
      y = (y * 3.14159267f) / 180.0f;
      z = (z * 3.14159267f) / 180.0f;
    }
  }
}

onAccuracyChanged 方法留空,因为它必须实现以满足接口。然而,onSensorChanged 方法很重要,因为它将接收新的陀螺仪输入值。我们首先检查传入的 SensorEvent 是否包含可靠的数据(设备本身将确定什么构成可靠数据);然后,我们只需提取新的陀螺仪值并将它们存储在我们的成员变量中。

如果我们确定设备返回的是每秒度数,我们进行快速转换到弧度,以确保我们的扩展始终返回一致值。

构建 Android 扩展

我们现在 Android 扩展的代码已经准备好构建,这甚至比 Windows 版本还要简单。我们只需要打开 Windows 资源管理器,导航到 Gyroscope 目录,然后双击第一个 Gyroscope_android_java.mkb 文件,然后是 Gyroscope_android.mkb 文件。第一个 MKB 文件将构建 Java 代码,而第二个将构建将被我们的项目代码调用的 C++ 代码,该代码反过来会调用 Java 代码。

创建 iOS 扩展

构建 iOS 的 EDK 扩展稍微复杂一些,因为它要求我们能够访问 Apple iOS SDK,因此需要一个 Apple Mac。

安装 iOS 开发所需的软件

首先,你需要下载与 Apple 的 XCode 开发环境捆绑在一起的 iOS SDK。访问以下网页,其中将包含一个链接到 Mac App Store,可以下载最新的 XCode 版本:

developer.apple.com/xcode/index.php

一旦下载了 XCode 并安装了它,你接下来需要下载 Marmalade SDK 的 Mac OS X 版本。访问以下 URL 的 Marmalade 网站,登录并下载 Marmalade 的 Mac 版本。

www.madewithmarmalade.com/downloads

将 Marmalade SDK 安装到默认位置。如果您只有一个 Marmalade 许可证,您将需要使用 Marmalade 网站,从您的 PC 释放许可证,以便您可以在 Mac 上使用它。有关如何操作的更多信息,请参阅本书的 第一章,Marmalade 入门

创建 iOS 扩展

毫不奇怪,我们以类似的方式创建 iOS 扩展所需的文件,就像 Windows 和 Android 扩展一样。只需右键单击 Gyroscope.s4e 文件,然后选择菜单选项 构建 iPhone 扩展

为 iOS 扩展将创建两个新的文件。这些是 Gyroscope_iphone.mkb,这是我们用来构建扩展代码的 MKB 文件,以及 source\iphone\Gyroscope_platform.mm,其中包含我们 API 函数的自动生成的存根。

实现 iOS 扩展

要实现 Gyroscope 扩展的 iOS 版本,我们需要编辑 Gyroscope_platform.mm 文件。此文件是一个 Objective-C 源文件,它还允许我们在同一文件中使用 C 和 C++ 代码。函数存根都是标准的 C 风格函数,但我们可以仍然在它们中使用 Objective-C 类和功能。

在 iOS 上,我们使用名为 CMMotionManager 的 Objective-C 类来访问陀螺仪数据,因此我们首先需要通过更改包含文件列表来让我们的代码了解这个类:

#include <CoreMotion/CoreMotion.h>
#include "Gyroscope_internal.h"

我们还将声明一个指向 CMMotionManager 实例的全局指针,我们将在代码的其余部分使用它。在包含文件之后添加以下行:

CMMotionManager* gpMotionManager = nil;

在我们可以访问陀螺仪之前,我们需要为这个类分配一个实例。幸运的是,EDK 构建脚本已经生成了一个名为 GyroscopeInit_platform 的函数,当我们在项目中使用扩展时,它会自动为我们调用,因此这是一个分配新的 CMMotionManager 实例的好地方,如下面的代码所示:

s3eResult GyroscopeInit_platform()
{
  gpMotionManager = [[CMMotionManager alloc] init];

  return S3E_RESULT_SUCCESS;
}

我们还需要在应用程序终止时释放该实例,并且 EDK 构建脚本再次通过 GyroscopeTerminate_platform 函数来帮助我们。我们需要修改此函数,以便在陀螺仪仍然活跃时停止它,然后释放 CMMotionManager 实例。以下是完成后的函数:

void GyroscopeTerminate_platform()
{
  GyroscopeStop_platform();
  [gpMotionManager release];
}

实现的其余部分实际上非常简单,因为 CMMotionManager 类的工作方式与我们所选择的扩展 API 非常相似。我们将从检查陀螺仪硬件是否可用开始。GyroscopeSupported_platform 函数看起来如下:

bool GyroscopeSupported_platform()
{
    return gpMotionManager.gyroAvailable;
}

启动和停止陀螺仪硬件实际上也不过是调用 CMMotionManager 类的一个方法。为了安全起见,我们用进一步的检查来包装这些调用,以确保陀螺仪可用且尚未启动或停止。

void GyroscopeStart_platform()
{
  if (gpMotionManager.gyroAvailable && !gpMotionManager.gyroActive)
  {
    [gpMotionManager startGyroUpdates];
  }
}

void GyroscopeStop_platform()
{
  if (gpMotionManager.gyroAvailable && gpMotionManager.gyroActive)
  {
    [gpMotionManager stopGyroUpdates];
  }
}

剩下的唯一事情就是获取当前的陀螺仪输入值。CMMotionManager类包含一个名为gyroDataCMGyroData类属性,该属性反过来包含一个名为rotationRateCMRotationRate属性,它保存当前的陀螺仪数据。

以下代码展示了获取 x 轴陀螺仪数据的实现方法。从这一点可以看出,如何获取 y 轴和 z 轴的值应该是相当明显的!

float GyroscopeGetX_platform()
{
  CMGyroData* lpGyroData = [gpMotionManager gyroData];
  if (lpGyroData)
  {
    CMRotationRate lpRotRate = [lpGyroData rotationRate];
    return lpRotRate.x;
  }
  else
  {
    return 0.0f;
  }
}

在我们构建扩展之前,还有最后一件事要做,那就是告诉 EDK 构建工具我们需要包含 iOS SDK 框架CoreMotion,因为这个框架包含了CMMotionManager类的代码。

要将框架添加到我们的扩展中,我们必须编辑Gyroscope.mkf文件。在文件的底部查找 iOS 的deployments部分(由于历史原因,Marmalade 将其称为“iphone 平台”)并添加以下行到其中:

iphone-link-opts="-framework CoreMotion"

构建 iOS 扩展

到目前为止,创建 iOS 扩展的所有先前步骤都可以在 Windows 或 Mac 上同样很好地完成,但这个最后一步绝对需要我们使用 Mac。

注意

我们需要确保 Mac 可以访问整个Gyroscope目录。你如何实现这一点取决于你,但一种好方法是共享Gyroscope目录到你的开发 Windows PC 上,然后在 Mac 上访问这个共享。这样代码在 Mac 上构建,但所有编译文件都已经在你 Windows 开发机器的正确位置。

要构建扩展,首先需要打开 Mac 终端窗口。将Gyroscope目录设置为终端窗口中的当前目录,然后输入以下命令行:

mkb Gyroscope_iphone.mkb –arm

这将构建扩展,我们在 Mac 上的工作就完成了。简单,但只执行一个命令就有点烦人,不是吗?

使用陀螺仪扩展

我们现在已经看到了如何为 Windows、Android 和 iOS 创建和构建扩展模块,但我们如何在 Marmalade 项目中使用它们呢?

实际上,这出奇地简单。我们只需要在项目 MKB 文件的subprojects部分引用我们的扩展(最简单的方法是从主项目目录提供到Gyroscope目录的相对路径),就像我们处理任何正常的代码模块一样,然后包含自动生成的Gyroscope.h头文件,这样我们就可以调用扩展函数。

需要注意的唯一一点是,由于可能没有为我们要针对的每个平台创建扩展,我们必须确保在调用其任何函数之前扩展可用。这可以通过使用 EDK 构建脚本为我们自动生成的GyroscopeAvailable函数轻松完成。如果这个函数返回S3E_TRUE,则扩展可用。如果它返回S3E_FALSE,对扩展函数的任何调用将触发断言,但否则不会做任何事情。

即使在 Android 上,构建或部署我们的应用程序也不需要任何特殊步骤,因为在 Java 中编写的任何代码都需要以 JAR 文件的形式提供。部署工具将自动将所需的扩展文件添加到安装包中,而无需我们做任何事情。

示例代码

以下部分详细介绍了伴随本章的代码示例。

陀螺仪项目

本项目包含本章开发过程中开发的陀螺仪扩展的完整源代码。还包含了扩展的编译版本,这样您就可以构建本章的其他示例项目,而无需首先构建扩展本身。

GyroTest 项目

GyroTest 项目是一个简单的示例,它使用了陀螺仪扩展。它演示了如何将陀螺仪扩展包含到项目中,如何检查扩展是否可用,以及如果可用,如何调用扩展函数。

无论陀螺仪支持是否可用,样本都将显示在屏幕上。如果可用,原始陀螺仪值也将显示在屏幕上。

滑雪项目

我们对滑雪项目的最终更新使其能够利用本章开发的陀螺仪扩展作为另一种可能的控制方法。

与游戏中其他输入方法一样,创建了一个名为 GyroscopeManager 的类,它封装了陀螺仪扩展。这样,所有对扩展函数的使用都集中在一个源文件中,如果将来需要以任何方式更改扩展的 API,这将使得更新变得更加容易。

无论设备多么静止,即使放在稳定的表面上,陀螺仪值总会有一定程度的抖动。GyroscopeManager 类通过维护用于控制游戏中滑雪者的陀螺仪输入的过滤版本来处理这个问题。

在主游戏循环的每次更新中,通过将每个轴的当前过滤值与新原始值之间的差异的百分比加到当前过滤值上,计算每个陀螺仪轴的新过滤值。这导致大部分抖动效果被忽略,同时不会丢失玩家有意输入的较大陀螺仪输入。

概述

在本章中,我们探讨了如何使用扩展开发工具包来扩展 Marmalade 的功能。如您所见,创建一个可以利用 EDK 当前支持的每个平台上的 API 的扩展相对容易。

陀螺仪扩展是支持尚未在主 Marmalade SDK 中公开的硬件特性的好例子,但如果您想使用可能直接为特定平台使用其本地 SDK 创建的任何第三方库,扩展也会非常有用。

随着本书的结束,你现在应该已经很好地掌握了 Marmalade SDK 的强大功能,并且希望你能抓住开发游戏并在多个极受欢迎的平台上线的机会。祝您编码愉快,创作出下一个大型游戏现象的最佳祝福!

posted @ 2025-10-09 13:24  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报