Corona-SDK-移动游戏初学者指南-全-

Corona SDK 移动游戏初学者指南(全)

原文:zh.annas-archive.org/md5/A062C0ACF1C6EB24D4DCE7039AD45F82

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书旨在介绍你在 iOS 和 Android 平台使用 Corona SDK 的基本标准。通过按部就班地构建三款独特的游戏,你将增强学习体验。除了开发游戏,你还将学习社交网络集成、应用内购买,以及将应用程序发布到苹果 App Store 和/或谷歌 Play 商店。

本书涵盖内容

第一章,开始使用 Corona SDK,首先教你如何在 Mac OS X 和 Windows 操作系统上安装 Corona SDK。你将学会如何仅用两行代码创建你的第一个程序。最后,我们将介绍构建和加载应用程序到 iOS 或 Android 设备的过程。

第二章,Lua 速成与 Corona 框架,深入探讨用于 Corona SDK 开发的 Lua 编程语言。我们将介绍 Lua 中变量、函数和数据结构的基础知识。本章还将介绍如何在 Corona 框架内实现各种显示对象。

第三章,制作我们的第一款游戏——打砖块,讨论了制作你的第一款游戏,打砖块的前半部分。你将学习如何在 Corona 项目中构建游戏文件,并创建将在屏幕上显示的游戏对象。

第四章,游戏控制,继续讨论制作你的第一款游戏,打砖块的后半部分。我们将涵盖游戏对象移动以及场景中对象之间的碰撞检测。你还将学习如何创建一个计分系统,该系统将实现游戏的胜利和失败条件。

第五章,让我们的游戏动起来,解释了如何使用精灵表来动画化游戏。本章将深入探讨在创建新游戏框架时管理动作和过渡。

第六章,播放声音和音乐,提供了如何在应用程序中应用声音效果和音乐的信息。在增强游戏开发感官体验方面,包含某种类型的音频至关重要。你将学习如何通过加载、执行和循环技术,利用 Corona 音频系统融入音频。

第七章,物理现象——下落物体,涵盖了如何在 Corona SDK 中使用显示对象实现 Box2D 引擎。你将能够自定义构建物体,并处理下落物体的物理行为。在本章中,我们将应用动态/静态物体的使用,并解释碰撞后处理的目的。

第八章,操作编排器,讨论如何使用 Composer API 管理所有游戏场景。我们还将详细介绍菜单设计,例如创建暂停菜单和主菜单。此外,你将学习如何在游戏中保存高分。

第九章,处理多设备和网络应用,提供了将你的应用程序与如 Twitter 或 Facebook 等社交网络集成的信息。这将使你的应用程序能够全球范围内触及更多受众。

第十章,优化、测试和发布你的游戏,解释了针对 iOS 和 Android 设备的应用提交过程。本章将指导你如何为 Apple App Store 设置分发供应配置文件,并在 iTunes Connect 中管理你的应用信息。Android 开发者将学习如何为发布签署他们的应用程序,以便提交到 Google Play Store。

第十一章,实现应用内购买,介绍了如何通过创建可消耗、不可消耗或订阅购买来为你的游戏实现货币化。你将使用 Corona 的商店模块在 Apple App Store 申请应用内购买。我们还将查看在设备上测试购买,以检查是否使用沙盒环境应用了交易。

附录,弹出式测验答案,包含了本书所有弹出式测验部分的答案。

你需要为本书准备以下物品

在使用 Corona SDK for Mac 开发游戏之前,你需要准备以下物品:

  • 如果你正在安装适用于 Mac OS X 的 Corona,请确保你的系统具备以下条件:

    • Mac OS X 10.9 或更高版本

    • 运行 Lion、Mountain Lion、Mavericks 或 Yosemite 的基于 Intel 的系统

    • 64 位 CPU(Core 2 Duo)

    • OpenGL 2.0 或更高版本的图形系统

  • 你必须注册 Apple Developer Program

  • XCode

  • 文本编辑器,如 TextWrangler、BBEdit 或 TextMate

在使用 Corona SDK for Windows 开发游戏之前,你需要准备以下物品:

  • 如果你使用的是 Microsoft Windows,请确保你的系统具备以下条件:

    • Windows 8、Windows 7、Vista 或 XP(Service Pack 2)操作系统

    • 1 GHz 处理器(推荐)

    • 80 MB 磁盘空间(最低要求)

    • 1 GB 内存(最低要求)

    • OpenGL 2.1 或更高版本的图形系统(大多数现代 Windows 系统中可用)

    • Java 开发工具包(JDK)的 32 位(x86)版本

    • 使用 Corona 在 Mac 或 Windows 上创建 Android 设备构建时,不需要 Android SDK

  • Java 6 SDK

  • 文本编辑器,如 Notepad ++

如果你想为 Android 设备提交和发布应用,你必须注册为 Google Play Developer。

游戏教程需要使用本书提供的资源文件,也可以从 Packt Publishing 网站下载。

最后,你需要 Corona SDK 的最新稳定版本。这适用于所有订阅级别。

本书适合的对象

这本书适合任何想要尝试为 Android 和 iOS 创建商业上成功的游戏的人。你不需要游戏开发或编程经验。

部分

在这本书中,你会发现有几个经常出现的标题(动手时间、刚刚发生了什么?、小测验和动手英雄)。

为了清楚地说明如何完成一个过程或任务,我们使用以下部分:

动手时间——标题

  1. 操作 1

  2. 操作 2

  3. 操作 3

指令通常需要一些额外的解释以确保其意义明确,因此它们后面会跟着这些部分:

刚刚发生了什么?

本节解释了你刚刚完成的工作或指令的运作方式。

你在书中还会发现一些其他的学习辅助工具,例如:

小测验——标题

这些是简短的选择题,旨在帮助你测试自己的理解。

动手英雄——标题

这些是实践挑战,为你提供实验所学知识的想法。

约定

你还会发现文本中有多种样式,用于区分不同类型的信息。以下是一些样式的例子及其含义的解释。

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理程序如下显示:"我们可以通过使用include指令包含其他上下文。"

代码块如下设置:

textObject = display.newText( "Hello World!", 160, 80, native.systemFont, 36 )
textObject: setFillColor ( 1, 1, 1 )

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

    local buyLevel2 = function ( product ) 
      print ("Congrats! Purchasing " ..product)

     -- Purchase the item
      if store.canMakePurchases then 
        store.purchase( {validProducts[1]} ) 
      else
        native.showAlert("Store purchases are not available, please try again later",  { "OK" } ) – Will occur only due to phone setting/account restrictions
      end 
    end 
    -- Enter your product ID here
 -- Replace Product ID with a valid one from iTunes Connect
 buyLevel2("com.companyname.appname.NonConsumable")

任何命令行输入或输出都如下书写:

keytool -genkey -v -keystore my-release-key.keystore -alias aliasname -keyalg RSA -validity 999999

术语重要 词汇以粗体显示。你在屏幕上看到的词,例如菜单或对话框中的,会在文本中这样出现:"点击立即注册按钮,并按照苹果的指示完成流程。"

注意

警告或重要提示会以这样的框显示。

提示

技巧和窍门会像这样出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。

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

如果你有一个擅长的主题并且有兴趣撰写或参与书籍编写,请查看我们的作者指南:www.packtpub.com/authors

客户支持

既然你现在拥有了 Packt Publishing 的一本书,我们有许多方法帮助你最大限度地利用你的购买。

下载示例代码

您可以从您的账户www.packtpub.com下载所有您购买过的 Packt Publishing 书籍的示例代码文件。如果您在别处购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给您。

下载本书的彩色图像

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

勘误

尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现了一个错误——可能是文本或代码中的错误——如果您能报告给我们,我们将不胜感激。这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击Errata Submission Form链接,并输入您的勘误详情来报告。一旦您的勘误被验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书名。所需信息将在Errata部分出现。

盗版

在互联网上,盗版受版权保护的材料是所有媒体都面临的持续问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上以任何形式遇到我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请在<copyright@packtpub.com>联系我们,并提供疑似盗版材料的链接。

我们感谢您保护我们的作者以及我们为您带来有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以联系我们<questions@packtpub.com>,我们将尽力解决问题。

第一章:开始使用 Corona SDK

在我们开始编写一些简单的游戏之前,我们需要安装并运行必要的程序,这些程序将使我们的应用程序变得生动。Corona SDK主要是一个 2D 开发引擎。如果你有 iOS 或 Android 开发的经验,你会发现使用 Corona 的经历令人耳目一新。它也非常易于使用。很快,你就能创建出可以在 Apple App Store 和 Google Play Store 发布的成品。

在本章中,我们将:

  • 在 Mac OS X 和 Windows 上设置 Corona SDK

  • 为 Mac OS X 安装 Xcode

  • 用两行代码创建一个 Hello World 程序

  • 在 iOS Provisioning Portal 中添加设备

  • 将应用程序加载到 iOS 设备上

  • 将应用程序加载到 Android 设备上

下载并安装 Corona

你可以选择 Mac OS X 或 Microsoft Windows 操作系统进行开发。请记住运行此程序所需的以下系统要求。本书中最兼容的版本是 Build 2014.2511。

如果你是在 Mac OS X 上安装 Corona,请确保你的系统具备以下特性:

  • Mac OS X 10.9 或更高版本

  • 运行 Lion、Mountain Lion、Mavericks 或 Yosemite 的基于 Intel 的系统

  • 一个 64 位的 CPU(Core 2 Duo)

  • OpenGL 2.0 或更高版本的图形系统

如果你使用的是 Microsoft Windows,请确保你的系统具备以下特性:

  • Windows 8、Windows 7、Vista 或 XP(Service Pack 2)操作系统

  • 1 GHZ 处理器(推荐)

  • 80 MB 的磁盘空间(最低)

  • 1 GB 的 RAM(最低)

  • 需要 OpenGL 2.1 或更高版本的图形系统(大多数现代 Windows 系统都可用)

  • Java 开发工具包JDK)的 32 位(x86)版本

  • 使用 Corona 在 Mac 或 Windows 上创建 Android 设备构建时,不需要 Android SDK

动手操作——在 Mac OS X 上设置并激活 Corona

让我们从在桌面上设置 Corona SDK 开始:

  1. 如果你还没有下载 SDK,请从www.coronalabs.com/downloads/coronasdk下载。在访问 SDK 之前,你需要注册成为用户。

  2. 任何 Mac 程序的文件扩展名应以.dmg结尾;这被称为 Apple 磁盘映像。下载磁盘映像后,双击磁盘映像文件进行挂载。名称应类似于CoronaSDK-XXXX.XXXX.dmg。挂载后,你应该能看到如下截图所示的已挂载磁盘映像文件夹:动手操作——在 Mac OS X 上设置并激活 Corona

  3. 接下来,将CoronaSDK文件夹拖到Applications文件夹中。这将把 Corona 文件夹的内容复制到/Applications。如果你不是账户的主要管理员,系统会提示你输入管理员密码。成功安装后,你可以在/Applications中看到CoronaSDK文件夹。为了方便访问文件夹内容,你可以将CoronaSDK文件夹拖到 Mac 桌面的 dock 上创建别名:行动时间——在 Mac OS X 上设置和激活 Corona

第一次使用 Corona SDK 的用户需要完成一次快速简单的一次性授权过程才能使用。你需要连接到互联网才能完成授权过程。

  1. 在 SDK 文件夹中启动 Corona 模拟器。

  2. 假设这是你第一次操作,系统会展示一个最终用户许可协议EULA)。接受协议后,输入你用来注册 Corona 的电子邮件和密码以激活 SDK。否则,点击注册创建一个账户。

    注意

    如果你以独立开发者的身份注册 Corona,那么在 iOS 和/或 Android 设备上进行开发是免费的。

    行动时间——在 Mac OS X 上设置和激活 Corona

  3. 登录成功后,你会看到一个确认对话框,表明 SDK 已经可以使用:行动时间——在 Mac OS X 上设置和激活 Corona

  4. 点击继续按钮,你将看到欢迎来到 Corona 的屏幕:行动时间——在 Mac OS X 上设置和激活 Corona

刚才发生了什么?

在你的 Mac 操作系统上设置 Corona SDK 就像安装其他任何专门的 Mac 程序一样简单。在你在机器上授权 SDK 并用你的电子邮件和密码登录后,它就可以使用了。从现在开始,每次你启动 Corona,它都会自动登录到你的账户。当这种情况发生时,你会注意到屏幕上会出现 Corona SDK 的欢迎界面。

行动时间——在 Windows 上设置和激活 Corona

让我们按照以下步骤在桌面上安装 Corona SDK:

  1. www.coronalabs.com/downloads/coronasdk下载 Corona SDK。在访问 SDK 之前,你需要注册成为用户。

  2. Corona 在 Windows 版本的文件扩展名应为.msi,这是微软制作的 Windows 安装程序的一部分,用于安装程序。双击该文件。文件名应该类似于CoronaSDK.msi

  3. 按照屏幕上的指示进行安装。

  4. Corona 默认会直接安装到您的Programs文件夹中。在 Microsoft Windows 上,您可以从开始菜单的程序列表中选择Corona Simulator,或者双击桌面上的 Corona 图标。成功激活后,您应该会看到以下屏幕:行动时间 - 在 Windows 上设置和激活 Corona

  5. 启动 Corona SDK 的激活过程应该与 Mac 上的操作相同,这是您第一次启动 Corona 时的步骤。

    注意

    如果您遇到图像显示不正常的问题,请检查您是否使用的是最新的 OpenGL 图形驱动程序,至少是 2.1 版本。

    请注意,Windows 上的 Corona SDK 只能为 Android 设备构建,不能为 iOS 设备(iPhone、iPad 或 iPod Touch)构建。而 Mac 不仅可以为 iOS 构建,也可以为 Android 设备构建 Corona。

  6. 要创建设备构建,您需要在 PC 上安装 Java 6 SDK。您需要访问 Oracle 网站 www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloads-javase6-419409.html 下载 JDK,并点击Java SE Development Kit 6u45链接。

  7. 在下一页,选择接受许可协议的单选按钮,然后点击Windows x86链接下载安装程序。如果您还没有 Oracle 网站的用户账户,系统会要求您登录或创建一个。

  8. 一旦下载了 JDK,请运行安装程序。安装完成后,您就可以在 PC 上为 Android 创建设备构建了。

刚才发生了什么?

在 Windows 上安装 SDK 的过程与在 Mac OS X 上的设置过程不同。执行安装文件时,Windows 会自动提供一个指定的位置来安装应用程序,比如Programs文件夹,这样您就不必手动选择目的地。安装成功后,您会在桌面上看到 Corona SDK 的图标以便快速访问,或者在您首次访问时,它可能会在开始菜单的程序列表中突出显示。当您在计算机上授权 Corona 并使用您的登录信息登录后,它就可以使用了,并且每次启动时都会自动登录。

在 Mac 和 Windows 上使用模拟器

在 Mac OS X 上,可以通过选择Applications目录中的 Corona 终端或 Corona 模拟器来启动 Corona SDK。这两个选择都可以访问 SDK。Corona 模拟器只打开模拟器,而 Corona 终端会同时打开模拟器和终端窗口。终端有助于调试您的程序,并显示模拟器错误/警告和print()消息。

在 Microsoft Windows 上,选择 Corona SDK 文件夹,并从开始菜单中的程序列表中点击 Corona Simulator,或者双击桌面上的 Corona 图标。如果你使用的是 Windows,模拟器和终端将始终一起打开。

让我们回顾一下 Corona SDK 文件夹(在 Mac 上的 Applications/Corona SDK,在 Windows 上的 Start/All Apps/Corona SDK)中有用的内容:

  • 调试器(Mac)/Corona 调试器(Windows):这是一个工具,用于查找并隔离代码中的问题。

  • Corona 模拟器:这是用于启动你的应用程序进行测试的环境。它在你本地计算机上模拟你正在开发的移动设备。在 Windows 上,它将同时打开模拟器和终端。

  • Corona 终端:这会启动 Corona 模拟器并打开一个终端窗口,以显示错误/警告消息和 print() 语句。这对于调试代码非常有帮助,但仅在 Mac 上可用。

  • 模拟器:这具有与 Corona 终端相同的属性,但它是从命令行调用的,并且仅在 Mac 上可用。

  • 示例代码:这是一组示例应用程序,帮助你开始使用 Corona。它包含代码和艺术资源以供使用。

启动模拟器时,Corona SDK 窗口会自动打开。你可以在模拟器中打开 Corona 项目,创建设备构建以进行测试或分发,并查看一些示例游戏和应用,以便熟悉 SDK。

动手时间——在模拟器中查看示例项目

让我们在模拟器中看看 HelloPhysics 示例项目:

  1. 点击 Corona SDK 文件夹中的 Corona Simulator

  2. 当 Corona SDK 窗口启动时,点击 Samples 链接。在出现的 Open 对话框中,导航到 Applications/CoronaSDK/SampleCode/Physics/HelloPhysics(Mac)或 C:\Program Files (x86)\Corona Labs\Corona SDK\Sample Code\Physics\HelloPhysics(Windows)。在 Mac 上,点击 Open,它将自动打开 main.lua。在 Windows 上,双击 main.lua 打开文件。HelloPhysics 应用程序将在模拟器中打开并运行。

刚才发生了什么?

通过 Corona 终端或 Corona 模拟器访问 SDK 是个人偏好的问题。许多 Mac 用户更喜欢使用 Corona 终端,这样他们可以追踪输出到终端的消息。当你通过 Corona 模拟器启动 SDK 时,将显示模拟器,但不会显示终端窗口。当 Windows 用户启动 Corona 模拟器时,它将同时显示模拟器和终端窗口。当你想要尝试 Corona 提供的示例应用程序时,这种方式很方便。

main.lua 文件是一个特殊的文件名,它告诉 Corona 在项目文件夹中从哪里开始执行。这个文件还可以加载其他代码文件或程序资源,如声音或图形。

当你在 Corona 中启动HelloPhysics应用程序时,你会观察到模拟器中的盒子对象从屏幕顶部落下并与地面对象碰撞。从启动main.lua文件到在模拟器中查看结果的过程几乎是立即的。

动手试试——使用不同的设备壳。

当你开始熟悉 Corona 模拟器时,无论是在 Windows 还是 Mac OS X 中,启动应用程序时总是使用默认设备。Windows 使用 Droid 作为默认设备,而 Mac OS X 使用常规 iPhone。尝试在不同的设备壳中启动示例代码,以查看模拟器所有可用设备之间的屏幕分辨率差异。

当将构建版本移植到多个平台时,你需要考虑 iOS 和 Android 设备中各种屏幕分辨率。构建是你所有源代码的编译版本,转换成一个文件。让你的游戏构建适用于多个平台可以扩大你的应用程序受众。

选择文本编辑器

Corona 没有指定的程序编辑器用于编写代码,因此你需要找到一个符合你需求的编辑器。

对于 Mac OS,TextWrangler 是一个不错的选择,而且它是免费的!你可以在www.barebones.com/products/textwrangler/download.html下载它。其他文本编辑器如 BBEdit(www.barebones.com/thedeck)和 TextMate(macromates.com/)也非常好,但使用它们需要购买。TextMate 还兼容 Corona TextMate Bundle,可在www.ludicroussoftware.com/corona-textmate-bundle/index.html获取。

对于 Microsoft Windows,推荐使用 Notepad++,可以从notepad-plus-plus.org/下载。

以下文本编辑器兼容 Mac OS 和 Microsoft Windows:

操作系统自带的任何文本编辑器,如 Mac 的 TextEdit 或 Windows 的 Notepad,都可以使用,但使用专为编程设计的编辑器会更容易。对于 Corona 来说,使用支持 Lua 语法高亮的编辑器在编码时会更加高效。语法高亮通过为关键字和标点添加格式化属性,使读者更容易区分代码与文本。

在设备上开发

如果你只想使用 Corona 模拟器,则无需下载 Apple 的开发工具包 Xcode 或 Android SDK。若要在 iOS 设备(iPhone、iPod Touch 和 iPad)上构建和测试你的代码,你需要注册成为 Apple 开发者并创建和下载配置文件。如果你想在 Android 上开发,除非你想使用 ADB 工具帮助安装构建版本和查看调试信息,否则不需要下载 Android SDK。

Corona SDK 入门版本允许你为 iOS 构建 Adhoc(测试版)和调试版本(Android),以便在你的设备上进行测试。Corona Pro 用户还能享受特殊功能,如访问每日构建版本、高级功能、所有插件和高级支持。

操作时间——下载和安装 Xcode

要开发任何 iOS 应用程序,你需要加入 Apple 开发者计划,这需要每年支付 99 美元,并在 Apple 网站 developer.apple.com/programs/ios/ 上按照以下步骤创建一个账户:

  1. 点击立即注册按钮,并按照 Apple 的说明完成流程。在添加程序时,选择iOS 开发者计划

  2. 完成注册后,点击标记为开发中心的部分下的 iOS 链接。

  3. 滚动到下载部分,下载当前的 Xcode,或者你也可以从 Mac App Store 下载 Xcode。

  4. 完全下载 Xcode 后,从/Applications/Xcode目录中双击 Xcode。系统会要求你作为管理员用户进行身份验证:操作时间——下载和安装 Xcode

  5. 输入您的凭据后,点击确定按钮完成安装。你将看到以下屏幕:操作时间——下载和安装 Xcode

  6. 安装完 Xcode 开发者工具后,你可以通过启动 Xcode 并选择帮助菜单中的任何项目来访问文档。像 Xcode 和 Instruments 这样的开发者应用程序安装在/Applications/Xcode目录下。你可以将这些应用程序图标拖到你的 Dock 中以便快速访问。

刚才发生了什么?

我们刚才走过了如何在 Mac OS X 上安装 Xcode 的步骤。通过加入 Apple 开发者计划,你可以在网站上访问最新的开发工具。记住,要继续作为 Apple 开发者,你必须支付每年 99 美元的费用以保持订阅。

Xcode 文件相当大,所以下载需要一些时间,具体取决于你的互联网连接速度。安装完成后,Xcode 就可以使用了。

操作时间——用两行代码创建一个 Hello World 应用程序

既然我们已经设置好了模拟器和文本编辑器,让我们开始制作我们的第一个 Corona 程序吧!我们将要制作的第一款程序叫做Hello World。这是一个传统程序,许多人在开始学习一门新的编程语言时都会学习它。

  1. 打开你喜欢的文本编辑器,并输入以下几行:

    textObject = display.newText( "Hello World!", 160, 80, native.systemFont, 36 )
    textObject: setFillColor ( 1, 1, 1 )
    
  2. 接下来,在桌面上创建一个名为Hello World的文件夹。将前面的文本保存为名为main.lua的文件,保存在你的项目文件夹位置。

  3. 启动 Corona。你会看到 Corona SDK 的界面。点击打开,导航到你刚刚创建的Hello World文件夹。你应该在这个文件夹中看到你的main.lua文件:动手时间——用两行代码创建一个 Hello World 应用

  4. 在 Mac 上,点击打开按钮。在 Windows 上,选择main.lua文件并点击打开按钮。你会在 Corona 模拟器中看到你的新程序运行:动手时间——用两行代码创建一个 Hello World 应用

提示

下载示例代码

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

动手时间——修改我们的应用程序

在我们深入更复杂的示例之前,通过执行以下步骤,让我们对程序进行一些小修改:

  1. 让我们将main.lua的第二行更改为如下显示:

    textObject = display.newText( "Hello World!", 160, 80, native.systemFont, 36 )
    textObject:setFillColor( 0.9, 0.98 ,0 )
    
  2. 保存你的文件,回到 Corona 模拟器。模拟器将检测文件中的更改并自动重新启动并应用更改。如果保存文件后模拟器没有自动重新启动,请按 Command + R (Mac) / Ctrl + R (Windows)。你将在屏幕上看到以下输出:动手时间——修改我们的应用程序

注意

当你继续学习更多 Corona 函数时,你会注意到一些文本值是可选的。在这种情况下,我们需要使用五个值。

动手时间——将新字体名称应用到我们的应用程序

现在,通过执行以下步骤,让我们来玩转字体名称:

  1. 将第一行更改为以下代码行:

    textObject = display.newText( "Hello World!", 160, 80, "Times New Roman", 36 )
    
  2. 在对main.lua文件进行任何修改后,请确保保存文件;然后在 Corona 中按下 Command + R (Mac) / Ctrl + R (Windows) 重新启动模拟器以查看新字体。如果你使用的是 Mac,通常在保存文件后模拟器会自动重新启动,或者它可能会询问你是否想重新启动程序。你可以在模拟器中看到新字体:动手时间——将新字体名称应用到我们的应用程序

刚才发生了什么?

现在你已经制作了你的第一个完整的移动应用程序!更令人惊叹的是,这是一个完整的 iPhone、iPad 和 Android 应用程序。这个两行程序实际上可以安装并在你的 iOS/Android 设备上运行,如果你创建了一个构建。现在你已经了解了 Corona 中的基本工作流程。

如果你查看main.lua文件中的第 2 行,你会注意到setFillColor改变了Hello World!的文本颜色。

颜色由三组 RGB 数字组成,分别代表颜色中包含的红、绿、蓝的数量。它们用三个数字表示,数值范围从 0 到 1。例如,黑色为(0,0,0),蓝色为(0,0,1),白色为(0.6, 0.4, 0.8)。

继续尝试不同的颜色值,以查看不同的结果。当你保存main.lua文件并重新启动 Corona 时,你可以在模拟器中看到代码的更改。

当你查看main.lua文件的第一行时,你会注意到newText()是由显示对象调用的。返回的引用是textObjectnewText()函数返回一个将在屏幕上表示文本的对象。newText()函数是显示库的一部分。

当你需要访问newText的显示属性时,输入display.newTextHello World!后面的两个数字控制了文本在屏幕上的水平和垂直位置,单位为像素。接下来的项指定了字体。我们使用了native.systemFont这个名字,默认情况下,它指的是当前设备上的标准字体。例如,iPhone 的默认字体是 Helvetica。你可以使用任何标准字体名称,比如前面示例中使用的 Times New Roman。最后一个数字是字体大小。

尝试英雄——添加更多文本对象。

既然你现在开始对编程有了初步了解,请尝试在你的当前项目文件中按照以下步骤操作:

  1. 创建一个新的显示对象,并使用不同的字体和文字颜色。确保它显示在Hello World!文字下方。同时,请确保你的新显示对象拥有一个不同的对象名称。

  2. 继续改变当前显示对象textObject的值。更改xy坐标、字符串文本、字体名称,甚至字体大小。

  3. 虽然object:setFillColor( r,g,b )设置了文本的颜色,但你可以添加一个可选参数来控制文本的不透明度。尝试使用object:setFillColor( r, g, b [, a] )a的值也在 0 到 1 之间(1 是不透明,这是默认值)。观察你的文本颜色的结果。

在 iOS 设备上测试我们的应用程序

如果你只想在 Android 设备上测试应用程序,可以跳过本章节的这部分内容,直接阅读在 Android 设备上测试我们的应用程序。在我们能够将第一个 Hello World 应用程序上传到 iOS 设备之前,我们需要登录我们的 Apple 开发者账户,这样我们才能在开发机上创建和安装签名证书。如果你还没有创建开发者账户,请访问developer.apple.com/programs/ios/进行创建。记住,成为 Apple 开发者每年需要支付 99 美元的费用。

注意

Apple 开发者账户仅适用于在 Mac OS X 上开发的用户。确保你的 Xcode 版本与手机上的操作系统版本相同或更新。例如,如果你安装了 iPhone OS 版本 5.0,你将需要包含 iOS SDK 版本 5.0 或更高版本的 Xcode。

行动时间——获取 iOS 开发者证书

确保你已经注册了开发者计划;你将需要使用位于/Applications/Utilities的钥匙串访问工具,以便创建一个证书请求。在 Apple 设备上进行任何测试之前,必须使用有效的证书对所有的 iOS 应用程序进行签名。以下步骤将向你展示如何创建 iOS 开发者证书:

  1. 前往钥匙串访问 | 证书助手 | 从证书授权中心请求证书:行动时间——获取 iOS 开发者证书

  2. 用户电子邮件地址字段中,输入你注册 iOS 开发者时使用的电子邮件地址。在常用名称中,输入你的名字或团队名称。确保输入的名称与注册 iOS 开发者时提交的信息相匹配。CA 电子邮件地址字段无需填写,你可以将其留空。我们不将证书通过电子邮件发送给证书授权中心CA)。勾选保存到磁盘让我指定密钥对信息。点击继续后,系统会要求你选择一个保存位置。将你的文件保存到一个你能轻松找到的位置,比如桌面。行动时间——获取 iOS 开发者证书

  3. 在以下窗口中,确保已选择2048 位作为密钥大小,选择RSA作为算法,然后点击继续。这将生成密钥并将其保存到你指定的位置。在下一个窗口中点击完成行动时间——获取 iOS 开发者证书

  4. 接下来,访问苹果开发者网站developer.apple.com/,点击iOS 开发者中心,并登录到你的开发者账户。在屏幕右侧选择iOS 开发者计划下的证书、标识符和配置文件,然后导航到iOS 应用下的证书。点击页面右侧的+图标。在开发下,点击iOS 应用开发单选按钮。点击继续按钮,直到你到达生成证书的屏幕:行动时间——获取 iOS 开发者证书

  5. 点击选择文件按钮,找到你保存到桌面上的证书文件,然后点击生成按钮。

  6. 点击生成后,你会收到来自钥匙串访问的 CA 请求表单中指定的电子邮件通知,或者你可以直接从开发者门户下载。创建证书的人将收到此电子邮件,并通过点击批准按钮来批准请求。行动时间——获取 iOS 开发者证书

  7. 点击下载按钮,并将证书保存到一个容易找到的位置。完成此操作后,双击该文件,证书将自动添加到钥匙串访问中。行动时间——获取 iOS 开发者证书

刚才发生了什么?

现在我们有了 iOS 设备的有效证书。iOS 开发证书仅用于开发目的,有效期为大约一年。密钥对由你的公钥和私钥组成。私钥允许 Xcode 为 iOS 应用程序签名。私钥只对密钥对创建者可用,并存储在创建者的机器的系统钥匙串中。

添加 iOS 设备

在 iPhone 开发者计划中,你可以分配最多 100 个设备用于开发和测试。要注册一个设备,你需要唯一设备识别(UDID)号码。你可以在 iTunes 和 Xcode 中找到这个信息。

Xcode

要查找设备的 UDID,请将设备连接到 Mac 并打开 Xcode。在 Xcode 中,导航到菜单栏,选择窗口,然后点击组织者标识符字段中的 40 个十六进制字符字符串就是你的设备 UDID。打开组织者窗口后,你应该能在左侧的设备列表中看到你的设备名称。点击它,并用鼠标选择标识符,复制到剪贴板。

Xcode

通常,当你第一次将设备连接到组织者时,你会收到一个按钮通知,上面写着用于开发。选择它,Xcode 将为你的设备在 iOS 预配门户中完成大部分预配工作。

iTunes

在连接设备的情况下,打开 iTunes 并点击设备列表中的您的设备。选择 摘要 选项卡。点击 序列号 标签以显示 标识符 字段和 40 个字符的 UDID。按 Command + C 将 UDID 复制到剪贴板。

iTunes

行动时间 - 添加/注册您的 iOS 设备

要添加用于开发/测试的设备,请执行以下步骤:

  1. 在开发者门户中选择 设备 并点击 + 图标以注册新设备。选择 注册设备 单选按钮以注册一个设备。

  2. 名称 字段中为您的设备创建一个名称,并通过按 Command + V 将您保存在剪贴板上的设备 UDID 粘贴到 UDID 字段中。

  3. 完成后点击 继续 并在验证设备信息后点击 注册行动时间 - 添加/注册您的 iOS 设备

行动时间 - 创建一个 App ID

现在,您已经在门户中添加了一个设备,接下来需要创建一个 App ID。App ID 具有一个由 Apple 生成的唯一 10 个字符的 Apple ID 前缀和一个由配置门户中的团队管理员创建的 Apple ID 后缀。App ID 可能如下所示:7R456G1254.com.companyname.YourApplication。要创建新的 App ID,请使用以下步骤:

  1. 在门户的 标识符 部分点击 App IDs 并选择 + 图标。行动时间 - 创建一个 App ID

  2. App ID 描述 字段中填写您的应用程序名称。

  3. 您已经分配了一个 Apple ID 前缀(也称为团队 ID)。

  4. App ID 后缀 字段中,为您的应用指定一个唯一标识符。您可以根据自己的意愿来标识应用,但建议您使用反向域名风格的字符串,即 com.domainname.appname。点击 继续 然后点击 提交 以创建您的 App ID。

注意

您可以在捆绑标识符中创建一个通配符字符,以便在共享同一钥匙串访问的一组应用程序之间共享。为此,只需创建一个带有星号 (*) 结尾的单个 App ID。您可以将此字符单独放在捆绑标识符字段中,或者作为字符串的结尾,例如,com.domainname.*。关于此主题的更多信息可以在 iOS 配置门户的 App IDs 部分找到,链接为developer.apple.com/ios/manage/bundles/howto.action

刚才发生了什么?

所有设备的 UDID 都是唯一的,我们可以在 Xcode 和 iTunes 中找到它们。当我们在 iOS 配置门户中添加设备时,我们获取了由 40 个十六进制字符组成的 UDID,并确保我们创建了一个设备名称,以便我们可以识别用于开发的设备。

现在我们有了想要安装在设备上的应用程序的 App ID。App ID 是 iOS 用来允许您的应用程序连接到 Apple Push Notification 服务、在应用程序之间共享钥匙串数据以及与您希望与 iOS 应用程序配对的外部硬件配件进行通信的唯一标识符。

配置文件

配置文件是一组数字实体,它将应用程序和设备独特地绑定到一个授权的 iOS 开发团队,并使设备能够用来测试特定的应用程序。配置文件定义了应用程序、设备和开发团队之间的关系。它们需要为应用程序的开发和分发方面进行定义。

行动时间 - 创建配置文件

要创建配置文件,请访问开发者门户的配置文件部分,并点击+图标。执行以下步骤:

  1. 开发部分下选择iOS App Development单选按钮,然后选择继续

  2. 在下拉菜单中选择您为应用程序创建的App ID,然后点击继续

  3. 选择您希望包含在配置文件中的证书,然后点击继续

  4. 选择您希望授权此配置文件的设备,然后点击继续

  5. 创建一个配置文件名称,完成后点击生成按钮:行动时间 - 创建配置文件

  6. 点击下载按钮。在文件下载时,如果 Xcode 尚未打开,请启动 Xcode,并在键盘上按Shift + Command + 2打开组织者

  7. 下,选择配置文件部分。将您下载的.mobileprovision文件拖到组织者窗口中。这将自动将您的.mobileprovision文件复制到正确的目录中。行动时间 - 创建配置文件

刚才发生了什么?

在配置文件中有权限的设备只要证书包含在配置文件中,就可以用于测试。一个设备可以安装多个配置文件。

应用程序图标

当前,我们的应用程序在设备上没有图标图像显示。默认情况下,如果应用程序没有设置图标图像,一旦构建被加载到您的设备上,您将看到一个浅灰色框以及下面的应用程序名称。因此,启动您喜欢的创意开发工具,让我们创建一个简单的图像。

标准分辨率 iPad2 或 iPad mini 的应用程序图标图像文件为 76 x 76 px PNG。图像应始终保存为Icon.png,并且必须位于您当前的项目文件夹中。支持视网膜显示的 iPhone/iPod touch 设备需要一个额外的 120 x 120 px 高分辨率图标,而 iPad 或 iPad mini 需要一个 152 x 152 px 的图标,命名为Icon@2x.png

您当前项目文件夹的内容应如下所示:

Hello World/       name of your project folder
 Icon.png           required for iPhone/iPod/iPad
 Icon@2x.png   required for iPhone/iPod with Retina display
 main.lua

为了分发你的应用,App Store 需要一张 1024 x 1024 像素的应用图标。最好先以更高分辨率创建你的图标。参考Apple iOS Human Interface Guidelines获取最新的官方 App Store 要求,访问developer.apple.com/library/ios/#documentation/userexperience/conceptual/mobilehig/Introduction/Introduction.html

创建应用程序图标是你应用程序名称的视觉表示。一旦你编译了构建,你将能在设备上查看该图标。该图标也是启动你应用程序的图像。

为 iOS 创建 Hello World 版本构建

现在我们准备为我们的设备构建 Hello World 应用程序。由于我们已经有了配置文件,从现在开始的构建过程非常简单。在创建设备版本之前,请确保你已连接到互联网。你可以为 Xcode 模拟器或设备测试你的应用。

动手操作时间——创建 iOS 版本

按以下步骤在 Corona SDK 中创建一个新的 iOS 版本:

  1. 打开 Corona 模拟器并选择打开

  2. 导航至你的 Hello World 应用程序,并选择你的main.lua文件。

  3. 当应用程序在模拟器中启动后,请导航至 Corona 模拟器的菜单栏,选择文件 | 构建 | iOS,或者在你的键盘上按Command + B。以下对话框将会出现:动手操作时间——创建 iOS 版本

  4. 应用名称字段中为你的应用创建一个名称。我们可以保持名称为Hello World。在版本字段中,保持数字为1.0。为了在 Xcode 模拟器中测试应用,从构建为下拉菜单中选择Xcode Simulator。如果你想为设备构建,选择Device以构建应用包。接下来,从支持设备下拉菜单中选择目标设备(iPhone 或 iPad)。从代码签名标识下拉菜单中选择你为特定设备创建的配置文件。它与 Apple 开发者网站上 iOS Provisioning Portal 中的Profile Name相同。在保存到文件夹部分,点击浏览并选择你希望保存应用的位置。

    如果对话框中的所有信息都已确认,请点击构建按钮。

提示

将你的应用程序设置为保存到桌面会更加方便;这样容易找到。

刚才发生了什么?

恭喜你!现在你已经创建了可以上传到设备的第一款 iOS 应用程序文件。随着你开始开发用于分发的应用程序,你将希望创建应用程序的新版本,以便跟踪每次新构建所做的更改。你的所有信息从你的供应配置文件在 iOS 供应门户中创建,并应用于构建。Corona 编译完构建后,应用程序应该位于你保存它的文件夹中。

行动时间 – 在你的 iOS 设备上加载应用程序

选择你创建的 Hello World 构建版本,并选择 iTunes 或 Xcode 将你的应用程序加载到 iOS 设备上。它们可以用来传输应用程序文件。

如果使用 iTunes,将你的构建版本拖到 iTunes 资料库中,然后像以下屏幕截图所示正常同步你的设备:

行动时间 – 在你的 iOS 设备上加载应用程序

将应用程序安装到设备上的另一种方式是使用 Xcode,因为它提供了一种方便的方法来安装 iOS 设备应用程序。执行以下步骤:

  1. 连接设备后,通过菜单栏的 窗口 | Organizer 打开 Xcode 的 Organizer,然后在左侧的 设备 列表中找到你的连接设备。

  2. 如果建立了正确的连接,你会看到一个绿色指示灯。如果几分钟之后变成黄色,尝试关闭设备然后再打开,或者断开设备连接并重新连接。这通常可以建立正确的连接。行动时间 – 在你的 iOS 设备上加载应用程序

  3. 只需将你的构建文件拖放到 Organizer 窗口的 应用程序 区域,它就会自动安装到你的设备上。

刚才发生了什么?

我们刚刚学习了两种将应用程序构建加载到 iOS 设备上的不同方法:使用 iTunes 和使用 Xcode。

使用 iTunes 可以轻松实现拖放功能到你的资料库,并且只要设备同步,就可以传输构建的内容。

Xcode 方法可能是将构建加载到设备上最简单且最常见的方式。只要你的设备在 Organizer 中正确连接并准备好使用,你只需将构建拖放到应用程序中,它就会自动加载。

在 Android 设备上测试我们的应用程序

在 Android 设备上创建和测试构建版本不需要像苹果为 iOS 设备那样需要开发者账户。为 Android 构建所需的唯一工具是 PC 或 Mac、Corona SDK、安装的 JDK6 和一个 Android 设备。如果你打算将应用程序提交到 Google Play 商店,你需要注册成为 Google Play 开发者,注册地址为 play.google.com/apps/publish/signup/。如果你想在 Google Play 商店上发布软件,需要支付一次性的 25 美元注册费。

为 Android 创建 Hello World 构建版本

构建我们的 Hello World 应用程序相当简单,因为我们不需要为调试构建创建唯一的密钥库或密钥别名。当你准备将应用程序提交到 Google Play 商店时,你需要创建一个发布构建并生成自己的私钥来签名你的应用。我们将在本书的后面详细讨论发布构建和私钥。

动手操作——创建一个 Android 构建

按照以下步骤在 Corona SDK 中创建一个新的 Android 构建:

  1. 启动 Corona 模拟器,并选择模拟器

  2. 导航到你的 Hello World 应用程序,并选择你的main.lua文件。

  3. 当你的应用程序在模拟器中运行时,转到Corona Simulator菜单栏,然后导航至文件 | 构建为 | Android(Windows)/在键盘上按Shift + Command + B(Mac)。以下对话框将出现:动手操作——创建一个 Android 构建

  4. 应用程序名称字段中为你的应用创建一个名称。我们可以保持相同的名称,即Hello World。在版本代码字段中,如果默认数字不是 1,则将其设置为1。这个特定的字段必须始终是一个整数,并且对用户不可见。在版本名称字段中,保持数字为1.0。这个属性是显示给用户的字符串。在字段中,你需要指定一个使用传统 Java 方案的名称,这基本上是你的域名反转格式;例如,com.mycompany.app.helloworld可以作为包名称。项目路径显示你的项目文件夹的位置。最低 SDK 版本目前支持运行在 ArmV7 处理器上的 Android 2.3.3 及更新设备。在目标应用商店下拉菜单中,默认商店可以保持为 Google Play。在密钥库字段中,你将使用 Corona 提供的Debug密钥库来签名你的构建。在密钥别名字段中,如果尚未选择,请从下拉菜单中选择androiddebugkey。在保存到文件夹部分,点击浏览并选择你希望保存应用程序的位置。

  5. 如果对话框中的所有信息都已确认,请点击构建按钮。

提示

有关 Java 包名称的更多信息,请参阅 Java 文档中关于唯一包名称的部分,链接为:java.sun.com/docs/books/jls/third_edition/html/packages.html#40169

刚才发生了什么?

你已经创建了你的第一个 Android 构建!看看这有多简单?由于 Corona SDK 已经在引擎中提供了Debug密钥库和androiddebugkey密钥别名,所以大部分签名工作已经为你完成。你唯一需要做的是填写应用程序的构建信息,然后点击构建按钮来创建一个调试构建。你的 Hello World 应用程序将保存为.apk文件,保存在你指定的位置。文件名将显示为Hello World.apk

动手时间——在 Android 设备上加载应用程序

有多种方法可以将你的 Hello World 构建加载到 Android 设备上,这些方法并不需要你下载 Android SDK。以下是一些简单的方法。

一个方便的方法是通过 Dropbox。你可以在www.dropbox.com/创建一个账户。Dropbox 是一个免费服务,允许你在 PC/Mac 和移动设备上上传/下载文件。以下是通过 Dropbox 加载 Hello World 构建的步骤:

  1. 下载 Dropbox 安装程序,并在你的电脑上安装它。同时,在设备上从 Google Play 商店(也是免费的)下载移动应用并安装。

  2. 在你的电脑和移动设备上登录你的 Dropbox 账户。从电脑上,上传你的Hello World.apk文件。

  3. 上传完成后,在设备上打开 Dropbox 应用,选择你的Hello World.apk文件。你将看到一个询问你是否要安装该应用程序的屏幕。选择安装按钮。假设它安装正确,另一个屏幕将出现,显示应用程序已安装,你可以通过按打开按钮来启动你的 Hello World 应用。

.apk文件上传到设备上的另一种方法是,通过 USB 接口将其传输到 SD 卡。如果你的设备没有配备某种文件管理应用程序,你可以从 Google Play 商店下载一个很好的 ASTRO 文件管理器,它的下载地址是play.google.com/store/apps/details?id=com.metago.astro。你总是可以通过设备上的 Google Play 应用正常搜索前面提到的应用程序或类似的 apk 安装器。要将.apk文件传输到 SD 卡,请执行以下步骤:

  1. 在设备的设置中,选择应用程序,然后选择开发。如果 USB 调试模式未激活,请点击USB 调试

  2. 回到前几屏的应用程序部分。如果未知来源尚未激活,请启用它。这将允许你安装任何非市场应用程序(即调试版本)。设置完毕后,选择设备上的主页按钮。

  3. 使用 USB 电缆将设备连接到电脑。你会看到一个新通知,一个新的驱动器已经连接到你的 PC 或 Mac。访问 SD 驱动器并创建一个新文件夹。给你的 Android 构建容易识别的名字。将 Hello World.apk 文件从桌面拖放到文件夹中。

  4. 从桌面上弹出驱动器并断开设备与 USB 电缆的连接。启动 ASTRO 文件管理器或使用你在 Google Play 商店下载的任何应用。在 ASTRO 中,选择文件管理器,找到你添加到 SD 卡的文件夹并选择它。你会看到你的 Hello World.apk 文件。选择该文件,会出现一个提示询问你是否安装它。选择安装按钮,你应该会在设备的应用文件夹中看到你的 Hello World 应用程序。行动时间——在 Android 设备上加载应用程序

最简单的方法之一是通过 Gmail。如果你还没有 Gmail 账户,可以在mail.google.com/创建一个。按照以下步骤在 Gmail 账户上发送 .apk 文件:

  1. 登录你的账户,撰写一封新邮件,并将你的 Hello World.apk 文件作为附件添加到消息中。

  2. 将消息的收件人地址设为你自己的电子邮件地址并发送。

  3. 在你的 Android 设备上,确保你的电子邮件账户已经关联。一收到消息,就打开邮件。你将有机会在设备上安装该应用程序。将会有一个安装按钮或类似的显示。

刚才发生了什么?

我们刚刚学习了几种将 .apk 文件加载到 Android 设备上的方法。前面的方法是最简单的方式之一,可以快速加载应用程序而不会遇到任何问题。

使用文件管理器方法可以轻松访问你的 .apk 文件,无需任何运营商数据或 Wi-Fi 连接。使用与你的设备兼容的 USB 电缆并将其连接到电脑是一个简单的拖放过程。

一旦在电脑和移动设备上设置好,Dropbox 方法是最方便的。你需要做的就是将 .apk 文件拖放到你的账户文件夹中,任何安装了 Dropbox 应用程序的设备都可以立即访问。你也可以通过下载链接分享你的文件,这也是 Dropbox 提供的另一个很棒的功能。

如果你不想在设备和电脑上下载任何文件管理器或其他程序,设置一个 Gmail 账户并将你的 .apk 文件作为附件发送给自己很简单。唯一要记住的是,你不能在 Gmail 中发送超过 25 MB 的附件。

小测验——了解 Corona

Q1. 关于使用 Corona 模拟器,以下哪个是正确的?

  1. 你需要一个 main.lua 文件来启动你的应用程序。

  2. Corona SDK 只能在 Mac OS X 上运行。

  3. Corona 终端不会启动模拟器。

  4. 以上都不是。

Q2. 在 iPhone 开发者计划中,你可以使用多少个 iOS 设备进行开发?

  1. 50

  2. 75

  3. 5

  4. 100

Q3. 在 Corona SDK 中为 Android 构建时,版本代码必须是什么?

  1. 一个字符串。

  2. 一个整数。

  3. 它必须遵循 Java 方案格式。

  4. 以上都不是。

概要

在本章中,我们介绍了一些开始使用 Corona SDK 开发应用程序所必需的工具。无论你是在 Mac OS X 还是 Microsoft Windows 上工作,你都会注意到在两个操作系统上工作的相似性,以及运行 Corona SDK 是多么简单。

为了进一步熟悉 Corona,尝试执行以下操作:

  • 花时间查看 Corona 提供的示例代码,以了解 SDK 的功能。

  • 请随意修改任何示例代码,以更好地理解 Lua 编程。

  • 无论你是在 iOS(如果你是注册的 Apple 开发者)还是 Android 上工作,尝试在你的设备上安装任何示例代码,看看应用程序在模拟器环境之外是如何工作的。

  • 访问 Corona 实验室论坛 forums.coronalabs.com/,浏览一下 Corona SDK 开发者和工作人员关于 Corona 开发的最新讨论。

既然你已经了解了如何在 Corona 中显示对象的过程,我们将能够深入探讨其他有助于创建可操作的移动游戏的函数。

在下一章中,我们将进一步了解 Lua 编程语言的细节,你将学习到类似于 Corona 示例代码的简单编程技巧。你将对 Lua 语法有更深入的理解,并注意到与其他编程语言相比,它是多么快速和容易学习。那么,让我们开始吧!

第二章:Lua 快速入门与 Corona 框架

Lua 是用于在 Corona SDK 上进行开发的编程语言。到目前为止,你已经学会了如何使用主要资源来运行 SDK 和其他开发工具,在移动设备上创建应用程序。现在我们已经涉足编写几行代码让程序运行,让我们深入到基础中,这将使你更好地了解 Lua 的能力。

在本章中,你将学习如何:

  • 在脚本中应用变量

  • 使用数据结构来构建表

  • 使用显示对象进行操作

  • 使用对象方法和参数实现函数

  • 优化你的工作流程

那么让我们开始吧。

Lua 来拯救

Lua 是游戏编程的行业标准。它类似于 JavaScript 和 Flash 的 ActionScript。任何在这些语言中做过脚本编写的人几乎可以立即过渡到 Lua。

Lua 在创建各种应用程序和游戏中都很有用。由于它易于嵌入、执行速度快和学习曲线平缓,许多游戏程序员发现 Lua 是一种方便的脚本语言。《魔兽世界》中到处都在使用它。它还被 Electronic Arts、Rovio、ngmoco 和 Tapulous 在如《愤怒的小鸟》、《敲击复仇》、《餐厅大亨》等游戏中使用。

有关 Lua 的更多信息,请参考www.lua.org

有价值的变量

与许多脚本语言一样,Lua 也有变量。你可以将变量视为存储值的东西。当你在变量中应用一个值时,你可以使用相同的变量名来引用它。

一个应用程序由注释、块、语句和变量组成。注释永远不会被处理,但它被包含在内是为了解释一个语句或块的目的。是一组语句的集合。语句提供关于需要执行哪些操作和计算的指令;变量存储这些计算的结果。在变量中设置值称为赋值

Lua 使用三种类型的变量,如下所示:

  • 全局变量

  • 局部变量

  • 表字段(属性)

变量占用内存空间,这在各种移动设备上可能是有限的。当一个变量不再需要时,最好将其值设置为 nil,这样它可以被快速清理。

全局变量

全局变量可以在每个作用域中访问,并且可以从任何地方修改。术语“作用域”用于描述一组变量可访问的区域。你不需要声明全局变量。在你为其赋值时它就会被创建:

myVariable = 10
print( myVariable ) -- prints the number 10

局部变量

局部变量从局部作用域访问,通常从函数或代码块中调用。当我们创建一个块时,我们正在创建一个变量可以存在的作用域或一系列按顺序执行的语句。当引用一个变量时,Lua 必须找到该变量。局部化变量有助于加快查找过程,提高代码性能。使用 local 语句,它声明了一个局部变量:

local i = 5 -- local variable

下面的代码行展示了如何在块中声明一个局部变量:

x = 10    -- global 'x' variable
local i = 1

while i <= 10 do
   local x = i * 2  -- a local 'x' variable for the while block
   print( x )       -- 2, 4, 6, 8, 10 ... 20
   i = i + 1
end

print( x )  -- prints 10 from global x

表字段(属性)

表字段是通过索引唯一访问的一组变量。数组可以用数字和字符串索引,或者任何属于 Lua 的值,除了 nil。你使用整数或字符串索引到数组来为字段赋值。当索引是字符串时,该字段称为属性。所有属性都可以使用点操作符(x.y)或字符串(x["y"])来索引表。结果是一样的:

x = { y="Monday" }  -- create table 
print( x.y )  -- "Monday"
z = "Tuesday"    -- assign a new value to property "Tuesday"
print( z )  -- "Tuesday"
x.z = 20  -- create a new property 
print( x.z )  -- 20
print( x["z"] )  -- 20

关于表的更多信息将在后面的一节中讨论。

你可能已经注意到,在前面的示例代码中的某些行中有额外的文本。这些就是你所称的注释。注释以双连字符 -- 开头,但不能放在字符串内部。它们一直持续到行尾。块注释也是可用的。注释掉一个块的一个常见技巧是用 --[[]] 包围它。

下面是如何注释一行代码的示例:

a = 2
--print(a)    -- 2

这是一个块注释的示例:

--[[
k = 50
print(k)    -- 50
--]]

赋值约定

变量命名有规则。变量以字母或下划线开头,除了字母、下划线或数字外不能包含其他任何字符。变量名还不能是 Lua 的以下保留字:

  • and

  • break

  • do

  • else

  • elseif

  • end

  • false

  • for

  • function

  • if

  • in

  • local

  • nil

  • not

  • or

  • repeat

  • return

  • then

  • true

  • until

  • while

以下是有效的变量:

  • x

  • X

  • ABC

  • _abc

  • test_01

  • myGroup

以下是不合法的变量:

  • function

  • my-variable

  • 123

注意

Lua 也是一个大小写敏感的语言。例如,else 是一个保留字,但 Else 和 ELSE 是两个不同的有效名称。

值的类型

Lua 是一种动态类型的语言。在 Lua 语言中没有定义变量类型。这使得每个值都可以携带自己的类型。

正如你所注意到的,值可以存储在变量中。它们可以操作以生成任何类型的值。这也允许你将参数传递给其他函数,并将它们作为结果返回。

你将处理的值的基本类型如下:

  • Nil:这是唯一一个值为 nil 的类型。任何未初始化的变量都有 nil 作为其值。像全局变量一样,默认是 nil,可以被赋值为 nil 以删除它。

  • 布尔值:这种类型有两个值:falsetrue。你会注意到,条件表达式将 falsenil 视为假,其他任何值视为 true

  • 数字:这些代表实数(双精度浮点数)。

  • 字符串:这是一系列字符。允许 8 位字符和嵌入的零。

  • :这些是 Lua 中的数据结构。它们通过关联数组实现,这是一个不仅可以使用数字索引,还可以使用字符串或其他任何值(除了nil)索引的数组(关于这一点,本章后面会详细介绍)。

  • 函数:这些被称为 Lua 中的一等值。通常,函数可以存储在变量中,作为参数传递给其他函数,并作为结果返回。

行动时间——使用代码块打印值

让我们试一试,看看 Lua 语言有多强大。我们开始了解变量是如何工作的,以及当你给它们赋值时会发生什么。如果你有一个带有多个值的变量会怎样?Lua 如何区分它们?我们将使用 Corona 终端,这样我们就可以在终端框中看到输出的值。在这个过程中,你还会学习到其他编程技术。我们在这项练习中也会提到代码块。Lua 中执行单元被称为代码块。代码块是按顺序执行的一块代码。按照以下步骤开始学习 Lua:

如果你记得,在前面的章节中,你学习了如何为 Hello World 应用程序创建自己的项目文件夹和main.lua文件。

  1. 在你的桌面上创建一个新的项目文件夹,并将其命名为Variables

  2. 打开你喜欢的文本编辑器,并将其保存为Variables项目文件夹中的main.lua

  3. 创建以下变量:

    local x = 10 -- Local to the chunk
    local i = 1  -- Local to the chunk        
    
  4. while循环中加入以下内容:

    while (i<=x) do
      local x = i  -- Local to the "do" body
      print(x)       -- Will print out numbers 1 through 10 
      i = i + 1
    end
    
  5. 创建一个表示另一个局部体的if语句:

    if i < 20 then
      local x          -- Local to the "then" body
      x = 20
      print(x + 5)  -- 25
    else
      print(x)         -- This line will never execute since the above "then" body is already true
    end
    
    print(x)  -- 10
    
  6. 保存你的脚本。

  7. 启动 Corona 终端。确保你看到 Corona SDK 屏幕和终端窗口弹出。

  8. 导航到Variables项目文件夹,并在模拟器中打开你的main.lua文件。你会注意到模拟器中的设备是空白的,但如果你查看终端窗口,会看到代码输出的结果,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    25
    10
    
    

刚才发生了什么?

创建的前两个变量是每个代码块外的局部变量。注意在while循环的开始部分,i <= x指的是第 1 行和第 2 行的变量。while循环内的local x = i语句只对do体局部有效,并不等同于local x = 10while循环运行 10 次,每次递增一并输出值。

if语句会对比i < 20,此时i等于 11,并使用另一个在then体内的局部变量local x。因为语句为真,x等于 20 并输出x + 5的值,即25

最后一行代码 print(x) 没有附加到 while 循环或 if 语句中的任何代码块。因此,它指的是 local x = 10 并在终端窗口输出 10 的值。这可能看起来有些混淆,但理解 Lua 中局部变量和全局变量如何工作是很重要的。

表达式

表达式是代表值的实体。它可以包括数字常量、字符串、变量名、一元和二元运算以及函数调用。

算术运算符

+-*/%^ 被称为算术运算符。

这是一个二元算术运算符的例子:

t = 2*(2-5.5)/13+26
print(t)  -- 25.461538461538

这是一个模运算(除法余数)运算符的例子:

m = 18%4
print(m)  -- 2

运算符强大之处的一个例子如下:

n = 7²
print(n)  -- 49

关系运算符

关系运算符总是返回 false 或 true,并询问是或否的问题。关系运算符有 <><=>===~=

== 运算符用于测试相等性,而 ~= 运算符用于测试不等性。如果值类型不同,结果为假。否则,Lua 根据类型比较值。数字和字符串以常规方式比较。只要两个这样的值被认为是相等的,表和函数就通过引用比较,只有当它们是同一个对象时才相等。当创建新对象时,新对象与之前存在的对象是不同的。

这里有一些关系运算符的例子。它们将显示布尔结果,不能与字符串拼接:

print(0 > 1)  --false
print(4 > 2)  --true
print(1 >= 1)  --true
print(1 >= 1.5)  --false
print(0 == 0)  --true
print(3 == 2)  --false
print(2 ~= 2)  -- false
print(0 ~= 2)  -- true

逻辑运算符

Lua 中的逻辑运算符有 andornot。所有逻辑运算符将 falsenil 视为假,其他任何值视为真。

and 运算符如果其值为 falsenil,则返回第一个参数;否则,返回第二个参数。or 运算符如果其值不是 nilfalse,则返回第一个参数;否则,返回第二个参数。andor 都使用短路评估;这意味着只有必要时才会评估第二个操作数。以下是一些逻辑运算符的例子:

print(10 and 20)      -- 20
print(nil and 1)      -- nil
print(false and 1)    -- false
print(10 or 20)       -- 10
print(false or 1)     -- 1

not 运算符总是返回 true 或 false:

print(not nil)      -- true
print(not true)    -- false
print(not 2)        -- false

连接

Lua 中的字符串连接运算符由两个点表示,即".."。它将两个值作为操作数并将它们拼接在一起。如果其操作数中的任何一个为数字,则也会被转换成字符串。以下是一些连接运算符的例子:

print("Hello " .. "World")  -- Hello World

myString = "Hello"
print(myString .. " World")   -- Hello World

长度运算符

# 长度运算符用于测量字符串的长度或表的大小。字符串的长度就是它包含的字符数。一个字符被认为是一个字节。以下是一些长度运算符的例子:

print(#"*") --1
print(#"\n") --1
print(#"hello") --5
myName = "Jane Doe"
print(#myName) --8

优先级

下表显示了 Lua 中的运算符优先级,从最高到最低优先级:

  • ^

  • not#-(一元)

  • */

  • +-

  • ..

  • <><=>=~===

  • and

  • or

所有的二元运算符都是左结合的,除了^指数和..连接运算符,它们是右结合的。你可以使用括号来改变表达式的优先级。

在两个相同优先级的操作数争夺操作数的情况下,操作数属于左侧的操作符:

print(5 + 4 – 2) -- This returns the number 7

前一个表达式显示了加法和减法运算符,它们的优先级相等。第二个元素(数字4)属于加法运算符,因此表达式从数学上评估如下:

print((5 + 4) – 2) -- This returns the number 7

让我们关注基于优先级的优先规则。以下是一个示例:

print (7 + 3 * 9) -- This returns the number 34

一个没有经验的程序员可能会认为,如果从前到后评估,前一个示例的值是 90。正确的值是 34,因为乘法比加法的优先级高,所以它首先执行。为同一表达式添加括号将使其更容易阅读:

print (7 + (3 * 9)) -- This returns the number 34

字符串

在本章前面,你看到了一些使用字符序列的代码示例。这些字符序列称为字符串。字符串可以包含任何字符,包括数值。

引用字符串

有三种方式来引用字符串:使用双引号、使用单引号以及使用方括号。

注意

在引用字符串时,请确保代码中只使用直引号,而不是弯引号;否则,它将无法编译。

双引号字符"标记字符串的开始和结束。以下是一个示例:

print("This is my string.")  -- This is my string.

你也可以使用单引号字符'来引用字符串。单引号与双引号的作用相同,不同之处在于单引号字符串可以包含双引号。以下是一个示例:

print('This is another string.')  -- This is another string.

print('She said, "Hello!" ')  -- She said, "Hello!"

最后,使用一对方括号也可以引用字符串。它们主要用于当双引号或单引号不能使用时的字符串。没有很多这样的情况,但它们可以完成任务:

print([[Is it 'this' or "that?"]]) -- Is it 'this' or "that?"

动手时间——让我们充分使用字符串

我们开始熟悉几段代码以及它们之间的相互作用。让我们看看当我们添加一些使用字符串的表达式时会发生什么,以及它们与在终端中打印的普通字符串有何不同:

  1. 在你的桌面上创建一个新的项目文件夹,并将其命名为Working With Strings

  2. 在你的文本编辑器中创建一个新的main.lua文件,并将其保存到你的文件夹中。

  3. 输入以下几行(代码中不要包含行号,它们仅用于行参考):

    1 print("This is a string!") -- This is a string!
    2 print("15" + 1) -- Returns the value 16
    
  4. 添加以下变量。注意它使用了相同的变量名:

    3 myVar = 28
    4 print(myVar)  -- Returns 28
    
    5 myVar = "twenty-eight"
    6 print(myVar) -- Returns twenty-eight
    
  5. 让我们添加一些带有字符串值的变量,并使用不同的运算符进行比较:

    7 Name1, Phone = "John Doe", "123-456-7890"
    8 Name2 = "John Doe"
    
    9 print(Name1, Phone) -- John Doe  123-456-7890
    10 print(Name1 == Phone) -- false
    11 print(Name1 <= Phone) -- false
    12 print(Name1 == Name2) -- true
    
  6. 保存你的脚本并在 Corona 中启动你的项目。在终端窗口中观察结果:

    This is a string!
    16
    28
    twenty-eight
    John Doe  123-456-7890
    false
    false
    true
    
    

刚才发生了什么?

你可以看到第 1 行只是一个普通的字符串,字符被打印出来。在第 2 行,注意数字 15 在字符串中,然后与字符串外的数字 1 相加。Lua 在运行时提供数字和字符串之间的自动转换。对字符串应用数值运算会尝试将字符串转换为数字。

在使用变量时,你可以使用同一个变量,并让它们在不同时间包含字符串和数字,如第 3 行和第 5 行(myVar = 28myVar = "twenty-eight")。

在最后一段代码(第 7-12 行)中,我们使用关系运算符比较了不同的变量名。首先,我们打印了 Name1Phone 的字符串。接下来的行比较了 Name1Name2Phone。当两个字符串具有完全相同的字符顺序时,它们被认为是相同的字符串并且相等。当你查看 print(Name1 == Phone)print(Name1 <= Phone) 时,这些语句返回 false,因为它们是根据 ASCII 顺序。数字在字母之前,比较时被视为较小。在 print(Name1 == Name2) 中,两个变量包含相同的字符,因此它返回 true

动手实践——进一步操作字符串

字符串很容易处理,因为它们只是字符序列。尝试根据前面的示例进行修改,创建你自己的表达式。

  1. 创建一些带有数值的变量,再创建一组带有数值字符串的变量。使用关系运算符比较这些值,然后将结果打印出来。

  2. 使用连接运算符,将几个字符串或数字组合在一起,并使它们均匀地分隔开。在终端窗口中打印结果。

表是 Lua 中特有的数据结构。它们可以表示数组、列表、集合、记录、图等。Lua 中的表类似于关联数组。关联数组可以使用任何类型的值进行索引,不仅仅是数字。表高效地实现所有这些结构。例如,可以通过使用整数索引表来实现数组。数组没有固定的大小,但会根据需要增长。初始化数组时,其大小是间接定义的。

这是一个如何构建表的例子:

1 a = {}    -- create a table with reference to "a"
2 b = "y"
3 a[b] = 10    -- new entry, with key="y" and value=10
4 a[20] = "Monday"  -- new entry, with key=20 and value="Monday"
5 print(a["y"])    -- 10
6 b = 20
7 print(a[b])     -- "Monday"
8 c = "hello"     -- new value assigned to "hello" property
9 print( c )    -- "hello"

你会注意到第 5 行中的 a["y"] 正在从第 3 行索引值。在第 7 行,a[b] 使用变量 b 的新值并将数值 20 索引到字符串 "Monday" 上。最后一行 c 与之前的变量无关,其唯一的值是字符串 "hello"

将表作为数组传递

表的键可以是连续的整数,从 1 开始。它们可以被制作成数组(或列表):

colors =  {
[1] = "Green", 
[2] = "Blue", 
[3] = "Yellow", 
[4] = "Orange", 
[5] = "Red"
}
print(colors[4]) -- Orange

下面展示了一种更快、更方便的编写表构造函数来构建数组的方法,该方法不需要写出每个整数键:

colors = {"Green", "Blue", "Yellow", "Orange", "Red"}
print(colors[4]) -- Orange

更改表中的内容

在处理表时,你可以修改或删除表中已有的值,也可以添加新值。这可以通过赋值语句完成。以下示例创建了一个包含三个人及其最喜欢的饮料类型的表。你可以进行赋值以更改一个人的饮料,向表中添加新的人员-饮料配对,以及移除现有的人员-饮料配对:

drinks = {Jim = "orange juice", Matt = "soda", Jackie = "milk"}
drinks.Jackie = "lemonade" -- A change.
drinks.Anne = "water" -- An addition.
drinks.Jim = nil -- A removal.

print(drinks.Jackie, drinks.Anne, drinks.Matt, drinks.Jim)
-- lemonade water soda nil

drinks.Jackie = "lemonade"覆盖了drinks.Jackie = "milk"的原始值。

drinks.Anne = "water"这行代码为表格添加了一个新的键值对。在这行代码之前,drinks.Anne的值是 nil。

由于没有对其进行修改,drinks.Matt = "soda"的值保持不变。

drinks.Jim = nilnil覆盖了drinks.Jim = "orange juice"的原始值。它从表格中移除了Jim键。

填充表

填充表的方法是从一个空表开始,逐一添加内容。我们将使用构造函数,这些是创建和初始化表的表达式。最简单的构造函数是空构造函数,{}

myNumbers = {} -- Empty table constructor

for i = 1, 5 do
  myNumbers[i] = i 
end

for i = 1, 5 do
print("This is number " .. myNumbers[i])
end

以下是终端的输出结果:

--This is number 1
--This is number 2
--This is number 3
--This is number 4
--This is number 5

前面的示例表明myNumbers = {}是一个空表构造器。创建了一个for循环,并调用myNumbers[i]五次,从数字 1 开始。每次调用时,它都会增加 1,然后被打印出来。

对象

表和函数是对象;变量实际上并不包含这些值,只包含对它们的引用。表也用于所谓的面向对象编程。可以收集变量和操作这些变量的方法到对象中。这样的值称为对象,其函数称为方法。在 Corona 中,我们将更多地关注显示对象,因为它们对游戏开发至关重要。

显示对象

屏幕上显示的任何内容都是由显示对象制成的。在 Corona 中,你在模拟器中看到的资源都是显示对象的实例。你可能已经看到过形状、图像和文本,这些都是显示对象的形式。当你创建这些对象时,你将能够对它们进行动画处理,将它们变成背景,使用触摸事件与它们互动,等等。

显示对象是通过调用一个称为工厂函数的函数来创建的。每种类型的显示对象都有一个特定的工厂函数。例如,display.newCircle()创建一个矢量对象。

显示对象的实例行为类似于 Lua 表。这使得你可以在不与系统分配的属性和方法名称发生冲突的情况下,向对象添加自己的属性。

显示属性

点运算符用于访问属性。显示对象共享以下属性:

  • object.alpha:这是对象的透明度。0 表示完全透明,1.0 表示不透明。默认值为 1.0。

  • object.height:这是在本地坐标系中的高度。

  • object.isVisible:这个属性控制对象是否在屏幕上可见。True 表示可见,false 表示不可见。默认值为 true。

  • object.isHitTestable:即使对象不可见,这也允许对象继续接收击中事件。如果为 true,无论可见性如何,对象都会接收击中事件;如果为 false,则只有可见对象会发送事件。默认为 false。

  • object.parent:这是一个只读属性,返回对象的父对象。

  • object.rotation:这是当前的旋转角度(以度为单位)。可以是负数或正数。默认值为 0。

  • object.contentBounds:这是一个表格,包含屏幕坐标中的xMinxMaxyMinyMax属性。它通常用于将组中的对象映射到屏幕坐标。

  • object.contentHeight:这是屏幕坐标中的高度。

  • object.contentWidth:这是屏幕坐标中的宽度。

  • object.width:这是局部坐标中的宽度。

  • object.x:这指定了对象相对于父对象的x位置(在局部坐标中)——确切地说是相对于父对象的原点。它提供了对象的参考点相对于父对象的x位置。改变这个值将会在x方向移动对象。

  • object.anchorX:这指定了对象的对齐位置相对于父对象原点的x位置。锚点范围从 0.0 到 1.0。默认情况下,新对象的锚点设置为 0.5。

  • object.xScale:获取或设置x缩放因子。值为 0.5 会将对象在x方向缩放到 50%。缩放围绕对象的参考点进行。大多数显示对象的默认参考点是中心。

  • object.y:这指定了对象相对于父对象的y位置(在局部坐标中)——确切地说是相对于父对象的原点。

  • object.anchorY:这指定了对象的对齐位置相对于父对象原点的y位置。锚点范围从 0.0 到 1.0。默认情况下,新对象的锚点设置为 0.5。

  • object.yScale:获取或设置y缩放因子。值为 0.5 会将对象在y方向缩放到 50%。缩放围绕对象的锚点进行。大多数显示对象的默认参考点是中心。

对象方法

Corona 可以创建显示对象,将对象方法作为属性存储。有两种方法可以实现:使用点操作符(".")或使用冒号操作符(":")。这两种方式都是创建对象方法的有效方式。

这是点操作符的一个例子:

object = display.newRect(110, 100, 50, 50)
object.setFillColor(1.0, 1.0, 1.0)
object.translate( object, 10, 10 )

这是冒号操作符的一个例子:

object = display.newRect(110, 100, 50, 50)
object:setFillColor(1.0, 1.0, 1.0)
object:translate( 10, 10 )

使用点操作符调用对象方法的第一个参数会传递给对象。冒号操作符方法只是创建函数的快捷方式,涉及到的输入更少。

显示对象共享以下方法:

  • object:rotate(deltaAngle)object.rotate(object, deltaAngle):这实际上将deltaAngle(以度为单位)添加到当前的旋转属性中。

  • object:scale(sx, sy)object.scale(object, sx, sy):这有效地使用 sxsy 分别乘以 xScaleyScale 属性。如果当前的 xScaleyScale 值为 0.5,而 sxsy 也是 0.5,那么结果的比例将是 xScaleyScale 的 0.25。这将对象从原始大小的 50%缩放到 25%。

  • object:translate(deltaX, deltaY)object.translate(object, deltaX, deltaY):这将有效地将 deltaXdeltaY 分别加到 xy 属性上。这将把对象从当前位置移动。

  • object:removeSelf()object.removeSelf(object):这移除了显示对象并释放其内存,假设没有其他引用它。这相当于在同一个显示对象上调用 group:remove(IndexOrChild),但语法更简单。removeSelf() 语法也支持在其他情况下使用,例如在物理中移除物理关节。

图像

Corona 应用程序中使用了许多艺术资源图像。你会注意到,位图图像对象是一种显示对象类型。

加载图像

使用 display.newImage(filename [, baseDirectory] [, left, top]),将返回一个图像对象。图像数据是从你为图像指定的文件名中加载的,并在 system.ResourceDirectory 中查找该文件。支持的图像文件类型有 .png(仅限 PNG-24 或更高)和 .jpg 文件。避免高 .jpg 压缩,因为它可能会在设备上加载时间更长。.png 文件的质量比 .jpg 文件好,用于显示透明图像。.jpg 文件不能保存透明图像。

图像自动缩放

display.newImage() 的默认行为是自动缩放大图像。这是为了节省纹理内存。然而,有时你可能不希望图像自动缩放,参数列表中有一个可选的布尔标志可以手动控制这一点。

要覆盖自动缩放并在其全分辨率下显示图像,请使用可选的 isFullResolution 参数。默认情况下,它是 false,但如果你指定为 true,则新图像以其全分辨率加载:

display.newImage( [parentGroup,] filename [, baseDirectory] [, x, y] [,isFullResolution] )

限制和已知问题如下:

  • 不支持索引 PNG 图像文件。

  • 当前不支持灰度图像;图像必须是 RGB 格式。

  • 如果图像大于设备可能的最大纹理尺寸,图像仍将被自动缩放。这通常是 2048 x 2048 像素(iPad)对于较新、速度更快的设备来说会更大。

  • 如果你多次重新加载同一图像,后续调用 display.newImage 会忽略 isFullResolution 参数,并采用第一次传递的值。换句话说,你第一次加载图像文件的方式会影响下一次加载同一文件时的自动缩放设置。这是因为 Corona 通过自动复用已经加载的纹理来节省纹理内存。因此,你可以多次使用相同的图像,而不会消耗额外的纹理内存。

有关 Corona SDK 文档的更多信息可以在 Corona 的官方网站上找到,网址为 coronalabs.com

动手操作时间——在屏幕上放置图像

我们终于要进入本章的视觉吸引部分,开始通过图像添加显示对象。现在我们不需要参考终端窗口。因此,让我们专注于模拟器屏幕。我们将通过执行以下步骤来创建一个背景图像和一些美术资源:

  1. 首先,在桌面上创建一个新的项目文件夹,并将其命名为 Display Objects

  2. Chapter 2 Resources 文件夹中,将 glassbg.pngmoon.png 图像文件以及 config.lua 文件复制到你的 Display Objects 项目文件夹中。

  3. 启动你的文本编辑器,为当前项目创建一个新的 main.lua 文件。

  4. 编写以下几行代码:

    local centerX = display.contentCenterX
    local centerY = display.contentCenterY
    
    local background = display.newImage( "glassbg.png", centerX, centerY, true)
    local image01 = display.newImage( "moon.png", 160, 80 )
    
    local image02 = display.newImage( "moon.png" )
    image02.x = 160; image02.y = 200
    
    image03 = display.newImage( "moon.png" )
    image03.x = 160; image03.y = 320
    

    背景显示对象应该包含项目文件夹中背景图像的文件名。例如,如果背景图像文件名为 glassbg.png,那么你可以像这样显示图像:

    local background = display.newImage( "glassbg.png", centerX, centerY, true)
    

    使用 image02.x = 160; image02.y = 200 与以下几行代码是等效的:

    image02.x = 160
    image02.y = 200
    

    分号(;)表示语句的结束,是可选的。它使得在单行中分隔两个或多个语句变得更加容易,也避免了在代码中添加多余的行。

  5. 保存你的脚本并在模拟器中启动你的项目。

    注意

    如果你是在 Mac OS X 上使用 Corona SDK,默认设备是 iPhone。如果你是在 Windows 上使用,默认设备是 Droid。

  6. 你应该会看到一个背景图像和三个相同的图像显示对象,如下屏幕所示。显示结果将根据你用于模拟的设备而有所不同。动手操作时间——在屏幕上放置图像

image01image02image03 变量的显示对象应包含 moon.png 文件名。代码中的文件名区分大小写,因此请确保你按照项目文件夹中显示的格式准确书写。

刚才发生了什么?

当前,background 使用 contentCenterXcontentCenterY 被缩放以适应设备屏幕的高度和宽度。由于没有应用顶部或左侧(xy)坐标,图像在其本地原点居中。由于我们在显示对象中指定了 true,它也被设置为全分辨率。

当你在模拟器中观察image01image02image03的位置时,它们实际上是垂直对齐的,尽管image01image02/image03的脚本样式编写不同。这是因为image01的坐标基于显示对象的(左,上)坐标。你可以选择性地指定图像的左上角位于坐标(左,上);如果你没有提供两个坐标,图像将围绕其本地原点居中。

image02image03的位置是从显示对象的本地原点指定的,并通过设备屏幕的xy属性的本地值定位。本地原点位于图像的中心;参考点初始化为此点。由于我们没有为image02image03应用(左,上)值,因此进一步访问xy属性将参考图像的中心。

现在,你可能已经注意到 iPhone 4 的输出看起来很好,但 Droid 的输出显示背景图像以全分辨率显示,而其他对象则位于屏幕下方。我们可以看到我们指定的所有对象都在那里,但缩放比例不对。这是因为每个 iOS 和 Android 设备的屏幕分辨率都不同。iPhone 4 的屏幕分辨率为 640 x 960 像素,而 Droid 的屏幕分辨率为 480 x 854 像素。在一个类型的设备上看起来可能很好,但在另一个设备上可能不会完全相同。别担心,在接下来的几节中,我们将讨论使用一个config.lua文件来解决这个问题。

尝试英雄——调整显示对象属性

既然你知道如何将图像添加到设备屏幕,尝试测试其他显示属性。尝试以下任何一项:

  • 更改image01image02image03显示对象的所有xy坐标

  • 选择任何显示对象并更改其旋转

  • 更改单个显示对象的可视性

如果你不确定如何进行上述调整,请参考本章前面提到的显示属性。

运行时配置

所有项目文件不仅包含一个main.lua文件,还包含根据项目需要而定的其他.lua和相关资源。一些 Corona 项目使用config.lua文件配置,该文件编译到你的项目中,并在运行时访问。这使得你可以同时指定动态内容缩放、动态内容对齐、动态图像分辨率、帧率控制和抗锯齿,以便在每种类型的设备上显示类似的输出。

动态内容缩放

Corona 允许你指定你打算针对的屏幕尺寸。这是通过一个叫做config.lua的文件来完成的。你将能够根据设备屏幕尺寸的大小,为你的应用程序缩放资源。

应该使用以下值来缩放内容:

  • width(数字):这是原始目标设备在纵向模式下的屏幕分辨率宽度

  • height(数字):这是原始目标设备在纵向模式下的屏幕分辨率高度。

  • scale(字符串):这是以下值的自动缩放类型:

    • letterbox:这种缩放方式尽可能均匀地放大内容。

    • zoomEven:这种缩放方式均匀地放大内容以填满屏幕,同时保持宽高比。

    • zoomStretch:这种缩放方式非均匀地放大内容以填满屏幕,并会垂直或水平拉伸。

    注意

    zoomStretch值在处理 Android 设备缩放时效果很好,因为它们有许多不同的屏幕分辨率。

动态内容对齐

默认情况下,动态缩放的内容已经居中。你可能会遇到不希望内容居中的情况。例如 iPhone 3G 和 Droid 具有完全不同的屏幕分辨率。为了使 Droid 上显示的内容与 iPhone 3G 相似,需要调整对齐方式,使内容填满整个屏幕,而不留下任何空白的黑色屏幕空间。对齐方式如下:

  • xAlign:这是一个指定x方向对齐的字符串。可以使用以下值:

    • left

    • center(默认)

    • right

  • yAlign:这是一个指定y方向对齐的字符串。可以使用以下值:

    • top

    • center(默认)

    • bottom

动态图像分辨率

Corona 允许你为更高分辨率的设备替换更高分辨率的图片版本,而无需更改布局代码。如果要在具有不同屏幕分辨率的多个设备上构建,这是一个需要考虑的情况。

你想要显示高分辨率图片的一个例子是在 iPhone 4 上,其分辨率为 640 x 960 像素。它是早期 iOS 设备(如 iPhone 3GS,分辨率为 320 x 480 像素)分辨率的的两倍。将 iPhone 3GS 的内容放大以适应 iPhone 4 屏幕是可行的,但图片不会那么清晰,在设备上看起来会有些模糊。

通过在文件名末尾(但在句号和文件扩展名之前)添加@2x后缀,可以为 iPhone 4 替换更高分辨率的图片。例如,如果你的图片文件名是myImage.png,那么更高分辨率的文件名应该是myImage@2x.png

在你的config.lua文件中,需要添加一个名为imageSuffix的表格,以使图像命名约定和图像分辨率生效。config.lua文件位于你的项目文件夹中,该文件夹存储了所有的.lua文件和图像文件。请看以下示例:

application =
{
  content =
  {
    width = 320,
    height = 480,
    scale = "letterbox",

    imageSuffix =
    {
       ["@2x"] = 2,
    },
  },
}

当调用你的显示对象时,使用display.newImageRect( [parentGroup,] filename [, baseDirectory] w, h)而不是display.newImage()。目标高度和宽度需要设置为你的基础图像的尺寸。

帧率控制

默认帧率为 30 fps(每秒帧数)。Fps 指的是游戏中图像刷新的速度。30 fps 是移动游戏的标准,特别是对于较旧的设备。当你添加了 fps 键时,可以将其设置为 60 fps。使用 60 fps 会使你的应用程序运行更加流畅。在运行动画或碰撞检测时,你可以轻松地检测到动作的逼真流畅性。

行动时间 – 在多个设备上缩放显示对象

在我们的 Display Objects 项目中,我们在模拟器中留下了一个背景图像和三个类似的显示对象未显示。在不同的设备上运行项目时,坐标和分辨率大小与 iPhone 最兼容。在为 iOS 和 Android 平台上的多个设备构建应用程序时,我们可以使用编译到项目中并在运行时访问的 config.lua 文件进行配置。那么,让我们开始吧!

  1. 在你的文本编辑器中,创建一个新文件并写下以下几行:

    application =
    {
      content =
      {
        width = 320,
        height = 480,
        scale = "letterbox",
        xAlign = "left",
        yAlign = "top"
      },
    }
    
  2. 在你的 Display Objects 项目文件夹中将脚本保存为 config.lua

  3. 对于 Mac 用户,在 Corona 下以 iPhone 设备启动你的应用程序。完成此操作后,在 Corona 模拟器菜单栏下,选择 Window | View As | iPhone 4。你会注意到显示对象完美地适应屏幕,并且没有出现任何空黑的空白。

  4. Windows 用户,在 Corona 下以 Droid 设备启动你的应用程序。你会注意到所有内容都被适当地缩放和对齐。在 Corona 模拟器菜单栏下,选择 Window | View As | NexusOne。观察内容布局与 Droid 的相似之处。在以下截图中,从左到右,你可以看到 iPhone 3GS、iPhone 4、Droid 和 NexusOne:行动时间 – 在多个设备上缩放显示对象

刚才发生了什么?

你现在已经学会了一种方法,可以在 iOS 和 Android 上的多种设备上轻松配置显示内容。内容缩放功能对于多屏幕开发很有用。如果你查看我们创建的 config.lua 文件,width = 320height = 480。这是内容最初针对的分辨率大小。在本例中,它是 iPhone 3G。由于我们使用了 scale = "letterbox",它使得内容尽可能均匀地放大,同时仍然在屏幕上显示全部内容。

我们还设置了 xAlign = "left"yAlign = "top"。这填补了 Droid 设备上特别显示的空黑屏幕空间。默认情况下,内容缩放是在中心的,因此将内容对齐到屏幕的左上角将消除额外的屏幕空间。

动态分辨率图像

之前,我们提到了动态图像分辨率。iOS 设备就是这种情况的一个完美例子。Corona 能够在同一个项目文件中使用基本图像(针对 3GS 及以下设备)和双倍分辨率图像(针对拥有视网膜显示屏的 iPhone 4),你的双倍分辨率图像可以无需修改代码,直接切换到高端 iOS 设备上。这将使得你的构建能够支持旧设备,并让你处理更复杂的多屏幕部署情况。你会注意到,动态图像分辨率与动态内容缩放是协同工作的。

使用这行代码 display.newImageRect( [parentGroup,] filename [, baseDirectory] w, h),可以调用你的动态分辨率图像。

在这里,w 指的是图像的内容宽度,而 h 指的是图像的内容高度

这是一个示例:

myImage = display.newImageRect( "image.png", 128, 128 )

请记住,这两个值代表基本图像的大小,不是图像在屏幕上的位置。你必须在代码中定义基本大小,这样 Corona 才知道如何渲染更高分辨率的替代图像。你的项目文件夹内容将按如下方式设置:

My New Project/    name of your project folder
  Icon.png         required for iPhone/iPod/iPad
  Icon@2x.png      required for iPhone/iPod with Retina display
  main.lua
  config.lua
  myImage.png      Base image (Ex. Resolution 128 x 128 pixels)
  myImage@2x.png   Double resolution image (Ex. Resolution 256 x 256 pixels)

在创建双倍分辨率图像时,请确保它是基本图像大小的两倍。在创建显示资源时,最好从双倍分辨率图像开始。Corona 允许你选择自己的图像命名模式。@2x 是一个可以使用的约定示例,但你也可以根据个人偏好选择命名后缀。现在,我们将使用 @2x 后缀,因为它可以区分双分辨率引用。创建双倍分辨率图像时,请包含 @2x 后缀进行命名。取相同的图像,将其大小调整为原始大小的 50%,然后使用不包含 @2x 后缀的相同文件名。

其他命名后缀的例子可能如下所示:

  • @2

  • -2

  • -two

如本章前面所述,你需要在 config.lua 文件中的 imageSuffix 表中为你的双倍分辨率图像定义图像后缀。你设置的内容缩放比例将允许 Corona 确定当前屏幕与基本内容尺寸之间的比例。以下示例使用 @2x 后缀来定义双倍分辨率图像:

application =
{
  content =
  {
    width = 320,
    height = 480,
    scale = "letterbox",

    imageSuffix =
    {
      ["@2x"] = 2,
    },
  },
}

是时候来一些形状了。

创建显示对象的另一种方式是使用矢量对象。你可以使用矢量对象来创建如下形状的矩形、圆角矩形和圆形:

  • display.newRect([parentGroup,] x, y, width, height): 这个函数用于创建一个由宽度和高度确定的矩形。xy 值决定了矩形的中心坐标。局部原点位于矩形的中心,锚点初始化为此局部原点。

  • display.newRoundedRect([parentGroup,] x, y, width, height, cornerRadius): 这将创建一个宽度和高度的圆角矩形。xy值决定了矩形的中心坐标。局部原点位于矩形的中心,锚点初始化为此局部原点。您可以使用cornerRadius来圆滑角。

  • display.newCircle([parentGroup,] xCenter, yCenter, radius): 这将创建一个以xCenteryCenter为中心的半径的圆。

应用笔触宽度、填充颜色和笔触颜色

所有矢量对象都可以使用笔触进行勾勒。您可以使用以下方法设置笔触宽度、填充颜色和笔触颜色:

  • object.strokeWidth: 这创建笔触宽度,以像素为单位

  • object:setFillColor(red, green, blue, alpha): 我们可以使用 0 到 1 之间的 RGB 代码。alpha参数是可选的,默认值为 1.0。

  • object:setStrokeColor(red, green, blue, alpha): 我们可以使用 0 到 255 之间的 RGB 代码。alpha参数是可选的,默认值为 1.0。

下面是使用笔触显示矢量对象的示例:

local rect = display.newRect(160, 130, 150, 150)
rect:setFillColor(1.0, 1.0, 1.0) 
rect:setStrokeColor(0.1, 0.6, 0.2) 
rect.strokeWidth = 5

您将在模拟器上获得与以下图像相似的输出:

应用笔触宽度、填充颜色和笔触颜色

文本,文本,文本

在第一章,开始使用 Corona SDK中,我们使用文本显示对象创建了 Hello World 应用程序。让我们详细了解一下文本如何在屏幕上实现:

  • display.newText( [parentGroup,] text, x, y, font, fontSize)方法使用xy值创建文本对象。默认情况下没有文本颜色。在font参数中,应用库中的任何字体名称。fontSize参数显示文本的大小。

  • 如果您不想应用字体名称,可以使用以下一些默认常量:

    • native.systemFont

    • native.systemFontBold

应用颜色和字符串值

在文本显示对象中可以设置或检索大小、颜色和文本字段:

  • object.size: 这是文本的大小。

  • object:setFillColor(red, green, blue, alpha): 我们可以使用 0 到 1 之间的 RGB 代码。alpha参数是可选的,默认值为 1.0。

  • object.text: 这包含文本对象的文本。它允许您更新测试对象的字符串值。

函数是什么?

函数可以执行一个过程或计算并返回值。我们可以将函数调用作为语句,也可以将其作为表达式使用。您还可以将对象方法作为函数使用。您知道函数可以是变量。表可以使用这些变量将它们作为属性存储。

函数是 Lua 中最重要的抽象手段。我们经常使用的一个函数是print。在以下示例中,print函数被告诉执行一个数据块——"My favorite number is 8"字符串:

print("My favorite number is 8") -- My favorite number is 8

另一种表述方式是,print函数被调用时带有一个参数。print函数是 Lua 语言众多内置函数中的一个,但几乎你编写的任何程序都会涉及定义自己的函数。

定义一个函数

当尝试定义一个函数时,你必须给它一个名字,当你想要返回一个值时可以调用这个名字。然后,你需要创建一个语句或语句块来输出值,并在完成定义后为函数应用end。以下是一个示例:

function myName()
  print("My name is Jane.")
end

myName()  -- My name is Jane.

注意,函数名为myName,它被用来调用print("My name is Jane.")函数定义中的内容。

对定义函数的一个扩展如下:

function myName(Name)
  print("My name is " .. Name .. ".")
end

myName("Jane")  -- My name is Jane.
myName("Cory")  -- My name is Cory.
myName("Diane")  -- My name is Diane.

新的myName函数有一个使用Name变量的参数。"My name is "字符串与Name连接,然后以句号作为打印结果。当调用函数时,我们使用了三个不同的名字作为参数,并为每一行打印了一个新的自定义名称。

更多显示功能

在 Corona 中,你可以改变设备上状态栏的外观。这是代码中的一行设置,一旦你启动应用程序就会生效。你可以使用display.setStatusBar(mode)方法来改变状态栏的外观。这将在 iOS 设备(iPad、iPhone 和 iPod Touch)和 Android 2.x 设备上隐藏或改变状态栏的外观。Android 3.x 设备不受支持。

参数模式应该是以下之一:

  • display.HiddenStatusBar:若要隐藏状态栏,你可以在代码开始处使用以下这行代码:

    display.setStatusBar(display.HiddenStatusBar)
    

    在以下截图中,你可以看到状态栏已被隐藏:

    更多显示功能

  • display.DefaultStatusBar:若要显示默认状态栏,你可以在代码开始处使用以下这行代码:

    display.setStatusBar(display.DefaultStatusBar)
    

    代码将显示默认状态栏,如下截图所示:

    更多显示功能

  • display.TranslucentStatusBar:若要显示半透明状态栏,你可以在代码开始处使用以下这行代码:

    display.setStatusBar(display.TranslucentStatusBar)
    

    半透明状态栏将如下截图所示:

    更多显示功能

  • display.DarkStatusBar:若要显示深色状态栏,你可以在代码开始处使用以下这行代码:

    display.setStatusBar(display.DarkStatusBar)
    

    以下截图是深色状态栏:

    更多显示功能

内容大小属性

当你想要获取设备上的显示信息时,可以使用内容大小属性来返回值。这些属性如下:

  • display.contentWidth:这会返回内容原始宽度的像素值。默认情况下,这将是屏幕宽度。

  • display.contentHeight:这会返回内容原始高度的像素值。默认情况下,这将是屏幕高度。

  • display.viewableContentWidth:这是一个只读属性,包含视图屏幕区域的宽度(以像素为单位),在原始内容的坐标系内。访问这个属性将显示内容是如何被查看的,无论你是在纵向还是横向模式。以下是一个示例:

    print(display.viewableContentWidth)
    
  • display.viewableContentHeight:这是一个只读属性,包含视图屏幕区域的高度(以像素为单位),在原始内容的坐标系内。访问这个属性将显示内容是如何被查看的,无论你是在纵向还是横向模式。以下是一个示例:

    print(display.viewableContentHeight)
    
  • display.statusBarHeight:这是一个只读属性,表示状态栏的高度(以像素为单位,仅在 iOS 设备上有效)。以下是一个示例:

    print(display.statusBarHeight)
    

优化你的工作流程

到目前为止,我们已经接触了 Lua 编程中的一些基本要点以及 Corona SDK 中使用的术语。一旦你开始开发交互式应用程序,准备在 App Store 或 Android 市场上销售,你需要注意你的设计选择以及它们如何影响应用程序的性能。这意味着要考虑你的移动设备在处理应用程序时使用的内存量。以下是一些如果你刚开始接触 Corona SDK 需要注意的事项。

高效使用内存

在我们早期的例子中,有时在代码中使用了全局变量。像这样的情况是个例外,因为示例没有包含大量的函数、循环调用或显示对象。一旦你开始构建一个与函数调用和众多显示对象高度相关的游戏,局部变量将提高应用程序的性能,并放置在栈上,以便 Lua 可以更快地接口它们。

以下代码将导致内存泄漏:

-- myImage is a global variable
myImage = display.newImage( "image.png" )
myImage.x = 160;  myImage.y = 240

-- A touch listener to remove object
local removeBody = function( event )
  local t = event.target
  local phase = event.phase

  if "began" == phase then
    -- variable "myImage" still exists even if it's not displayed
    t:removeSelf() -- Destroy object
  end

  -- Stop further propagation of touch event
  return true
end

myImage:addEventListener( "touch", removeBody )

前面的代码在myImage被触摸后将其从显示层次结构中移除。唯一的问题是,由于myImage变量仍然引用它,myImage使用的内存会泄漏。由于myImage是一个全局变量,它引用的显示对象即使不在屏幕上显示也不会被释放。

与全局变量不同,局部化变量可以帮助加快显示对象的查找过程。它也只存在于定义它的代码块或片段中。在以下代码中使用局部变量将完全移除对象并释放内存:

-- myImage is a local variable
local myImage = display.newImage( "image.png" )
myImage.x = 160;  myImage.y = 240

-- A touch listener to remove object
local removeBody = function( event )
  local t = event.target
  local phase = event.phase

  if "began" == phase then
    t:removeSelf() -- Destroy object
    t = nil
  end

  -- Stop further propagation of touch event
  return true
end

myImage:addEventListener( "touch", removeBody )

优化你的显示图像

优化你的图像文件大小非常重要。使用全屏图像可能会影响应用程序的性能。它们需要更长的时间在设备上加载,并且消耗大量的纹理内存。当应用程序消耗大量内存时,在大多数情况下它会被迫退出。

iOS 设备在可用内存大小上有所不同,具体取决于以下设备中的哪一个:

  • iPhone 3GS、iPad 和拥有 256 MB RAM 的 iTouch 3G/4G

  • iPhone 4/4S、iPad 2、iPad Mini 和拥有 512 MB RAM 的 iTouch 5G

  • iPhone 5/5S/6, 6 Plus, iPad 3G, 以及 1 GB RAM 的 iPad 4G

例如,在 iPhone 3GS 上,纹理内存应保持在 25 MB 以下,以免出现性能问题,如减慢应用程序速度甚至强制退出。iPad 2 在这方面可以更宽松,因为它有更多的可用内存。

注意

有关为 iOS 设备应用内存警告,请参考 docs.coronalabs.com/api/event/memoryWarning/index.html

对于 Android 设备,大约有 24 MB 的内存限制。因此,了解你的场景中有多少显示对象以及当你的应用程序不再需要它们时如何管理它们是非常重要的。

当你不再需要在屏幕上显示一个图像时,请使用以下代码:

image.parent:remove( image ) -- remove image from hierarchy

或者,你也可以使用以下代码行:

image:removeSelf( ) -- same as above

如果你想要在应用程序的生命周期内完全移除一个图像,请在你的 image.parent:remove( image )image:removeSelf() 代码后包含以下行:

image = nil

在应用程序中保持低内存使用可以防止崩溃并提高性能。有关优化的更多信息,请访问 developer.coronalabs.com/content/performance-and-optimization

快速测验 - Lua 基础

Q1. 以下哪项是值?

  1. 数字

  2. nil

  3. 字符串

  4. 所有以上选项

Q2. 哪个关系运算符是错误的?

  1. print(0 == 0)

  2. print(3 >= 2)

  3. print(2 ~= 2)

  4. print(0 ~= 2)

Q3. 在 x 方向缩放对象的正确方法是什么?

  1. object.scaleX

  2. object.xscale

  3. object.Xscale

  4. object.xScale

总结

本章讨论了 Lua 编程的部分内容,这将为你开始创建自己的 Corona 应用程序铺平道路。随着你继续使用 Lua,你会开始更好地理解术语。最终,你会发现新的编程解决方案,这将有利于你的开发过程。

你到目前为止学到的技能包括以下内容:

  • 创建变量并赋值

  • 使用运算符建立表达式

  • 使用 Corona 终端输出或打印结果

  • 使用表来构建列表、数组、集合等

  • 在模拟器中添加显示对象

  • 配置你的应用程序构建以在不同的移动设备上工作

  • 实现动态分辨率图像

  • 创建函数以运行代码块

这一部分确实有很多内容需要消化。关于 Lua 的还有很多信息我们没有涉及到,但你已经学到了足够多的知识来开始。有关在 Lua 中编程的更多信息,你可以参考 www.lua.org/pil/index.html 或 Corona 网站上的资源部分 www.coronalabs.com/resources/

在下一章中,我们将开始制作我们的第一个游戏——打砖块!你将亲身体验在 Corona 中创建游戏框架,并应用所有必要的资源来开发一款移动游戏。你会惊讶地发现创建一个游戏竟然如此迅速和简单。

第三章:制作我们的第一个游戏 - 破坏者

到目前为止,我们已经学习了 Lua 编程中的一些重要基础,并在 Corona 模拟器中应用了一些代码。了解术语只是学习如何创建应用程序的一小部分。我们需要更进一层,亲身体验从开始到结束构建一个项目的全过程。我们将通过从零开始创建我们的第一个游戏来实现这一点。这将推动你进一步理解更大的代码块,并应用一些游戏逻辑来创建一个功能性的游戏。

到本章结束时,你将理解:

  • 如何在 Corona 项目中构建游戏文件结构

  • 如何为游戏创建变量

  • 如何向屏幕添加游戏对象

  • 如何创建警告信息

  • 如何显示得分和关卡数字

让我们开始享受乐趣!

破坏者 - 重温旧式游戏

在过去几十年里,你可能已经见过许多破坏者的版本,尤其是在雅达利时代。为了让你对这款游戏有一个大致的了解,以下是 Big Fish Games 关于破坏者历史的简要编辑:www.bigfishgames.com/blog/the-history-of-breakout/。以下截图是破坏者游戏的示例:

破坏者 - 重温旧式游戏

在游戏屏幕上,有几列砖块放置在屏幕顶部附近。一个球在屏幕上移动,从屏幕顶部和侧壁弹回。当击中砖块时,球会弹开,砖块被摧毁。当球触碰到屏幕底部时,玩家将输掉这一轮。为了防止这种情况发生,玩家有一个可移动的挡板来将球弹起,保持游戏进行。

我们将使用触摸事件和加速度计来创建一个克隆版本,玩家将控制挡板的活动。我们将为球添加一些物理效果,使其能在屏幕上弹跳。

在下一章中,我们将添加游戏对象的活动、碰撞检测、计分以及胜利/失败条件。现在,我们要专注于如何设置破坏者游戏模板。

理解 Corona 物理 API

Corona 使向游戏中添加物理效果变得方便,尤其是如果你以前从未处理过这类工作。这个引擎使用 Box2D,只需几行代码就可以将其集成到你的应用程序中,而这通常需要更多的设置。

在 Corona 中使用物理引擎相当简单。你使用显示对象并在代码中将它们设置为物理实体。图像、精灵和矢量形状可以被转化为物理对象。这对于可视化你想要在创建的环境中对象如何反应非常有帮助。你可以立即看到结果,而不是猜测它们在物理世界中可能的行为。

设置物理世界

在你的应用程序中使物理引擎可用需要以下这行代码:

local physics = require "physics"

启动、暂停和停止物理引擎

有三个主要函数会影响物理模拟。以下是启动、暂停和停止物理引擎的命令:

  • physics.start():这将启动或恢复物理环境。通常在应用程序开始时激活,使物理实体生效。

  • physics.pause():这会暂时停止物理引擎。

  • physics.stop():这基本上完全销毁物理世界。

physics.setGravity

此函数用于设置全局重力向量的 x 和 y 参数,单位为每秒平方米(加速度单位)。默认值为 (0, 9.8),以模拟标准的地球重力,指向 y 轴的下方。其语法为 physics.setGravity(gx, gy)

physics.setGravity( 0, 9.8 ): Standard Earth gravity

physics.getGravity

此函数返回全局重力向量的 x 和 y 参数,单位为每秒平方厘米(加速度单位)。

语法为 gx, gy = physics.getGravity()

基于倾斜的重力

当你应用了 physics.setGravity(gx, gy) 和加速度计 API,实现基于倾斜的动态重力是简单的。以下是创建基于倾斜功能的示例:

function movePaddle(event)

  paddle.x = display.contentCenterX - (display.contentCenterX * (event.yGravity*3))

end

Runtime:addEventListener( "accelerometer", movePaddle )

Corona 模拟器中没有加速度计;必须创建设备构建才能看到效果。

physics.setScale

此函数设置内部每米像素比率,用于在屏幕上的 Corona 坐标和模拟物理坐标之间转换。这应该在实例化任何物理对象之前完成。

默认缩放值为 30。对于分辨率较高的设备,如 iPad、Android 或 iPhone 4,你可能希望将此值增加到 60 或更多。

语法为 physics.setScale(value)

physics.setScale( 60 )

physics.setDrawMode

物理引擎有三种渲染模式。这可以在任何时候更改。

语法为 physics.setDrawMode(mode)。三种渲染模式分别为:

  • physics.setDrawMode("debug"):此模式仅显示碰撞引擎轮廓,如下面的截图所示:physics.setDrawMode

  • physics.setDrawMode("hybrid"):此模式在正常 Corona 对象上叠加碰撞轮廓,如下面的截图所示:physics.setDrawMode

  • physics.setDrawMode("normal"):此模式是默认的 Corona 渲染器,没有碰撞轮廓:physics.setDrawMode

物理数据使用颜色编码的矢量图形显示,反映了不同的对象类型和属性:

  • 橙色:用于表示动态物理实体(默认实体类型)

  • 深蓝色:用于表示运动学物理实体

  • 绿色:用于表示静态物理实体,如地面或墙壁

  • 灰色:用于表示因缺乏活动而处于 休眠 状态的实体

  • 浅蓝色:用于表示关节

physics.setPositionIterations

这个函数设置了引擎位置计算的精确度。默认值是 8,意味着引擎将每帧为每个对象进行八次位置近似迭代,但这会增加处理器的参与度,因此需要小心处理,因为它可能会减慢应用程序的运行。

语法是 physics.setPositionIterations(值)

physics.setPositionIterations(16)

physics.setVelocityIterations

这个函数设置了引擎速度计算的精确度。默认值是 3,意味着引擎将每帧为每个对象进行三次速度近似迭代。然而,这将增加处理器的参与度,因此需要小心处理,因为它可能会减慢应用程序的运行。

语法是 physics.setVelocityIterations(值)

physics.setVelocityIterations( 6 )

配置应用程序

本教程兼容 iOS 和 Android 设备。图形设计已调整以适应两个平台的多种屏幕尺寸。

构建配置

默认情况下,所有设备屏幕上显示的项目都以竖屏模式展示。我们将特别在横屏模式下创建这个游戏,因此我们需要更改一些构建设置并配置屏幕上所有项目的显示方式。在横屏模式下玩游戏实际上会增加更多玩家互动,因为挡板将有更多的屏幕空间移动,球体的空中时间也会减少。

动手时间——添加 build.settings 文件

构建时属性可以在可选的 build.settings 文件中提供,该文件使用 Lua 语法。build.settings 文件用于设置应用程序的屏幕方向和自动旋转行为以及各种特定平台的构建参数。要在你的项目文件夹中添加 build.settings 文件,请执行以下步骤:

  1. 在你的桌面上创建一个名为 Breakout 的新项目文件夹。

  2. 在你偏好的文本编辑器中,创建一个名为 build.settings 的新文件,并将其保存在你的项目文件夹中。

  3. 输入以下几行:

    settings =
    {
      orientation =
      {
        default = "landscapeRight",
        supported = { "landscapeLeft", "landscapeRight" },
      }
    }
    
  4. 保存并关闭文件。build.settings 文件已完成。

刚才发生了什么?

默认方向设置决定了设备上的初始启动方向以及 Corona 模拟器的初始方向。

默认方向不会影响 Android 设备。方向初始化为设备的实际方向(除非只指定了一个方向)。另外,唯一支持的方向是 landscapeRightportrait。在设备上,你可以切换到 landscapeRightlandscapeLeft,但操作系统只报告一种横屏模式,而 Corona 的方向事件选择 landscapeRight

我们创建这个应用程序是为了支持landscapeRight的横屏方向。我们将这个方向设置为默认值,这样它就不会切换到landscapeLeft或任何portrait模式。在 iOS 设备上工作时,如果在启动应用程序之前没有设置build.settings,它将进入默认的竖屏模式。

动态缩放

Corona 可以针对 iOS 和 Android 多个设备构建应用程序,显示不同分辨率的各种艺术资源。Corona 可以根据你的起始分辨率向上或向下缩放。它还可以在需要时替换高分辨率的图像文件,确保你的应用程序在所有设备上清晰锐利。

动手时间——添加config.lua文件

如果没有指定内容大小,返回的内容宽度和高度将与设备的物理屏幕宽度和高度相同。如果在config.lua中指定了不同的内容宽度和高度,内容宽度和高度将采用这些值。要在你的项目文件夹中添加config.lua文件,请执行以下步骤:

  1. 在你的文本编辑器中,创建一个名为config.lua的新文件,并将其保存到你的项目文件夹中。

  2. 输入以下几行:

    application =
    {
      content =
      {
        width = 320,
        height = 480, 
        scale = "letterbox",
        fps = 60,
      },
    }
    
  3. 保存并关闭你的文件。

刚才发生了什么?

内容宽度和高度允许你选择一个与物理设备屏幕尺寸无关的虚拟屏幕尺寸。我们将尺寸设置为针对 iPhone 3GS,因为它在 iOS 和 Android 平台的大多数设备上显示的是常见的尺寸之一。

这个应用程序使用的缩放比例设置为letterbox。它将尽可能统一放大内容,同时仍然在屏幕上显示所有内容。

我们将fps设置为60。默认情况下,帧率是 30 fps。在这个应用程序中,这将使球的移动看起来更快,便于我们方便地提高速度。我们可以将帧率拉伸到 60 fps,这是 Corona 允许的最大值。

构建应用程序

现在我们已经将应用程序配置为横屏模式,并设置显示内容在多个设备上缩放,我们准备开始设计游戏。在我们开始为游戏编写代码之前,我们需要添加一些将在屏幕上显示的艺术资源。你可以在第三章资源文件夹中找到它们。你可以从 Packt Publishing 网站下载伴随这本书的项目文件。以下是你需要添加到你的Breakout项目文件夹中的文件:

  • alertBox.png

  • bg.png

  • mmScreen.png

  • ball.png

  • paddle.png

  • brick.png

  • playbtn.png

显示组

我们将在游戏中介绍一个重要的功能 display.newGroup()。显示组允许你添加和移除子显示对象,并收集相关的显示对象。最初,组中没有子对象。本地原点位于父对象的原点;锚点初始化为此本地原点。你可以轻松地将显示对象组织在单独的组中,并通过组名称引用它们。例如,在 Breakout 中,我们将标题屏幕和播放按钮等菜单项组合在一个名为 menuScreenGroup 的组中。每次我们访问 menuScreenGroup,显示组中包含的任何显示对象都将被处理。

display.newGroup()

这个函数创建了一个组,你可以在其中添加和移除子显示对象。

语法是 display.newGroup()

例如:

local rect = display.newRect(0, 0, 150, 150)
rect:setFillColor(1, 1, 1)

local myGroup = display.newGroup()
myGroup:insert(rect)

使用系统函数

我们在本章中将要介绍的系统函数将返回有关系统(设备信息和当前方向)的信息,并控制系统函数(启用多点触控和控制空闲时间、加速度计和 GPS)。我们将使用以下系统函数返回应用程序将运行的环境信息以及加速度计事件的响应频率。

system.getInfo()

这个函数返回有关应用程序正在运行上的系统的信息。

语法是 system.getInfo(param):

print(system.getInfo("name")) -- display the deviceID

参数的有效值如下:

  • "name": 这将返回设备的型号名称。例如,在 iTouch 上,这将是出现在 iTunes 中的手机名称,如"Pat's iTouch"。

  • "model": 这将返回设备类型。包括以下内容:

    • iPhone

    • iPad

    • iPhone 模拟器

    • Nexus One

    • Droid

    • myTouch

    • Galaxy Tab

  • "deviceID": 这将返回设备的哈希编码设备 ID。

  • "environment": 这将返回应用程序正在运行的环境。包括以下内容:

    • "simulator": Corona 模拟器

    • "device": iOS, Android 设备以及 Xcode 模拟器

  • "platformName": 这将返回平台名称(操作系统名称),可以是以下任何一个:

    • Mac OS X (Corona 模拟器在 Mac 上)

    • Win (Corona 模拟器在 Windows 上)

    • iPhone OS (所有 iOS 设备)

    • Android (所有 Android 设备)

  • "platformVersion": 这将返回平台版本的字符串表示。

  • "build": 这将返回 Corona 构建字符串。

  • "textureMemoryUsed": 这将返回纹理内存使用量(字节)。

  • "maxTextureSize": 这将返回设备支持的最大纹理宽度或高度。

  • "architectureInfo": 这将返回描述你正在运行的设备底层 CPU 架构的字符串。

system.setAccelerometerInterval()

此函数设置加速度计事件的频率。在 iPhone 上,最低频率为 10 Hz,最高为 100 Hz。加速度计事件对电池的消耗很大,因此只有在你需要更快响应时,比如在游戏中,才增加频率。尽可能降低频率以节省电池寿命。

语法是 system.setAccelerometerInterval( frequency )

system.setAccelerometerInterval( 75 )

该函数设置样本间隔,单位为赫兹。赫兹是每秒的周期数,即每秒要进行的测量次数。如果你将频率设置为 75,那么系统将每秒进行 75 次测量。

在将 第三章Resources 文件夹中的资源添加到你的项目文件夹后,我们将开始编写一些代码!

动手操作——为游戏创建变量

为了启动任何应用程序,我们需要创建一个 main.lua 文件。这在第二章 Lua 速成与 Corona 框架 中讨论过,当时我们使用了一些示例代码并通过模拟器运行了它。

当游戏完成时,代码将相应地在你的 main.lua 文件中构建:

  • 必要的类(例如,physicsui

  • 变量和常量

  • 主函数

  • 对象方法

  • 调用主函数(必须始终调用,否则你的应用程序将无法运行)

将代码组织成前面的结构是一种保持事物有序和高效运行应用程序的好习惯。

在本节中,我们将介绍一个显示组,该显示组将展示主菜单屏幕和一个播放按钮,用户可以通过与该按钮互动进入主游戏屏幕。游戏中的所有元素,如挡板、球、砖块对象以及抬头显示元素,都是在玩家与播放按钮互动后出现的。我们还将介绍胜利和失败的条件,这些条件将被称作alertDisplayGroup。所有这些游戏元素都将在代码开始时初始化。

  1. 在你的文本编辑器中创建一个新的 main.lua 文件,并将其保存到项目文件夹中。

  2. 我们将隐藏状态栏(特别是针对 iOS 设备)并加载物理引擎。Corona 使用的是已经内置在 SDK 中的 Box2D 引擎:

    display.setStatusBar(display.HiddenStatusBar)
    
    local physics = require "physics"
    physics.start()
    physics.setGravity(0, 0)
    
    system.setAccelerometerInterval(100)
    

    注意

    有关 Corona 物理 API 的更多信息可以在 Corona 网站找到,地址是docs.coronalabs.com/guide/physics/physicsSetup/index.html

    Corona SDK 中使用的 Box2D 物理引擎是由 Blizzard Entertainment 的 Erin Catto 编写的。关于 Box2D 的更多信息可以在box2d.org/manual.pdf找到。

  3. 添加菜单屏幕对象:

    local menuScreenGroup  -- display.newGroup()
    local mmScreen
    local playBtn
    
  4. 添加游戏屏幕对象:

    local background
    local paddle
    local brick
    local ball
    
  5. 添加分数和等级的 HUD 元素:

    local scoreText
    local scoreNum
    local levelText
    local levelNum
    

    注意

    HUD 也被称为抬头显示。它是在游戏屏幕上视觉化表示角色信息的方法。

  6. 接下来,我们将添加用于胜利/失败条件的警告显示组:

    local alertDisplayGroup    -- display.newGroup()
    local alertBox
    local conditionDisplay
    local messageText
    
  7. 以下变量保存了砖块显示组、得分、球速度和游戏内事件的值:

    local _W = display.contentWidth / 2
    local _H = display.contentHeight / 2
    local bricks = display.newGroup()
    local brickWidth = 35
    local brickHeight = 15
    local row
    local column
    local score = 0
    local scoreIncrease = 100
    local currentLevel
    local vx = 3
    local vy = -3
    local gameEvent = ""
    
  8. 加速度计事件只能在设备上测试,因此我们将通过调用 "simulator" 环境为桨添加一个触摸事件变量。这样我们可以在 Corona 模拟器中测试桨的运动。如果你在设备上测试应用程序,桨上的触摸和加速度计事件监听器不会发生冲突:

    local isSimulator = "simulator" == system.getInfo("environment")
    
  9. 最后,加入 main() 函数。这将启动我们的应用程序:

    function main()
    
    end
    
    --[[
    This empty space will hold other functions and methods to run the application
    ]]--
    
    main()
    

刚才发生了什么?

display.setStatusBar(display.HiddenStatusBar) 方法仅适用于 iOS 设备。它隐藏了设备上状态栏的外观。

我们为这个游戏添加的新 Corona API 是物理引擎。我们将为主要的游戏对象(桨、球和砖块)添加物理参数以进行碰撞检测。设置 setGravity(0,0) 将允许球在游戏场内自由弹跳。

local menuScreenGrouplocal alertDisplayGrouplocal bricks 对象都是显示组的类型,我们可以通过它们来分离和组织显示对象。例如,local menuScreenGroup 专门用于主菜单屏幕上出现的对象。因此,它们可以作为一个组被移除,而不是单个对象。

某些已添加的变量已经具有应用于特定游戏对象的值。球体已经使用 local vx = 3local vy = -3 设置了速度。x 和 y 速度决定了球在游戏屏幕上的移动方式。根据球与对象碰撞的位置,球将沿着连续的路径移动。brickWidthbrickHeight 对象具有在应用程序的整个过程中保持恒定的值,因此我们可以将砖块对象在屏幕上均匀排列。

local gameEvent = " " 将存储游戏事件,如 "win""lose""finished"。当函数检查游戏状态是否有这些事件之一时,它将在屏幕上显示适当的状态。

我们还加入了一些系统函数。我们创建了 local isSimulator = "simulator" == system.getInfo("environment") 以返回有关运行应用程序的系统的信息。这将用于桨触控事件,以便我们可以在模拟器中测试应用程序。如果将构建移植到设备上,你只能使用加速度计来移动桨。模拟器无法测试加速度计事件。另一个系统函数是 system.setAccelerometerInterval( 100 )。它设置了加速度计事件的频率。iPhone 上的最低频率是 10 Hz,最高是 100 Hz。

main()空函数集将开始显示层次结构。可以把它看作是一个故事板。你首先看到的是介绍,然后中间发生一些动作,告诉你主要内容是什么。在这种情况下,主要内容是游戏玩法。你最后看到的是某种结尾或闭合,将故事联系在一起。结尾是在关卡结束时显示的胜负条件。

理解事件和监听器

事件被发送到监听者,由移动屏幕上的触摸、点击、加速度计等执行。函数或对象可以作为事件监听器。当事件发生时,监听器将被调用,并通过一个表示事件的表进行通知。所有事件都将有一个标识事件类型的属性名。

注册事件

显示对象和全局运行时对象可以作为事件监听器。你可以使用以下对象方法添加和移除事件监听器:

  • object:addEventListener(): 这将一个监听器添加到对象的监听器列表中。当命名的事件发生时,将调用监听器,并提供一个表示事件的表。

  • object:removeEventListener(): 这将指定的监听器从对象监听器列表中移除,使其不再接收与指定事件对应的事件通知。

在以下示例中,一个图像显示对象注册以接收触摸事件。触摸事件不会全局广播。注册了事件并在其下方的显示对象将成为接收事件的候选对象:

local playBtn = display.newImage("playbtn.png")
playBtn.name = "playbutton"

local function listener(event)
  if event.target.name == "playbutton" then

    print("The button was touched.")

end
end

playBtn:addEventListener("touch", listener )

运行时事件由系统发送,会广播给所有监听者。以下是注册enterFrame事件的一个例子:

local playBtn = display.newImage("playbtn.png") 

local function listener(event) 
  print("The button appeared.")
end

Runtime:addEventListener("enterFrame", listener )

运行时事件

我们正在创建的应用程序使用了运行时事件。运行时事件没有特定的目标,只发送到全局运行时。它们广播给所有注册的监听者。

运行时事件由系统发送,会广播给所有监听者。以下是注册enterFrame事件的一个例子:

local playBtn = display.newImage("playbtn.png")

local function listener(event)
  print("The button appeared.")
end

Runtime:addEventListener("enterFrame", listener )

以下事件都有字符串名称,并将应用于 Breakout 游戏。

enterFrame

enterFrame事件在应用程序的帧间隔发生。它们只发送到全局运行时对象。例如,如果帧率是 30 fps,那么它将大约每秒发生 30 次。

此事件中可用的属性如下:

  • event.name是字符串"enterFrame"

  • event.time是自应用程序开始以来的毫秒数

加速度计

加速度计事件允许你检测移动并确定设备相对于重力的方向。这些事件只发送到支持加速度计的设备。它们只发送到全局运行时对象。

此事件可用的属性如下:

  • event.name是字符串"accelerometer"

  • event.xGravityx方向上的重力加速度

  • event.yGravityy 方向的由重力引起的加速度。

  • event.zGravityz 方向的由重力引起的加速度。

  • event.xInstantx 方向的瞬时加速度。

  • event.yInstanty 方向的瞬时加速度。

  • event.zInstantz 方向的瞬时加速度。

  • event.isShake 是当用户摇动设备时为真。

触摸事件(Touch events)

当用户的手指触摸屏幕时,会生成一个命中事件并将其派发到显示层次结构中的显示对象。只有与屏幕上手指位置相交的对象才可能接收到事件。

单点触摸(Touch,single touch)

触摸事件是一种特殊的命中事件。当用户的手指触摸屏幕时,它们开始了一系列具有不同阶段的触摸事件。

  • event.name 是字符串 "touch"

  • event.x 是触摸点在屏幕坐标中的 x 位置。

  • event.y 是触摸点在屏幕坐标中的 y 位置。

  • event.xStart 是触摸序列 "began" 阶段的 x 位置。

  • event.yStart 是触摸序列 "began" 阶段的 y 位置。

  • event.phase 是一个字符串,用于标识事件在触摸序列中的哪个阶段发生:

    • "began":这表示手指触摸了屏幕。

    • "moved":这表示手指在屏幕上移动。

    • "ended":这表示手指从屏幕上抬起。

    • "cancelled":这表示系统取消了触摸的跟踪。

轻击(tap)

当用户触摸屏幕时,它会生成一个命中事件。该事件被派发到显示层次结构中的显示对象。这与触摸事件类似,不同之处在于事件回调中提供了点击次数(轻击次数),并且不使用事件阶段。事件 API 如下:

  • event.name 是字符串 "tap"

  • event.numTaps 返回屏幕上的轻击次数。

  • event.x 是轻击在屏幕坐标中的 x 位置。

  • event.y 是触摸点在屏幕坐标中的 y 位置。

过渡(Transitions)

在本章中,我们将介绍 transition.to()transition.from()

  • transition.to():这会随着时间的推移,使用 easing 过渡动画显示对象的属性。

    语法为 handle = transition.to( target, params )

  • transition.from():这与 transition.to() 类似,不同之处在于起始属性值在函数参数表中指定,最终值是在调用之前目标中的相应属性值。语法为 handle = transition.from( target, params )

    使用的参数如下:

    • target:这是过渡动画的目标显示对象。

    • params:这是一个指定将进行动画的显示对象属性以及以下一个或多个可选的非动画属性的表:

      • params.time:这指定了过渡的持续时间(以毫秒为单位)。默认情况下,持续时间为 500 毫秒(0.5 秒)。

      • params.transition:默认为easing.linear

      • params.delay:这指定了补间开始前延迟的毫秒数(默认为无)。

      • params.delta:这是一个布尔值,指定非控制参数是作为最终结束值还是作为值的变化来解释。默认为nil,即假。

      • params.onStart:这是一个在补间开始之前调用的函数或表监听器。

      • params.onComplete:这是一个在补间完成后调用的函数或表监听器。

例如:

_W = display.contentWidth
_H = display.contentHeight

local square = display.newRect( 0, 0, 100, 100 )
square:setFillColor( 1, 1, 1 )
square.x = _W/2; square.y = _H/2

local square2 = display.newRect( 0, 0, 50, 50 )
square2:setFillColor( 1, 1, 1 )
square2.x = _W/2; square2.y = _H/2

transition.to( square, { time=1500, x=250, y=400 } )
transition.from( square2, { time=1500, x=275, y=0 } )

前面的示例展示了两个显示对象如何在设备屏幕上过渡空间。从当前位置开始,square显示对象将在 1500 毫秒内移动到新的位置x = 250y = 400square2显示对象将从x = 275y = 0的位置在 1500 毫秒内过渡到其初始位置。

创建菜单屏幕

拥有菜单屏幕可以让玩家在应用程序的不同部分之间过渡。通常,游戏会从显示游戏标题的某种屏幕开始,并带有一个标有播放开始的交互式用户界面按钮,让玩家选择玩游戏。在任何移动应用程序中,在过渡到主要内容之前都有一个菜单屏幕是标准的。

行动时间——添加主菜单屏幕

主菜单界面将是玩家在应用程序启动后与菜单系统交互的第一个东西。这是介绍游戏标题并让玩家了解他们将面对的游戏环境类型的好方法。我们肯定不希望玩家在没有适当通知的情况下突然跳入应用程序。当玩家启动应用程序时,让他们为即将到来的内容做好准备是很重要的。

  1. 我们将创建一个名为mainMenu()的函数来介绍标题屏幕。所以,在function main()结束后,加入以下几行:

    function mainMenu()  
    
    end
    
  2. 我们将向这个函数中添加一个显示组和两个显示对象。一个显示对象是将代表主菜单屏幕的图像,另一个是一个名为播放的 UI 按钮。将它们添加到function mainMenu()内部:

      menuScreenGroup = display.newGroup()
    
      mmScreen = display.newImage("mmScreen.png", 0, 0, true)
      mmScreen.x = _W
      mmScreen.y = _H
    
      playBtn = display.newImage("playbtn.png")
      playBtn.anchorX = 0.5; playBtn.anchorY = 0.5  
      playBtn.x = _W; playBtn.y = _H + 50
      playBtn.name = "playbutton"
    
      menuScreenGroup:insert(mmScreen)
      menuScreenGroup:insert(playBtn)
    
  3. 记得那个空的main()函数集吗?我们需要在其中调用mainMenu()。整个函数应该像这样:

    function main()
      mainMenu()
    end 
    
  4. mainMenu()函数之后,我们将创建另一个名为loadGame()的函数。这个函数将初始化来自playbtn的事件以过渡到主游戏屏幕。事件将改变menuScreenGroup的 alpha 为0,使其在屏幕上不可见。通过调用addGameScreen()函数完成过渡(将在本章的行动时间——添加游戏对象部分讨论addGameScreen()):

    function loadGame(event)
      if event.target.name == "playbutton" then
    
        transition.to(menuScreenGroup,{time = 0, alpha=0, onComplete = addGameScreen})
    
        playBtn:removeEventListener("tap", loadGame)
      end
    end
    
  5. 接下来,我们需要为playBtn添加一个事件监听器,这样当它被点击时,就会调用loadGame()函数。在mainMenu()函数中的最后一个方法后添加以下这行代码:

    playBtn:addEventListener("tap", loadGame)
    
  6. 在模拟器中运行项目。你应该会看到主菜单屏幕显示BreakoutPlay按钮。

刚才发生了什么?

创建一个主菜单屏幕只需要几块代码。对于loadGame(event),我们传递了一个名为event的参数。当调用if语句时,它取playbutton,它引用显示对象playBtn,并检查它是否为真。既然如此,menuScreenGroup将从舞台中移除并在addGameScreen()函数中被调用。同时,playBtn的事件监听器将从场景中移除。

动手试试——创建帮助屏幕

目前,菜单系统的设计是设置成从主菜单屏幕过渡到游戏玩法屏幕。你可以选择扩展菜单屏幕,而不必立即跳转到游戏中。可以在主菜单屏幕之后添加的一个额外功能是帮助菜单屏幕,它向玩家解释如何玩游戏。

在你喜欢的图像编辑程序中创建一个新的图像,并写出如何进行游戏的步骤。然后你可以创建一个名为Next的新按钮,并将这两个艺术资源添加到你的项目文件夹中。在你的代码中,你将必须为你的Next按钮创建一个新的函数和事件监听器,它会过渡到游戏玩法屏幕。

创建游戏玩法场景

现在我们已经有一个菜单系统在位,我们可以开始处理应用程序的游戏玩法元素。我们将开始添加玩家将与之互动的所有主要游戏对象。在添加游戏对象时需要注意的一件事是它们在屏幕上的位置。考虑到这个游戏将在横屏模式下进行,我们必须记住在x方向上有足够的空间,而在y方向上的空间较少。根据游戏的原始设计,屏幕底部的墙壁会导致玩家失去关卡或转向,如果球落在这个区域。因此,如果我们要确定一个放置挡板对象的位置,我们不会将其设置在屏幕顶部附近。让挡板尽可能靠近屏幕底部以更好地保护球更有意义。

动手时间——添加游戏对象

让我们添加玩家在游戏玩法中会看到的显示对象:

  1. loadGame()函数之后,我们将创建另一个函数,用于在屏幕上显示所有游戏对象。以下几行将显示为这个教程创建的艺术资源:

    function addGameScreen()
    
      background = display.newImage("bg.png", 0, 0, true )
      background.x = _W 
      background.y = _H
    
      paddle = display.newImage("paddle.png")
      paddle.x = 240; paddle.y = 300
      paddle.name = "paddle"
    
      ball = display.newImage("ball.png")
      ball.x = 240; ball.y = 290
      ball.name = "ball"
    
  2. 接下来,我们将添加在游戏中显示分数和关卡编号的文本:

      scoreText = display.newText("Score:", 25, 10, "Arial", 14)
      scoreText:setFillColor( 1, 1, 1 )
    
      scoreNum = display.newText("0", 54, 10, "Arial", 14)
      scoreNum: setFillColor( 1, 1, 1 )
    
      levelText = display.newText("Level:", 440, 10, "Arial", 14)
      levelText:setFillColor( 1, 1, 1 )
    
      levelNum = display.newText("1", 470, 10, "Arial", 14)
      levelNum:setFillColor( 1, 1, 1 )
    
  3. 为了构建第一个游戏关卡,我们将调用gameLevel1()函数,该函数将在本章后面解释。别忘了用end结束addGameScreen()函数:

      gameLevel1() 
    
    end
    

刚才发生了什么?

addGameScreen() 函数显示游戏过程中出现的所有游戏对象。我们从本章提供的美工资源中添加了 backgroundpaddleball 显示对象。

我们在游戏屏幕顶部添加了分数和等级的文本。scoreNum 最初设置为 0。在下一章,我们将讨论当砖块碰撞时如何更新分数。levelNum 从 1 开始,完成等级后更新,并进入下一个等级。

我们通过调用 gameLevel1() 来结束函数,这将在下一节中实现,以开始第一关。

是时候行动了——构建砖块。

砖块是我们需要为这个应用程序添加的最后一个游戏对象。我们将为这个游戏创建两个不同的等级,每个等级的砖块布局都不同于另一个:

  1. 我们将要为第一关创建一个函数。让我们创建一个新函数 gameLevel1()。我们还将 currentLevel 设置为 1,因为应用程序从第一关开始。然后,我们将添加 bricks 显示组并将其设置为 toFront(),使其在游戏背景前显示:

    function gameLevel1()
    
      currentLevel = 1
    
      bricks:toFront()
    

    object:toFront() 方法将目标对象移动到其父组 (object.parent) 的视觉最前方。在这种情况下,我们将 bricks 组设置为游戏过程中最前端的显示组,使其在背景图片前方显示。

  2. 接下来,添加一些局部变量,以显示屏幕上将显示多少行和列的砖块,以及每个砖块在游戏场中的位置:

      local numOfRows = 4
      local numOfColumns = 4
      local brickPlacement = {x = (_W) - (brickWidth * numOfColumns ) / 2  + 20, y = 50}
    
  3. 创建双重 for 循环,一个用于 numOfRows,另一个用于 numOfColumns。根据其宽度、高度以及 numOfRowsnumOfColumns 的对应数字创建一个砖块实例。本章提供了砖块显示对象的美工资源。然后,使用 end 结束函数:

      for row = 0, numOfRows - 1 do
        for column = 0, numOfColumns - 1 do
    
          local brick = display.newImage("brick.png")
          brick.name = "brick"
          brick.x = brickPlacement.x + (column * brickWidth)
          brick.y = brickPlacement.y + (row * brickHeight)
          physics.addBody(brick, "static", {density = 1, friction = 0, bounce = 0})
          bricks.insert(bricks, brick)
    
        end
      end
    end
    
  4. 第二关的设置与第一关的排列类似。代码几乎相同,除了我们新的函数名为 gameLevel2()currentLevel 设置为 2,并且 numOfRowsnumOfColumns 的值不同。在 gameLevel1() 函数后添加以下代码块:

    function gameLevel2()
    
      currentLevel = 2
    
      bricks:toFront()
    
      local numOfRows = 5
      local numOfColumns = 8
      local brickPlacement = {x = (_W) - (brickWidth * numOfColumns ) / 2  + 20, y = 50}
    
      for row = 0, numOfRows - 1 do
        for column = 0, numOfColumns - 1 do
    
          -- Create a brick
          local brick = display.newImage("brick.png")
          brick.name = "brick"
          brick.x = brickPlacement.x + (column * brickWidth)
          brick.y = brickPlacement.y + (row * brickHeight)
          physics.addBody(brick, "static", {density = 1, friction = 0, bounce = 0})
          bricks.insert(bricks, brick)
    
        end
      end
    end
    
  5. 保存你的文件并重新启动模拟器。你将能够与 Play 按钮互动,并从主菜单屏幕过渡到游戏屏幕。你将在屏幕上看到第一关的游戏布局。

刚才发生了什么?

bricks 显示组被设置为 bricks:toFront()。这意味着除了 backgroundpaddleball 显示对象之外,该组将始终位于显示层次结构的前面。

gameLevel1()方法为游戏场地中显示的砖块对象数量设定了固定值。它们将基于设备外壳的contentWidth居中,并在 y 方向上设置为50。通过brickPlacement将砖块组放置在左上角附近,占据屏幕中间位置,并减去所有砖块对象总宽度的一半。然后在 x 方向上再加上 20 个像素,使其与挡板居中。

我们为numOfRowsnumOfColumns创建了双层for循环,从屏幕左上角开始创建砖块对象。

请注意,brick显示对象被命名为brick。只需记住,在调用对象时,不能像使用brick那样使用brickbrick对象是brick的一个实例。它仅当调用事件参数时作为字符串使用,例如:

if event.other.name == "brick" and ball.x + ball.width * 0.5 < event.other.x + event.other.width * 0.5 then
        vx = -vx 
elseif event.other.name == "brick" and ball.x + ball.width * 0.5 >= event.other.x + event.other.width * 0.5 then
        vx = vx 
end

brick的物理体被设置为"static",因此它不会受到重力下拉的影响。然后,通过bricks.insert(bricks, brick)将其添加到bricks组中。

做一个尝试英雄——专注于平台游戏

在完成本章和下一章后,请随意重新设计显示图像,以便关注特定平台。例如,你可以轻松地将代码转换为兼容所有 iOS 设备。这可以通过将显示对象转换为display.newImageRect( [parentGroup,] filename [, baseDirectory] w, h )来实现,这样你就可以替换具有更大屏幕尺寸的设备(如 iPhone 5/Samsung Galaxy S5)上的图像尺寸。请记住,你将不得不调整配置设置以应用这些更改。这涉及到在你的config.lua文件中添加独特的图像后缀(或你喜欢的后缀命名约定)。

红色警报!

在每个游戏中,当主要动作结束时,都会有一种消息告诉你进度状态。对于这个应用程序,我们需要一种方法让玩家知道他们是否赢得或输掉了一轮,他们如何再次玩,或者游戏何时正式完成。

是时候采取行动了——显示游戏消息

让我们设置一些胜利/失败的提示,以便我们可以显示游戏中发生的事件:

  1. 创建一个名为alertScreen()的新函数,并传递两个名为titlemessage的参数。添加一个新的显示对象alertbox,并使用easing.outExpo使其从xScaleyScale为 0.5 的过渡效果:

    function alertScreen(title, message)
    
      alertBox = display.newImage("alertBox.png")
      alertBox.x = 240; alertBox.y = 160
    
      transition.from(alertBox, {time = 500, xScale = 0.5, yScale = 0.5, transition = easing.outExpo})
    
  2. title参数存储在名为conditionDisplay的文本对象中:

      conditionDisplay = display.newText(title, 0, 0, "Arial", 38)
      conditionDisplay:setFillColor( 1, 1, 1 )
      conditionDisplay.xScale = 0.5
      conditionDisplay.yScale = 0.5
      conditionDisplay.anchorX = 0.5
      conditionDisplay.x =  display.contentCenterX
      conditionDisplay.y = display.contentCenterY - 15
    
  3. message参数存储在名为messageText的文本对象中:

      messageText = display.newText(message, 0, 0, "Arial", 24)
      messageText:setFillColor( 1, 1, 1 )
      messageText.xScale = 0.5
      messageText.yScale = 0.5
      messageText.anchorX = 0.5  
      messageText.x = display.contentCenterX
      messageText.y = display.contentCenterY + 15
    
  4. 创建一个新的显示组,名为alertDisplayGroup,并将所有对象插入到该组中。关闭函数:

      alertDisplayGroup = display.newGroup()
      alertDisplayGroup:insert(alertBox)
      alertDisplayGroup:insert(conditionDisplay)
      alertDisplayGroup:insert(messageText)
    end
    
  5. 保存你的文件并在模拟器中运行项目。Play按钮的功能仍然会进入Level: 1的游戏玩法屏幕。目前,所有对象都没有任何移动。我们将在下一章添加触摸事件、球体移动和碰撞。所有游戏对象应如以下截图所示布局:Time for action – displaying game messages

刚才发生了什么?

我们已经为游戏设置了警报系统,但在我们添加更多游戏功能使游戏对象动起来之前,它目前还不能操作。下一章将展示alertScreen()函数如何传递两个参数,titlemessage。当满足条件后,alertBox显示对象会作为警报文本的背景弹出。当alertBox弹出时,它会从 0.5 的xScaleyScale过渡到全图像大小,耗时 500 毫秒。这基本上相当于半秒钟。

conditionDisplay对象传递title参数。这将显示You WinYou Lose的文本。

messageText对象传递message参数。当达到某个条件后,带有此参数的文本会显示如Play AgainContinue的消息。

此函数中的所有对象都将被插入到alertDisplayGroup = display.newGroup()中。它们在舞台上出现和消失时,会作为一个整体而不是单独的对象。

在模拟器中运行代码时,如果终端窗口出现错误,务必检查导致错误的行。有时,一个简单的字母大小写错误,甚至是一个缺失的逗号或引号,都可能导致你的应用无法在模拟器中运行。请留意这些常见错误,它们很容易被忽视。

你可以参考第三章文件夹中的Breakout – Part 1文件夹,了解本教程前半部分代码的设置。

小测验——构建一个游戏

Q1. 在你的代码中添加物理引擎时,哪些函数可以添加到你的应用程序中?

  1. physics.start()

  2. physics.pause()

  3. physics.stop()

  4. 以上都不对

Q2. 添加事件监听器以下哪个是正确的?

  1. button:addeventlistener("touch", listener)

  2. button:AddEventListener("touch", listener)

  3. button:addEventListener(touch, listener)

  4. button:addEventListener("touch", listener)

Q3. 以下显示对象正确过渡到x = 300y = 150,并将 alpha 改为 0.5,耗时 2 秒的方式是?

local square = display.newRect( 0, 0, 50, 50 )
square:setFillColor( 1, 1, 1 )
square.x = 100 square2.y = 300
  1. transition.to( square, { time=2000, x=300, y=150, alpha=0.5 })

  2. transition.from( square, { time=2000, x=300, y=150, alpha=0.5 })

  3. transition.to( square, { time=2, x=300, y=150, alpha=0.5 })

  4. 以上都不对

总结

我们已经完成了这个游戏教程的前半部分。正确理解如何构建 Corona 项目结构,可以让你的代码更有组织性,更好地追踪你的资源。我们已经尝试处理了与游戏中所需的小部分逻辑相关的代码块,这些代码块使得应用程序能够运行。

到目前为止,我们已经完成了:

  • 指定了在 Android 和 iOS 设备上显示内容的构建配置

  • 介绍了将在应用程序中运行的主要变量和常量

  • 实例化了物理引擎,并开始将其应用到需要物理体的游戏对象上

  • 创建了从菜单到游戏玩屏幕的过渡

  • 向屏幕添加了显示对象和游戏信息

到目前为止我们已经完成了很多工作,包括在编码应用程序的过程中学习了一个新的 API,这已经是一个相当大的成就了。在游戏能够完全功能之前,我们还有很多内容需要添加。

在下一章中,我们将完成这个游戏教程的后半部分。我们将处理挡板、球、砖块和墙壁对象的碰撞检测。同时,我们还将学习如何在移除场景中的砖块时更新得分,并激活我们的赢/输条件。我们已经进入最后的冲刺阶段,让我们继续前进!

第四章:游戏控制

到目前为止,我们在上一章完成了游戏的前半部分。我们通过向屏幕引入游戏对象来开发项目的初始结构。目前,挡板和球体的移动是无效的,但在模拟器中显示的所有内容都根据原始游戏设计进行了缩放。完成本教程的最后阶段是添加游戏中将发生的所有动作,包括对象移动和更新得分。

在本章中,我们将涵盖以下主题:

  • 使用触摸事件和加速度计移动挡板

  • 场景中所有游戏对象之间的碰撞检测

  • 在碰撞检测时移除对象

  • 在屏幕边界内球体的移动

  • 计算得分

  • 胜利和失败条件

最后阶段!我们能行!

向上移动

如果你认为让对象在屏幕上出现很有趣,那么等到你看到它们移动时!Breakout游戏的主要目标是保持球在挡板位置之上以保持游戏进行,并让它与所有砖块碰撞以完成关卡。让悬念持续的是对球在游戏屏幕周围移动的期待。如果没有在游戏对象上添加物理边界以对碰撞检测做出反应,这是不可能的。

让我们变得更加物理化

在上一章中,我们讨论了如何将物理引擎集成到代码中。我们还开始为砖块对象实现物理实体,现在,我们需要对其他活动游戏对象(如挡板和球)做同样的处理。让我们继续后半部分的教程。我们将继续使用Breakout项目文件夹中的main.lua文件。

physics.addBody()

Corona 显示对象可以用一行代码变成模拟的物理对象。以下信息解释了不同的物理实体形式:

  • 如果没有指定形状信息,显示对象将采用原始图像的实际矩形边界来创建物理实体。例如,如果一个显示对象是 100x100 像素,那么这将是物理实体的实际大小。

  • 如果指定了一个形状,那么实体的边界将遵循该形状提供的多边形。形状坐标必须按顺时针顺序定义,且结果形状只能是凸的。

  • 如果指定了半径,那么实体边界将是圆形的,以用于创建物理实体的显示对象的中心为中心。

一个实体形状是一个相对于显示对象中心的本地(x,y)坐标表。

实体形状的语法如下:

  • 圆形形状:

    physics.addBody(object, [bodyType,] {density=d, friction=f, bounce=b [,radius=r]})
    
  • 多边形形状:

    physics.addBody(object, [bodyType,] {density=d, friction=f, bounce=b [,shape=s]})
    

以下是实体形状的示例:

  • 圆形实体:

    local ball = display.newImage("ball.png")
    physics.addBody( ball, "dynamic" { density = 1.0, friction = 0.3, bounce = 0.2, radius = 25 } )
    
  • 多边形实体:

    local rectangle = display.newImage("rectangle.png")
    rectangleShape = { -6,-48, 6,-48, 6,48, -6,48 }
    physics.addBody( rectangle, { density=2.0, friction=0.5, bounce=0.2, shape=rectangleShape } )
    

现在,我们将讨论前面方法的相关参数:

  • 对象:这是一个显示对象。

  • bodyType:这是一个字符串,用于指定身体类型是可选的。它在第一个身体元素之前使用一个字符串参数。可能的类型是"static"(静态)、"dynamic"(动态)和"kinematic"(动力学)。如果未指定值,默认类型是"dynamic"。让我们来谈谈这些类型:

    • 静态物体除非在代码中手动移动,否则不会移动,它们也不会相互交互;静态物体的例子包括弹球机的地面或墙壁。

    • 动态物体受重力和与其他物体类型的碰撞影响。

    • 动力学物体受力的影响,但不受重力影响,因此你通常应该将可拖动的物体设置为动力学物体,至少在拖动事件期间是这样。

  • Density:这是一个数值,通过乘以物体形状的面积来确定质量。它基于水的标准值 1.0。较轻的材料(如木材)的密度低于 1.0,而较重的材料(如石头)的密度则高于 1.0。默认值为1.0

  • Friction:这是一个数值。可以是任何非负值;0 表示没有摩擦力,1.0 表示相当强的摩擦力。默认值为0.3

  • Bounce:这是一个数值,决定了物体碰撞后返回的速度。默认值为0.2

  • Radius:这是一个数值。这是边界圆的半径,单位为像素。

  • Shape:这是一个数值。它是形状顶点的表格形式的形状值,即{x1, y1, x2, y2, …, xn, yn},例如rectangleShape = { -6,-48, 6,-48, 6,48, -6,48 }。坐标必须按顺时针顺序定义,且结果形状必须是凸的。物理引擎假设物体的(0,0)点是物体的中心。一个负 x坐标将位于物体中心的左侧,而负 y坐标将位于物体中心的顶部。

动手时间——为挡板和球启动物理效果。

目前,我们的显示对象相当静止。为了让游戏开始,我们必须为挡板和球激活物理效果,以发生碰撞检测。执行以下步骤:

  1. gameLevel1()函数之上,创建一个名为startGame()的新函数:

    function startGame()
    
  2. 添加以下几行代码来为挡板和球实例化物理效果:

      physics.addBody(paddle, "static", {density = 1, friction = 0, bounce = 0})
      physics.addBody(ball, "dynamic", {density = 1, friction = 0, bounce = 0})
    
  3. 创建一个事件监听器,使用背景显示对象来移除startGame()"tap"事件。使用end关闭函数:

      background:removeEventListener("tap", startGame)
    end
    
  4. 在上一章中我们创建的addGameScreen()函数里,需要在调用gameLevel1()函数之后添加以下这行代码。这样,当触摸背景时,就会开始实际的游戏:

      background:addEventListener("tap", startGame)
    

刚才发生了什么?

挡板对象有一个"static"(静态)的物体类型,所以它不会受到任何与之相撞的碰撞影响。

球对象有一个"dynamic"(动态)的物体类型,因为我们需要它受到屏幕上由于墙壁边界、砖块和挡板造成的方向改变而产生的碰撞影响。

startGame()函数中从背景移除了事件监听器;这样它就不会影响游戏中应用的其他触摸事件。

挡板移动

让挡板左右移动是必须完成的关键动作之一。游戏设计的一部分是防止球到达屏幕底部。我们将把模拟器中的挡板移动与加速度计分离。在模拟器中的移动使我们能够通过触摸事件进行测试,因为加速度计动作无法在模拟器中测试。

动作时间——在模拟器中拖动挡板

目前,挡板根本不会移动。没有设置允许挡板在屏幕上左右移动的坐标。所以让我们通过执行以下步骤来创建它们:

  1. addGameScreen()函数下方,创建一个名为dragPaddle(event)的新函数:

    function dragPaddle(event)
    
  2. 接下来,我们将关注在游戏屏幕边界内左右移动挡板。添加以下代码块以在模拟器中启用挡板移动,然后关闭函数。添加此代码块的原因是模拟器不支持加速度计事件:

      if isSimulator then
    
        if event.phase == "began" then
          moveX = event.x - paddle.x
        elseif event.phase == "moved" then
          paddle.x = event.x - moveX
        end
    
        if((paddle.x - paddle.width * 0.5) < 0) then
          paddle.x = paddle.width * 0.5
        elseif((paddle.x + paddle.width * 0.5) > display.contentWidth) then
          paddle.x = display.contentWidth - paddle.width * 0.5
        end
    
      end
    
    end
    

查看以下图像,预测球与砖块和挡板碰撞后球将向何处移动:

动作时间——在模拟器中拖动挡板

刚才发生了什么?

我们创建了一个仅在模拟器中起作用的拖动事件函数。对于if event.phase == "began",已经对挡板进行了触摸事件。在elseif event.phase == "moved",已经对挡板从原始位置移动的触摸事件进行了处理。

为了防止挡板移动超过墙壁边界,当挡板碰到坐标时,paddle.xx方向上不会小于0。当挡板滑向屏幕右侧时,paddle.xx方向上不会大于display.contentWidth

由于代码应该适用于 iOS 和 Android 设备上所有屏幕尺寸,因此没有指定屏幕右侧的坐标。这两个平台具有不同的屏幕分辨率,所以display.contentWidth考虑到了这一点。

动作时间——使用加速度计移动挡板

如前所述,加速度计事件无法在模拟器中测试。它们仅在将游戏构建上传到设备以查看结果时才起作用。挡板移动将保持在关卡x轴上的墙壁边界内。要移动挡板,请按照以下步骤操作:

  1. dragPaddle()函数下方,创建一个名为movePaddle(event)的新函数:

    function movePaddle(event)
    
  2. 使用yGravity添加加速度计移动。它提供了y方向上的重力加速度:

      paddle.x = display.contentCenterX - (display.contentCenterX * (event.yGravity*3))
    
  3. 添加关卡墙壁边界并在函数末尾关闭:

      if((paddle.x - paddle.width * 0.5) < 0) then
        paddle.x = paddle.width * 0.5
      elseif((paddle.x + paddle.width * 0.5) > display.contentWidth) then
        paddle.x = display.contentWidth - paddle.width * 0.5
      end
    end
    

刚才发生了什么?

要使加速度计移动在设备上工作,我们必须使用yGravity

注意事项

当使用xGravityyGravity时,加速度计事件基于竖屏刻度。当显示对象被指定为横屏模式时,xGravityyGravity的值会交换,以补偿事件正常工作。

我们对挡板应用了与function dragPaddle()中相同的代码:

  if((paddle.x - paddle.width * 0.5) < 0) then
    paddle.x = paddle.width * 0.5
  elseif((paddle.x + paddle.width * 0.5) > display.contentWidth) then
    paddle.x = display.contentWidth - paddle.width * 0.5
  end

这仍然可以防止挡板越过任何墙壁边界。

球与挡板的碰撞

每次球与挡板碰撞时,其运动都必须流畅。这意味着在游戏场的所有侧面都要有适当的方向改变。

动手时间——让球反弹到挡板上

我们将检查球击中了挡板的哪一侧,以选择它接下来将移动的一侧。让运动跟随任何方向打击,就像在真实环境中一样,这很重要。每次与挡板碰撞,我们都要确保球向上移动。为此,请按照以下步骤操作:

  1. movePaddle()函数后创建一个名为bounce()的新函数,用于处理球:

    function bounce()
    
  2. y方向上添加一个值为-3的速度。这将使球向上移动:

      vy = -3
    
  3. 检查paddleball对象之间的碰撞,并关闭函数:

      if((ball.x + ball.width * 0.5) < paddle.x) then
        vx = -vx
      elseif((ball.x + ball.width * 0.5) >= paddle.x) then
        vx = vx
      end
    end
    

刚才发生了什么?

当球与挡板碰撞时,其运动取决于球接触挡板的哪一侧。在if语句的第一部分,球在x方向上向 0 移动。if语句的最后部分显示了球在x方向上向屏幕的另一侧移动。

从场景中移除对象

设备上的资源是有限的。我们希望它们能像桌面一样强大,拥有如此多的内存,但现在还没有达到这个水平。这就是为什么当您在应用程序中不再使用显示对象时,从显示层次结构中移除它们很重要的原因。这有助于通过减少内存消耗来提高整体系统性能,并消除不必要的绘制。

当创建显示对象时,默认会添加到显示层次结构的根对象中。这个对象是一种特殊的组对象,称为舞台对象。

为了防止对象在屏幕上渲染,需要将其从场景中移除。需要明确地从其父对象中移除该对象。这将对象从显示层次结构中移除。可以通过以下方式完成:

myImage.parent:remove( myImage ) -- remove myImage from hierarchy

或者,可以使用以下代码行完成此操作:

myImage:removeSelf( ) -- same as above

这并不会释放显示对象所有的内存。为了确保显示对象被正确移除,我们需要消除所有对其的变量引用。

变量引用

即使显示对象已从层次结构中移除,但在某些情况下,对象仍然存在。为此,我们将属性设置为nil

local ball = display.newImage("ball.png")
local myTimer = 3

function time()
  myTimer = myTimer - 1
  print(myTimer)

  if myTimer == 0 then 

    ball:removeSelf()
    ball = nil

  end
end

timer.performWithDelay( 1000, time, myTimer )

一砖一瓦

游戏中的砖块是主要的障碍物,因为必须清除它们才能进入下一轮。在这个版本的打砖块游戏中,玩家必须一次性摧毁所有砖块。如果做不到这一点,则需要从当前关卡的开始处重新开始。

行动时间——移除砖块

当球与砖块碰撞时,我们将使用与挡板相同的技术来确定球的路径。当击中砖块时,我们需要找出哪块砖被触碰,然后将其从舞台和砖块组中移除。每移除一块砖,分数增加 100 分。分数将从score常数中取出,并作为文本添加到当前分数中。要移除游戏中的砖块,请按照以下步骤操作:

  1. gameLevel2()函数下方,创建一个名为removeBrick(event)的函数:

    function removeBrick(event)
    
  2. 使用if语句检查球击中砖块的哪一侧。在检查事件时,我们将事件引用到对象名称"brick"。这是我们给brick显示对象起的名字:

      if event.other.name == "brick" and ball.x + ball.width * 0.5 < event.other.x + event.other.width * 0.5 then
        vx = -vx 
      elseif event.other.name == "brick" and ball.x + ball.width * 0.5 >= event.other.x + event.other.width * 0.5 then
        vx = vx 
      end
    
  3. 添加以下if语句,当球与砖块碰撞时,从场景中移除砖块。碰撞发生后,将score增加 1。将scoreNum初始化为取分数的值,并将其乘以scoreIncrease

      if event.other.name == "brick" then
        vy = vy * -1
        event.other:removeSelf()
        event.other = nil
        bricks.numChildren = bricks.numChildren - 1
    
        score = score + 1
        scoreNum.text = score * scoreIncrease
        scoreNum.anchorX = 0
        scoreNum.x = 54 
      end
    
  4. 当关卡中的所有砖块被摧毁时,创建一个if语句,弹出胜利条件的警告屏幕,并将gameEvent字符串设置为"win"

      if bricks.numChildren < 0 then
        alertScreen("YOU WIN!", "Continue")
        gameEvent = "win"
      end
    
  5. 使用end关闭函数:

    end
    

以下是球与挡板碰撞的截图:

行动时间——移除砖块

刚才发生了什么?

如果你记得上一章的内容,我们给brick对象起了一个名为"brick"的名字。

当球击中砖块的左侧时,它会向左移动。当球击中砖块的右侧时,它会向右移动。每个对象的宽度被视为整体,以计算球移动的方向。

当砖块被击中时,球会向上弹起(即y方向)。球与每块砖碰撞后,砖块会从场景中移除,并从内存中销毁。

bricks.numChildren – 1语句从最初开始的总砖块数中减去计数。每当移除一块砖,分数增加 100 分。每当击中砖块时,scoreNum文本对象会更新分数。

当所有砖块都被移除时,警告屏幕会弹出通知玩家已经赢得关卡。我们还设置gameEvent等于"win",这将在另一个函数中使用,以将事件过渡到新场景。

方向变化

除了球与挡板相对运动之外,另一个因素是球与墙壁边界的碰撞状态。当发生碰撞时,球会以相反的方向改变其移动方向。每个动作都有相应的反应,就像现实世界中的物理一样。

动作时间——更新球的位置

球需要以连续的运动移动,不受重力影响。我们需要考虑侧墙以及顶底墙壁。当球在任何边界上发生碰撞时,xy方向的速度必须反射回来。我们需要设置坐标,以便球只能通过并在穿过挡板区域以下时发出警告。让我们执行以下步骤:

  1. removeBrick(event)函数下方创建一个名为function updateBall()的新函数:

    function updateBall()
    
  2. 添加球的移动:

      ball.x = ball.x + vx
      ball.y = ball.y + vy
    
  3. 添加球在x方向上的移动:

      if ball.x < 0 or ball.x + ball.width > display.contentWidth then
        vx = -vx
      end
    

    下面的截图展示了球在x方向上的移动:

    动作时间——更新球的位置

  4. 添加球在y方向上的移动:

      if ball.y < 0 then 
        vy = -vy 
      end
    

    下面的截图展示了球在y方向上的移动:

    动作时间——更新球的位置

  5. 添加球与游戏屏幕底部碰撞时的移动。创建失败警告屏幕并设置一个"lose"的游戏事件。使用end结束函数:

      if ball.y + ball.height > paddle.y + paddle.height then 
        alertScreen("YOU LOSE!", "Play Again") gameEvent = "lose" 
      end
    end
    

    下面的截图显示了当球与游戏屏幕底部碰撞时出现的失败警告屏幕:

    动作时间——更新球的位置

刚才发生了什么?

球移动的每个位置,当它撞击墙壁时都需要改变方向。每当球撞击侧墙,我们使用vx = -vx。当球撞击顶部边界时,使用vy = -vy。唯一球不会反射相反方向的情况是它撞击屏幕底部。

警告屏幕显示了失败条件,这强调了玩家需要再次游戏。gameEvent = "lose"的声明将在另一个if语句中使用,以重置当前关卡。

转换关卡

当出现胜利或失败的条件时,游戏需要一种方式来转换到下一个关卡或重复当前关卡。主要游戏对象必须被重置到起始位置,并且重新绘制砖块。这与游戏开始时的想法基本相同。

动作时间——重置和改变关卡

我们需要创建一些函数来设置游戏中的第一关和第二关。如果一个关卡需要重玩,只能访问用户在当前关卡中失败的那一关。以下是转换关卡之间的步骤:

  1. 创建一个名为changeLevel1()的新函数。这将被放置在updateBall()函数下方:

    function changeLevel1()
    
  2. 当玩家输掉一轮游戏时,清除bricks组,然后重置它们:

      bricks:removeSelf()
    
      bricks.numChildren = 0
      bricks = display.newGroup()
    
  3. 移除alertDisplayGroup

      alertBox:removeEventListener("tap", restart)
      alertDisplayGroup:removeSelf()
      alertDisplayGroup = nil
    
  4. 重置ballpaddle的位置:

      ball.x = (display.contentWidth * 0.5) - (ball.width * 0.5)
      ball.y = (paddle.y - paddle.height) - (ball.height * 0.5) -2
    
      paddle.x = display.contentWidth * 0.5
    
  5. 重新绘制当前关卡的砖块:

    gameLevel1()
    
  6. background对象上添加一个startGame()的事件监听器,并结束此函数:

      background:addEventListener("tap", startGame)
    end
    
  7. 接下来,创建一个名为changeLevel2()的新函数。应用与changeLevel1()相同的代码,但确保为gameLevel2()重绘砖块:

    function changeLevel2()
    
      bricks:removeSelf()
    
      bricks.numChildren = 0
      bricks = display.newGroup()
    
      alertBox:removeEventListener("tap", restart)
      alertDisplayGroup:removeSelf()
      alertDisplayGroup = nil
    
      ball.x = (display.contentWidth * 0.5) - (ball.width * 0.5)
      ball.y = (paddle.y - paddle.height) - (ball.height * 0.5) -2
    
      paddle.x = display.contentWidth * 0.5
    
     gameLevel2() -- Redraw bricks for level 2
    
      background:addEventListener("tap", startGame)
    end
    

刚才发生了什么?

当需要重置或更改关卡时,必须从屏幕上清除显示对象。在这种情况下,我们使用bricks:removeSelf()移除了bricks组。

当任何提示屏幕弹出时,无论是赢还是输,整个alertDisplayGroup在重置时也会被移除。ballpaddle对象会被设置回起始位置。

gameLevel1()函数被调用,以重绘第一关的砖块。该函数负责brick显示对象和bricks组的初始设置。

background对象再次使用事件监听器调用startGame()函数。当需要设置第二关时,使用与changeLevel1()函数相同的程序,但是调用changeLevel2()gameLevel2()来重绘砖块。

尝试英雄——添加更多关卡。

目前,游戏只有两个关卡。要扩展这个游戏,可以添加更多的关卡。它们可以使用与gameLevel1()gameLevel2()相同的逻辑来创建,通过调整用于创建砖块行和列的数字。你需要创建一个新的函数来重置关卡。我们可以使用与changeLevel1()changeLevel2()相同的方法来重新创建并重置关卡。

有赢就有输。

没有什么比期待胜利更令人兴奋了。直到你犯了一个小错误,导致你必须重新开始。别担心,这并不是世界末日;你总是可以再次尝试并从错误中学习,以打败这一关卡。

游戏事件,如胜负条件,会提示玩家他们的进度。游戏必须有某种方式指导玩家下一步需要采取什么行动来重玩关卡或进入下一关。

是时候制定胜负条件了。

为了让游戏中的任何提示出现,我们需要为每个关卡中可能出现的每一种情况创建一些if语句。当这种情况发生时,分数需要重置回零。要制定胜负条件,请按照以下步骤操作:

  1. alertScreen()函数下面,创建一个名为restart()的新函数:

    function restart()
    
  2. 为在完成第一关并过渡到第二关时创建一个游戏胜利的if语句:

      if gameEvent == "win" and currentLevel == 1 then
        currentLevel = currentLevel + 1
        changeLevel2()
        levelNum.text = tostring(currentLevel)
    

    注意

    tostring()方法将任何参数转换为字符串。在前面示例中,当发生"win"游戏事件时,currentLevel的值从1变为2。该值将转换为字符串格式,以便levelNum文本对象可以在屏幕上显示第二关的数字。

  3. 为在完成第二关时创建一个游戏胜利的elseif语句,并在通知玩家游戏已完成时:

      elseif gameEvent == "win" and currentLevel == 2 then
        alertScreen("  Game Over", "  Congratulations!")
        gameEvent = "completed"
    
  4. 在第一级中为"lose"游戏事件添加另一个elseif语句。将分数重置为零,重新开始第一级:

      elseif gameEvent == "lose" and currentLevel == 1 then
        score = 0
        scoreNum.text = "0"
        changeLevel1()
    
  5. 为第二级的"lose"游戏事件添加另一个elseif语句。将分数重置为零,重新开始第二级:

      elseif gameEvent == "lose" and currentLevel == 2 then
        score = 0
        scoreNum.text = "0"
        changeLevel2()
    
  6. 最后,为gameEvent = "completed"添加另一个elseif语句。用end结束函数:

      elseif gameEvent == "completed" then
        alertBox:removeEventListener("tap", restart)
      end
    end
    
  7. 现在,我们需要回溯并在alertScreen()函数中使用alertBox对象添加一个事件监听器。我们将它添加到函数底部。这将激活restart()函数:

      alertBox:addEventListener("tap", restart)
    

刚才发生了什么?

restart()函数检查游戏过程中发生的所有gameEventcurrentLevel变量。当一个游戏事件检查到"win"字符串时,它也会继续执行下面的语句,看哪个为真。例如,如果玩家赢了且当前在第一级,那么玩家将进入第二级。

如果玩家输了,gameEvent == "lose"变为真,代码会检查玩家在哪个级别输掉。无论玩家在哪个级别输掉,分数都会重置为 0,并且玩家所在的当前级别将重新设置。

激活事件监听器

这个游戏中的事件监听器基本上控制了物体的运动开关。我们已经编写了执行游戏对象动作的函数来运行级别。现在是时候通过特定类型的事件来激活它们了。正如你在前一章注意到的,我们可以向显示对象添加事件监听器,或者让它们全局运行。

碰撞事件

物理引擎中的碰撞事件通过 Corona 的事件监听器模型发生。有三个新的事件类型,如下所示:

  • "collision":此事件包括"began""ended"阶段,分别表示初次接触和接触断开时刻。这些阶段适用于正常的两物体碰撞和物体传感器碰撞。如果你没有实现"collision"监听器,此事件将不会触发。

  • "preCollision":这是一个在物体开始交互之前触发的事件类型。根据你的游戏逻辑,你可能希望检测此事件并有条件地覆盖碰撞。它也可能导致每个接触点多次报告,影响应用程序的性能。

  • "postCollision":这是一个在物体交互后立即触发的事件类型。这是唯一一个报告碰撞力的事件。如果你没有实现"postCollision"监听器,此事件将不会触发。

碰撞事件在对象对之间报告,可以通过运行时监听器全局检测,或者在对象内部使用表监听器本地检测。

全局碰撞监听器

当作为运行时事件检测时,每个碰撞事件包括event.object1,其中包含涉及到的 Corona 显示对象的表 ID。

这是一个例子:

local physics = require "physics"
physics.start()

local box1 = display.newImage( "box.png" )
physics.addBody( box1, "dynamic", { density = 1.0, friction = 0.3, bounce = 0.2 } )
box1.myName = "Box 1"

local box2 = display.newImage( "box.png", 0, 350)
physics.addBody( box2, "static", { density = 1.0, friction = 0.3, bounce = 0.2 } )
box2.myName = "Box 2"

local function onCollision( event )
  if event.phase == "began" and event.object1.myName == "Box 1" then

    print( "Collision made." )

  end
end

Runtime:addEventListener( "collision", onCollision )

本地碰撞监听器

当在对象内部使用表监听器检测时,每个碰撞事件都包括event.other,其中包含参与碰撞的另一个显示对象的表 ID。

这是一个示例:

local physics = require "physics"
physics.start()

local box1 = display.newImage( "box.png" )
physics.addBody( box1, "dynamic", { density = 1.0, friction = 0.3, bounce = 0.2 } )
box1.myName = "Box 1"

local box2 = display.newImage( "box.png", 0, 350)
physics.addBody( box2, "static", { density = 1.0, friction = 0.3, bounce = 0.2 } )
box2.myName = "Box 2"

local function onCollision( self, event )
  if event.phase == "began" and self.myName == "Box 1" then

    print( "Collision made." )

  end
end

box1.collision = onCollision
box1:addEventListener( "collision", box1 )

box2.collision = onCollision
box2:addEventListener( "collision", box2 )

行动时间——添加游戏监听器

对于我们为游戏对象创建的许多功能,我们需要激活事件监听器,以便它们能够运行代码,并在游戏停止时禁用它们。要添加游戏监听器,请按照以下步骤操作:

  1. 为了完成这个游戏,我们需要创建的最后一个函数叫做gameListeners(),它还将有一个名为event的参数。这应该在gameLevel2()函数之后直接添加:

    function gameListeners(event)
    
  2. 添加以下事件监听器,它们将使用if语句在应用程序中启动多个事件:

      if event == "add" then
        Runtime:addEventListener("accelerometer", movePaddle)
        Runtime:addEventListener("enterFrame", updateBall)
        paddle:addEventListener("collision", bounce)
        ball:addEventListener("collision", removeBrick)
        paddle:addEventListener("touch", dragPaddle)
    
  3. 接下来,我们将为事件监听器添加一个elseif语句,以移除事件并关闭函数:

      elseif event == "remove" then
        Runtime:removeEventListener("accelerometer", movePaddle)
        Runtime:removeEventListener("enterFrame", updateBall)
        paddle:removeEventListener("collision", bounce)
        ball:removeEventListener("collision", removeBrick)
        paddle:removeEventListener("touch", dragPaddle)
    
      end
    end
    
  4. 为了使function gameListeners()正常工作,我们需要在startGame()函数中使用参数中的"add"字符串对其进行实例化。将其放在函数末尾之前:

      gameListeners("add")
    
  5. alertScreen()函数中,将"remove"字符串添加到参数中,并将其放在函数开始处:

      gameListeners("remove")
    
  6. 所有代码都已经编写完成!继续在模拟器中运行游戏。该应用程序也适用于设备。为你正在开发的设备制作一个符合所需尺寸的简单图标图像。编译构建并在你的设备上运行。

刚才发生了什么?

对于event参数,有两个if语句集:"add""remove"

这个函数中的所有事件监听器在使游戏运行方面都起着重要作用。"accelerometer""enterframe"事件被用作运行时事件,因为它们没有特定的目标。

挡板对象都具有"collision"事件,在任何对象接触时都会执行其功能。

"touch"事件允许用户触摸并拖动挡板,使其在模拟器中来回移动。

请注意,当event == "remove"时,它会移除游戏中所有活动的事件监听器。当游戏开始时,gameListeners("add")会被激活。当达到胜利或失败条件时,gameListeners("remove")会被激活。

尝试一下吧——让我们将一切颠倒过来

如果我们决定将游戏上下颠倒,也就是说,将挡板放置在屏幕顶部附近,球在挡板下方,砖块组靠近屏幕底部,该怎么办?

你需要考虑的事情如下:

  • 现在顶部墙壁是你必须防止球进入的区域

  • 当球与砖块碰撞时,y方向是球移动的方向

  • 当球与底部墙壁碰撞时,它必须从底部墙壁反射回来

如你所见,在将值从负数切换到正数以及反之之前,有一些事情需要考虑。在创建这个新变体时,请确保验证你的逻辑,并确保它是有意义的。

结果出来了!

让我们总结一下你所做的工作,确保你的游戏中已经包含了所有内容。你也可以参考Chapter 4文件夹中的Breakout Final文件夹,查看最终的代码。你确保了在游戏中引入了必要的变量。你还初始化了启动游戏玩的main()函数。实现了一个主菜单屏幕,带有游戏标题和一个播放按钮。

接下来,你将menuScreenGroup从屏幕上移开,加载主游戏区域。添加了游戏的主要显示对象,如挡板、球和砖块。分数和关卡数作为 UI 元素显示并在游戏过程中更新。还添加了模拟器和加速度计中的挡板移动以及挡板和球的碰撞检测。

在游戏开始时添加了挡板和球的物理属性。为两个关卡创建了砖块布局。你还在游戏对象需要激活时添加了事件监听器,并在游戏结束时移除。

每当球与砖块碰撞,砖块就会从场景中移除。球的方向变化在每次与墙壁、挡板或砖块碰撞后都会更新。每当出现赢或输的条件时,所有游戏对象都会重置,以便开始当前或新关卡。

当发生某个条件时,会弹出一个警告屏幕,通知玩家发生了什么。触发警告的显示对象是在一个函数中创建的。最后,创建了赢和输的参数,以确定是否需要重玩当前关卡,玩家是否进入下一关,或者游戏是否已经完成。

注意大小写敏感的变量和函数,以免遇到错误。同时,确保你没有遗漏代码中所需的标点符号。这些容易被忽视。如果在模拟器中遇到错误,请参考终端窗口中的错误引用。

小测验——使用游戏控制

Q1. 你应该如何正确地从舞台中移除一个显示对象?

  1. remove()

  2. object: remove()

  3. object:removeSelf()

    object = nil

  4. 以上都不是。

Q2. 将以下显示对象转换为物理对象正确的方法是什么?

local ball = display.newImage("ball.png")
  1. physics.addBody( ball, { density=2.0, friction=0.5, bounce=0.2,radius = 25 })

  2. physics.addBody( ball, "dynamic", { density=2.0, friction=0.5, bounce=0.2,radius = 15 } )

  3. 1and 2.(这一行似乎不完整,但按照要求保留原文)

  4. 以上都不是。

Q3. 在以下函数中,"began"一词的最佳解释是什么?

local function onCollision( event )
  if event.phase == "began" and event.object1.myName == "Box 1" then

    print( "Collision made." )

  end
end
  1. 手指在屏幕上移动。

  2. 一个手指从屏幕上抬起。

  3. 系统取消了开始触摸的跟踪。

  4. 一个手指触摸了屏幕。

总结

恭喜你!你已经完成了你的第一个游戏制作!你应当为自己感到非常骄傲。现在,你已经体验到了使用 Corona SDK 制作应用程序有多么简单。只需几百行代码就能制作一个应用程序。

在本章中,我们完成了以下工作:

  • 为挡板添加了触摸事件移动

  • 引入了加速度计功能

  • 为所有受影响的游戏对象实现了碰撞事件监听器

  • 当游戏屏幕不再需要对象时,从内存中移除它们

  • 将球的移动实现为物理对象

  • 更新了每次砖块碰撞的计分板

  • 学习了如何处理胜利和失败的条件

最后两章并没有那么糟糕,不是吗?随着你继续使用 Lua 编程,你会越来越熟悉工作流程。只要你不断进步并与不同的游戏框架合作,理解起来肯定会更加容易。

下一章将介绍另一个肯定会吸引你注意的游戏。你将为你的显示对象创建动画精灵表。这对视觉来说是不是很棒?

第五章:动画我们的游戏

在我们移动游戏开发的旅程中,我们已经开始了很好的起步。我们已经经历了大量的编程,从游戏逻辑到在屏幕上显示对象。Corona SDK 最强大的功能之一就是任何显示对象都可以被动画化。这是对 Corona 提供的灵活图形模型的证明。

动画为游戏中的用户体验增添了大量的角色。这是通过生成一系列帧来实现的,这些帧从一帧平滑地演变到下一帧。我们将学习这项技能并将其应用于将要创建的新游戏。

在本章中,我们将:

  • 使用动作和过渡进行操作

  • 使用图像表进行动画

  • 为显示对象创建一个游戏循环

  • 构建我们的下一个游戏框架

让我们开始动画吧!

熊猫星星捕手

本节将创建我们的第二个游戏,名为熊猫星星捕手。主要角色是一只名叫玲玲的熊猫,它需要被发射到空中,并在计时器耗尽之前捕捉尽可能多的星星。熊猫将会有动画效果,每个行动过程都有不同的动作,例如发射前的设置和空中的动作。还将应用弹弓机制将玲玲发射到空中。你可能已经在如愤怒的小鸟城堡破坏者之类的游戏中见过类似的功能。

让我们来让一切动起来

我们在第三章中介绍了过渡,并简要地接触了它。让我们更详细地了解。

过渡效果

过渡库允许你通过一行代码创建动画,通过允许你补间显示对象的一个或多个属性。我们在第三章中讨论了过渡的基础,创建我们的第一个游戏 - 破坏者

这可以通过transition.to方法实现,它接收一个显示对象和一个包含控制参数的表。控制参数指定动画的持续时间以及显示对象的属性的最终值。属性的中间值由可选的缓动函数确定,该函数也作为控制参数指定。

transition.to() 方法使用“缓动”算法,随时间动画显示对象的属性。

语法是 handle = transition.to( target, params )

返回函数是一个对象。参数如下:

  • target:这是一个将成为过渡目标的对象。这包括显示对象。

  • params:这是一个指定要动画显示对象的属性的表,以及以下一个或多个可选的非动画属性:

    • params.time:这指定了过渡的持续时间(以毫秒为单位)。默认情况下,持续时间为 500 毫秒(0.5 秒)。

    • params.transition: 默认情况下,此参数为 easing.linear

    • params.delay: 此参数指定了补间动画开始前的延迟时间(默认为无延迟),单位为毫秒。

    • params.delta: 这是一个布尔值,指定非控制参数是作为最终结束值还是作为值的改变量来解释。默认为 nil,即 false。

    • params.onStart: 这是一个在补间动画开始前调用的函数或表监听器。

    • params.onComplete: 这是一个在补间动画完成后调用的函数或表监听器。

缓动函数

缓动库是过渡库使用的一系列插值函数的集合。例如,打开抽屉的动作,最初是快速移动,然后在停止之前进行缓慢精确的移动。以下是几个缓动示例:

  • easing.linear(t, tMax, start, delta): 此函数定义了一个没有加速度的恒定运动

  • easing.inQuad(t, tMax, start, delta): 此函数在过渡中对动画属性值进行二次插值运算

  • easing.outQuad(t, tMax, start, delta): 此函数一开始速度很快,然后在执行过程中减速至零速度

  • easing.inOutQuad(t, tMax, start, delta): 此函数从零速度开始动画,加速然后减速至零速度

  • easing.inExpo(t, tMax, start, delta): 此函数从零速度开始,然后在执行过程中逐渐加速

  • easing.outExpo(t, tMax, start, delta): 此函数一开始速度很快,然后在执行过程中减速至零速度

  • easing.inOutExpo(t, tMax, start, delta): 此函数从零速度开始,使用指数缓动方程加速然后减速至零速度

你可以创建自己的缓动函数来在起始值和最终值之间插值。函数的参数定义如下:

  • t: 这是过渡开始后的毫秒数时间

  • tMax: 这是过渡的持续时间

  • start: 这是起始值

  • delta: 这是值的改变量(最终值 = start + delta

例如:

local square = display.newRect( 0, 0, 50, 50 )
square:setFillColor( 1,1,1 )
square.x = 50; square.y = 100

local square2 = display.newRect( 0, 0, 50, 50 )
square2:setFillColor( 1,1,1 )
square2.x = 50; square2.y = 300

transition.to( square, { time=1500, x=250, y=0 } )
transition.from( square2, { time=1500, x=250, y=0, transition = easing.outExpo } )

定时函数的价值

使用可以在稍后调用的函数,在组织应用程序中游戏对象出现的时间时可能很有帮助。定时器库将允许我们及时处理函数。

定时器

定时器函数使你能够选择一个特定的延迟(以毫秒为单位)来触发事件。

  • timer.performWithDelay(delay, listener [, iterations]): 此函数在指定的延迟毫秒数后调用监听器,并返回一个句柄对象,你可以通过传递给 timer.cancel() 来取消定时器,防止在调用监听器之前触发。例如:

    local function myEvent()
      print( "myEvent called" )
    end
    timer.performWithDelay( 1000, myEvent )
    
  • timer.cancel(timerId): 这取消了使用 timer.performWithDelay() 初始化的定时器操作。参数如下:

    • timerId: 这是通过调用 timer.performWithDelay() 返回的对象句柄。例如:

      local count = 0
      
      local function myEvent()
        count = count + 1
        print( count )
      
        if count >= 3 then
          timer.cancel( myTimerID ) -- Cancels myTimerID
          end
        end
      
  • timer.pause(timerId): 这将暂停使用timer.performWithDelay()启动的定时器对象。参数如下:

    • timerId: 这是来自timer.performWithDelay()的定时器 ID 对象。例如:

      local count = 0
      
      local function myEvent()
        count = count + 1
        print( count )
      
        if count >= 5 then
          timer.pause( myTimerID ) -- Pauses myTimerID
          end
      end
      
      myTimerID = timer.performWithDelay(1000, myEvent, 0)
      
  • timer.resume(timerId): 这将恢复使用timer.pause(timerId)暂停的定时器。参数如下:

    • timerID: 这是来自timer.performWithDelay()的定时器 ID。例如:

      local function myEvent()
        print( "myEvent called" )
      end
      
      myTimerID = timer.performWithDelay( 3000, myEvent )  -- wait 3 seconds
      
      result = timer.pause( myTimerID ) -- Pauses myTimerID
      print( "Time paused at " .. result )
      
      result = timer.resume( myTimerID ) -- Resumes myTimerID
      print( "Time resumed at " .. result )
      

什么是图像表?

Corona SDK 包括一个图像表功能,用于构建动画精灵(也称为精灵表)。

注意

有关图像表的更多信息,请参考以下链接:docs.coronalabs.com/guide/media/imageSheets/index.html

图像表是节省纹理内存的有效方式。建议在复杂的角色动画或涉及大量动画类型时使用。

图像表需要更多的编码和更高级的设置。它们需要构建一个大型动画帧表。

这是精灵狂热!

图像表是将多个帧编译成单个纹理图像的 2D 动画。这是一种节省纹理内存的有效方式。它对移动设备有益,并最小化加载时间。

图像表 API

graphics.newImageSheet函数创建一个新的图像表。参考以下代码:

graphics.newImageSheet( filename, [baseDir, ] options )

例如,图像表中的帧数假定为floor(imageWidth/frameWidth) * floor(imageHeight/frameHeight)。第一帧放置在左上角位置,从左到右读取,并在适用的情况下继续下一行。以下图像表有五个 128 x 128 像素的帧。整个图像表图像是 384 像素 x 256 像素。如果要在 Corona 中集成,一个示例方法将如下所示:

local options =
{
  width = 128,
  height = 128,
  numFrames = 5,
  sheetContentWidth=384, 
  sheetContentHeight=256
}
local sheet = graphics.newImageSheet( "mySheet.png", options )

图像表 API

display.newSprite(imageSheet, sequenceData)函数从一个图像表中创建一个新的精灵。精灵定义了属于同一个角色或其他移动资产的帧集合,然后可以将其细分为不同的动画序列以供播放。sequenceData参数是你设置的一系列动画序列数组。序列可以在多个精灵对象之间共享。以下是一些示例:

  • 单序列(连续帧):

    local sequenceData =
    {
      name="run", start=1, count=5, time=200, loopCount=0
    }
    
    local myCharacter = display.newSprite(imageSheet, sequenceData)
    
  • 单序列(非连续帧):

    local sequenceData =
    {
      name="jump", 
      frames= { 6, 7, 8 }, 
      time=200,
      loopCount=0
    }
    
    local myCharacter = display.newSprite(imageSheet, sequenceData)
    
  • 多序列(连续和非连续帧):

    local sequenceData =
    {
      { name="run", start=1, count=5, time=200 },
      {name="jump", frames= { 6, 7, 8 }, time=200, loopCount=0 }
    }
    
    local myCharacter = display.newSprite(imageSheet, sequenceData)
    
  • object:pause(): 这将暂停当前动画。帧将保持在当前显示的帧。

  • object:play(): 这将从当前帧开始播放动画序列。

  • object:setFrame(): 这在当前加载的序列中设置帧。

  • object:setSequence(): 这通过名称加载一个动画序列。

游戏时间!

既然我们已经学会了如何设置图像表,那么让我们尝试将它们应用到Panda Star Catcher中!你可以从 Packt Publishing 网站下载伴随这本书的项目文件。在Chapter 5文件夹中有一个名为Panda Star Catcher的项目文件夹。它已经为你设置了config.luabuild.settings文件。文件夹中还包括了美术资源。从第三章,构建我们的第一个游戏——Breakout和第四章,游戏控制,你可能已经注意到构建和运行时的配置有类似的设置。本教程适用于 iOS 和 Android 设备。项目文件夹中包含的图形已经设计好,可以在两个平台上正确显示。游戏的欢迎屏幕将如下所示:

游戏时间!

动手时间——设置变量

让我们先介绍运行游戏所需的所有变量:

  1. 创建一个全新的main.lua文件,并将其添加到Panda Star Catcher项目文件夹中。

  2. 让我们从设备上隐藏状态栏,并设置游戏中所需的所有变量:

    display.setStatusBar( display.HiddenStatusBar ) -- Hides the status bar in iOS only
    
    -- Display groups
    local hudGroup = display.newGroup() -- Displays the HUD
    local gameGroup = display.newGroup()
    local levelGroup = display.newGroup()
    local stars = display.newGroup() -- Displays the stars
    
    -- Modules
    local physics = require ("physics")
    
    local mCeil = math.ceil
    local mAtan2 = math.atan2
    local mPi = math.pi
    local mSqrt = math.sqrt
    
    -- Game Objects
    local background
    local ground
    local powerShot
    local arrow
    local panda
    local poof
    local starGone
    local scoreText
    local gameOverDisplay
    
    -- Variables
    local gameIsActive = false
    local waitingForNewRound
    local restartTimer
    local counter
    local timerInfo 
    local numSeconds = 30 -- Time the round starts at
    local counterSize = 50
    local gameScore = 0 -- Round starts at a score of 0
    local starWidth = 30
    local starHeight = 30
    

刚才发生了什么?

在应用程序开始时,我们隐藏了状态栏。这仅适用于 iOS 设备。有四个不同的组设置,它们在游戏中都扮演着重要的角色。

注意gameIsActive设置为false。这使我们能够激活应用程序的属性,以便在显示对象需要停止动画、出现在屏幕上并受触摸事件影响时影响回合。

代码开始部分也设置了计时器的元素。将numSeconds设置为30表示回合将倒计时多少秒。starWidthstarHeight描述了对象的尺寸。

让我们开始这一轮

在游戏屏幕上的熊猫发射之前,我们需要先加载熊猫。熊猫将从屏幕底部过渡并在屏幕上移,然后才能发生触摸事件。

动手时间——开始游戏

现在,我们需要为熊猫设置一个屏幕外的位置,并让它过渡到起始发射位置,以便用户可以与之互动。

  1. 添加变量后,创建一个名为startNewRound()的新局部函数,并添加一个if语句来初始化panda对象进入场景:

    local startNewRound = function()
      if panda then
    
  2. startNewRound()内添加一个名为activateRound()的新局部函数。设置屏幕上panda显示对象的起始位置,并添加ground:toFront(),使地面出现在熊猫角色前面:

      local activateRound = function()
    
        waitingForNewRound = false
    
        if restartTimer then
          timer.cancel( restartTimer )
        end
    
        ground:toFront()
        panda.x = 240
        panda.y = 300
        panda.rotation = 0
        panda.isVisible = true
    
  3. 创建另一个名为pandaLoaded()的局部函数。将gameIsActive设置为true,并将panda对象的空气和击打属性设置为false。添加panda:toFront(),使其在屏幕上所有其他游戏对象的前面,并将身体类型设置为"static"

        local pandaLoaded = function()
    
          gameIsActive = true
          panda.inAir = false
          panda.isHit = false
          panda:toFront()
    
          panda.bodyType = "static"
    
        end
    
  4. 在 1,000 毫秒内将熊猫过渡到y=225。当补间动画完成后,使用onComplete命令调用pandaLoaded()函数。使用end关闭activateRound()函数,并调用它。关闭pandaif语句和startNewRound()函数,使用end

        transition.to( panda, { time=1000, y=225, onComplete=pandaLoaded } )
        end
    
        activateRound()
    
      end
    end
    

    行动时间——开始游戏

刚才发生了什么?

当关卡被激活时,熊猫被放置在地面以下,在玩家可见之前。对于pandaLoaded(),游戏通过gameIsActive = true激活,熊猫准备好被玩家发射。熊猫从地面过渡到屏幕上可以被访问的区域。

嘭!消失了!

在一轮结束后,熊猫需要从舞台上消失。我们不是让它消失在空气中,而是当它与屏幕上的任何物体发生碰撞时,添加一个“poof”效果。

行动时间——在舞台上重新加载熊猫

当熊猫在空中停留一定时间或碰到屏幕外的任何边界区域时,它将变成一股烟雾。当与屏幕边缘或地面发生碰撞事件时,熊猫将被“poof”图像替换。为了使“poof”效果起作用,必须关闭熊猫的可见属性。当发生碰撞后,需要将熊猫重新加载到屏幕上,同时游戏仍然激活。

  1. 创建一个名为callNewRound()的局部函数。包括一个名为isGameOver的局部变量,并将其设置为false

    local callNewRound = function()
      local isGameOver = false
    
  2. 在当前函数内,创建一个名为pandaGone()的新局部函数。为熊猫添加新属性,使其不再在游戏舞台上显示:

      local pandaGone = function()
    
        panda:setLinearVelocity( 0, 0 )
        panda.bodyType = "static"
        panda.isVisible = false
        panda.rotation = 0
    
        poof.x = panda.x; poof.y = panda.y
        poof.alpha = 0
        poof.isVisible = true
    
  3. poof对象添加一个名为fadePoof()的新函数。使用onComplete命令,设置time50alpha1进行过渡。让poof对象在time设置为100alpha设置为0时淡出。关闭pandaGone()函数,并使用timer.performWithDelay调用它:

        local fadePoof = function()
           transition.to( poof, { time=100, alpha=0 } )
        end
        transition.to( poof, { time=50, alpha=1.0, onComplete=fadePoof } )
    
        restartTimer = timer.performWithDelay( 300, function()
           waitingForNewRound = true; 
           end, 1)
    
      end
    
      local poofTimer = timer.performWithDelay( 500, pandaGone, 1 )
    
  4. isGameOver仍为false时,为startNewRound()添加timer.performWithDelay方法。关闭callNewRound()函数:

      if isGameOver == false then
        restartTimer = timer.performWithDelay(1500, startNewRound, 1)
      end
    end
    

刚才发生了什么?

当熊猫不再在屏幕上显示且倒计时仍在进行时,将开始新一轮。当isGameOver仍为false时,通过调用startNewRound()重新加载熊猫。

熊猫碰撞通过pandaGone()发生。通过应用panda.isVisible = false,所有物理属性都变为不活跃。

烟雾正好在熊猫消失的地方出现。当poof.x = panda.x; poof.y = panda.y时,会发生这种情况。通过fadePoof()poof短暂可见。一旦它淡出,新一轮即将到来,将waitingForNewRound设置为true

赚取一些分数

当熊猫捕捉到天空中的任何星星时,都会获得分数。游戏是在计时器上进行的,所以玩家的任务是尽可能在时间耗尽前捕捉更多星星。让我们积累一些分数吧!

行动时间——跟踪分数

分数通过名为scoreNum的参数更新,并在游戏进行时显示。分数是通过gameScore接收的。

  1. 下一个要创建的函数名为setScore,带有一个名为scoreNum的参数:

    local setScore = function( scoreNum )
    
  2. 使用名为newScore的局部变量并将其设置为scoreNum。设置gameScore = newScore。为gameScore提供一个if语句,以便在游戏进行时将分数设置为 0:

      local newScore = scoreNum
      gameScore = newScore
    
      if gameScore < 0 then gameScore = 0; end
    
  3. 添加scoreText显示对象,并将其设置为等于gameScore。关闭函数:

      scoreText.text = gameScore
      scoreText.xScale = 0.5; scoreText.yScale = 0.5
      scoreText.x = (480 - (scoreText.contentWidth * 0.5)) - 15
      scoreText.y = 20
    end
    

刚才发生了什么?

对于setScore = function(scoreNum)函数,我们设置了一个名为scoreNum的参数。scoreNum参数会通过local newScore持续更新游戏分数。newScore将通过gameScore更新,这是计分的基础。同时,在游戏中,scoreText会显示gameScore的值。

当游戏结束时

这场游戏没有输家,每个人都是赢家!在计时器耗尽前,尽可能多地收集星星,你的肾上腺素仍会激增。当一切结束时,我们还需要通知大家时间已到。

行动时间——显示游戏结束屏幕

我们需要设置游戏结束屏幕,并在本回合结束时显示玩家获得的最终得分:

  1. 创建一个名为callGameOver()的新局部函数:

    local callGameOver = function()
    
  2. gameIsActive设置为false并暂停物理引擎。从舞台中移除pandastars对象:

      gameIsActive = false
      physics.pause()
    
      panda:removeSelf()
      panda = nil
      stars:removeSelf()
      stars = nil
    
  3. 显示游戏结束对象并将它们插入到hudGroup组中。使用transition.to方法在屏幕上显示游戏结束对象:

      local shade = display.newRect( 0, 0, 480, 320 )
      shade:setFillColor( 0, 0, 0, 0.5)
      shade.x = display.contentCenterX
      shade.y = display.contentCenterY
    
      gameOverDisplay = display.newImage( "gameOverScreen.png")
      gameOverDisplay.x = 240; gameOverDisplay.y = 160
      gameOverDisplay.alpha = 0
    
      hudGroup:insert( shade )
      hudGroup:insert( gameOverDisplay )
    
      transition.to( shade, { time=200 } )
      transition.to( gameOverDisplay, { time=500, alpha=1 } )
    
  4. 使用名为newScore的局部变量更新最终得分。将counterscoreTextisVisible设置为false。再次引入scoreText以在设备屏幕的另一位置显示最终得分。关闭函数:

      local newScore = gameScore
      setScore( newScore )
    
      counter.isVisible = false
    
      scoreText.isVisible = false
      scoreText.text = "Score: " .. gameScore
      scoreText.xScale = 0.5; scoreText.yScale = 0.5
      scoreText.x = 280
      scoreText.y = 160
      scoreText:toFront()
      timer.performWithDelay( 1000, function() scoreText.isVisible = true; end, 1 )
    
    end
    

    行动时间——显示游戏结束屏幕

刚才发生了什么?

当时间耗尽或所有星星被收集时,callGameOver()方法会显示游戏结束屏幕。我们将gameIsActive设置为false并暂停所有物理效果,这样熊猫就不能通过任何屏幕触摸来移动了。然后从场景中移除熊猫和星星。通过transition.to使shadegameOverDisplay对象可见,这样它就会通知玩家本回合已经结束。最终得分将在回合结束时在gameOverDisplay对象前显示。

背景展示

熊猫在游戏中需要一个关于其所在位置的通用设置。让我们设置背景和地面对象。

行动时间——添加背景元素

  1. backgroundground 显示对象添加到 drawBackground() 函数中。将这些对象插入到名为 gameGroup 的组中:

    local drawBackground = function()
    
      background = display.newImage( "background.png" )
      background.x = 240; background.y = 160
    
      gameGroup:insert( background )
    
      ground = display.newImage( "ground.png" )
      ground.x = 240; ground.y = 300
    
      local groundShape = { -240,-18, 240,-18, 240,18, -240,18 }
      physics.addBody( ground, "static", { density=1.0, bounce=0, friction=0.5, shape=groundShape } )
    
      gameGroup:insert( ground )
    
    end
    

刚才发生了什么?

backgroundground 显示对象被放置在 drawBackground() 函数中。ground 对象有一个自定义的物理形状,它的大小与原始显示对象不同。所以如果熊猫碰巧撞到地面,它会与之碰撞,但不会穿过。

注意!

在游戏开始之前,我们需要了解如何操作游戏控制。幸运的是,我们将添加一个帮助屏幕,解释如何进行游戏。还需要显示抬头显示HUD),以便玩家了解剩余时间以及他们积累了多少分。

行动时间——显示计时器和得分

让我们设置在游戏中需要显示的帮助屏幕和 HUD 元素:

  1. 创建一个名为 hud() 的新本地函数:

    local hud = function()
    
  2. 在游戏开始时显示 helpText 10 秒钟。通过向左滑动并设置可见性为 false 来过渡它。将 helpText 添加到 hudGroup 组中:

      local helpText = display.newImage("help.png")
      helpText.x = 240; helpText.y = 160
      helpText.isVisible = true
      hudGroup:insert( helpText )
    
      timer.performWithDelay( 10000, function() helpText.isVisible = false; end, 1 )
    
      transition.to( helpText, { delay=9000, time=1000, x=-320, transition=easing.inOutExpo })
    
  3. 在屏幕顶部附近显示 counterscoreText。也将 scoreText 添加到 hudGroup 组中。使用 end 结束函数:

      counter = display.newText( "Time: " .. tostring(numSeconds), 0, 0, "Helvetica-Bold", counterSize )
      counter:setFillColor( 1, 1, 1 )
      counter.xScale = 0.5; counter.yScale = 0.5
      counter.x = 60; counter.y = 15 
      counter.alpha = 0
    
      transition.to( counter, { delay=9000, time=1000, alpha=1, transition=easing.inOutExpo })
    
      hudGroup:insert( counter )
    
      scoreText = display.newText( "0", 470, 22, "Helvetica-Bold", 52 )
      scoreText: setFillColor( 1, 1, 1 )--> white
      scoreText.text = gameScore
      scoreText.xScale = 0.5; scoreText.yScale = 0.5
      scoreText.x = (480 - (scoreText.contentWidth * 0.5)) - 15
      scoreText.y = 15
      scoreText.alpha = 0
    
      transition.to( scoreText, { delay=9000, time=1000, alpha=1, transition=easing.inOutExpo })
    
      hudGroup:insert( scoreText )
    
    end
    

刚才发生了什么?

helpText 对象在游戏开始前出现,并在主设备显示上停留 9 秒钟,然后在 1 秒内沿 x 方向过渡到 -320。这是通过 transition.to( helpText, { delay=9000, time=1000, x=-320, transition=easing.inOutExpo }) 实现的。

counter 对象显示 "Time: " .. tostring(numSeconds),其中 numSeconds 表示从 30 开始倒数的秒数。它位于屏幕左上角附近。

scoreText 对象显示 gameScore,并且每次星星碰撞都会更新。这将被放置在屏幕的右上角。local hud = function() 中的所有对象都插入到 hudGroup 中。

一次又一次

这个游戏有一个定时器,玩家需要在它用完之前尽可能多地捕捉星星。我们将在帮助文本离开舞台后立即开始倒计时。

行动时间——设置定时器

我们需要创建几个函数,激活倒计时并在游戏结束时停止在 0 秒:

  1. 使用名为 myTimer() 的本地函数为游戏设置定时器倒计时:

    local myTimer = function()
    
  2. 将定时器倒计时的秒数增加 1。使用 counter 文本对象,通过 numSeconds 显示时间。在终端窗口中输出 numSeconds 来查看倒计时:

      numSeconds = numSeconds - 1
      counter.text = "Time: " .. tostring( numSeconds )
      print(numSeconds)
    
  3. 创建一个 if 语句,用于当定时器用完或所有星星消失时。在块内,取消定时器并调用 callGameOver() 来结束这一轮。使用 end 结束 myTimer() 函数。

      if numSeconds < 1 or stars.numChildren <= 0 then
        timer.cancel(timerInfo)
        panda:pause()
        restartTimer = timer.performWithDelay( 300, function() callGameOver(); end, 1 )
      end
    
    end
    
  4. 使用名为startTimer()的新局部函数启动myTimer()函数。这将开始游戏玩法开始时的倒计时:

    local startTimer = function()
      print("Start Timer")
      timerInfo = timer.performWithDelay( 1000, myTimer, 0 )
    end
    

刚才发生了什么?

主要的计时器函数在myTimer()中。我们使用numSeconds = numSeconds – 1来倒数秒数。秒数将在counter显示对象中更新。print(numSeconds)将在终端窗口中更新,以查看倒计时在代码内部运行的速度。

当时间耗尽或所有星星都被收集时,将创建一个if语句来检查是否有任何参数为真。当任何语句评估为真时,计时器停止倒数,熊猫动画暂停,并调用callGameOver()函数。这将调用显示游戏结束屏幕的函数。

计时器通过local startTimer = function()以每 1,000 毫秒的速度启动倒计时,这相当于 1 秒。

它如此发光

熊猫需要另一个元素来显示发射它到天空所需的力量。我们将添加一个微妙的类似发光的显示对象来表示这一点。

动作时间——制作能量射击

我们需要为powerShot创建一个单独的函数,以便在熊猫准备发射时调用:

  1. 通过名为createPowerShot()的新局部函数显示powerShot对象。将其插入到gameGroup组中:

    local createPowerShot = function()
      powerShot = display.newImage( "glow.png" )
      powerShot.xScale = 1.0; powerShot.yScale = 1.0
      powerShot.isVisible = false
    
      gameGroup:insert( powerShot )
    end
    

刚才发生了什么?

通过createPowerShot()函数创建powerShot对象,并在熊猫准备发射时调用。

熊猫!

在屏幕上看到动画的东西将会很激动人心。我们的主角将为游戏玩法中应用的每个动作指定动画。

动作时间——创建熊猫角色

我们需要设置熊猫的碰撞事件,并相应地为其设置动画,使用图像表:

  1. 我们需要创建一个局部函数来处理熊猫的碰撞和触摸事件。我们将它称为createPanda()

    local createPanda = function()
    
  2. 当熊猫与星星碰撞时,使用带有参数selfeventonPandaCollision()。每次与星星或屏幕边缘发生碰撞时,使用callNewRound()重新加载panda

      local onPandaCollision = function( self, event )
        if event.phase == "began" then
    
          if panda.isHit == false then
    
            panda.isHit = true
    
            if event.other.myName == "star" then
              callNewRound( true, "yes" )
            else
              callNewRound( true, "no" )
            end
    
            if event.other.myName == "wall" then
              callNewRound( true, "yes" )
            else
              callNewRound( true, "no" )
            end
    
            elseif panda.isHit then
              return true
            end
        end
      end
    
  3. 创建一个方向箭头,允许用户瞄准发射熊猫的区域。将其插入到gameGroup组中:

      arrow = display.newImage( "arrow.png" )
      arrow.x = 240; arrow.y = 225
      arrow.isVisible = false
    
      gameGroup:insert( arrow )
    
  4. 创建一个具有三种不同动画序列(称为"set""crouch""air")的熊猫图像表:

      local sheetData = { width=128, height=128, numFrames=5, sheetContentWidth=384, sheetContentHeight=256 }
      local sheet = graphics.newImageSheet( "pandaSprite.png", sheetData )
    
      local sequenceData = 
      {
        { name="set", start=1, count=2, time=200 }, 
        { name="crouch", start=3, count= 1, time=1 }, 
        { name="air", start=4, count=2, time=100 }  
      }
    
      panda = display.newSprite( sheet, sequenceData )
    
      panda:setSequence("set")
      panda:play()
    
  5. 在熊猫发射到空中之前,为其添加以下属性:

      panda.x = 240; panda.y = 225
      panda.isVisible = false
    
      panda.isReady = false
      panda.inAir = false
      panda.isHit = false
      panda.isBullet = true
      panda.trailNum = 0
    
      panda.radius = 12
      physics.addBody( panda, "static", { density=1.0, bounce=0.4, friction=0.15, radius=panda.radius } )
      panda.rotation = 0
    
  6. 使用"collision"panda设置碰撞,并应用事件监听器:

      panda.collision = onPandaCollision
      panda:addEventListener( "collision", panda )
    
  7. 创建poof对象:

      poof = display.newImage( "poof.png" )
      poof.alpha = 1.0
      poof.isVisible = false
    
  8. pandapoof对象插入到gameGroup组中。关闭函数:

      gameGroup:insert( panda )
      gameGroup:insert( poof )
    end
    
  9. 我们需要滚动到activateRound()函数,并为熊猫添加"set"动画序列:

      panda:setSequence("set")
      panda:play()
    

刚才发生了什么?

熊猫发生的碰撞事件从if event.phase == "began"开始。通过几个if语句的情况,熊猫在屏幕上重新加载。当熊猫向舞台的右侧、左侧或顶部发射离开屏幕时,event.other.myName == "star"将调用新一轮。

熊猫的图片表有三个动画组。它们被称为"set""air""crouch"。图片表总共有五个帧。

在发射前设置熊猫的物理属性。身体类型设置为"static",在空中时将改变。

熊猫的碰撞事件通过panda:addEventListener( "collision", panda )调用。

图片表设置好后,需要在activateRound()函数中添加"set"动画以启动移动。

星空。

星星在游戏中扮演着重要角色。它们是熊猫在倒计时结束前为了获得分数必须克服的主要障碍。

是时候行动了——创建星星碰撞。

星星碰撞需要被创建并从舞台移除,以便玩家可以累积分数。

  1. 为星星碰撞创建一个名为onStarCollision()的函数,并带有selfevent参数:

    local onStarCollision = function( self, event )
    
  2. 添加if语句,当发生碰撞时,从游戏屏幕上移除stars子项。每次从屏幕上移除一个星星,分数增加 500。用end关闭函数:

      if event.phase == "began" and self.isHit == false then
    
        self.isHit = true
        print( "star destroyed!")
        self.isVisible = false
    
        stars.numChildren = stars.numChildren - 1
    
        if stars.numChildren < 0 then
          stars.numChildren = 0
        end
    
        self.parent:remove( self )
        self = nil
    
        local newScore = gameScore + 500
        setScore( newScore )
      end
    end
    

    是时候行动了——创建星星碰撞

刚才发生了什么?

星星碰撞在第一次接触时发生,条件是if event.phase == "began"self.isHit == false,假设星星还没有被熊猫触碰。通过self.parent:remove( self )self = nil,星星从屏幕上移除。分数通过gameScore增加 500,并更新为setScore = (scoreNum)

尝试英雄——跟踪星星计数。

尝试跟踪游戏过程中熊猫捕捉到的星星数量。逻辑与创建游戏分数类似。每次捕捉到的星星都需要在每次碰撞时增加 1。星星计数放在onStarCollision()函数中。需要创建一个新的函数和方法来显示星星计数的文本,并且每次计数更改时都要更新。

屏幕触摸。

熊猫需要通过创建类似弹弓的发射机制来穿越游戏场地,以到达星星。力量在推动熊猫上升的过程中将发挥重要作用。

是时候行动了——发射熊猫。

让我们为熊猫添加一个触摸事件,使其向星星弹射。powerShot对象将帮助玩家可视化在熊猫起飞前需要施加多大的力量。

  1. 为熊猫实现触摸事件。创建一个名为onScreenTouch()的局部函数,带有事件参数:

    local onScreenTouch = function( event )
    
  2. 当启动gameIsActive时,添加一个if语句,用于当触摸事件开始时,通过使用event.phase == "began"。在此事件期间,使用“蹲下”动画集来准备panda的发射:

      if gameIsActive then
        if event.phase == "began" and panda.inAir == false then
    
          panda.y = 225
          panda.isReady = true
          powerShot.isVisible = true
          powerShot.alpha = 0.75
          powerShot.x = panda.x; powerShot.y = panda.y
          powerShot.xScale = 0.1; powerShot.yScale = 0.1
    
          arrow.isVisible = true
    
          panda:setSequence("crouch")
          panda:play()
    
  3. 添加一个elseif语句,用于当触摸事件结束时,通过使用event.phase == "ended"。创建一个名为fling()的新局部函数,它将在发射pandastar对象时保存panda的属性。应用一个与触摸事件拖动方向相反的力。当触摸事件从角色处拉远时,向外扩展powerShot显示对象的大小:

        elseif event.phase == "ended" and panda.isReady then
    
          local fling = function()
            powerShot.isVisible = false
            arrow.isVisible = false
    
            local x = event.x
            local y = event.y
            local xForce = (panda.x-x) * 4
            local yForce = (panda.y-y) * 4
    
            panda:setSequence("air")
            panda:play()
    
            panda.bodyType = "dynamic"
            panda:applyForce( xForce, yForce, panda.x, panda.y )
            panda.isReady = false
            panda.inAir = true
    
          end
    
        transition.to( powerShot, { time=175, xScale=0.1, yScale=0.1, onComplete=fling} )
    
        end
    
        if powerShot.isVisible == true then
    
          local xOffset = panda.x
          local yOffset = panda.y
    
          local distanceBetween = mCeil(mSqrt( ((event.y - yOffset) ^ 2) + ((event.x - xOffset) ^ 2) ))
    
          powerShot.xScale = -distanceBetween * 0.02
          powerShot.yScale = -distanceBetween * 0.02
    
          local angleBetween = mCeil(mAtan2( (event.y - yOffset), (event.x - xOffset) ) * 180 / mPi) + 90
    
          panda.rotation = angleBetween + 180
          arrow.rotation = panda.rotation
        end
    
      end
    end
    

    行动时间——发射熊猫

刚才发生了什么?

一旦游戏激活并在屏幕上加载了熊猫,就可以启动一个触摸事件来发射熊猫。熊猫将从“静态”物理状态变为“动态”物理状态。powerShot显示对象的大小随着事件触摸将熊猫拉得越远而增加。

熊猫发射的力由local fling = function()应用。发射力由xForceyForce产生。熊猫对象通过panda:applyForce( xForce, yForce, panda.x, panda.y )推进。注意,身体类型变为“动态”,这样重力就可以影响对象。

组织显示对象

当设置好回合后,需要重新排列游戏对象的显示层次结构。最重要的对象显示在屏幕前方。

行动时间——重新排序层次

  1. 需要创建一个新的局部函数reorderLayers(),以在游戏进行时组织屏幕上对象的显示层次结构:

    local reorderLayers = function()
    
      gameGroup:insert( levelGroup )
      ground:toFront()
      panda:toFront()
      poof:toFront()
      hudGroup:toFront()
    
    end
    

刚才发生了什么?

gameGrouphudGroup和其他显示对象在游戏屏幕的显示层次结构中重新组织。最重要的对象被设置在前面,而最不重要的对象在后面。

创建星星

天空背景需要填满星星,这样熊猫就能捕捉到尽可能多的星星。

行动时间——在关卡中创建星星

我们需要在游戏中添加星星的布局,并使它们移动,以添加一些效果来显示它们是活跃的。需要应用一个碰撞事件,当熊猫与它们相撞时,将它们移除。

  1. 创建一个名为createStars()的新函数,并通过for循环布置star对象。添加一个"collision"事件,该事件会被onStarCollision()调用,以在星星被熊猫击中时移除它们。让星星每 10 秒向前和向后旋转 1,080 度和-1,080 度,这将使星星前后旋转三个完整的周期。为屏幕左右两侧创建墙壁:

    local createStars = function()
    
      local numOfRows = 4
      local numOfColumns = 12
      local starPlacement = {x = (display.contentWidth  * 0.5) - (starWidth * numOfColumns ) / 2  + 10, y = 50}
    
      for row = 0, numOfRows - 1 do
        for column = 0, numOfColumns - 1 do
    
          -- Create a star
          local star = display.newImage("star.png")
          star.name = "star"
          star.isHit = false
          star.x = starPlacement.x + (column * starWidth)
          star.y = starPlacement.y + (row * starHeight)
          physics.addBody(star, "static", {density = 1, friction = 0, bounce = 0, isSensor = true})
          stars.insert(stars, star)
    
          star.collision = onStarCollision
          star:addEventListener( "collision", star )
    
          local function starAnimation()
            local starRotation = function()
              transition.to( star, { time=10000, rotation = 1080, onComplete=starAnimation })
            end
    
            transition.to( star, { time=10000, rotation = -1080, onComplete=starRotation })
          end
    
          starAnimation()
    
        end
      end
    
      local leftWall  = display.newRect (0, 0, 0, display.contentHeight)
      leftWall.name = "wall"
    
      local rightWall = display.newRect (display.contentWidth, 0, 0, display.contentHeight)
        rightWall.name = "wall"
    
        physics.addBody (leftWall, "static", {bounce = 0.0, friction = 10})
        physics.addBody (rightWall, "static", {bounce = 0.0, friction = 10})
    
        reorderLayers()
    end
    

刚才发生了什么?

屏幕上显示的星星数量由 numOfRowsnumOfColumns 设置。一个 for 循环用于显示每个单独的星星对象,并将其放置在 stars 组中。通过 onStarCollision() 的事件监听器检测 star 的碰撞。

leftWallrightWall 对象也有物理属性,并将考虑与熊猫的碰撞检测。

星星通过 starAnimation()starRotation() 进行动画处理。每个函数轮流旋转每个星星对象 10 秒钟(10,000 毫秒),在 1,080 度和-1,080 度之间交替。

开始游戏

游戏从倒计时开始时启动,熊猫被加载到屏幕上。一旦熊猫在屏幕上设定,玩家需要迅速瞄准并发射它,以便立即重新加载熊猫。

动手时间——初始化游戏

要运行游戏,需要初始化物理和剩余的游戏功能。所有游戏动作都需要延迟,直到帮助屏幕离开舞台。

  1. 通过创建一个名为 gameInit() 的新函数来启动游戏,该函数将包含物理属性并在舞台上激活显示对象:

    local gameInit = function()
      physics.start( true )
      physics.setGravity( 0, 9.8 )
    
      drawBackground()
      createPowerShot()
      createPanda()
      createStars()
      hud()
    
  2. 添加一个 Runtime 事件监听器,使用 "touch"onScreenTouch()

      Runtime:addEventListener( "touch", onScreenTouch )
    
  3. 让关卡和计时器在 10 秒后开始,这样用户就有时间阅读帮助文本。关闭函数并通过 gameInit() 开始游戏:

      local roundTimer = timer.performWithDelay( 10000, function() startNewRound(); end, 1 )
      local gameTimer = timer.performWithDelay( 10000, function() startTimer(); end, 1 )
    end
    
    gameInit()
    

所有代码都完成了!在模拟器中运行游戏,亲自看看它是如何工作的。如果出现错误,请确保检查代码中是否有任何拼写错误。

刚才发生了什么?

通过 gameInit() 初始化一轮游戏。此时运行物理引擎和剩余的函数。同时添加 onScreenTouch() 的事件监听器。通过 timer.performWithDelay 在启动应用程序 10 秒后初始化 startNewRound()startTimer() 函数。

小测验——动画图形

Q1. 正确暂停图像表的动画的方法是什么?

  1. object:stop()

  2. object:pause()

  3. object:dispose()

  4. 以上都不正确

Q2. 如何让动画序列永远循环?

  1. local sequenceData =

     {
     name="run", start=1, count=5, time=100, loopCount=1 
     }
    
    
  2. local sequenceData =

     {
     name="run", start=1, count=5, time=100, loopCount=0 
     }
    
    
  3. local sequenceData =

     {
     name="run", start=1, count=5, time=100, loopCount=-1
     }
    
    
  4. local sequenceData =

     {
     name="run", start=1, count=5, time=100, loopCount=100
     }
    
    

Q3. 如何创建一个新的图像表?

  1. myCharacter = display.newSprite(sequenceData)

  2. myCharacter = display.newSprite(imageSheet, sequenceData)

  3. myCharacter = sprite.newSpriteSheet("myImage.png", frameWidth, frameHeight)

  4. 以上都不正确

概括

我们的第二款游戏《熊猫星星捕手》终于完成了!我们现在对编写更多函数和不同类型的游戏逻辑有了很好的掌握,而且我们还掌握了动画制作!干的漂亮!

在本章中,我们完成了以下工作:

  • 更深入地了解了过渡,并应用了缓动技术

  • 理解了图像表和精灵动画

  • 为需要在屏幕上连续重新加载的显示对象创建了一个游戏循环

  • 对一个显示对象施加力,使其向指定方向推进

  • 添加了一个碰撞事件,用以从一个显示对象切换到另一个显示对象

我们在整整一个章节中完成了一个游戏的制作!使用 Corona SDK 进行开发是如此简单和快速上手。即便创建一个简单的游戏,也无需编写成千上万行代码。

在下一章中,我们将学习创建游戏、音效和音乐的另一个重要元素!这将是一段美妙的旅程。

第六章:播放声音和音乐

我们在日常生活中遇到的几乎所有类型的媒体中都能听到声音效果和音乐。许多著名游戏如《吃豆人》《愤怒的小鸟》《水果忍者》仅凭它们的主题音乐或声音效果就能被识别出来。除了我们在游戏中看到的视觉图像,声音帮助影响故事情节中传达的情绪和/或游戏过程中的氛围。与游戏主题相关的优质声音效果和音乐,有助于给体验带来真实感。

在本章中,你将学习如何为你的应用程序添加声音效果和音乐。在前面章节中创建 Breakout 和 Panda Star Catcher 时,你已经掌握了视觉吸引力。现在,让我们为我们的耳朵提升感官体验!

你将要学习的主要内容包括:

  • 加载、播放和循环音频

  • 了解如何播放、暂停、恢复、倒带和停止音频

  • 内存管理(处理音频)

  • 音量控制

  • 性能和编码技巧

让我们创造更多的魔法!

Corona 音频系统

Corona 音频系统具有先进的开放音频库OpenAL)功能。OpenAL 专为高效渲染多通道三维定位音频而设计。OpenAL 的一般功能编码在源对象、音频缓冲区和单一监听器中。源对象包含指向缓冲区的指针、声音的速度、位置和方向,以及声音的强度。缓冲区包含 PCM 格式的音频数据,可以是 8 位或 16 位,单声道或立体声格式。监听器对象包含监听者的速度、位置和方向,以及应用于所有声音的总增益。

注意

想要了解更多关于 Corona 音频系统的信息,你可以访问developer.coronalabs.com/partner/audionotes。关于 OpenAL 的一般信息可以在www.openal.org找到。

声音格式

以下是与 iOS 和安卓平台兼容的声音格式:

  • 所有平台都支持 16 位、小端、线性的.wav格式文件

  • iOS 支持.mp3.aif.caf.aac格式

  • Mac 模拟器支持.mp3.aif.caf.ogg.aac格式

  • Windows 模拟器支持.mp3.ogg格式

  • 安卓支持.mp3.ogg格式

安卓上的声音文件名限制

在 Android 构建时,文件扩展名被忽略,因此无论扩展名如何,文件都被视为相同。目前的解决办法是更改文件名以区分扩展名。请参阅以下列出的示例:

  • tap_aac.aac

  • tap_aif.aif

  • tap_caf.caf

  • tap_mp3.mp3

  • tap_ogg.ogg

单声道声音效果最佳

使用单声道声音比立体声声音节省一半的内存。由于 Corona 音频系统使用 OpenAL,它只会对单声道声音应用空间化/3D 效果。OpenAL 不对立体声样本应用 3D 效果。

同时播放的最大通道数

可以运行的最大通道数为 32,这使得最多可以同时播放 32 个不同的声音。在你的代码中查看结果通道数的 API 是 audio.totalChannels

是时候播放音乐了

音频可以通过以下两种不同的方式加载:

  • loadSound(): 这会将整个声音预加载到内存中

  • loadStream(): 这会分小块读取声音以节省内存,准备播放

audio.loadSound()

audio.loadSound()函数将整个文件完全加载到内存中,并返回对音频数据的引用。完全加载到内存中的文件可以重复使用、播放,并同时在多个通道上共享。因此,你只需要加载文件的单一实例。在游戏中用作音效的声音将属于这一类。

语法为 audio.loadSound(audiofileName [, baseDir ])

参数如下:

  • audiofileName: 这指定了你想要加载的音频文件的名称。支持的文件格式取决于运行该文件的平台。

  • baseDir: 默认情况下,声音文件应位于应用程序资源目录中。如果声音文件位于应用程序文档目录中,请使用 system.DocumentsDirectory

例如:

  • tapSound = audio.loadSound("tap.wav")

  • smokeSound = audio.loadSound("smoke.mp3")

audio.loadStream()

audio.loadStream()函数用于加载一个文件,以流的形式读取。流式文件是分小块读取的,以最小化内存使用。对于体积大、时长长的文件,这种方式非常理想。这些文件不能同时在多个通道间共享。如果需要,你必须加载该文件的多个实例。

语法为 audio.loadStream( audioFileName [, baseDir ] )

参数如下:

  • audiofileName: 这指定了你想要加载的音频文件的名称。支持的文件格式取决于运行该文件的平台。

  • baseDir: 默认情况下,声音文件应位于应用程序资源目录中。如果声音文件位于应用程序文档目录中,请使用 system.DocumentsDirectory

例如:

  • music1 = audio.loadStream("song1.mp3")

  • music2 = audio.loadStream("song2.wav")

audio.play()

audio.play()函数在通道上播放由音频句柄指定的音频。如果没有指定通道,将自动为你选择一个可用通道。函数返回音频播放的通道号。

语法为 audio.play( audioHandle [, options ] )

参数如下:

  • audioHandle: 这是你想播放的音频数据

  • options:这是播放的附加选项,格式为表。

options 的参数:

  • channel:这个选项允许你选择希望音频播放的通道号。从 1 到最大通道数 32 都是有效的通道。如果你指定 0 或省略,系统将自动为你选择通道。

  • loops:这个选项允许你选择音频循环的次数。0 表示不循环,意味着声音将播放一次并不循环。-1 表示系统将无限循环样本。

  • duration:这个选项以毫秒为单位,它将使系统播放指定时间的音频。

  • fadein:这个选项以毫秒为单位,它将使声音从最小通道音量开始播放,并在指定毫秒数内过渡到正常通道音量。

  • onComplete:这是一个回调函数,当播放结束时将被调用。onComplete 回调函数会传递一个事件参数。

例如:

backgroundMusic = audio.loadStream("backgroundMusic.mp3")
backgroundMusicChannel = audio.play( backgroundMusic, { channel=1, loops=-1, fadein=5000 }  )  
-- play the background music on channel 1, loop infinitely, and fadein over 5 seconds

循环

高度压缩的格式,如 MP3、AAC 和 Ogg Vorbis,可能会移除音频样本末端的采样点,可能会破坏正确循环的剪辑。如果你在播放过程中遇到循环间隙,请尝试使用 WAV(兼容 iOS 和 Android)。确保你的引导和结束点干净清晰。

同时播放

通过 loadSound() 加载的声音可以在多个通道上同时播放。例如,你可以如下加载一个音效:

bellSound = audio.loadSound("bell.wav")

如果你想要为多个对象产生各种铃声,你可以这么做。音频引擎经过高度优化,可以处理这种情况。使用相同的句柄调用 audio.play(),次数可达最大通道数(32 次):

audio.play(bellSound)
audio.play(bellSound)
audio.play(bellSound)

动手操作时间 – 播放音频

我们将学习声音效果和音乐在 Corona 中的实现方式,以了解它实际是如何工作的。要播放音频,请按照以下步骤操作:

  1. 在你的桌面上创建一个名为 Playing Audio 的新项目文件夹。

  2. Chapter 6 Resources 文件夹中,将 ring.wavsong1.mp3 声音文件复制到你的项目文件夹中,并创建一个新的 main.lua 文件。你可以从 Packt Publishing 网站下载伴随这本书的项目文件。

  3. 使用 loadSound()loadStream() 预加载以下音频:

    ringSound = audio.loadSound( "ring.wav" )
    backgroundSound = audio.loadStream( "song1.mp3" )
    
  4. backgroundSound 设置为通道 1,无限循环,并在 3 秒后淡入:

    mySong = audio.play( backgroundSound, { channel=1, loops=-1, fadein=3000 }  )
    
  5. 添加 ringSound 并播放一次:

    myRingSound = audio.play( ringSound )
    
  6. 保存项目并在 Corona 模拟器中运行,以听取结果。

刚才发生了什么?

对于仅是短音效的音频,我们使用 audio.loadSound() 来准备声音。对于大小较大或时长较长的音频,使用 audio.loadStream()

backgroundSound 文件设置为通道 1,并在开始播放 3 秒后淡入。loops = -1 表示文件将无限循环从开始到结束。

尝试英雄 – 延迟重复音频

如你所见,加载和播放音频真的很简单。只需两行代码就可以播放一个简单的声音。让我们看看你是否能把它提升一个档次。

使用 ring.wav 文件并通过 loadSound() 加载它。创建一个播放音频的函数。让声音每 2 秒播放一次,重复五次。

是时候掌控一切了

现在我们可以在模拟器中播放它们,因此我们有能力控制我们的声音。回想一下卡带播放器的日子,它有暂停、停止和倒带等功能。Corona 的音频 API 库也可以做到这一点。

audio.stop()

audio.stop() 函数会停止通道上的播放并清除通道,以便可以再次播放。

语法为 audio.stop( [channel] )audio.stop( [ { channel = c } ] )

不带参数会停止所有活动通道。channel 参数指定要停止的通道。指定 0 会停止所有通道。

audio.pause()

audio.pause() 函数会在通道上暂停播放。这对没有播放的通道没有影响。

语法为 audio.pause( [channel] )audio.pause( [ {channel = c} ] )

不带参数会暂停所有活动通道。channel 参数指定要暂停的通道。指定 0 会暂停所有通道。

audio.resume()

audio.resume() 函数会恢复暂停的通道上的播放。这对没有暂停的通道没有影响。

语法为 audio.pause( [channel] )audio.pause( [ {channel = c} ] )

不带参数会恢复所有暂停的通道。channel 参数指定要恢复的通道。指定 0 会恢复所有通道。

audio.rewind()

audio.rewind() 函数会将音频倒带到活动通道或直接在音频句柄上的开始位置。

语法为 audio.rewind( [, audioHandle ] [, { channel=c } ] )

参数如下:

  • audioHandleaudioHandle 参数允许你倒带所需的数据。它最适合用 audio.loadStream() 加载的音频。不要尝试与 channel 参数在同一调用中使用。

  • channelchannel 参数允许你选择要应用倒带操作的通道。它最适合用 audio.loadSound() 加载的音频。不要尝试与 audioHandle 参数在同一调用中使用。

行动时间 – 控制音频

让我们通过创建用户界面按钮来模拟我们自己的小音乐播放器,以下面的方式控制音频调用:

  1. Chapter 6 文件夹中,将 Controlling Audio 项目文件夹复制到你的桌面。你会注意到有几个艺术资源,一个 ui.lua 库,一个 config.lua 文件,以及一个 song2.mp3 文件。你可以从 Packt Publishing 网站下载本书附带的的项目文件。行动时间 – 控制音频

  2. 在同一个项目文件夹中,创建一个全新的 main.lua 文件。

  3. 通过 loadStream() 加载音频文件,将其命名为 music,并调用 UI 库。还在一个名为 myMusic 的局部变量中添加它:

    local ui = require("ui")
    local music = audio.loadStream( "song2.mp3" ) local myMusicChannel
    
  4. 创建一个名为 onPlayTouch() 的局部函数,带有一个 event 参数以播放音频文件。添加一个包含 event.phase == "release"if 语句,以便在按钮释放时开始播放音乐。将 playBtn 显示对象作为一个新的 UI 按钮应用:

    local onPlayTouch = function( event )
      if event.phase == "release" then
        myMusicChannel = audio.play( music, { loops=-1 }  )
      end
    end
    
    playBtn = ui.newButton{
      defaultSrc = "playbtn.png",
      defaultX = 100,
      defaultY = 50,
      overSrc = "playbtn-over.png",
      overX = 100,
      overY = 50,
      onEvent = onPlayTouch,
      id = "PlayButton",
      text = "",
      font = "Helvetica",
      size = 16,
      emboss = false
    }
    
    playBtn.x = 160; playBtn.y = 100
    
  5. 创建一个名为 onPauseTouch() 的局部函数,带有一个 event 参数以暂停音频文件。当 event.phase == "release" 时添加一个 if 语句,以便音乐暂停。将 pauseBtn 显示对象作为一个新的 UI 按钮应用:

    local onPauseTouch = function( event )
      if event.phase == "release" then
        audio.pause( myMusicChannel )
        print("pause")
      end
    end
    
    pauseBtn = ui.newButton{
      defaultSrc = "pausebtn.png",
      defaultX = 100,
      defaultY = 50,
      overSrc = "pausebtn-over.png",
      overX = 100,
      overY = 50,
      onEvent = onPauseTouch,
      id = "PauseButton",
      text = "",
      font = "Helvetica",
      size = 16,
      emboss = false
    }
    
    pauseBtn.x = 160; pauseBtn.y = 160
    
  6. 添加一个名为 onResumeTouch() 的局部函数,带有一个 event 参数以恢复音频文件。当 event.phase == "release" 时添加一个 if 语句,以便音乐恢复。将 resumeBtn 显示对象作为一个新的 UI 按钮应用:

    local onResumeTouch = function( event )
      if event.phase == "release" then
        audio.resume( myMusicChannel )
        print("resume")
      end
    end
    
    resumeBtn = ui.newButton{
      defaultSrc = "resumebtn.png",
      defaultX = 100,
      defaultY = 50,
      overSrc = "resumebtn-over.png",
      overX = 100,
      overY = 50,
      onEvent = onResumeTouch,
      id = "ResumeButton",
      text = "",
      font = "Helvetica",
      size = 16,
      emboss = false
    }
    
    resumeBtn.x = 160; resumeBtn.y = 220
    
  7. 添加一个名为 onStopTouch() 的局部函数,带有一个 event 参数以停止音频文件。当 event.phase == "release" 时创建一个 if 语句,以便音乐停止。将 stopBtn 显示对象作为一个新的 UI 按钮应用:

    local onStopTouch = function( event )
      if event.phase == "release" then
        audio.stop() 
        print("stop")
    
      end
    end
    
    stopBtn = ui.newButton{
      defaultSrc = "stopbtn.png",
      defaultX = 100,
      defaultY = 50,
      overSrc = "stopbtn-over.png",
      overX = 100,
      overY = 50,
      onEvent = onStopTouch,
      id = "StopButton",
      text = "",
      font = "Helvetica",
      size = 16,
      emboss = false
    }
    
    stopBtn.x = 160; stopBtn.y = 280
    
  8. 添加一个名为 onRewindTouch() 的局部函数,带有一个 event 参数以倒带音频文件。当 event.phase == "release" 时创建一个 if 语句,以便音乐倒带到曲目开头。将 rewindBtn 显示对象作为一个新的 UI 按钮应用:

    local onRewindTouch = function( event )
      if event.phase == "release" then
        audio.rewind( myMusicChannel )
        print("rewind")
      end
    end
    
    rewindBtn = ui.newButton{
      defaultSrc = "rewindbtn.png",
      defaultX = 100,
      defaultY = 50,
      overSrc = "rewindbtn-over.png",
      overX = 100,
      overY = 50,
      onEvent = onRewindTouch,
      id = "RewindButton",
      text = "",
      font = "Helvetica",
      size = 16,
      emboss = false
    }
    
    rewindBtn.x = 160; rewindBtn.y = 340
    
  9. 保存你的项目并在模拟器中运行。现在你已经创建了一个功能齐全的媒体播放器!!行动时间——控制音频

刚才发生了什么?

我们通过调用 require("ui") 为我们的用户界面按钮添加了一个 UI 库。这会在按钮被按下时产生按下时的外观。

创建了各种功能来运行每个按钮。它们如下:

  • onPlayTouch():当用户按下按钮触发事件时,调用 myMusicChannel = audio.play( music, { loops=-1 } )

  • onPauseTouch():当按下按钮时,调用 audio.pause( myMusicChannel ) 暂停歌曲

  • onResumeTouch():如果歌曲已经被暂停,调用 audio.resume( myMusicChannel ) 恢复歌曲

  • onStopTouch():如果歌曲当前正在播放,调用 audio.stop() 停止音频

  • onRewindTouch():调用 audio.rewind( myMusicChannel ) 将歌曲倒带到曲目开头。

注意

当一首歌曲被暂停时,只有按下恢复按钮才会继续播放。当按下暂停按钮时,播放按钮将不起作用。

内存管理

当你完全完成音频文件时,调用 audio.dispose() 非常重要。这样做可以让你回收内存。

audio.dispose()

audio.dispose() 函数释放与句柄关联的音频内存。

语法是 audio.dispose( audioHandle )

参数如下:

  • audioHandle:由你想要释放的 audio.loadSound()audio.loadStream() 函数返回的句柄。

    提示

    在释放内存后,你一定不能使用该句柄。当尝试释放音频时,音频不应该在任何通道上播放或暂停。

例如:

mySound = audio.loadSound( "sound1.wav" )
myMusic = audio.loadStream( "music.mp3" )

audio.dispose( mySound )
audio.dispose( myMusic )

mySound = nil
myMusic = nil

尝试英雄——处理音频

你刚刚学会了如何正确处理音频文件,以便在应用程序中回收内存。尝试以下操作:

  • 加载你的音频文件,并让它播放指定的时间。创建一个函数,当调用onComplete命令时处理文件。

  • 控制音频项目文件中,在onStopTouch()函数中处理音频。

音频更改

音频系统还具备更改音频音量的最小和最大状态的能力,以及在需要时淡入淡出音频。

音量控制

音频的音量可以设置为 0 到 1.0 之间的值。此设置可以在扩展声音播放之前或播放期间的任何时间调整。

audio.setVolume()

audio.setVolume函数设置音量。

语法是 audio.setVolume( volume [, [options] ] ) -- 成功后,应返回 true

参数如下:

  • volume:这允许你设置想要应用的音量级别。有效的数字范围从 0.0 到 1.0,其中 1.0 是最大音量值。默认音量基于你的设备铃声音量,并会有所不同。

  • options:这是一个支持你想要设置音量的通道号的表。你可以设置 1 到 32 之间的任何通道的音量。指定 0 以将音量应用到所有通道。完全省略此参数将设置主音量,这与通道音量不同。

例如:

  • audio.setVolume( 0.75 ) -- 设置主音量

  • audio.setVolume( 0.5, { channel=2 } ) -- 设置通道音量,相对于主通道音量缩放

audio.setMinVolume()

audio.setMinVolume()函数将最小音量限制在设定的值上。任何低于最小音量的音量将以最小音量级别播放。

语法是 audio.setMinVolume( volume, options )

参数如下:

  • volume:这允许你设置想要应用的新最小音量级别。有效的数字范围从 0.0 到 1.0,其中 1.0 是最大音量值。

  • options:这是一个支持你想要设置最小音量的单一关键字通道号的表。1 到最小通道数是有效的通道。指定 0 以将最小音量应用到所有通道。

示例如下:

audio.setMinVolume( 0.10, { channel=1 } ) -- set the min volume on channel 1

audio.setMaxVolume()

audio.setMaxVolume()函数将最大音量限制在设定的值上。任何超过最大音量的音量将以最大音量级别播放。

语法是 audio.setMaxVolume( volume, options )

参数如下:

  • volume:这允许你设置想要应用的新最大音量级别。有效的数字范围从 0.0 到 1.0,其中 1.0 是最大值。

  • options:这是一个支持单个键为你要设置最大音量的通道号的表。1 到最大通道数都是有效的通道。指定 0 将把最大音量应用到所有通道。

示例如下:

audio.setMaxVolume( 0.9, { channel=1 } ) -- set the max volume on channel 1

audio.getVolume()

audio.getVolume()函数可以获取特定通道或主音量的音量。

语法为 audio.getVolume( { channel=c } )

参数如下:

  • channel:设置你想要获取音量的通道号。有效的通道号最多可以有 32 个。指定 0 将返回所有通道的平均音量。完全省略此参数将获取主音量,这与通道音量不同。

以下是一些示例:

  • masterVolume = audio.getVolume() -- 获取主音量

  • channel1Volume = audio.getVolume( { channel=1 } ) -- 获取通道 1 的音量

audio.getMinVolume()

audio.getMinVolume()函数可以获取特定通道的最小音量。

语法为 audio.getMinVolume( { channel=c } )

参数如下:

  • channel:设置你想要获取最小音量的通道号。有效的通道号最多可以有 32 个。指定 0 将返回所有通道的平均最小音量。

示例如下:

channel1MinVolume = audio.getMinVolume( { channel=1 } ) -- get the min volume on channel 1

audio.getMaxVolume()

audio.getMaxVolume()函数可以获取特定通道的最大音量。

语法为 audio.getMaxVolume( { channel=c } )

参数如下:

  • channel:设置你想要获取最大音量的通道号。有效的通道号最多可以有 32 个。指定 0 将返回所有通道的平均音量。

示例如下:

channel1MaxVolume = audio.getMaxVolume( { channel=1 } ) -- get the max volume on channel 1

淡入淡出音频

你可以在任何音频开始播放时淡入音量,但也有其他控制方法。

audio.fade()

audio.fade()函数会在指定的时间内将播放中的声音淡入到指定的音量。淡出完成后,音频将继续播放。

语法为 audio.fade( [ { [channel=c] [, time=t] [, volume=v] } ] )

参数如下:

  • channel:设置你想要淡入的通道号。1 到最大通道数都是有效的通道。指定 0 将把淡入应用到所有通道。

  • time:设置从现在开始,你希望音频淡出并停止的时间量。省略此参数将调用默认的淡出时间,即 1,000 毫秒。

  • volume:设置你想要改变淡入的目标音量。有效的数值为 0.0 到 1.0,其中 1.0 是最大音量。如果省略此参数,默认值为 0.0。

请看以下示例:

audio.fade({ channel=1, time=3000, volume=0.5 } )

audio.fadeOut()

audio.fadeOut()函数会在指定的时间内停止播放声音,并淡出到最小音量。在时间结束时音频将停止,通道将被释放。

语法为 audio.fadeOut( [ { [channel=c] [, time=t] } ] )

参数如下:

  • channel:设置你要淡出的通道号。1 到最大通道数都是有效的通道。指定 0 以对所有通道应用淡出。

  • time:此参数设置从现在开始音频淡出并停止的时间长度。省略此参数将调用默认的淡出时间,即 1,000 毫秒。

示例如下:

audio.fadeOut({ channel=1, time=5000 } )

性能提示

在为你的游戏创建高质量音频时,可以参考这里提到的有用说明。

预加载阶段

最好在应用程序启动时预加载你经常使用的文件。虽然loadStream()通常很快,但loadSound()可能需要一段时间,因为它必须在需要时立即加载并解码整个文件。通常,你不想在应用程序需要流畅运行事件的部分调用loadSound(),比如在游戏玩法中。

audioPlayFrequency

config.lua文件中,你可以指定一个名为audioPlayFrequency的字段:

application =
{
  content =
  {
    width = 480,
    height = 960,
    scale = "letterbox",
    audioPlayFrequency = 22050
  },
}

这告诉 OpenAL 系统应以什么采样率进行混音和播放。为了获得最佳效果,此设置不应高于实际需求。例如,如果你不需要超过 22,050 Hz 的播放质量,就将其设置为 22,050。这样可以产生高质量的语音录音或中等质量的乐曲录音。如果你确实需要高音质,那么将其设置为 44,100 以在播放时产生类似音频 CD 的质量。

当你设置了此参数时,最好将所有音频文件编码为相同的频率。支持的值有 11,025、22,050 和 44,100。

专利和版税

对于高度压缩的格式,如 MP3 和 AAC,AAC 是更好的选择。AAC 是 MPEG 集团官方指定的 MP3 的继承者。如果你分发任何东西,可能需要关心 MP3 的专利和版税问题。你可能需要咨询律师以获得指导。当 AAC 被批准时,同意分发时不需要版税。如果你偏好使用 AAC 而非 MP3,这里有一个关于如何将 MP3 转换为 AAC 或你喜欢的任何文件格式的教程,可以在support.apple.com/kb/ht1550查看。

Ogg Vorbis 是一种无版税和无专利的格式。然而,这种格式在 iOS 设备上不支持。

注意

关于音频格式的更多信息可以在www.nch.com.au/acm/formats.html找到。移动开发者 Ray Wenderlich 也有一篇关于音频文件和数据格式的教程,可以在www.raywenderlich.com/204/audio-101-for-iphone-developers-file-and-data-formats查看。

音频小测验

Q1. 清除内存中音频文件的正确方法是什么?

  1. audio.pause()

  2. audio.stop()

  3. audio.dispose()

  4. audio.fadeOut()

Q2. 应用程序中可以同时播放多少个音频通道?

  1. 10

  2. 18

  3. 25

  4. 32

Q3. 你如何使音频文件无限循环?

  1. loops = -1

  2. loops = 0

  3. loops = 1

  4. 以上都不对

总结

现在你已经了解了在 Corona SDK 中使用音频文件的重要方面。现在,你可以开始为你的游戏添加自己的声音效果和音乐,甚至可以添加到之前章节中你制作的任何示例中。这样做,你将为用户增加另一部分体验,这将吸引玩家进入你创造的环境。

到目前为止,你已经学会了如何:

  • 使用loadSound()loadStream()预加载和播放声音效果及音乐

  • 在音频系统 API 下控制暂停、恢复、停止和倒带音乐轨道的音频功能

  • 当音频不再使用时,从内存中释放

  • 调整音频文件中的音量

在下一章中,你将结合到目前为止所学的所有内容来创建本书中的最终游戏。你还将学习目前市场上流行的移动游戏中实现物理对象和碰撞机制的其他方法。更多令人兴奋的学习内容在等着你。让我们加油!

第七章:物理现象——下落物体

关于如何使用显示对象整合物理引擎,有许多不同的方法。到目前为止,我们已经研究了移除碰撞物体、通过舞台区域移动物体以及通过施加力对抗重力来发射物体等方法,仅举几例。现在,我们将探索另一种允许重力控制环境的机制。我们接下来要创建的游戏涉及下落的物理物体。

在本章中,我们将:

  • 与更多物理实体合作

  • 定制身体构建

  • 跟踪被捕捉的物体

  • 处理碰撞后的事件

  • 创建下落的物体

在这一章中,让我们再创建一个有趣简单的游戏。开始行动吧!

创建我们的新游戏——蛋落

迄今为止的每一步都教会了我们更多关于 iOS/Android 设备上的游戏开发知识。在这个新的环节中,我们的游戏将包含音效,这将增强游戏中的感官体验。

提示

确保你使用的是 Corona SDK 的最新稳定版本。

我们将要创建的新游戏叫做蛋落。玩家控制主角,一个拿着平底锅的伐木工。在游戏过程中,蛋从天空中开始下落,伐木工的工作是用他的平底锅接住鸡蛋,不让它们掉到地上。每个被接住的蛋可以获得 500 分。玩家开始时有三个生命值。当一个蛋没有击中平底锅而是掉到地上时,就会失去一个生命值。当所有三个生命值都失去时,游戏结束。

在开始新的游戏项目时,请确保从Chapter 7文件夹中获取Egg Drop文件。你可以从 Packt Publishing 网站www.packtpub.com/下载本书附带的工程文件。其中包含了为你构建的所有必要文件,比如build.settingsconfig.lua、音频文件以及游戏所需的艺术资源。然后你需要在项目文件夹中创建一个新的main.lua文件,再开始编码。

创建我们的新游戏——蛋落

初始变量

这将是我们第一个完整的游戏设置,其中充满了显著的 Corona SDK 特性。我们将把我们迄今为止学到的关于变量、显示对象、物理引擎、触摸/加速度计事件和音频的基础知识结合起来。Corona 的许多 API 都易于使用和理解。这表明即使只有基本的编程知识甚至没有编程知识,也能快速学习 Corona。

动手操作——设置变量

让我们开始介绍我们将要用来创建游戏的变量。将会有显示对象和整数的组合来进行计数;我们还需要预加载游戏过程中使用的主要音效。按照步骤声明所有必需的变量:

  1. 隐藏状态栏并在display.newGroup()组中添加名为gameGroup的组:

        display.setStatusBar( display.HiddenStatusBar )
        local gameGroup = display.newGroup()
    
  2. 在游戏中包含外部模块:

        local physics = require "physics"
    
  3. 添加显示对象:

        local background
        local ground
        local charObject
        local friedEgg
        local scoreText
        local eggText
        local livesText
        local shade
        local gameOverScreen
    
  4. 添加变量:

        local gameIsActive = false
        local startDrop -- Timer object
        local gameLives = 3
        local gameScore = 0
        local eggCount = 0
        local mRand = math.random
    
  5. 创建鸡蛋的边界和密度:

        local eggDensity = 1.0
        local eggShape = { -12,-13, 12,-13, 12,13, -12,13 }
        local panShape = { 15,-13, 65,-13, 65,13, 15,13 }
    
  6. 设置加速度计和音频:

        system.setAccelerometerInterval( 100 )
        local eggCaughtSound = audio.loadSound( "friedEgg.wav" )
        local gameOverSound = audio.loadSound( "gameOver.wav" )
    

刚才发生了什么?

我们继续创建类似于 Panda Star Catcher 游戏中变量的设置。通过将它们按组别、显示对象、音频等分类组织,效率会更高。

展示的许多变量都有指定的整数,以满足游戏玩法的目标。这包括像gameLives = 3eggCount = 0这样的值。

控制主角

加速度计事件最好在游戏的主要范围内工作。它使你能够查看游戏环境的全部,而不必与屏幕上的触摸交互。必要的触摸事件对于像暂停、菜单、播放等用户界面按钮来说是有意义的。

动手时间——移动角色

鸡蛋将从天空的不同区域掉落到屏幕上。让我们准备让主角移动到屏幕上所有潜在的区域:

  1. 创建一个名为moveChar()的新本地函数,并带有event参数:

    local moveChar = function(event)
    
  2. 为角色添加加速度计移动:

      charObject.x = display.contentCenterX - (display.contentCenterX* (event.yGravity*3))
    
  3. 创建角色在屏幕上移动的边界。这使得角色能够保持在游戏屏幕内,不会超出屏幕外的边界:

      if((charObject.x - charObject.width * 0.5) < 0) then charObject.x = charObject.width * 0.5
      elseif((charObject.x + charObject.width * 0.5) > display.contentWidth) then
      charObject.x = display.contentWidth - charObject.width * 0.5
      end
    end
    

刚才发生了什么?

为了让加速度计移动与设备一起工作,我们必须使用yGravity

注意

当相应地使用xGravityyGravity时,加速度计事件基于竖屏比例。当显示对象被指定为横屏模式时,xGravityyGravity的值会交换,以补偿事件正常工作。

注意,在第 3 步的代码中,防止了charObject显示对象越过任何墙边界。

动手英雄——添加触摸事件

角色目前由加速度计控制。控制角色的另一个选项是通过触摸事件。尝试将事件监听器替换为"touch",并使用事件参数,以便触摸事件正常工作。

如果你记得我们在第三章,打造我们的第一款游戏 – Breakout和第四章,游戏控制中是如何将挡板移动与 Breakout 游戏结合在一起的,对于模拟器来说,这个过程应该非常相似。

更新得分

当更新得分时,它会引用我们的文本显示对象,并将数值转换为字符串。

这是一个示例:

gameScore = 100
scoreText = display.newText( "Score: " .. gameScore, 0, 0, "Arial", 45 )
scoreText:setTextColor( 1, 1, 1)
scoreText.x = 160; scoreText.y = 100

在上一个示例中,你会注意到我们将值100设置给了gameScore。在接下来的scoreText行中,使用了gameScore来连接"Score: "字符串和gameScore的值。这样做可以通过scoreText以字符串格式显示gameScore的值。

动手时间——设置得分

谁不喜欢友好的竞争呢?我们对前面章节中制作的游戏的计分板很熟悉。因此,我们对跟踪得分并不陌生。执行以下步骤来设置得分:

  1. 创建一个名为setScore()的局部函数,它有一个名为scoreNum的参数:

        local setScore = function( scoreNum )
    
  2. 设置变量以计算得分:

          local newScore = scoreNum
          gameScore = newScore
          if gameScore < 0 then gameScore = 0; end
    
  3. 当在游戏玩法中获得分数时更新得分,并关闭函数:

          scoreText.text = "Score: " .. gameScore
          scoreText.xScale = 0.5; scoreText.yScale = 0.5
          scoreText.x = (scoreText.contentWidth * 0.5) + 15
          scoreText.y = 15
        end
    

刚才发生了什么?

当在任何函数内调用setScore(scoreNum)时,它将引用使用gameScore变量的所有方法。假设在应用程序开始时gameScore = 0,则该值会增加到gameScore设置的数量。

scoreText.text = "Score: " .. gameScore中,"Score: "是在游戏过程中在设备上显示的字符串。gameScore变量获取赋予变量的当前值并将其显示为字符串。

显示游戏环境

为显示对象设置逻辑环境可以帮助玩家想象主角与环境之间的关系。由于我们的主角是伐木工人,将他在一个森林或完全专注于自然的环境中设置是有意义的。

动手操作——绘制背景

在本节中,我们将屏幕用环境显示对象填充。这包括我们的背景和地面对象,我们还可以为地面添加物理元素,以便我们可以为其指定碰撞事件。要绘制背景,请执行以下步骤:

  1. 创建一个名为drawBackground()的局部函数:

        local drawBackground = function()
    
  2. 添加背景图像:

          background = display.newImageRect( "bg.png", 480, 320 )
          background.x = 240; background.y = 160
          gameGroup:insert( background )
    
  3. 添加地面元素并创建地面物理边界。关闭函数:

          ground = display.newImageRect( "grass.png", 480, 75 )
          ground.x = 240; ground.y = 325
          ground.myName = "ground"
          local groundShape = { -285,-18, 285,-18, 285,18, -285,18}
          physics.addBody( ground, "static", { density=1.0, bounce=0, friction=0.5, shape=groundShape } )
          gameGroup:insert( ground )
        end
    

刚才发生了什么?

backgroundground显示对象被放置在名为drawBackground()的函数中。由于我们对一些图像进行了动态缩放,因此使用了display.newImageRect()函数。地面显示对象有一个自定义的物理形状,其大小与原始显示对象不同。

我们的background对象被居中到设备屏幕区域的尺寸中,并插入到gameGroup

ground显示对象被放置在显示区域的底部附近。通过ground.myName = "ground"为其分配一个名称。我们将在后面使用名称"ground"来确定碰撞事件。通过groundShape为地面创建了一个自定义的物理边界。这使得地面的主体可以影响显示对象的指定尺寸。当初始化physics.addBody()时,我们使用了groundShape作为形状参数。接下来,将ground也设置为gameGroup

显示抬头显示器

在游戏中,抬头显示HUD)是用于视觉上向玩家传递信息的方法。在许多游戏中,常见的信息包括健康/生命值、时间、武器、菜单、地图等。这使玩家在游戏过程中对当前发生的事情保持警惕。在跟踪生命值时,你希望知道在角色用完继续游戏的机会之前还剩下多少生命值。

行动时间——设计 HUD

尽管我们希望玩家的游戏体验愉快,但显示的信息必须与游戏相关,并且要策略性地放置,以免干扰主要游戏区域。因此,在设计 HUD 时,请执行以下步骤:

  1. 创建一个名为 hud() 的新本地函数:

        local hud = function()
    
  2. 显示在游戏过程中捕获的鸡蛋的文本:

          eggText = display.newText( "Caught: " .. eggCount, 0, 0, "Arial", 45 )
          eggText:setTextColor( 1, 1, 1 )
          eggText.xScale = 0.5; eggText.yScale = 0.5
          eggText.x = (480 - (eggText.contentWidth * 0.5)) - 15
          eggText.y = 305
          gameGroup:insert( eggText )
    
  3. 添加跟踪生命值的文本:

          livesText = display.newText( "Lives: " .. gameLives, 0, 0, "Arial", 45 )
          livesText:setTextColor( 1, 1, 1 )--> white
          livesText.xScale = 0.5; livesText.yScale = 0.5  --> for clear retina display text
          livesText.x = (480 - (livesText.contentWidth * 0.5)) - 15
          livesText.y = 15
          gameGroup:insert( livesText )
    
  4. 添加分数的文本并关闭函数:

          scoreText = display.newText( "Score: " .. gameScore, 0, 0, "Arial", 45 )
          scoreText:setTextColor( 1, 1, 1 )--> white
          scoreText.xScale = 0.5; scoreText.yScale = 0.5  --> for clear retina display text
          scoreText.x = (scoreText.contentWidth * 0.5) + 15
          scoreText.y = 15
          gameGroup:insert( scoreText )
        end
    

    行动时间——设计 HUD

刚才发生了什么?

eggText 显示对象可以在屏幕的右下角找到。它在游戏过程中对用户仍然可见,同时又不占据主要焦点。注意 eggText = display.newText( "Caught: " .. eggCount, 0, 0, "Arial", 45 ) 将在值更新时引用 eggCount

livesText 显示对象的设置与 eggText 类似。它被放置在屏幕的右上角附近。由于这个对象在游戏中非常重要,它的位置相当突出。它位于一个可以从背景中注意到并且在游戏中可以参考的区域。当 gameLives 更新时,livesText 显示对象会减少数字。

scoreText 的初始设置在 hud() 函数中开始。它被放置在屏幕的左上角,与 livesText 相对。

创建游戏生命值

如果游戏中没有后果,那么完成主要目标就没有紧迫感。为了保持玩家在游戏中的参与度,引入一些具有挑战性的元素将保持竞争性和兴奋感。在游戏中添加后果为玩家创造紧张感,并给他们更多保持生存的动力。

行动时间——计算生命值

跟踪游戏中的剩余生命值,让玩家了解游戏结束还有多久。为了计算游戏中剩余的生命值,请执行以下步骤:

  1. 设置一个名为 livesCount() 的函数:

        local livesCount = function()
    
  2. 每次生命值减少时,显示生命值的文本:

          gameLives = gameLives - 1
          livesText.text = "Lives: " .. gameLives
          livesText.xScale = 0.5; livesText.yScale = 0.5  --> for clear retina display text
          livesText.x = (480 - (livesText.contentWidth * 0.5)) - 15
          livesText.y = 15
          print(gameLives .. " eggs left")
          if gameLives < 1 then
            callGameOver()
          end
        end
    

刚才发生了什么?

livesCount()函数是一个单独的函数,用于更新gameLives。它确保你注意到gameLives = gameLives – 1。这减少了代码开始时实例化的设定值。当gameLives的值发生变化时,它通过livesText显示更新。在函数末尾使用print语句,在终端窗口中跟踪计数。

gameLives < 1时,将调用callGameOver()函数,并显示游戏结束元素。

动手试试看——为游戏生命值添加图像

目前,游戏在屏幕上使用显示文本来显示游戏进行期间还剩下多少生命值。使 HUD 显示更具吸引力的方法之一是创建/添加与游戏相关的小图标,例如鸡蛋或煎锅。

需要创建三个独立的显示对象,并有序地放置,以便当生命值被扣除时,对象的透明度降低到 0.5。

需要创建一个方法,以便当游戏生命值降至零时,所有三个显示对象都会受到影响。

介绍主角

我们的主角将在游戏过程中对每个应用的动作进行动画处理。我们还将创建一个复杂的身体构造,因为其碰撞点的焦点将放在角色持有的物体上,而不是整个身体。

复杂身体构造

也可以从多个元素构建身体。每个身体元素都是一个单独的多边形形状,具有自己的物理属性。

由于 Box2D 中的碰撞多边形必须是凸面,因此任何具有凹形状的游戏对象都必须通过附加多个身体元素来构建。

复杂身体的构造函数与简单多边形身体的构造函数相同,不同之处在于它有不止一个身体元素列表:

physics.addBody( displayObject, [bodyType,] bodyElement1, [bodyElement2, ...] )

每个身体元素可能都有自己的物理属性,以及其碰撞边界的形状定义。以下是一个示例:

local hexagon = display.newImage("hexagon.png")
hexagon.x = hexagon.contentWidth
hexagon.y = hexagon.contentHeight
hexagonShape = { -20,-40, 20, -40, 40, 0, 20,40, -20,40, -40,0 }
physics.addBody( hexagon, "static", { density = 1.0, friction = 0.8, bounce = 0.3, shape=hexagonShape } )

与更简单的情况一样,bodyType属性是可选的,如果没有指定,将默认为"dynamic"

动手操作——创建角色

主角是用一个精灵表创建的,需要设置以查看它提供的动画。其他将出现的显示图像包括当与物理对象发生碰撞时出现的破裂鸡蛋。要创建角色,请执行以下步骤:

  1. 创建一个名为createChar()的新局部函数:

        local createChar = function()
    
  2. 为主角创建精灵表:

    local sheetData = { width=128, height=128, numFrames=4, sheetContentWidth=256, sheetContentHeight=256 }
    local sheet = graphics.newImageSheet( "charSprite.png", sheetData )
    
        local sequenceData = 
        {
          { name="move", start=1, count=4, time=400 } 
        }
    
        charObject = display.newSprite( sheet, sequenceData )
        charObject:setSequence("move")
        charObject:play()
    
  3. 设置主角的起始位置和物理属性:

        charObject.x = 240; charObject.y = 250
        physics.addBody( charObject, "static", { density=1.0, bounce=0.4, friction=0.15, shape=panShape } )
        charObject.rotation = 0
        charObject.isHit = false -- When object is not hit
        charObject.myName = "character"
    
  4. 在鸡蛋发生碰撞后添加过渡图像:

        friedEgg = display.newImageRect( "friedEgg.png", 40, 23 )
        friedEgg.alpha = 1.0
        friedEgg.isVisible = false
        gameGroup:insert( charObject )
        gameGroup:insert( friedEgg )
      end
    

    动手操作——创建角色

刚才发生了什么?

所引用的图像集被称为sheetData,它从"charSprite.png"中获取前4帧动画。我们创建了一个名为"move"的动画集。每次调用"move"时,都会从第1帧开始播放,每400毫秒播放从开始的前4帧。

主显示对象称为charObject,它具有sheetData的特征。当它调用setSequence("move")时,执行play()命令时会播放该动画序列。

对角色物理身体的一个重要更改是,它的主要碰撞点将指向动画中使用的煎锅。角色身体上的任何碰撞检测都不会被读取。charObject显示对象被赋予一个名为"character"的名字,这将用于检测与掉落鸡蛋的碰撞。

我们还在这个函数中放置了煎蛋,为碰撞做准备。

添加后碰撞

我们要确保当一个对象与另一个对象交互后,紧接着就会发生一个事件类型。在碰撞后的瞬间,我们可以确认两个对象之间的碰撞力。这有助于我们确定被销毁的对象是受到一定力量的完全撞击。

碰撞处理

请注意您处理 Box2D 物理引擎的方式。如果 Corona 代码在碰撞过程中尝试修改仍在碰撞中的对象,Box2D 将会崩溃,因为 Box2D 仍在对它们进行迭代数学计算。

为了防止碰撞检测时立即发生崩溃,不要让碰撞立即发生。

在碰撞过程中,请勿修改/创建/销毁物理对象,以防止程序崩溃。

如果您需要在碰撞后修改/创建/销毁一个对象,您的碰撞处理程序应设置一个标志或添加一个时间延迟,以便稍后使用timer.performWithDelay()进行更改。

刚体属性

许多原生的 Box2D 方法已经被简化为显示对象的点属性。以下示例显示,一个名为newBody的刚体是使用其中一个构造方法创建的。

body.isAwake

这是一个表示当前唤醒状态的布尔值。默认情况下,当所有刚体在几秒钟内没有交互时,它们会自动进入 休眠状态。刚体停止模拟,直到某种碰撞或其他交互唤醒它们。

这是一个示例:

newBody.isAwake = true
local object = newBody.isAwake

body.isBodyActive

这是一个表示刚体激活状态的布尔值。非激活状态的刚体不会被销毁,但它们会从模拟中移除,并停止与其他刚体的交互。

这是一个示例:

newBody.isBodyActive = true
local object = newBody.isBodyActive

body.isBullet

这是一个将刚体视为子弹的布尔值。子弹将受到连续碰撞检测。默认值为false

这是一个示例:

newBody.isBullet = true
local object = newBody.isBullet

body.isSensor

这是一个布尔属性,用于设置整个物体中的isSensor属性。传感器可以穿过其他物体而不是反弹,但能检测到一些碰撞。这个属性作用于所有物体元素,并将覆盖元素本身的任何isSensor设置。

这是一个示例:

newBody.isSensor = true

body.isSleepingAllowed

这是一个布尔值,用于设置一个物体是否允许进入休眠状态。醒着的物体在比如倾斜重力的情况下很有用,因为休眠的物体不会对全球重力变化做出反应。默认值为true

这是一个示例:

newBody.isSleepingAllowed = true
local object = newBody.isSleepingAllowed

body.isFixedRotation

这是一个布尔值,用于设置一个物体的旋转是否应该被锁定,即使物体即将加载或受到偏心力的作用。默认值为false

这是一个示例:

newBody.isFixedRotation = true
local object = newBody.isFixedRotation

body.angularVelocity

这是当前旋转速度的值,单位为每秒度数。

这是一个示例:

newBody.angularVelocity = 50
local myVelocity = newBody.angularVelocity

body.linearDamping

这是用于控制物体线性运动阻尼的值。这是角速度随时间减少的速率。默认值为零。

这是一个示例:

newBody.linearDamping = 5
local object = newBody.linearDamping

body.angularDamping

这是用于控制物体旋转阻尼的值。默认值为零。

这是一个示例:

newBody.angularDamping = 5
local object = newBody.angularDamping

body.bodyType

这是一个字符串值,用于设置模拟的物理物体的类型。可用的值有"static""dynamic""kinematic",具体解释如下:

  • static(静止)物体不会移动也不会相互影响。静止物体的例子包括地面或迷宫的墙壁。

  • dynamic(动态)物体受重力影响,也会与其他类型的物体发生碰撞。

  • kinematic(运动学)物体受力影响但不受重力影响。那些可拖动的物体在拖动事件期间应该被设置为"kinematic"

默认的物体类型是"dynamic"

这是一个示例:

newBody.bodyType = "kinematic"
local currentBodyType = newBody.bodyType

行动时间——创建鸡蛋碰撞

在我们之前创建的示例游戏中已经处理过碰撞。处理碰撞后的事件需要引入力来完成碰撞后的动作:

  1. 创建一个名为onEggCollision()的新局部函数,它有两个参数,分别名为selfevent

        local onEggCollision = function( self, event )
    
  2. 当力大于1时创建一个if语句,并包含not self.isHit。加入eggCaughtSound音效:

          if event.force > 1 and not self.isHit then
            audio.play( eggCaughtSound )
    
  3. 使self变得不可见且不活跃,并用friedEgg显示对象替换它:

            self.isHit = true
            print( "Egg destroyed!")
            self.isVisible = false
            friedEgg.x = self.x; friedEgg.y = self.y
            friedEgg.alpha = 0
            friedEgg.isVisible = true
    
  4. 创建一个函数,通过使用onComplete命令将friedEgg显示对象过渡并使其在舞台上淡出:

            local fadeEgg = function()
              transition.to( friedEgg, { time=500, alpha=0 } )
            end
            transition.to( friedEgg, { time=50, alpha=1.0, onComplete=fadeEgg } )
            self.parent:remove( self )
            self = nil
    
  5. 使用if event.other.myName == "character",当主角接住鸡蛋时更新eggCount。并且,每次碰撞增加gameScore 500分。如果鸡蛋掉到地上,使用elseif event.other.myName == "ground"并通过livesCount()减少生命值:

            if event.other.myName == "character" then
              eggCount = eggCount + 1
              eggText.text = "Caught: " .. eggCount
              eggText.xScale = 0.5; eggText.yScale = 0.5  --> for clear retina display text
              eggText.x = (480 - (eggText.contentWidth * 0.5)) - 15
              eggText.y = 305
              print("egg caught")
              local newScore = gameScore + 500
              setScore( newScore )
            elseif event.other.myName == "ground" then
              livesCount()
              print("ground hit")
            end
          end
        end
    

    行动时间——创建鸡蛋碰撞

刚才发生了什么?

使用 onEggCollision( self, event ) 函数,我们通过 if 语句设置条件为 event.force > 1not self.isHit。当两个语句都返回 true 时,播放鸡蛋的声音效果。碰撞发生后,从天空中落下的初始鸡蛋从场景中移除,并在同一位置使用 friedEgg 显示对象替换,通过 friedEgg.x = self.x; friedEgg.y = self.y 实现。

fadeEgg() 函数通过 transition.to( eggCrack, { time=50, alpha=1.0, onComplete=fadeCrack } )50 毫秒内使新替换的鸡蛋对象出现,然后通过 onComplete 命令,使用 transition.to( eggCrack, { time=500, alpha=0 } ) 将对象返回到不可见状态。

当从 event.other.myName 调用 "character" 名称时,分配给该名称的每次碰撞都会使 eggCount + 1。因此,eggText 使用 eggCount 的值进行更新。setScore( newScore ) 语句在每次与 "character" 发生碰撞时将分数增加 500。当与 "ground" 发生碰撞时,调用 livesCount() 函数,该函数将生命值减去 1

使显示对象下落

我们将通过学习如何将物理对象添加到场景中,并让它们在游戏中的随机区域下落,来应用主要资源(鸡蛋对象)。物理引擎将考虑我们为鸡蛋显示对象创建的动态物理体。

行动时间——添加鸡蛋对象

想象一个充满下落鸡蛋的世界。这并不完全现实,但在这个游戏中,我们将创建这个元素。至少,我们将确保重力和现实世界物理被应用。要添加鸡蛋对象,请执行以下步骤:

  1. 创建一个名为 eggDrop() 的新本地函数:

        local eggDrop = function()
    
  2. 添加 egg 显示对象的属性:

          local egg = display.newImageRect( "egg.png", 26, 30 )
          egg.x = 240 + mRand( 120 ); egg.y = -100
          egg.isHit = false
          physics.addBody( egg, "dynamic",{ density=eggDensity, bounce=0, friction=0.5, shape=eggShape } )
          egg.isFixedRotation = true
          gameGroup:insert( egg )
    
  3. egg 显示对象添加 postCollision 事件:

          egg.postCollision = onEggCollision
          egg:addEventListener( "postCollision", egg )
        end
    

    行动时间——添加鸡蛋对象

刚才发生了什么?

我们用 240 + mRand( 120 ) 设置了 eggx 值。mRand 函数等于 math.random,这将允许鸡蛋在从 x 方向的 50 开始的 120 像素区域内随机位置出现。

确保在碰撞事件正确应用时 egg.isHit = false 是至关重要的。物理体设置为 "dynamic" 以便它对重力作出反应并使对象下落。我们创建的鸡蛋有一个自定义的密度和形状,这在代码开始时就已经设置好了。

为了让碰撞生效,最后一个重要的细节是使用 egg.postCollision = onEggCollisionegg 对象添加到 onEggCollision() 函数中,然后让事件监听器通过 egg:addEventListener( "postCollision", egg ) 使用 "postCollision" 事件。

行动时间——使鸡蛋降落

我们将执行鸡蛋的计时器,以便它们可以开始在屏幕上降落。要使鸡蛋降落,请执行以下步骤:

  1. 创建一个名为eggTimer()的局部函数,并使用timer.performWithDelay每 1 秒(1000 毫秒)重复投放一个鸡蛋。使用eggDrop()来激活下落:

        local eggTimer = function()
          startDrop = timer.performWithDelay( 1000, eggDrop, 0 )
        end
    
  2. onEggCollision()函数的第一个if语句内,使用timerIDstartDrop变量取消计时器。然后添加if gameLives < 1语句以停止鸡蛋下落:

          if gameLives < 1 then
            timer.cancel( startDrop )
            print("timer cancelled")
          end
    

刚才发生了什么?

为了让鸡蛋从天空中开始下落,我们创建了一个名为eggTimer()的函数。它通过startDrop = timer.performWithDelay( 1000, eggDrop, 0 )每隔 1000 毫秒(1 秒)无限次地激活eggDrop()函数,让一个鸡蛋下落。

回到onEggCollision(),我们要检查gameLives是否已经小于1。当这个语句为真时,鸡蛋将停止下落。这是通过timer.cancel( startDrop )实现的。我们在eggTimer()中设置的timerID就是startDrop

结束游戏玩法

每个游戏的开始总有一个结局,无论是简单的胜利失败,还是仅仅是一个游戏结束;所有这些都给玩家一个结束感。通知玩家这些事件很重要,这样他们才能反思所获得的成就。

行动时间——调用游戏结束

我们将确保当游戏结束显示屏幕弹出时,当前正在移动的任何显示对象停止移动,并且事件监听器被停用。除了我们的游戏结束屏幕的视觉显示外,我们还将添加一个声音通知,这将帮助触发事件。要结束游戏,请执行以下步骤:

  1. 创建一个名为callGameOver()的新局部函数,并将其放在setScore()函数之后,drawBackground()函数之前:

        local callGameOver = function()
    
  2. 当游戏结束显示弹窗时引入声音效果。将gameIsActive设置为false并在游戏中暂停物理效果:

          audio.play( gameOverSound )
          gameIsActive = false
          physics.pause()
    
  3. 创建一个覆盖当前背景的阴影:

          shade = display.newRect( 0, 0, 570, 320 )
          shade:setFillColor( 0, 0, 0 )
          shade.x = 240; shade.y = 160
          shade.alpha = 0  -- Getting shade ready to display at game end
    
  4. 显示游戏结束窗口并重申最终得分:

          gameOverScreen = display.newImageRect( "gameOver.png", 400, 300 )
          local newScore = gameScore
          setScore( newScore )
          gameOverScreen.x = 240; gameOverScreen.y = 160
          gameOverScreen.alpha = 0
          gameGroup:insert( shade )
          gameGroup:insert( gameOverScreen )
          transition.to( shade, { time=200, alpha=0.65 } )
          transition.to( gameOverScreen, { time=500, alpha=1 } )
    
  5. 在游戏结束屏幕上显示得分:

          scoreText.isVisible = false
          scoreText.text = "Score: " .. gameScore
          scoreText.xScale = 0.5; scoreText.yScale = 0.5  --> for clear retina display text
          scoreText.x = 240
          scoreText.y = 160
          scoreText:toFront()  -- Moves to front of current display group
          timer.performWithDelay( 0,
            function() scoreText.isVisible = true; end, 1 )
        end
    

    行动时间——调用游戏结束

刚才发生了什么?

我们的gameOver()函数触发了我们在代码开始时预加载的gameOverSound声音效果。我们确保通过gameIsActive = false禁用任何事件,比如加速度计的运动。

在这个时候,我们的显示对象元素会出现在shadegameOverScreenscoreText中。

如果你注意到,当游戏玩法结束时,scoreText通过scoreText.isVisible = false消失,然后在屏幕的另一区域使用timer.performWithDelay( 0, function() scoreText.isVisible = true; end, 1 )重新出现。

开始游戏

我们将激活所有剩余的函数,并让它们相应地运行。

行动时间——激活游戏

所有游戏玩法元素设置好后,是时候通过以下步骤启动应用程序了:

  1. 创建一个名为gameActivate()的新局部函数,并插入gameIsActive = true。将此函数放在moveChar()函数上方:

        local gameActivate = function()
          gameIsActive = true
        end
    
  2. 通过创建一个名为gameStart()的新函数来初始化所有游戏动作:

        local gameStart = function()
    
  3. 启动物理属性并为下落物体设置重力:

          physics.start( true )
          physics.setGravity( 0, 9.8 )
    
  4. 激活所有实例化的函数。为charObject添加事件监听器,使用"accelerometer"事件监听moveChar()函数:

          drawBackground()
          createChar()
          eggTimer()
          hud()
          gameActivate()
          Runtime:addEventListener("accelerometer", moveChar)
        end
    
  5. 实例化gameStart()函数并返回gameGroup组:

        gameStart()
        return gameGroup
    

刚才发生了什么?

如果你记得,在我们的代码开始时,我们设置了gameIsActive = false。现在我们将通过gameActivate()函数改变这个状态,使gameIsActive = true。我们让gameStart()函数应用所有初始游戏元素。这包括物理引擎和重力的启动。同时,我们取所有函数的余数并初始化它们。

一旦所有函数被激活,需要返回gameGroup,以便在游戏进行时显示所有显示对象。

为了确保你的显示对象的物理对象边界位于正确位置,在gameStart()函数中使用physics.setDrawMode( "hybrid" )

小测验 - 动画图形

问题 1. 什么可以检索或设置文本对象的文本字符串?

  1. object.text

  2. object.size

  3. object:setTextColor()

  4. 以上都不是

问题 2. 什么函数将任何参数转换成字符串?

  1. tonumber()

  2. print()

  3. tostring()

  4. nil

问题 3. 哪种体型受到重力和与其他体型碰撞的影响?

  1. 动态

  2. 动力学

  3. 静态

  4. 以上都不是

总结

我们的应用程序的游戏玩法构建现在已完成。现在我们熟悉了使用物理引擎的各种方式,这表明使用 Box2D 设计涉及物理体的其他游戏是多么容易。

我们现在对以下内容有了更好的了解:

  • 应用动态和静态物理体的使用

  • 为我们的显示对象的物理属性构建自定义形状

  • 使用给定变量的值跟踪捕获的对象数量

  • 使用后碰撞来切换图像

在下一章中,我们将通过使用 Composer API 创建多功能菜单屏幕来完成游戏体验。你还将学习如何添加暂停动作,保存高分以及了解有关数据保存和卸载文件更多信息。

使用 Corona SDK 帮助我们以最少的时间设计和开发游戏。让我们继续为我们的游戏添加最后的润色!

第八章:操作编排器

我们已经将游戏 Egg Drop 进行了探索,创建了游戏物理以反应碰撞检测并跟踪其他有用的数据,如生命值和积分系统。我们还处理了自定义物理实体,并为我们的显示对象创建了名称,这些名称适用于游戏分数计数。

接下来,我们将添加一个菜单系统,其中包括游戏介绍,游戏中应用暂停菜单,并在游戏结束时保存高分。

我们正在完成一个应用程序,它具备了发布到 App Store 和 Google Play Store 所需的必要元素。

在本章中,我们将:

  • 保存和加载高分

  • 添加暂停菜单

  • 使用 Composer API 管理场景

  • 添加加载屏幕

  • 添加主菜单、选项菜单和制作人员屏幕

那么,让我们开始吧!

继续鸡蛋掉落游戏(Egg Drop)

我们已经完成了 Egg Drop 的主要游戏部分,作为我们应用程序的基础。现在,是时候让我们加入如何在游戏中途暂停动作以及保存高分的方法了。我们还将添加一些新场景,帮助我们轻松快速地介绍和过渡到游戏。

第八章Resources文件夹中,获取所有图像和文件资源,并将它们复制到当前的Egg Drop项目文件夹中。你可以从 Packt Publishing 网站下载伴随这本书的项目文件。我们将使用这些文件为我们的游戏添加最后的润色。

数据保存

保存文件信息在游戏开发的许多方面都有应用。我们用它来保存高分和游戏设置,如声音开关、锁定/解锁关卡等。这些功能并非必须,但既然它们很好,也许你希望在应用程序中包含它们。

在 Corona SDK 中,应用程序是沙盒化的;这意味着你的文件(应用程序图片、数据和个人偏好设置)存储在一个其他应用程序无法访问的位置。你的文件将驻留在特定于应用程序的目录中,用于文档、资源或临时文件。这个限制与你在 Mac 或 PC 上编程时的文件有关,而不是设备上的文件。

BeebeGames 类用于保存和加载值

我们将使用由 Jonathan Beebe 创建的 BeebeGames 类。它提供了许多简单且实用的游戏功能。其中一些值得注意的功能包括一种简单保存和加载数据的方法,我们可以将其加入到我们的游戏中。关于 BeebeGames 类的更多文档可以在第八章文件夹中找到。

注意

你还可以参考github.com/lewisNotestine/luaCorona/blob/master/justATest/code/beebegames.lua,以跟踪类的更新。

如果你想将来使用它们,可以查看其他与动画、过渡、定时器等相关的方法。现在,我们将专注于可以使用这些方法轻松地为我们的游戏保存和加载值。

下面是一个保存和加载值的示例:

-- Public Method: saveValue() --> save single-line file (replace contents)

function saveValue( strFilename, strValue )
  -- will save specified value to specified file
  local theFile = strFilename
  local theValue = strValue

  local path = system.pathForFile( theFile, system.DocumentsDirectory)

  -- io.open opens a file at path. returns nil if no file found
  -- "w+": update mode, all previous data is erased
  local file = io.open( path, "w+" )
  if file then
  -- write game score to the text file
  file:write( theValue )
  io.close( file )
  end
end

-- Public Method: loadValue() --> load single-line file and store it into variable

function loadValue( strFilename )
  -- will load specified file, or create new file if it doesn't exist

  local theFile = strFilename

  local path = system.pathForFile( theFile, system.DocumentsDirectory)

  -- io.open opens a file at path. returns nil if no file found
  -- "r": read mode
  local file = io.open( path, "r" )
  if file then
    -- read all contents of file into a string
    -- "*a": reads the whole file, starting at the current position
    local contents = file:read( "*a" )
    io.close( file )
    return contents
  else
    -- create file b/c it doesn't exist yet
    -- "w": write mode
    file = io.open( path, "w" )
    file:write( "0" )
    io.close( file )
    return "0"
  end
end

获取文件的路径

这些文件的路径对于你的应用程序来说是唯一的。要创建文件路径,你可以使用system.pathForFile函数。这个函数会生成一个绝对路径到应用程序的图标文件,以应用程序的资源目录作为Icon.png的基础目录:

local path = system.pathForFile( "Icon.png", system.ResourceDirectory)

通常,你的文件必须位于三个可能的基础目录之一:

  • system.DocumentsDirectory:这应该用于需要在应用程序会话之间持久存在的文件。

  • system.TemporaryDirectory:这是一个临时目录。写入这个目录的文件不能保证在后续的应用程序会话中存在。它们可能存在,也可能不存在。

  • system.ResourceDirectory:这是所有应用程序资源的目录。注意,你不应该在这个目录中创建、修改或添加文件。

注意

关于文件的更多信息可以在docs.coronalabs.com/api/library/system/index.html找到。

读取文件

要读取文件,使用io库。这个库允许你管理文件,给定一个绝对路径。

写入文件

要写入文件,你可以按照很多与读取文件相同的步骤进行。不同的是,你不是使用读取方法,而是将数据(字符串或数字)写入文件。

是时候行动了——保存和加载最高分

游戏结束屏幕显示时,我们将保存并加载最终得分和最高分值。为此,执行以下步骤:

  1. 打开为 Egg Drop 创建的main.lua文件。我们将继续使用同一个文件,并添加更多代码以及对游戏的新的修改。

  2. 在代码顶部,所有其他初始化变量的位置加入两个新的变量,local highScoreTextlocal highScore

    local highScoreText
    local highScore
    
  3. 在预加载的音频文件后引入saveValue()函数:

      local saveValue = function( strFilename, strValue )
        -- will save specified value to specified file
        local theFile = strFilename
        local theValue = strValue
    
        local path = system.pathForFile( theFile, system.DocumentsDirectory )
    
        -- io.open opens a file at path. returns nil if no file found
        local file = io.open( path, "w+" )
        if file then
          -- write game score to the text file
          file:write( theValue )
          io.close( file )
        end
      end
    
  4. 加入loadValue()函数:

      local loadValue = function( strFilename )
        -- will load specified file, or create new file if it doesn't exist
    
        local theFile = strFilename
    
        local path = system.pathForFile( theFile, system.DocumentsDirectory )
    
        -- io.open opens a file at path. returns nil if no file found
        local file = io.open( path, "r" )
        if file then
          -- read all contents of file into a string
          local contents = file:read( "*a" )
          io.close( file )
          return contents
         else
          -- create file b/c it doesn't exist yet
          file = io.open( path, "w" )
          file:write( "0" )
          io.close( file )
           return "0"
        end
      end
    
  5. callGameOver()函数的最后,创建一个if语句来比较gameScorehighScore。使用saveValue()函数保存最高分:

        if gameScore > highScore then
          highScore = gameScore
          local highScoreFilename = "highScore.data"
          saveValue( highScoreFilename, tostring(highScore) )
        end
    
  6. 接下来,在同一个callGameOver()函数中加入highScoreText显示文本,以便在游戏结束时显示最高分:

        highScoreText = display.newText( "Best Game Score: " .. tostring( highScore ), 0, 0, "Arial", 30 )
        highScoreText:setTextColor( 1, 1, 1 )	
        highScoreText.xScale = 0.5; highScoreText.yScale = 0.5
        highScoreText.x = 240
        highScoreText.y = 120
    
        gameGroup:insert( highScoreText )
    
  7. gameStart()函数的最后,使用loadValue()函数加载最高分:

          local highScoreFilename = "highScore.data"
          local loadedHighScore = loadValue( highScoreFilename )
    
          highScore = tonumber(loadedHighScore)
    

    是时候行动了——保存和加载最高分

刚才发生了什么?

在游戏级别初始化了saveValue()loadValue()函数后,我们创建了一个if语句来比较gameScore(游戏进行时的当前得分)和highScore(迄今为止获得过的最高得分)。当gameScore的结果更高时,它就会替换保存的highScore数据。

为了保存这个值,需要创建一个数据文件。我们创建了一个名为local highScoreFilename = "highscore.data"的变量。我们使用highScoreFilename作为参数调用了saveValue()函数。tostring(highScore)参数会将highScore的数值转换为字符串。

游戏结束屏幕可见时,highScoreText会显示从highScore保存的值,位于达到的gameScore上方。添加高分可以激励玩家争取最高分,并增加游戏的重复可玩性。

gameStart()函数中,重要的是要在游戏开始时加载highScore.data的值。使用我们创建的用来保存highScore的同一个数据文件,我们也可以在游戏中加载这个值。为了加载这个值,local highScore调用loadValue(highScoreFileName)。这会从highScore.data获取信息。为了得到这个值,tonumber(loadedHighScore)将其从字符串转换为整数,并可以用来显示highScore的值。

暂停游戏

你是否曾在玩游戏时突然需要去洗手间或者手抽筋?显然,这些情况都需要你暂时将注意力从游戏进度上转移,并且需要暂时停止当前动作来处理这些需求。这时暂停按钮就显得非常方便,这样你就可以在那一刻停止动作,并在准备好再次游戏时从停止的地方继续。

动作时间——暂停游戏

这不仅仅是制作一个按钮;还包括通过执行以下步骤暂停屏幕上的所有动作,包括物理效果和计时器:

  1. 在代码开始部分初始化其他变量时,添加local pauseBtnlocal pauseBG变量。在脚本顶部gameOverSound之后预加载btnSound音频:

    -- Place near other game variables
    local pauseBtn
    local pauseBG
    
    -- Place after gameOverSound
    local btnSound = audio.loadSound( "btnSound.wav" )
    
  2. hud()函数内,在scoreText部分之后创建另一个函数,用于运行暂停按钮的事件。调用onPauseTouch(event)函数。通过将gameIsActive设置为false来暂停游戏中的物理效果,并让暂停元素在屏幕上显示:

        local onPauseTouch = function( event )
          if event.phase == "release" and pauseBtn.isActive then
            audio.play( btnSound )
    
            -- Pause the game
    
            if gameIsActive then
    
              gameIsActive = false
              physics.pause()
    
              local function pauseGame()
                timer.pause( startDrop )
                print("timer has been paused")
              end
              timer.performWithDelay(1, pauseGame)
    
              -- SHADE
              if not shade then
                shade = display.newRect( 0, 0, 570, 380 )
                shade:setFillColor( 0, 0, 0 )
                shade.x = 240; shade.y = 160
                gameGroup:insert( shade )
              end
              shade.alpha = 0.5
    
              -- SHOW MENU BUTTON
              if pauseBG then
                pauseBG.isVisible = true
                pauseBG.isActive = true
                pauseBG:toFront()
              end
    
              pauseBtn:toFront()
    
  3. 当游戏取消暂停时,让物理效果再次激活,并移除所有暂停显示对象:

              else
    
                if shade then
                  display.remove( shade )
                  shade = nil
                end
    
                if pauseBG then
                  pauseBG.isVisible = false
                  pauseBG.isActive = false
                end
    
                gameIsActive = true
                physics.start()
    
                local function resumeGame()
                timer.resume( startDrop )
                print("timer has been resumed")
              end
              timer.performWithDelay(1, resumeGame)
    
            end
          end
        end
    
  4. onPauseTouch()函数后添加pauseBtn UI 按钮和pauseBG显示对象:

        pauseBtn = ui.newButton{
          defaultSrc = "pausebtn.png",
          defaultX = 44,
          defaultY = 44,
          overSrc = "pausebtn-over.png",
          overX = 44,
          overY = 44,
          onEvent = onPauseTouch,
          id = "PauseButton",
          text = "",
          font = "Helvetica",
          textColor = { 255, 255, 255, 255 },
          size = 16,
          emboss = false
        }
    
        pauseBtn.x = 38; pauseBtn.y = 288
        pauseBtn.isVisible = false
        pauseBtn.isActive = false
    
        gameGroup:insert( pauseBtn )
    
        pauseBG = display.newImageRect( "pauseoverlay.png", 480, 320 )
        pauseBG.x = 240; pauseBG.y = 160
        pauseBG.isVisible = false
        pauseBG.isActive = false
    
        gameGroup:insert( pauseBG )
    
  5. 为了让pauseBtn在游戏过程中显示,需要在gameActivate()函数中使其可见并激活:

        pauseBtn.isVisible = true
        pauseBtn.isActive = true
    
  6. 游戏结束时,在callGameOver()函数中禁用pauseBtn,将代码放在physics.pause()行之后:

        pauseBtn.isVisible = false
        pauseBtn.isActive = false
    

    动作时间——暂停游戏

刚才发生了什么?

我们创建了onPauseTouch(event)函数,以控制游戏过程中发生的所有暂停事件。为了暂停游戏中的所有动作,我们将gameIsActive的布尔值改为false,并使用physics.pause()函数停止所有正在下落的鸡蛋。接下来,startDrop的计时器暂停,只要暂停功能仍然有效,从天空中下落的鸡蛋就不会随时间累积。

当按下暂停按钮时,会出现一个名为shade的略微透明的覆盖层。这将分散玩家对游戏场景的注意力,并让用户区分游戏是否处于非活动状态。

游戏暂停横幅也会在屏幕顶部显示,通过设置为可见和活动状态。pauseBG对象通过pauseBG:toFront()被推到显示层次结构的前面。

为了取消暂停游戏,我们反向执行了暂停显示项出现的过程。当pauseBtn第二次被按下时,通过display.remove(shade); shade = nil移除shadepauseBG.isVisiblepauseBG.isActive属性都被设置为false

记住我们之前将gameIsActive设置为false,现在是将它设回true的时候了。这也意味着通过physics.start()恢复物理效果。计时器通过resumeGame()本地函数恢复,并在函数中调用timer.resume(startDrop)

pauseBtnpauseBG显示对象被插入到if语句块的末尾。一旦游戏可以玩,pauseBtn对象就会显示为可见和活动状态。当游戏结束屏幕出现时,它是不可见和非活动的,这样当游戏结束时就不会有其他触摸事件干扰。

Composer API

Composer API 为开发者提供了一个简单的解决方案,用于控制具有或不具有过渡效果的场景。这是一个很棒的场景管理库,可以显示菜单系统,甚至管理游戏中的多个关卡。Composer 还附带多种过渡效果。更多信息可以在 Corona 文档中找到,地址是docs.coronalabs.com/api/library/composer/index.html

我们的场景管理与在docs.coronalabs.com/api/library/composer/index.html#scene-template展示的场景模板相似。

使用 Composer API 进行游戏开发

你可能会好奇我们如何将 Composer 应用于 Egg Drop。这真的很简单。我们只需修改游戏代码中的一些行,使其与 Composer 兼容,并为游戏开始前应用的菜单系统创建一些新场景。

动手时间——修改游戏文件

我们将当前的main.lua文件重命名为maingame.lua,并在游戏代码中添加一些额外的行。确保在Egg Drop项目文件夹中更改文件名。按照以下步骤重命名文件:

  1. 删除代码顶部附近的以下行。我们将在本章后面创建的另一个场景中隐藏状态栏。gameGroup显示组将被修改以适应 Composer 参数:

    display.setStatusBar( display.HiddenStatusBar )
    local gameGroup = display.newGroup()
    
  2. 在代码的最顶部,通过添加local composer = require( "composer" )local scene = composer.newScene()来实现 Composer,这样我们就可以调用场景事件:

    local composer = require( "composer" )
    local scene = composer.newScene()
    
  3. local loadValue = function( strFilename )之后,在create()事件中添加。我们还将重新添加我们的gameGroup显示组,但位于场景的 view 属性下。同时,加入composer.removeScene( "loadgame" )。本章后面将介绍"loadgame"场景:

    -- Called when the scene's view does not exist:
    function scene:create ( event )
      local gameGroup = self.view
    
      -- completely remove loadgame's view
      composer.removeScene( "loadgame" )
    
      print( "\nmaingame: create event")
    end
    
  4. create()事件之后,创建show()事件,并将其放在gameActivate()函数之前。show()事件将过渡我们所有的游戏玩法功能到屏幕上。同时,也将gameGroup包含在场景的 view 属性中:

    -- Called immediately after scene has moved onscreen:
    function scene:show( event )
      local gameGroup = self.view
    
  5. gameStart()函数之后,删除return gameGroup行:

    return gameGroup -- Code will not run if this line is not removed 
    
  6. 接下来,用end关闭function scene: show( event )

      print( "maingame: show event" )
    
    end
    
  7. 创建hide()destroy()事件:

    -- Called when scene is about to move offscreen:
    function scene:hide( event )
    
      print( "maingame: hide event" )
    
    end
    
    -- Called prior to the removal of scene's "view" (display group)
    function scene:destroy( event )
    
      print( "destroying maingame's view" )
    
    end 
    
  8. 最后,为所有场景事件创建事件监听器,并在代码末尾添加return scene

    -- "create" event is dispatched if scene's view does not exist
    scene:addEventListener( "create", scene )
    
    -- "show" event is dispatched whenever scene transition has finished
    scene:addEventListener( "show", scene )
    
    -- "hide" event is dispatched before next scene's transition begins
    scene:addEventListener( "hide", scene )
    
    -- "destroy" event is dispatched before view is unloaded, which can be
    scene:addEventListener( "destroy", scene )
    
    return scene 
    

刚才发生了什么?

使用 Composer API 将帮助我们更容易、更快速地过渡场景。每次你想将一个新场景加载到视图中时,需要添加require("composer")local scene = composer.newScene()声明将允许我们调用场景事件,create()show()hide(),和destroy()

在游戏代码的最后,我们为所有场景事件和return scene添加了事件监听器。

使用 Composer 管理每个场景的格式将与前面的代码类似。大部分游戏代码将在create()show()事件显示场景时派发。当你想要清理或卸载监听器、音频、资源等时,将使用hide()destroy()事件。

组织游戏

我们习惯于将main.lua作为我们的主源文件,以显示游戏代码的每个细节。现在是时候通过 Composer API 有效地组织它了。

行动时间——添加新的 main.lua 文件

使用 Composer 时,我们的main.lua文件仍然至关重要,因为它是 Corona SDK 启动模拟器中的应用程序时首先要查看的内容。我们将添加一些代码行,这些代码行将改变我们游戏的场景:

  1. 创建一个名为main.lua的新文件,并将其重新添加到我们的状态栏中:

    display.setStatusBar( display.HiddenStatusBar )
    
  2. 导入 Composer 并加载名为loadmainmenu的第一个场景。我们将在接下来的几节中创建这个场景:

    -- require controller module
    local composer = require ( "composer" )
    
    -- load first screen
    composer.gotoScene( "loadmainmenu" )
    

刚才发生了什么?

为了在应用程序中整合 Composer,我们调用了local composer = require ( "composer" )模块。场景将使用composer.gotoScene( "loadmainmenu" )进行更改,这是一个引导用户进入主菜单屏幕的加载屏幕。

新的游戏过渡

既然我们已经介绍了 Composer API,我们可以应用一些期待已久的过渡效果,这将对我们的游戏有所帮助。一种方法是游戏结束后退出游戏。

动手时间——游戏结束后切换屏幕

既然我们已经重命名了游戏文件,让我们添加一个场景过渡,这样游戏结束后就不会停留在游戏结束屏幕了。要更改屏幕,请执行以下步骤:

  1. 在我们的maingame.lua文件中,加入一个名为local menuBtn的新变量,其他所有变量都在代码开始时初始化。在callGameOver()函数内,在highScoreText代码之后添加以下几行:

        local onMenuTouch = function( event )
          if event.phase == "release" then
            audio.play( btnSound )
            composer.gotoScene( "mainmenu", "fade", 500  )
    
          end
        end
    
        menuBtn = ui.newButton{
          defaultSrc = "menubtn.png",
          defaultX = 60,
          defaultY = 60,
          overSrc = "menubtn-over.png",
          overX = 60,
          overY = 60,
          onEvent = onMenuTouch,
          id = "MenuButton",
          text = "",
          -- Can use any font available per platform
          font = "Helvetica",   
          textColor = { 255, 255, 255, 255 },
          size = 16,
          emboss = false
        }
    
        menuBtn.x = 100; menuBtn.y = 260
    
        gameGroup:insert( menuBtn )
    

    动手时间——游戏结束后切换屏幕

刚才发生了什么?

为了从游戏结束屏幕过渡出去,我们创建了一个菜单按钮来更改场景。在onMenuTouch()函数中,在按钮释放时,我们调用了composer.gotoScene( "mainmenu", "fade", 500 )。这将允许应用程序在 500 毫秒内使用淡入淡出效果过渡到主菜单,我们将在本章后面创建这个效果。

动手英雄——重新开始游戏

既然你已经充分了解 Composer API 如何与更改场景以及使用 UI 按钮在它们之间过渡,那么何不创建一个按钮,在游戏结束屏幕出现后重新开始游戏呢?到目前为止,该应用程序允许用户在游戏结束时返回菜单屏幕。

callGameOver()函数内,需要创建一个新的本地函数,该函数将使用 UI 按钮系统运行事件,通过 Composer 更改场景。注意,如果你当前已经在该场景中,则不能再次调用同一场景。

创建一个加载屏幕

加载屏幕提供了程序正在加载过程中的反馈。这有助于告知用户下一个屏幕正在加载,这样他们就不会认为应用程序已经崩溃了,尤其是如果下一个屏幕正在加载大量数据时。

动手时间——添加加载屏幕

我们将在应用程序启动和游戏关卡开始之前放置加载屏幕。这告诉用户更多内容或信息即将到来。

  1. 在你的项目文件夹中创建一个名为loadmainmenu.lua的新文件。

  2. 导入 Composer 并在其中加入composer.newScene()函数:

    local composer = require( "composer" )
    local scene = composer.newScene()
    
  3. 创建两个名为myTimerloadingImage的本地变量。加入create()事件和一个sceneGroup显示组:

    local myTimer
    local loadingImage
    
    -- Called when the scene's view does not exist:
    function scene:create( event )
      local sceneGroup = self.view
    
      print( "\nloadmainmenu: create event" )
    end
    
  4. 创建show()事件并加入一个sceneGroup显示组:

      -- Called immediately after scene has moved onscreen:
    function scene:show( event )
      local sceneGroup = self.view
    
      print( "loadmainmenu: show event" )
    
  5. 引入loadingImage显示对象:

      loadingImage = display.newImageRect( "loading.png", 480, 320)
      loadingImage.x = 240; loadingImage.y = 160
      sceneGroup:insert( loadingImage )
    
  6. 创建另一个名为goToMenu()的本地函数,并调用composer.gotoScene( "mainmenu", "zoomOutInFadeRotate", 500 )以将场景更改为"mainmenu"

        local goToMenu = function()
          composer.gotoScene( "mainmenu", "zoomOutInFadeRotate", 500)
        end
    
  7. 使用timer函数,每 1,000 毫秒调用一次goToMenu()。使用myTimer计时器 ID 定义它。使用end结束show()事件:

        myTimer = timer.performWithDelay( 1000, goToMenu, 1 )
      end
    
  8. 调用hide()destroy()事件。在hide()事件中,取消myTimer

    -- Called when scene is about to move offscreen:
    function scene:hide()
    
      if myTimer then timer.cancel( myTimer ); end
    
      print( "loadmainmenu: hide event" )
    
    end
    
    -- Called prior to the removal of scene's "view" (display group)
    function scene:destroy( event )
    
      print( "destroying loadmainmenu's view" )
    end
    
  9. 为所有场景事件和return scene添加事件监听器。保存并关闭文件:

    -- "create" event is dispatched if scene's view does not exist
    scene:addEventListener( "create", scene )
    
    -- "show" event is dispatched whenever scene transition has finished
    scene:addEventListener( "show", scene )
    
    -- "hide" event is dispatched before next scene's transition begins
    scene:addEventListener( "hide", scene )
    
    -- "destroy" event is dispatched before view is unloaded, which can be
    scene:addEventListener( "destroy", scene )
    
    return scene
    
  10. 在你的项目文件夹中创建一个名为loadgame.lua的新文件。我们将制作一个在游戏场景maingame.lua之前出现的加载屏幕。使用composer.gotoScene( "maingame", "flipFadeOutIn", 500 )进行场景过渡。保存并关闭你的文件:

    local composer = require( "composer" )
    local scene = composer.newScene()
    
    local myTimer
    local loadingImage
    
    -- Called when the scene's view does not exist:
    function scene:create( event )
      local sceneGroup = self.view
    
      -- completely remove mainmenu
      composer.removeScene( "mainmenu" )
    
      print( "\nloadgame: create event" )
    end
    
    -- Called immediately after scene has moved onscreen:
    function scene:show( event )
      local sceneGroup = self.view
    
      print( "loadgame: show event" )
    
      loadingImage = display.newImageRect( "loading.png", 480, 320)
      loadingImage.x = 240; loadingImage.y = 160
      sceneGroup:insert( loadingImage )
    
      local changeScene = function()
        composer.gotoScene( "maingame", "flipFadeOutIn", 500 )
      end
      myTimer = timer.performWithDelay( 1000, changeScene, 1 )
    
    end
    
    -- Called when scene is about to move offscreen:
    function scene:hide()
    
      if myTimer then timer.cancel( myTimer ); end
    
      print( "loadgame: hide event" )
    
    end
    
    -- Called prior to the removal of scene's "view" (display group)
    function scene:destroy( event )
    
      print( "destroying loadgame's view" )
    end
    
    -- "create" event is dispatched if scene's view does not exist
    scene:addEventListener( "create", scene )
    
    -- "show" event is dispatched whenever scene transition has finished
    scene:addEventListener( "show", scene )
    
    -- "hide" event is dispatched before next scene's transition begins
    scene:addEventListener( "hide", scene )
    
    -- "destroy" event is dispatched before view is unloaded, which can be
    scene:addEventListener( "destroy", scene )
    
    return scene
    

    行动时间 - 添加加载屏幕

刚才发生了什么?

loadmainmenu.lua文件中,一旦loadingImage被添加到屏幕上,我们就创建了goToMenu()函数,以将场景更改为"mainmenu",并使用"zoomOutInFadeRotate"过渡,让加载屏幕图像在淡出至背景时缩小并旋转。myTimer = timer.performWithDelay( 1000, goToMenu, 1 )语句在 1,000 毫秒(一秒)后执行该函数,并且只运行一次。这足够时间查看图像并让它淡出。

所有显示对象通过function scene:show( event )进入场景。loadingImage对象被放置在sceneGroup中。为了确保场景更改后没有定时器在运行,myTimerfunction scene:hide()下使用timer.cancel(myTimer)停止运行。

loadgame.lua的代码与loadmainmenu.lua类似。对于这个文件,Composer 将场景过渡到maingame.lua,即游戏玩法文件。

创建主菜单

主菜单或标题屏幕是玩家在玩游戏之前看到的第一印象之一。它通常显示与实际游戏相关的小图像或风景片段,并显示应用程序的标题。

有一些如开始播放的按钮,鼓励玩家如果他们选择的话进入游戏,还有一些次要的按钮如选项查看设置和其他信息。

行动时间 - 添加主菜单

我们将通过引入游戏标题和播放选项按钮来创建游戏的前端,这些按钮将在应用程序的不同场景中轻松过渡。

  1. 创建一个名为mainmenu.lua的新文件,并导入 Composer 和 UI 模块,composer.newScene()函数,以及定时器和音频的变量:

    local composer = require( "composer" )
    local scene = Composer.newScene()
    
    local ui = require("ui")
    
    local btnAnim
    
    local btnSound = audio.loadSound( "btnSound.wav" )
    
  2. 创建create()事件。添加composer.removeScene( "maingame" )composer.removeScene( "options" )行,这将移除"maingame""options"场景。可以在玩家从主游戏屏幕过渡并返回主菜单屏幕后移除"maingame"。可以在玩家从选项屏幕过渡并返回主菜单屏幕后移除"options"

    -- Called when the scene's view does not exist:
    function scene:create( event )
      local sceneGroup = self.view
    
      -- completely remove maingame and options
      composer.removeScene( "maingame" )
      composer.removeScene( "options" )
    
      print( "\nmainmenu: create event" )
    end
    
  3. show()事件中添加backgroundImage显示对象;

    -- Called immediately after scene has moved onscreen:
    function scene:show( event )
      local sceneGroup = self.view
    
      print( "mainmenu: show event" )
    
      local backgroundImage = display.newImageRect( "mainMenuBG.png", 480, 320 )
      backgroundImage.x = 240; backgroundImage.y = 160
      sceneGroup:insert( backgroundImage )
    
  4. 引入playBtn显示对象,并创建一个名为onPlayTouch(event)的函数,该函数使用composer.gotoScene()将场景更改为"loadgame"。使用"fade"效果进行场景变换:

      local playBtn
    
      local onPlayTouch = function( event )
        if event.phase == "release" then
    
          audio.play( btnSound )
          composer.gotoScene( "loadgame", "fade", 300  )
    
        end
      end
    
      playBtn = ui.newButton{
        defaultSrc = "playbtn.png",
        defaultX = 100,
        defaultY = 100,
        overSrc = "playbtn-over.png",
        overX = 100,
        overY = 100,
        onEvent = onPlayTouch,
        id = "PlayButton",
        text = "",
        font = "Helvetica",
        textColor = { 255, 255, 255, 255 },
        size = 16,
        emboss = false
      }
    
      playBtn.x = 240; playBtn.y = 440
        sceneGroup:insert( playBtn )
    
  5. 使用easing.inOutExpo过渡,在 500 毫秒内将playBtn显示对象转换到 y=260 的位置。通过btnAnim进行初始化:

    btnAnim = transition.to( playBtn, { time=1000, y=260, transition=easing.inOutExpo } )
    
  6. 引入optBtn显示对象,并创建一个名为onOptionsTouch(event)的函数。使用composer.gotoScene()"crossFade"效果将场景过渡到"options"

    local optBtn
    
      local onOptionsTouch = function( event )
        if event.phase == "release" then
    
          audio.play( btnSound )
          composer.gotoScene( "options", "crossFade", 300)
    
        end
      end
    
      optBtn = ui.newButton{
        defaultSrc = "optbtn.png",
        defaultX = 60,
        defaultY = 60,
        overSrc = "optbtn-over.png",
        overX = 60,
        overY = 60,
        onEvent = onOptionsTouch,
        id = "OptionsButton",
        text = "",
        font = "Helvetica",
        textColor = { 255, 255, 255, 255 },
        size = 16,
        emboss = false
      }
      optBtn.x = 430; optBtn.y = 440
      sceneGroup:insert( optBtn )
    
  7. 使用easing.inOutExpo过渡,在 500 毫秒内将optBtn显示对象转换到y = 280的位置。通过btnAnim进行初始化。使用end结束scene:show( event )函数:

      btnAnim = transition.to( optBtn, { time=1000, y=280, transition=easing.inOutExpo } )
    
    end
    
  8. 创建hide()事件并取消btnAnim过渡。同时,创建destroy()事件:

    -- Called when scene is about to move offscreen:
    function scene:hide()
    
      if btnAnim then transition.cancel( btnAnim ); end
    
      print( "mainmenu: hide event" )
    
    end
    
    -- Called prior to the removal of scene's "view" (display group)
    function scene:destroy( event )
    
      print( "destroying mainmenu's view" )
    end
    
  9. 为所有场景事件和return scene添加事件监听器。保存并关闭你的文件:

    -- "create" event is dispatched if scene's view does not exist
    scene:addEventListener( "create", scene )
    
    -- "show" event is dispatched whenever scene transition has finished
    scene:addEventListener( "show", scene )
    
    -- "hide" event is dispatched before next scene's transition begins
    scene:addEventListener( "hide", scene )
    
    -- "destroy" event is dispatched before view is unloaded, which can be
    scene:addEventListener( "destroy", scene )
    
    return scene
    

    行动时间 - 添加主菜单

刚才发生了什么?

在主菜单屏幕上,我们添加了一个显示游戏标题和播放选项按钮的图像。此时的选项按钮还不起作用。onPlayTouch()函数将场景过渡到"loadgame"。这将改变到loadgame.lua场景。播放按钮位于x = 240; y = 440(居中和屏幕外)。当场景加载时,playBtn过渡到y = 260,因此它会从屏幕底部向上弹出,耗时 1000 毫秒。

选项按钮执行类似操作。optBtn对象放置在舞台右侧,并在 500 毫秒内弹出至y = 280

btnAnim过渡通过scene:hide()函数中的transition.cancel( btnAnim )被取消。每次更改场景时清理定时器、过渡和事件监听器,以防止应用程序中可能发生的内存泄漏,这是非常重要的。

创建一个选项菜单

选项菜单允许用户在游戏中更改各种设置或包含无法在主菜单中显示的其他信息。游戏可以拥有许多选项,也可能只有几个。有时,选项菜单也可以称为设置菜单,为玩家的体验提供相同类型的自定义。

行动时间 - 添加一个选项菜单

我们将通过主菜单添加一个可以访问的选项菜单。我们将添加一个新的 UI 按钮,名为积分,一旦按下,它将引导用户进入积分屏幕。要添加选项菜单,请执行以下步骤:

  1. 创建一个名为options.lua的新文件,并导入 Composer 和 UI 模块,composer.newScene()函数,以及定时器和音频的变量:

    local composer = require( "composer" )
    local scene = composer.newScene()
    
    local ui = require("ui")
    
    local btnAnim
    
    local btnSound = audio.loadSound( "btnSound.wav" )
    
  2. 创建create()事件。加入composer.removeScene( "mainmenu" ),这将移除"mainmenu"场景。这会在玩家从主菜单屏幕过渡到选项屏幕后发生。接下来,加入composer.removeScene( "creditsScreen" )。这将会在玩家从积分屏幕返回到选项屏幕后移除"creditsScreen"

    -- Called when the scene's view does not exist:
    function scene:create( event )
      local sceneGroup = self.view
    
      -- completely remove mainmenu and creditsScreen
      composer.removeScene( "mainmenu" )
      composer.removeScene( "creditsScreen" )
    
      print( "\noptions: create event" )
    end
    
  3. 添加show()事件和backgroundImage显示对象:

    -- Called immediately after scene has moved onscreen:
    function scene:show( event )
      local sceneGroup = self.view
    
      print( "options: show event" )
    
      local backgroundImage = display.newImageRect( "optionsBG.png", 480, 320 )
      backgroundImage.x = 240; backgroundImage.y = 160
      sceneGroup:insert( backgroundImage )
    
  4. 为信用屏幕创建一个按钮。在 1000 毫秒内使用easing.inOutExpo过渡将creditsBtn显示对象过渡到y = 260。通过btnAnim初始化它:

      local creditsBtn
    
      local onCreditsTouch = function( event )
        if event.phase == "release" then
    
          audio.play( btnSound )
          Composer.gotoScene( "creditsScreen", "crossFade", 300 )
    
        end
      end
    
      creditsBtn = ui.newButton{
        defaultSrc = "creditsbtn.png",
        defaultX = 100,
        defaultY = 100,
        overSrc = "creditsbtn-over.png",
        overX = 100,
        overY = 100,
        onEvent = onCreditsTouch,
        id = "CreditsButton",
        text = "",
        font = "Helvetica",
        textColor = { 255, 255, 255, 255 },
        size = 16,
        emboss = false
      }
    
      creditsBtn.x = 240; creditsBtn.y = 440
      sceneGroup:insert( creditsBtn )
    
      btnAnim = transition.to( creditsBtn, { time=1000, y=260, transition=easing.inOutExpo } )
    
  5. 创建一个加载主菜单的关闭按钮。通过end结束scene:show( event )

      local closeBtn
    
      local onCloseTouch = function( event )
        if event.phase == "release" then
          audio.play( tapSound )
          composer.gotoScene( "mainmenu", "zoomInOutFadeRotate", 500 ) 
        end
      end
    
      closeBtn = ui.newButton{
        defaultSrc = "closebtn.png",
        defaultX = 60,
        defaultY = 60,
        overSrc = "closebtn-over.png",
        overX = 60,
        overY = 60,
        onEvent = onCloseTouch,
        id = "CloseButton",
        text = "",
        font = "Helvetica",
        textColor = { 255, 255, 255, 255 },
        size = 16,
        emboss = false
      }
    
      closeBtn.x = 50; closeBtn.y = 280
      sceneGroup:insert( closeBtn ) 
    end
    
  6. 创建hide()事件并取消btnAnim过渡。同时,创建destroy()事件。为所有场景事件和return scene语句添加事件监听器。保存并关闭你的文件:

    -- Called when scene is about to move offscreen:
    function scene:hide()
    
      if btnAnim then transition.cancel( btnAnim ); end
    
      print( "options: hide event" )
    
    end
    
    -- Called prior to the removal of scene's "view" (display group)
    function scene:destroy( event )
    
      print( "destroying options's view" )
    end
    
    -- "create" event is dispatched if scene's view does not exist
    scene:addEventListener( "create", scene )
    
    -- "show" event is dispatched whenever scene transition has finished
    scene:addEventListener( "show", scene )
    
    -- "hide" event is dispatched before next scene's transition begins
    scene:addEventListener( "hide", scene )
    
    -- "destroy" event is dispatched before view is unloaded, which can be
    scene:addEventListener( "destroy", scene )	
    
    return scene
    

    行动时间 – 添加选项菜单

刚才发生了什么?

在这个场景中,creditsBtn的操作方式与创建主菜单类似。此时的信用按钮尚不可用。在onCreditsTouch()函数中,场景过渡到"creditsScreen"并使用"crossFade"作为效果。当场景加载时,creditsBtn从屏幕外位置过渡到 y=260,耗时 1,000 毫秒。

为这个场景创建了一个关闭按钮,以便用户有一个返回上一个屏幕的方法。通过onCloseTouch()函数,当释放closeBtn时,Composer 将场景更改为"mainmenu"。按下关闭按钮时,将显示主菜单屏幕。scene:hide()函数取消了btnAnim过渡。

创建信用屏幕

信用屏幕通常会显示并列出参与游戏制作的所有人员。它还可以包括感谢某些个人和程序的信息,这些程序用于创建最终项目。

行动时间 – 添加信用屏幕

我们将要创建的信用屏幕将基于一个触摸事件,该事件从引入它的上一个屏幕过渡回来。要添加信用屏幕,请执行以下步骤:

  1. 创建一个名为creditsScreen.lua的新文件,并导入 Composer、composer.newScene()函数和backgroundImage变量:

    local composer = require( "composer" )
    local scene = composer.newScene()
    
    local backgroundImage
    
  2. 创建create()事件。添加composer.removeScene("options")行,这将移除"options"场景。这将在玩家从选项屏幕过渡到信用屏幕后发生:

    -- Called when the scene's view does not exist:
    function scene:create( event )
      local sceneGroup = self.view
    
      -- completely remove options
      composer.removeScene( "options" )
    
      print( "\ncreditsScreen: create event" )
    end
    
  3. 添加show()事件和backgroundImage显示对象:

    -- Called immediately after scene has moved onscreen:
    function scene:show( event )
      local sceneGroup = self.view
    
      print( "creditsScreen: show event" )
    
      backgroundImage = display.newImageRect( "creditsScreen.png", 480, 320 )
      backgroundImage.x = 240; backgroundImage.y = 160
      sceneGroup:insert( backgroundImage )
    
  4. 创建一个名为changeToOptions()的本地函数,带有一个事件参数。让该函数通过在backgroundImage上的触摸事件,使用 Composer 将场景改回选项屏幕。通过end结束scene:show(event)函数:

      local changeToOptions = function( event )
        if event.phase == "began" then
    
          composer.gotoScene( "options", "crossFade", 300  )
    
        end
      end
    
      backgroundImage:addEventListener( "touch", changeToOptions)
    end
    
  5. 创建hide()destroy()事件。为所有场景事件和return scene语句添加事件监听器。保存并关闭你的文件:

    -- Called when scene is about to move offscreen:
    function scene:hide()
    
      print( "creditsScreen: hide event" )
    
    end
    
    -- Called prior to the removal of scene's "view" (display group)
    function scene:destroy( event )
    
      print( "destroying creditsScreen's view" )
    end
    
    -- "create" event is dispatched if scene's view does not exist
    scene:addEventListener( "create", scene )
    
    -- "show" event is dispatched whenever scene transition has finished
    scene:addEventListener( "show", scene )
    
    -- "hide" event is dispatched before next scene's transition begins
    scene:addEventListener( "hide", scene )
    
    -- "destroy" event is dispatched before view is unloaded, which can be
    scene:addEventListener( "destroy", scene )
    
    return scene
    

    行动时间 – 添加信用屏幕

刚才发生了什么?

信用屏幕与事件监听器一起工作。changeToOptions(event)函数将告诉 Composer 使用composer.gotoScene( "options", "crossFade", 500 )更改场景为 "options"。在函数的末尾,backgroundImage将在屏幕被触摸时激活事件监听器。backgroundImage对象在scene:show( event )函数下的sceneGroup中插入。现在,Egg Drop 完全可以通过 Composer 操作。在模拟器中运行游戏。你将能够过渡到我们在本章中创建的所有场景,还可以玩游戏。

尝试英雄——添加更多关卡

现在,Egg Drop 已经完成,并且拥有一个工作的菜单系统,通过创建更多关卡来挑战自己。为了添加额外的关卡位置,将需要增加一些小的修改。在更改场景时请记得应用 Composer。

尝试创建以下内容:

  • 关卡选择屏幕

  • 添加额外关卡的关卡编号按钮

在创建新关卡时,请参考maingame.lua中显示的格式。新关卡可以通过改变蛋从天而降的速度间隔来改变,或者也许可以通过添加其他游戏资源来躲避以免受到惩罚。有如此多的可能性可以在这个游戏框架中添加你自己的创意。试一试吧!

小测验——游戏过渡和场景

Q1. 你调用哪个函数使用 Composer 更改场景?

  1. composer()

  2. composer.gotoScene()

  3. composer(changeScene)

  4. 以上都不是

Q2. 有哪个函数可以将任何参数转换成数字或 nil?

  1. tonumber()

  2. print()

  3. tostring()

  4. nil

Q3. 你如何暂停一个计时器?

  1. timer.cancel()

  2. physics.pause()

  3. timer.pause( timerID )

  4. 以上都不是

Q4. 你如何恢复一个计时器?

  1. resume()

  2. timer.resume( timerID )

  3. timer.performWithDelay()

  4. 以上都不是

总结

恭喜你!我们已经完成了一个完整的游戏,可以进入 App Store 或 Google Play 商店。当然,我们不会使用这个确切的游戏,但你已经学到了足够多的知识去创造一个。在如此短的时间内完成游戏框架是一个了不起的成就,尤其是创造出如此简单的东西。

在本章中你学会了以下技能:

  • 使用 saveValue()和 loadValue()保存高分

  • 理解如何暂停物理/计时器

  • 显示暂停菜单

  • 使用 Composer API 更改场景

  • 使用加载屏幕在场景间创建过渡

  • 使用主菜单介绍游戏标题和子菜单

在本章中,我们已经取得了重要的里程碑。我们在之前章节中讨论的所有内容都被应用到了这个示例游戏中。关于它最好的事情是,我们花了不到一天的开发时间来编写代码。而艺术资源则是另一回事了。

我们还需要学习更多关于 Corona SDK 的功能。在下一章中,我们将详细探讨如何为高分辨率设备优化游戏资源。我们还将了解如何通过应用程序在 Facebook 和 Twitter 上发布消息。

第九章:处理多设备和网络应用

允许您的应用程序与社交网络集成是推广您成品的好方法。许多游戏允许玩家上传他们的高分并与其他玩相同游戏的人分享。有些提供需要成功完成才能解锁成就的挑战。社交网络增强了游戏体验并为开发者提供了很好的曝光机会。

由于我们越来越习惯编程,我们还将更详细地介绍构建配置。理解配置设备构建的重要性对跨平台开发至关重要。这是 Corona SDK 可以轻松地在 iOS 和 Android 设备上处理的能力。

在本章中,我们将学习以下主题:

  • 重新访问配置设置

  • 发布消息到 Twitter

  • 发布消息到 Facebook

让我们添加这些最后的润色!

返回配置

在第二章中简要讨论了构建设置和运行时配置,Lua 速成课程和 Corona 框架。让我们深入了解如何处理在 iOS 和 Android 平台上工作的各种设备的具体细节。

构建配置

有多种方法可以处理设备方向,以匹配您的游戏设计所需的设置。

方向支持(iOS)

有时您希望原生用户界面(UI)元素自动旋转或以特定方式定向,但同时也需要在 Corona 中保持固定的坐标系统。

要锁定 Corona 的方向同时允许原生 iPhone UI 元素旋转,可以在 build.settings 中添加以下内容参数:

settings =
{
  orientation =
  {
 default = "portrait",
 content = "portrait",
    supported =
    {
      "landscapeLeft", "landscapeRight", "portrait", "portraitUpsideDown",
    },
  },
}

要将 Corona 的内部坐标系统锁定为纵向同时将 iPhone UI 元素锁定为横向,您可以在 build.settings 中执行以下操作:

settings =
{
  orientation =
  {
 default ="landscapeRight",
 content = "portrait",
    supported =
    {
      "landscapeRight", "landscapeLeft",
    },
  },
}

方向支持(安卓)

安卓平台支持纵向和横向方向。方向 portraitUpsideDown 在某些安卓设备上可能不可用。此外,目前安卓设备不支持自动旋转。默认方向不会影响安卓设备。方向初始化为设备的实际方向(除非只指定了一个方向)。

下面是一个针对安卓的 build.settings 文件的示例(您也可以在同一个文件中组合安卓和 iPhone 设置):

settings =
{
  android =
  {
    versionCode = "2",
    versionName = "2.0"

    usesPermissions =
    {
      "android.permission.INTERNET",
    },
  },

  orientation =
  {
    default = "portrait"
  },
}

版本代码和版本名称(安卓)

versionCodeversionName 字段可以在 build.settings 中的可选 "android" 表中设置。

如果在build.settings文件中没有设置,versionCode字段默认为"1",而versionName字段默认为"1.0"。当将应用程序的更新版本提交到 Google Play 商店时,也必须更新versionCodeversionName字段。versionCode的所有版本号都必须是整数。versionCode字段不能包含任何小数,而versionName字段可以包含小数。

想要了解更多信息,请查看developer.android.com/guide/topics/manifest/manifest-element.html#vcode中的android:versionCodeandroid:versionName

注意

versionCode属性是一个内部数字,用于在 Google Play 商店中区分应用程序版本。它与 Corona 构建对话框提供的版本不同。versionName属性是向用户显示的版本号。

应用权限(Android)

可以使用可选的"usesPermissions"表来指定权限,使用的是在 Android 清单参考中给出的字符串值:developer.android.com/reference/android/Manifest.permission.html

开发者应该使用符合他们应用程序需求的权限。例如,如果需要网络访问,就需要设置互联网权限。

注意

想要了解更多关于 Corona SDK 中应用的android.permission键的信息,请参考docs.coronalabs.com/guide/distribution/buildSettings/index.html#permissions

更简单的层次内容缩放

如果你在config.lua文件中从未处理过,那么在多个设备上调整内容大小有时可能会让人感到沮丧。有许多不同的屏幕尺寸。例如,iPhone 5 的尺寸为 640 x 1136 像素,iPad 2 的尺寸为 768 x 1024 像素,Droid 的尺寸为 480 x 854 像素,三星 Galaxy 平板的尺寸为 600 x 1024 像素等。由于图像大小限制,内存可能会很容易耗尽。

在设置你的config.lua时,就像我们在前面的章节中所做的那样,我们将内容设置为width = 320height = 480,以及scale = "letterbox"。如果为 Android 设备构建,"zoomStretch"最适合于适应该平台上不同的屏幕尺寸。这为 iOS 和 Android 创建了一个共同的构建,并展示了足够大的显示图像以适应各种屏幕尺寸。

如果你想要先为更大的屏幕尺寸进行缩放,然后再缩小,请使用 iPad 2 的屏幕尺寸。你的config.lua文件将类似于以下代码:

application =
{
  content =
  {
    width = 768,
    height = 1024,
    scale = "letterbox"
  }
}

虽然前面的例子是缩放内容的另一种解决方案,但重要的是要记住,较大(高分辨率)图像涉及的纹理内存限制。像 iPad 带 Retina 显示屏、iPhone 5s 和三星 Galaxy Tab 4 平板电脑这样的设备可以很好地处理这个问题,但 iPhone 4s 和更旧的设备可用的纹理内存要少得多,无法处理大图形。

解决这个潜在问题的方法之一是使用动态图像解析,以替换更适合低端设备和高端设备的资源。我们将在本节的后面更详细地讨论这个话题。

两全其美的方案

你可能已经注意到,我们在示例应用中使用的某些背景图像被缩放到了 380 x 570。这个尺寸恰好能填满 iOS 和 Android 所有常见设备的整个屏幕。更重要的是,它是任何设备上高低分辨率图像的折中方案。

为了让你的内容尽可能均匀地显示,以下设置必须相应地进行:

config.lua的设置如下:

application =
{
  content =
  {
    width = 320,
    height = 480,
    scale = "letterbox"
  }
}

在包含显示图像的任何文件中,典型的背景会如下所示:

local backgroundImage = display.newImage( "bg.png", true )
backgroundImage.x = display.contentCenterX
backgroundImage.y = display.contentCenterY

任何尺寸为 320 x 480 的内容都被认为是焦点区域。区域之外的内容将被裁剪,但在任何设备上都会用内容填满屏幕。

动态图像选择的深层含义

我们知道我们可以交换用于较小设备(iPhone 4s)和较大设备(iPhone 6 和 Kindle Fire HD)的基本图像。在尝试在同一个构建中缩放多个设备时,会发生这种情况。

针对 iOS 和 Android 设备,有一个文件命名方案可供使用。了解如何处理受提议设备影响的资源的缩放,是成功的一半。我们将需要定义 Corona 需要解决哪个分辨率比例,以便访问它们所指向的资源。

使用display.newImageRect( [parentGroup,] filename [, baseDirectory] w, h )这行代码将调用你的动态分辨率图像。

通常,我们在项目中为 iOS 设备调用更高分辨率图像时使用["@2x"] = 2

application =
{
  content =
  {
    width = 320,
    height = 480,
    scale = "letterbox",

    imageSuffix =
    {
      ["@2x"] = 2,
    },
  },
}

前面的例子只适用于 iPhone 4s 和 iPad 2,因为它超出了这两台设备的基本尺寸 320 x 480。如果我们想要让 Droid 2 也能访问,那么比例阈值将是 1.5。对于像三星 Galaxy 平板电脑这样的 Android 平板来说,比例阈值是 1.875。那么我们如何得出这些数字呢?简单。取高端设备的宽度,除以 320(基本尺寸)。例如,Droid 2 的尺寸是 480 x 854。将 480 除以 320,等于 1.5。

三星 Galaxy Tab 4 平板电脑的尺寸是 800 x 1280。将 800 除以 320,等于 2.5。

如果尝试在同一个项目中管理 iOS 和 Android 设备,你可以在config.lua中更改你的imageSuffix,如下代码所示:

    imageSuffix =
    {
 ["@2x"] = 1.5, -- this will handle most Android devices such as the Droid 2, Nexus, Galaxy Tablet, etc...
    }

或者,你可以使用以下代码:

    imageSuffix =
    {
 ["@2x"] = 2.5, -- this will handle the Galaxy Tab 4 and similar sized devices
    }

使用前面任一示例将触发提议的安卓设备显示更高分辨率的图像。

imageSuffix 字符串不一定非要是 "@2x";它可以是像 "@2""_lrg",甚至是 "-2x" 这样的任何东西。只要你的更高分辨率图像在主图像名称后具有预期的后缀,它就能正常工作。

高分辨率精灵表

高分辨率精灵表的处理方式与动态图像选择不同。虽然你可以继续使用相同的命名约定来区分你的高分辨率图像和基本图像,但图像将无法在引用精灵表时使用 display.newImageRect()

如果你的 config.lua 文件中当前的内容缩放设置为 width = 320height = 480,以及 scale = "letterbox",那么以下设备的缩放输出将展示如下:

  • iPhone = 1

  • iPhone 4s = 0.5

  • Droid 2 = 0.666666668653488

  • iPad 2 = 0.46875

应用与 iPhone 尺寸相匹配的基本精灵表将显示清晰锐利的图像。当相同的精灵表应用于 iPhone 4 时,显示将匹配设备的内容缩放,但精灵表在边缘处看起来会有些像素化和模糊。使用 display.contentScaleX 并调用一些方法将为你解决这个问题。注意 displayScale < 1 将根据前述设备比例访问高分辨率精灵表:

    local sheetData 
    local myObject

 local displayScale = display.contentScaleX –- scales sprite sheets down
 if displayScale < 1 then –- pertains to all high-res devices

      sheetData = { width=256, height=256, numFrames=4, sheetContentWidth=512, sheetContentHeight=512 }
    else
      sheetData = { width=128, height=128, numFrames=4, sheetContentWidth=256, sheetContentHeight=256 }
    end

    local sheet = graphics.newImageSheet( "charSprite.png", sheetData)

    local sequenceData = 
    {
      { name="move", start=1, count=4, time=400 } 
    }

    myObject = = display.newSprite( sheet, sequenceData )

 if displayScale < 1 then --scale the high-res sprite sheet if you're on a high-res device.
      myObject.xScale = .5; myObject.yScale = .5
    end

    myObject.x = display.contentWidth / 2
    myObject.y = display.contentHeight / 2

    myObject.x = 150; myObject.y = 195

    myObject: setSequence("move")
    myObject:play()

应用网络化

当你完成主要游戏框架的开发后,如果决定这样做,考虑如何将其网络化是很有好处的。

在我们生活的某个时刻,我们所有人都使用过某种网络工具,比如 Twitter 或 Facebook。你可能现在正在使用这些应用程序,但重点是,你可以从其他用户那里阅读关于新游戏发布的更新,或者有人传播下载游戏并与他们竞争的消息。你可以成为他们谈论的那个游戏的开发者!

在你的游戏中融入网络机制不必是一件麻烦事。只需几行代码就能让它工作。

发布到 Twitter

推推推……Twitter 是一个网络工具,能让你接触到吸引你兴趣的最新信息。它还是一个分享你业务信息,当然还有你的游戏的好工具。通过推广你的应用,接触游戏开发受众。

那些想要将帖子分享到 Twitter 的用户需要先在twitter.com/创建一个账户,并确保他们已经登录。

行动时间——将 Twitter 加入你的应用

我们将通过 UI 按钮访问网络服务,在我们的应用中实现 Twitter 功能。

  1. Chapter 9文件夹中,将Twitter Web Pop-Up项目文件夹复制到你的桌面。所有需要的配置、库和资源都已包含。你可以从 Packt Publishing 网站下载伴随这本书的项目文件。

  2. 创建一个新的main.lua文件并将其保存到项目文件夹中。

  3. 在代码开始时设置以下变量:

    display.setStatusBar( display.HiddenStatusBar )
    
    local ui = require("ui")
    
    local openBtn
    local closeBtn
    local score = 100
    
  4. 创建一个名为onOpenTouch()的本地函数,带有事件参数。添加一个if语句,以便事件接收一个"release"动作:

    local onOpenTouch = function( event )
      if event.phase == "release" then
    
  5. 使用名为message的局部变量,添加以下字符串语句并拼接score

    local message = "Posting to Twitter from Corona SDK and got a final score of " ..score.. "."
    
  6. 添加local myString并应用string.gsub()message进行处理,替换空格实例:

    local myString = string.gsub(message, "( )", "%%20")
    
  7. 引入链接到 Twitter 账户的native.showWebPopup()函数。将myString拼接进来以包含预加载的消息。关闭函数:

        native.showWebPopup(0, 0, 320, 300, "http://twitter.com/intent/tweet?text="..myString)
    
      end
    end
    
  8. 设置openBtn UI 函数:

      openBtn = ui.newButton{
      defaultSrc = "openbtn.png",
      defaultX = 90,
      defaultY = 90,
      overSrc = "openbtn-over.png",
      overX = 90,
      overY = 90,
      onEvent = onOpenTouch,
    }
    
    openBtn.x = 110; openBtn.y = 350
    
  9. 创建一个名为onCloseTouch()的本地函数,带有event参数。添加一个if语句,其中event.phase == "release"以激活native.cancelWebPopup()

    local onCloseTouch = function( event )
      if event.phase == "release" then    
    
        native.cancelWebPopup()    
    
      end
    end
    
  10. 设置closeBtn UI 函数:

      closeBtn = ui.newButton{
      defaultSrc = "closebtn.png",
      defaultX = 90,
      defaultY = 90,
      overSrc = "closebtn-over.png",
      overX = 90,
      overY = 90,
      onEvent = onCloseTouch,
    }
    
    closeBtn.x = 210; closeBtn.y = 350
    
  11. 保存文件并在模拟器中运行项目。确保你连接到互联网以查看结果。

    注意

    如果你当前没有登录你的 Twitter 账户,你将被要求在查看我们代码中的推文结果之前登录。

    行动时间——将 Twitter 添加到你的应用中

刚才发生了什么?

在代码的顶部,我们设置了一个变量local score = 100。这将在我们的 Twitter 消息中使用。

onOpenTouch(event)函数中,当释放openBtn时将加载一个网页弹窗。要发布的文本以字符串格式显示在变量local message下。你会注意到我们将score拼接到字符串中,以便在消息发布时显示其值。

local myStringstring.gsub()用于替换字符串中模式指示的所有实例。在这种情况下,它取消息中的字符串,并搜索每个单词之间的每个空格,并将其替换为%20%20编码 URL 参数以表示空格。额外的%充当转义字符。

native.showWebPopup()函数以 320 x 300 的尺寸显示,这大约是设备屏幕尺寸的一半。添加显示 Twitter 消息对话框的 URL 并拼接myString

当网页弹窗不再需要使用并需要关闭时,closeBtn会调用onCloseTouch(event)。这将传递参数"release"event,并调用native.cancelWebPopup()。这个特定的函数将会关闭当前的网页弹窗。

发布到 Facebook

另一个可以用来分享关于你的游戏信息的社交网络工具是 Facebook。你可以轻松地自定义一个帖子来链接关于你的游戏的信息,或者分享关于高分的消息,并鼓励其他用户下载。

为了在 Facebook 上发布消息,你需要登录到你的 Facebook 账户或创建一个账户,网址为 www.facebook.com/。你还需要从 Facebook 开发者网站 developers.facebook.com/ 获取一个 App ID。App ID 是你站点的唯一标识符,它决定了用户与应用页面/网站之间适当的安全级别。

创建 App ID 后,你还需要编辑应用信息,并选择应用与 Facebook 的集成方式。这里有几个选项,如网站、原生 iOS 应用和原生 Android 应用等。网站集成必须选中,并填写有效的 URL,以便 Facebook 在处理涉及网页弹窗的帖子时重定向到指定 URL。

行动时间——将 Facebook 添加到你的应用中

类似于我们的 Twitter 示例,我们也将通过网页弹窗整合 Facebook 帖子:

  1. Chapter 9 文件夹中,将 Facebook Web Pop-Up 项目文件夹复制到你的桌面。所有需要的配置、库和资源都已包含在内。你可以从 Packt Publishing 网站下载伴随本书的项目文件。

  2. 创建一个新的 main.lua 文件并将其保存到项目文件夹中。

  3. 在代码开始处设置以下变量:

    display.setStatusBar( display.HiddenStatusBar )
    
    local ui = require("ui")
    
    local openBtn
    local closeBtn
    local score = 100
    
  4. 创建一个名为 onOpenTouch() 的局部函数,并带有一个事件参数。当事件接收到 "release" 动作时,添加一个 if 语句:

    local onOpenTouch = function( event )
      if event.phase == "release" then
    
  5. 添加以下局部变量,包括我们将在 Facebook 帖子中实施的字符串:

     local appId = "0123456789" -- Your personal FB App ID from the facebook developer's website
    
        local message1 = "Your App Name Here"
        local message2 = "Posting to Facebook from Corona SDK and got a final score of " ..score.. "."
        local message3 = "Download the game and play!"
    
        local myString1 = string.gsub(message1, "( )", "%%20")
        local myString2 = string.gsub(message2, "( )", "%%20")
        local myString3 = string.gsub(message3, "( )", "%%20")
    
  6. 引入连接到 Facebook 账户的本地网页弹窗功能。包括 Facebook 对话框参数,用于重定向你首选网站的 URL,以触摸模式连接到你的应用 URL 的显示,以及展示你的应用图标或公司标志的图片 URL。使用字符串方法连接所有变量以输出所有消息。关闭函数。在 openBtn UI 函数中加入。你需要将以下所有 URL 信息替换为你自己的:

    native.showWebPopup(0, 0, 320, 300, "http://www.facebook.com/dialog/feed?app_id=" .. appId .. "&redirect_uri=http://www.yourwebsite.com&display=touch&link=http://www.yourgamelink.com&picture=http://www.yourwebsite.com/image.png&name=" ..myString1.. "&caption=" ..myString2.. "&description=".. myString3)  
    
      end
    end
    
      openBtn = ui.newButton{
      defaultSrc = "openbtn.png",
      defaultX = 90,
      defaultY = 90,
      overSrc = "openbtn-over.png",
      overX = 90,
      overY = 90,
      onEvent = onOpenTouch,
    }
    openBtn.x = 110; openBtn.y = 350
    

    注意

    关于 Facebook 对话框的更多信息可以在 Facebook 开发者网站找到,网址为 developers.facebook.com/docs/reference/dialogs/

  7. 创建一个名为 onCloseTouch() 的局部函数,并带有一个事件参数。添加一个 if 语句,判断 event.phase == "release" 以激活 native.cancelWebPopup()。设置 closeBtn UI 函数:

    local onCloseTouch = function( event )
      if event.phase == "release" then    
    
        native.cancelWebPopup()    
    
      end
    end
    
      closeBtn = ui.newButton{
      defaultSrc = "closebtn.png",
      defaultX = 90,
      defaultY = 90,
      overSrc = "closebtn-over.png",
      overX = 90,
      overY = 90,
      onEvent = onCloseTouch,
    }
    
    closeBtn.x = 210; closeBtn.y = 350
    
  8. 保存文件并在模拟器中运行项目。确保你已连接到互联网并登录你的 Facebook 账户以查看结果。行动时间——将 Facebook 添加到你的应用中

刚才发生了什么?

onOpenTouch(event) 函数内部,当按下并释放 openBtn 时会调用几个变量。注意 local appId 表示你在 Facebook Developers 网站上创建应用后可以获得的数字字符串。

message1message2message3 是显示信息帖子的字符串。myString1myString2myString3 用于替换 message1message2message3 中指定的空格。

native.showWebPopup() 函数以 320 x 300 的尺寸显示,并将对话框 URL 呈现给 Facebook。以下参数相应地显示:

  • app_id:这是你在 Facebook Developer 网站上创建的唯一 ID。例如,"1234567"

  • redirect_uri:用户在对话框上点击按钮后重定向的 URL。这是参数中必需的。

  • display:这显示渲染对话框的模式。

  • touch:这用于如 iPhone 和 Android 这样的智能手机设备。这使对话框屏幕适应较小的尺寸。

  • link:这是帖子附带的链接。

  • picture:这是帖子图片的 URL。

  • name:这是链接附件的名称。

  • caption:这是链接的标题(显示在链接名称下方)。

  • description:这是链接的描述(显示在链接标题下方)。

当网页弹窗不再需要并需要关闭时,closeBtn 会调用 onCloseTouch(event)。这将使用事件参数 "release" 并调用 native.cancelWebPopup()。这个特定的函数将关闭当前的网页弹窗。

Facebook Connect

这个库提供了一系列通过官方 Facebook Connect 接口访问 www.facebook.com 的功能。

动手操作时间——使用 Facebook Connect 发布分数。

Facebook Connect 是另一种使用原生 Facebook UI 功能在墙贴上发布信息的方式。我们将创建一种不同的方法来将消息和分数发布到新闻源。为了了解 Facebook Connect 的工作方式,你需要将构建加载到设备上查看结果。它不会在模拟器中运行。

  1. Chapter 9 文件夹中,将 Facebook Connect 项目文件夹复制到你的桌面。所有需要的配置、库和资源都已包含在内。你可以从 Packt Publishing 网站下载伴随这本书的项目文件。

  2. 创建一个名为 main.lua 的新文件并将其保存到项目文件夹中。

  3. 在代码开始时设置以下变量:

    display.setStatusBar( display.HiddenStatusBar )
    
    local ui = require("ui")
    local facebook = require "facebook"
    
    local fbBtn
    local score = 100
    
  4. 创建一个名为 onFBTouch() 的本地函数,带有一个事件参数。添加一个包含 event.phase == releaseif 语句。同时,以字符串格式包含你的 Facebook 应用 ID:

    local onFBTouch = function( event )
      if event.phase == "release" then    
    
     local fbAppID = "0123456789" -- Your FB App ID from facebook developer's panel
    
    
  5. onFBTouch(event) 内部创建另一个本地函数,名为 facebookListener(),同样带有一个事件参数。包含一个引用 "session" == event.typeif 语句:

        local facebookListener = function( event )
          if ( "session" == event.type ) then
    
  6. 在另一个 if 语句中添加 "login" 等于 event.phase 的条件。包含一个名为 theMessage 的局部变量,以显示你想要与其他 Facebook 用户分享的消息:

            if ( "login" == event.phase ) then  
    
              local theMessage = "Got a score of " .. score .. " on Your App Name Here!"  
    
  7. 添加 facebook.request() 函数,它将在用户的 Facebook 墙上发布以下消息。在 facebookListener(event) 函数中用 end 关闭任何剩余的 if 语句:

              facebook.request( "me/feed", "POST", {
                message=theMessage,
                name="Your App Name Here",
                caption="Download and compete with me!",
                link="http://itunes.apple.com/us/app/your-app-name/id382456881?mt=8",
                picture="http://www.yoursite.com/yourimage.png"} )
            end
          end
        end
    

    注意

    link 参数展示了一个 iOS 应用的 URL。你可以将 URL 指向类似 https://play.google.com/store/apps/details?id=com.yourcompany.yourappname 的 Android 应用或你选择的任何通用网站 URL。

  8. 调用 facebook.login() 函数,其中包括你的 App ID、监听器和在用户 Facebook 墙上发布的权限。关闭 onFBTouch(event) 函数的其余部分:

        facebook.login(fbAppID, facebookListener, {"publish_actions"})
    
      end
    end
    
  9. 启用 fbBtn UI 功能并保存你的文件:

    fbBtn = ui.newButton{
      defaultSrc = "facebookbtn.png",
      defaultX = 100,
      defaultY = 100,
      overSrc = "facebookbtn-over.png",
      overX = 100,
      overY = 100,
      onEvent = onFBTouch,
    }
    
    fbBtn.x = 160; fbBtn.y = 160
    
  10. 为 iOS 或 Android 创建一个新的设备构建。将构建加载到你的设备上并运行应用程序。在你能看到应用程序的结果之前,系统会要求你登录到你的 Facebook 账户。行动时间——使用 Facebook Connect 发布分数

刚才发生了什么?

需要完成的最重要的任务之一是 require "facebook" 以便 Facebook API 能够工作。我们还创建了一个名为 score 的局部变量,其值为 100。

onFBTouch(event) 函数将在 fbBtn"release" 上初始化事件参数。在函数中,fbAppID 以字符串格式包含字符。这将是一组你必须在 Facebook 开发者网站上获取的独特数字。当你在该网站上创建应用页面时,系统会为你创建 App ID。

另一个函数 facebookListener(event) 被创建,它将初始化所有的 fbConnect 事件。包含 ("login" == event.phase)if 语句将通过 "me/feed", "POST" 请求在你的动态中发布一条消息。该动态包含以下内容:

  • message=theMessage:这指的是属于变量的字符串。它还连接分数,因此也会显示值。

  • name:这是一条包含你的应用名称或主题的消息。

  • caption:这是一条简短的吸引其他用户关注玩游戏的宣传信息。

  • link:这提供了从 App Store 或 Google Play Store 下载游戏的 URL。

  • picture:这是一个包含你的应用图标或游戏视觉表示的图片 URL。

设置参数后,facebook.login() 将引用 fbAppIDfacebookListener() 以查看是否使用了有效的应用程序 ID 在 Facebook 上发布。成功后,将通过 "publish_actions" 发布帖子。

尝试成为英雄——创建一个对话框

看看你能否弄清楚如何使用 Facebook Connect 显示一个对话框,并使用前面示例中展示的相同设置。以下行将显示为:

facebook.showDialog( {action="stream.publish"} )

现在,查看代码中可以访问facebook.showDialog()的位置。这是发布消息到 Facebook 的另一种方式。

小测验——处理社交网络

Q1.哪个特定的 API 可以缩小高分辨率精灵表?

  1. object.xScale

  2. display.contentScaleX

  3. object.xReference

  4. 以上都不正确

Q2.允许在 Facebook 上在用户墙上发布的内容的发布权限叫什么?

  1. "publish_stream"

  2. "publish_actions"

  3. "post"

  4. "post_listener"

Q3. facebook.login()需要哪些参数?

  1. appId

  2. listener

  3. permissions

  4. 所有以上选项

总结

我们已经涵盖了关于增强配置设置以及将当今媒体中最受欢迎的三个社交网络整合到我们的应用中的多个领域。

我们还深入了解了以下内容:

  • 构建设置

  • 动态内容缩放和动态图像分辨率

  • 高分辨率精灵表

  • 将消息推送到 Twitter 和 Facebook

在下一章中,我们将详细介绍如何将我们的游戏提交到 App Store 和 Google Play Store。你绝对不想错过这个!

第十章:优化、测试和发布你的游戏

将游戏开发到完成阶段是一项伟大的成就。这离与全世界分享又近了一步,这样其他人就可以玩你新开发的游戏了。使用 Corona SDK 创建游戏的好处在于,你可以选择为 iOS 和/或 Android 构建游戏。你需要确保应用程序准备好提交,以便可以在你开发的移动平台上发布。我们将详细介绍准备游戏发布所需的过程。

注意

这里使用的应用程序界面经常更新;然而,无论你使用的是哪种界面,你都能完成所有步骤。

在本章中,我们将涵盖以下主题:

  • 提高应用程序的性能

  • 为 App Store 设置分发供应配置文件

  • 在 iTunes Connect 中管理应用程序信息

  • 学习如何将应用程序提交到 App Store 的应用程序加载器

  • 为 Android 签名应用程序

  • 学习如何将应用程序提交到 Google Play 商店

理解内存效率

在开发应用程序时,你应该始终考虑你的设计选择如何影响应用程序的性能。尽管计算能力和内存有所提升,但设备内存仍然有其限制。设备内的性能和优化不仅能实现更快的响应时间,还能帮助最小化内存使用并最大化电池寿命。如何检查内存使用的示例链接可以在gist.github.com/JesterXL/5615023找到。

内存是移动设备上重要的资源。当消耗过多内存时,设备可能会在你最意想不到的时候强制退出你的应用程序。以下是在开发过程中需要注意的一些事项:

  • 消除内存泄漏:允许内存泄漏存在意味着你的应用程序中有多余的已使用内存,这些内存占据了宝贵的空间。尽管 Lua 有自动内存管理,但你的代码中仍然可能出现内存泄漏。例如,当你向应用程序中引入全局变量时,你需要告诉 Lua 何时不再需要这些变量,以便释放内存。这可以通过在代码中使用nil来实现(myVariable = nil)。

  • 显示图像的文件大小应尽可能小:你可能希望在场景中拥有许多显示图像,但它们可能会占用过多的纹理内存。精灵表(Sprite sheets)可能会对应用程序的内存造成负担。它们应尽可能方便地创建得较小,并具有清晰展示动画的适当数量的帧数。对于所有你已显示的项目,规划出哪些元素始终在你的背景和前景中。如果可以将多个图像组合在一起,使它们不移动,那么就这样做。这将有助于在添加多个显示图像时节省内存。

  • 不要一次性加载所有资源:避免在需要之前加载资源文件。这将有助于节省内存,并防止应用程序在尝试一次性加载过多内容时崩溃。

  • 从显示层次结构中移除对象:创建显示对象时,它会隐式地添加到显示层次结构中。当你不再需要显示对象时,应该将其从显示层次结构中移除,特别是当对象包含图像时。这可以通过 display.remove( myImage ); myImage = nilmyImage:removeSelf() 来实现。

    这里有一个例子:

    local box = display.newRect( 0, 50, 100, 100)
    box:setFillColor( 1, 1, 1)
    box.alpha = 1
    
    local function removeBox()
      if box.alpha == 1 then
        print("box removed")
        display.remove( box )
        box = nil
      end
    end
    timer.performWithDelay( 1000, removeBox, 1 ) -- Runs timer to 1000 milliseconds before calling the block within removeBox()
    
  • 声音文件应尽可能小:使用免费程序,如 Audacity,或你偏爱的音频软件来压缩音乐或音效,并为设备构建。最好将未处理的音频与压缩后的音频进行比较,以听出质量上的差异。这将帮助你确定在音频质量和文件大小之间的良好折中。

图形

如果你没有注意同时使用的图片的大小和数量,显示图片会占用大量的纹理内存。

分组对象

如果多个对象的属性设置为相同的值,最好将对象添加到组中,然后修改组的属性。这将使编码变得更容易,同时也优化了你的动画。

在不使用动画时关闭它们

当不需要或在使它们不可见时,很容易忘记停止后台运行的动画。

当你包含如 "enterFrame" 的监听器,且监听器下注册的对象被设置为 .isVisible = false 时,即使屏幕上看不到,它仍会在后台运行。确保在不必要时移除监听器。

优化图片大小

当你的应用包含大文件大小,尤其是全屏图片时,由于加载所需时间,应用程序的响应速度会变慢,而且还会占用大量内存。在使用大图片时,尽量使用图像编辑工具(如 Photoshop 或 ImageOptim(imageoptim.com)压缩文件大小。这将帮助你减少文件体积,避免应用延迟带来的困扰。长期来看,压缩大图片尺寸是有益的。如果图片是背景,可以考虑切换到平铺图像。

分发 iOS 应用程序

当你的游戏最终调试完成,接下来要做什么呢?假设你已经注册了 iOS 开发者计划,那么在将应用程序提交到 App Store 之前,需要遵循一些指导原则。

准备你的应用图标

根据您的应用程序为哪些 iOS 设备开发,应用程序图标需要各种尺寸和命名约定。您可以在苹果开发者网站的iOS 人机界面指南中的图标和图像设计部分的应用图标子节找到最新信息。

以下是应用程序图标的要求,也需要采用非交错式的.png格式:

  • iTunesArtwork@2x:这是一张 1024 x 1024 像素的图片。这张图片需要移除.png扩展名。

  • Icon-60@2x.png:这是一张 120 x 120 像素的图片,用于 Retina iPhone。

  • Icon-60@3x.png:这是一张 180 x 180 像素的图片,用于 iPhone 6 Plus。

  • Icon-76.png:这是一张 76 x 76 像素的图片,用于 iPad。

  • Icon-76@2x.png:这是一张 152 x 152 像素的图片,用于 Retina iPad。

  • Icon-Small-40.png:这是一张 40 x 40 像素的图片,用于 iPad 2 和 iPad mini 搜索。

  • Icon-Small-40@2.png:这是一张 80 x 80 像素的图片,用于 Retina iPhone/iPad 搜索。

  • Icon-Small-40@3x.png:这是一张 120 x 120 像素的图片,用于 iPhone 6 Plus 搜索。

  • Icon-Small.png:这是一张 29 x 29 像素的图片,用于 iPad 2 和 iPad mini 设置。

  • Icon-Small@2x.png:这是一张 58 x 58 像素的图片,用于 Retina iPhone/iPad 设置。

  • Icon-Small@3x.png:这是一张 87 x 87 像素的图片,用于 iPhone 6 Plus 设置。

在您的build.settings文件中,您需要包含您应用程序支持的所有设备的图标引用。以下是如果您创建通用构建,如何设置文件的示例:

settings =
{
  orientation =
  {
    default = "landscapeRight", 
  },

  iphone =
    {
       plist =
       {
         CFBundleIconFiles = {
           "Icon-60@2x.png",
           "Icon-60@3x.png",
           "Icon-76.png",
           "Icon-76@2x.png",
           "Icon-Small-40.png",
           "Icon-Small-40@2x.png",
           "Icon-Small-40@3x.png",
           "Icon-Small.png",
           "Icon-Small@2x.png",
           "Icon-Small@3x.png",
         },

       },
    },

}

您不需要在plist中包含iTunesArtwork@2x图片,但请确保将其插入到应用程序的基础项目文件夹中。

是时候行动了——为 App Store 设置您的分发证书和配置文件。

我们一直专注于创建开发证书和配置文件,以便在设备上测试和调试我们的应用程序。现在,我们需要创建它们的分发版本,以便提交 iOS 应用程序。请注意,苹果公司可能会随时更改其网站的设计。因此,如果步骤和屏幕截图不匹配,请不要感到沮丧:

  1. 登录到你的 Apple 开发者账户,然后进入证书、标识符和配置文件。点击App IDs。在右上角选择+图标创建新的 App ID,并创建与应用程序相关的描述以便于识别。如果你在开发过程中已经使用了一个现有的 App ID,可以跳过这一步。行动时间——为 App Store 设置你的分发证书和配置文件

  2. 配置文件下点击分发。选择分发部分下的+按钮,然后选择App Store。按下继续

  3. 选择你希望与文件关联的 App ID,并点击继续。接下来,选择将与你配置文件关联的证书,并点击继续

  4. 为你的配置文件提供一个名称,并选择生成按钮。

  5. 在下一个屏幕上,点击下载按钮,然后双击文件将其安装在你的机器上。

刚才发生了什么?

你使用的 App ID 对于标识你将要提交的应用至关重要。最好使用独特的反向域名风格字符串。确保为 Corona 应用创建明确的 App ID。不要使用通配符 App ID。

为了在 App Store 上分发,你需要创建一个 App Store 分发配置文件和一个生产证书。任何开发配置文件都不会被接受。这个过程与创建开发配置文件和开发证书类似。

你可以在 Apple 开发者网站上的developer.apple.com/ios/manage/distribution/index.action(如果你还没有登录,系统会要求你登录到你的 Apple 开发者账户)和 Corona Labs 网站上的docs.coronalabs.com/guide/distribution/iOSBuild/index.html找到更多关于分发配置文件的信息。

iTunes Connect

iTunes Connect 是一套基于网络的工具,允许你提交和管理在 App Store 上分发的应用程序。在 iTunes Connect 中,你将能够检查合同的状态;设置你的税务和银行信息;获取销售和财务报告;请求促销代码;以及管理用户、应用程序、元数据和你的应用内购买目录。

合同、税务和银行

如果你打算出售你的应用,你需要有一个付费的商业协议,以便它可以被发布到 App Store。你将需要申请一个关于 iOS 付费应用的合同。所有这些都可以通过 iTunes Connect 下的合同税务银行链接完成。

当请求合同时,要注意可能发生的问题,比如苹果首次处理你的信息时产生的延迟,或在 iTunes Connect 中更改当前联系信息时(例如,如果你搬到不同的地点,更改地址)的问题。你有责任定期联系苹果支持,确保合同中的信息始终是最新的。

行动时间——在 iTunes Connect 中管理你的应用

我们现在将介绍如何在 iTunes Connect 中设置应用信息。任何关于用户账户、合同和银行的其他信息,你可以在developer.apple.com/app-store/review/找到。

  1. itunesconnect.apple.com/ 登录 iTunes Connect。你的登录信息与你的 iOS 开发者账户相同。登录后,选择管理你的应用。点击添加新应用按钮。应用名称是你的应用的名称。SKU 编号是应用唯一的字母数字标识符。捆绑 ID是在 iOS 供应门户中创建的那个。填写信息并点击继续行动时间——在 iTunes Connect 中管理你的应用

  2. 下一步是选择你希望应用在 App Store 上线的时间和想要的价格层级。有一个可选的对教育机构打折复选框。这只适用于那些希望为教育机构同时购买多份应用副本时打折的情况。完成后,点击继续行动时间——在 iTunes Connect 中管理你的应用

  3. 接下来,填写关于你应用的元数据部分。这包括版本号、游戏描述、分类、与应用相关的关键词、版权、联系方式和支持网址:行动时间——在 iTunes Connect 中管理你的应用

  4. 评级部分基于你的应用内容。对于每个描述,选择最能描述你应用频率的级别。某些内容类型会导致自动拒绝,比如应用中描绘的现实暴力或针对个人或团体的个人攻击。你可以了解更多关于App Store 审核指南的信息,请访问developer.apple.com/appstore/resources/approval/guidelines.html行动时间——在 iTunes Connect 中管理你的应用

  5. 如前文上传部分所述,你需要一个大型应用图标版本,即 iPhone/iPod Touch 截图和 iPad 截图(如果应用在 iPad 上运行)。

  6. 你将看到一个关于你的应用程序信息的页面摘要。检查显示的信息是否正确,然后点击完成行动时间 – 在 iTunes Connect 中管理你的应用程序

  7. 你将被送回到版本详细信息页面。注意一个写着准备上传二进制文件的按钮。点击该按钮,你将需要回答关于出口 合规性的几个问题。完成后,你将获得通过应用程序 加载器上传二进制文件的权利。行动时间 – 在 iTunes Connect 中管理你的应用程序

刚才发生了什么?

iTunes Connect 是你在此后管理应用程序并分发到 App Store 的地方。你想要展示关于应用程序的每一块信息都是通过 iTunes Connect 完成的。

一旦进入应用程序信息部分,请确保你的SKU 编号是唯一的,并且与你的应用程序相关,这样你以后可以识别它。同时,确保你为应用程序指定的捆绑 ID是正确的。

权利和定价部分,应用程序的可用性控制了当你提交的应用一旦获得批准,你希望它何时上线。设置一个从提交日期起几周后的日期是一个好选择。只要提交没有问题,从审核中准备销售的审核过程可能需要几天到几周的时间。价格层级是你为应用程序设置价格的地方,也可以设置为免费。你可以点击查看定价矩阵来确定你希望出售应用程序的价格。

元数据部分的信息是用户在 App Store 中将看到的内容。评级部分与 Apple 内容描述有关。确保将频率级别选择得尽可能接近你的应用程序内容。

上传部分是你添加 1024 x 1024 像素的应用程序图标和视觉上最适合你应用程序的截图的地方。确保你提供正确的图片尺寸。当你回到应用程序信息屏幕后,你会注意到状态显示为准备上传。当你在版本详细信息页面上点击准备上传二进制文件按钮时,你将回答关于出口合规性的问题。之后不久,状态将变为等待上传

有关 iTunes Connect 的更多信息可以在 developer.apple.com/library/ios/iTunesConnectGuide 找到。

在 Corona 中构建用于分发的 iOS 应用程序

我们已经进入了将您的 iOS 应用程序提交到 App Store 的最后阶段。假设您已经测试了您的应用程序,并使用开发配置文件进行了调试,那么您现在可以创建一个分发构建,这将生成您应用程序的二进制 ZIP 文件。

是时候行动了——构建您的应用程序并将其上传到应用程序加载器。

是时候创建最终的 iOS 分发游戏构建,并将其上传到应用程序加载器,以便在苹果公司的审查下进行审核。

  1. 启动 Corona 模拟器,导航到应用程序项目文件夹,并运行它。前往 Corona 模拟器的菜单栏,然后选择文件 | 构建 | iOS。填写您的所有应用程序详细信息。确保您的应用程序名称版本字段与您的 iTunes Connect 账户中显示的内容相匹配。选择设备以构建应用程序包。接下来,从支持设备下拉菜单中选择您的应用程序所针对的目标设备(iPhone 或 iPad)。在代码签名身份下拉菜单下,选择您在 iOS 配置门户中创建的分发 配置文件选项。在保存到文件夹部分,点击浏览并选择您希望保存应用程序的位置。完成后点击构建按钮:是时候行动了——构建您的应用程序并将其上传到应用程序加载器

  2. 当构建完成后,您将看到显示您的应用程序已准备好分发的界面。选择上传到 App Store按钮。

  3. 欢迎使用应用程序加载器窗口弹出时,使用您的 iTunes Connect 信息登录。然后您将被带到另一个窗口,窗口中有交付您的 App创建新包选项。选择交付您的 App。下一个窗口显示一个下拉菜单;选择您将提交的应用程序的名称,然后点击下一步按钮。

  4. 在 iTunes Connect 中显示的可用应用程序信息。验证其正确无误后,点击选择按钮。

  5. 点击省略号()按钮,在提交之前替换当前文件,然后选择发送按钮。

  6. 应用程序加载器将开始将您应用程序的二进制文件提交到 App Store。

  7. 如果您的二进制文件上传成功,您将收到确认您的应用程序已送达 App Store 的消息。当您的应用程序进入审查、准备销售、上线等状态时,您可以在 iTunes Connect 中检查应用程序的状态。每次应用程序状态发生变化时,都会向您发送电子邮件。就是这样!这就是您如何将应用程序提交到 App Store 的方法!

  8. 当你的应用经过审核并获得 App Store 批准后,你可以进入 iTunes Connect,如果批准时间早于你提出的发布日期,可以调整可用日期。你的应用将立即在 App Store 上线:行动时间——构建你的应用程序并将其上传到 Application Loader

刚才发生了什么?

当你在代码签名标识下构建你的应用时,重要的是选择为你分发构建创建的分发配置文件。在你的构建编译完成后,你可以启动 Application Loader。确保你已经安装了 Xcode。在选择上传到 App Store按钮后,Application Loader 将立即启动。

当你处于 Application Loader 中时,一旦你将二进制信息加载到 iTunes Connect,应用的名字就会显示在下拉菜单中。当你交付应用时,从你保存文件的地点选择压缩后的二进制文件。

文件上传后,确认窗口会出现,同时一封电子邮件会发送到分配给你 Apple 账户的 Apple ID。你的二进制文件将在 iTunes Connect 中显示为等待审核状态。

完成所有这些步骤后,你现在知道如何将 iOS 应用提交到 App Store 了。万岁!

尝试英雄——制作一个通用的 iOS 构建版本。

如果你只为 iPhone 开发了应用,尝试也实现一个 iPad 版本,这样它就可以成为一个通用构建版本。利用你在前面章节中学到的知识,使用你的 build.settingsconfig.lua 文件调整你的应用程序大小。同时,也不要忘记你的应用图标的要求。这可谓是一石二鸟!

Google Play 商店

Google Play 商店是一个发布平台,可以帮助你宣传、销售和向全球用户分发你的 Android 应用。

要注册成为 Google Play 开发者并开始发布应用,请访问 Google Play Android 开发者控制台发布商网站。你可以在play.google.com/apps/publish/注册一个账户。

创建启动器图标

启动器图标是代表你应用程序的图形。启动器图标由应用程序使用,并出现在用户的桌面上。它们也可以用来在应用程序中表示快捷方式。这些与为 iOS 应用程序创建的图标类似。以下是启动器图标的要求,也需要是 32 位 .png 格式:

  • Icon-ldpi.png:这是一张 120 dpi 的 36 x 36 像素图像,用于低密度屏幕。

  • Icon-mdpi.png:这是一张 160 dpi 的 48 x 48 像素图像,用于中等密度的屏幕。

  • Icon-hdpi.png:这是一张 240 dpi 的 72 x 72 像素图像,用于高密度屏幕。

  • Icon-xhdpi.png:这是一张 320 dpi 的 96 x 96 像素图像,用于超高密度屏幕。

  • Icon-xxhdpi.png:这是一个 144 x 144 像素,480 dpi 的图像,用于 xx 高密度屏幕。

  • Icon-xxxhdpi.png:这是一个 192 x 192 像素,640 dpi 的图像,用于 xxx 高密度屏幕。

启动器图标需要在构建应用程序时放置在你的项目文件夹中。Google Play 商店还要求你有一个 512 x 512 像素的图标版本,可以在上传构建时在开发者控制台上传。关于启动器图标的更多信息,请访问developer.android.com/guide/practices/ui_guidelines/icon_design_launcher.html

行动时间——为 Google Play 商店签名你的应用

安卓系统要求所有安装的应用程序都必须使用持有私钥的证书进行数字签名。安卓系统使用证书来识别应用程序的作者,并在应用程序之间建立信任关系。证书不用于控制用户可以安装哪些应用程序。证书不需要由证书颁发机构签名;它可以自签名。证书可以在 Mac 或 Windows 系统上签名。

  1. 在 Mac 上,前往应用程序 | 实用工具 | 终端。在 Windows 上,前往开始菜单 | 所有程序 | 附件 | 命令提示符。使用keytool命令,加入以下行并按下回车

    keytool -genkey -v -keystore my-release-key.keystore -alias aliasname -keyalg RSA -validity 999999
    
    

    注意

    my-release-key替换为你的应用程序名称,将aliasname替换为相似或相同的别名。另外,如果你在999999之后添加任何额外的数字(即额外的 9),应用程序将显示为损坏。

    行动时间——为 Google Play 商店签名你的应用

  2. 系统会要求你输入一个密钥库密码。从这里,你将创建一个独特的密码,作为开发者你必须想出一个。系统会要求你重新输入它。接下来会被问及的问题将涉及到你的开发者/公司信息、位置等。全部填写。一旦填写了所需信息,你就生成了一个用于签名你的 Android 构建的关键。关于应用签名的更多信息,请访问developer.android.com/tools/publishing/app-signing.html

  3. 启动 Corona 模拟器,导航到应用程序项目文件夹并运行它。前往 Corona 模拟器的菜单栏,然后选择文件 | 构建 | Android。填写与你的应用程序相关的应用名称版本代码版本名称。使用 Java 方案指定一个名称。从目标应用商店菜单中选择Google Play。在密钥库下,选择浏览按钮来定位你签名的私钥,然后从下拉菜单中选择你为发布构建生成的密钥。系统会提示你输入在keytool命令中用于签名应用程序的密钥库密码。在密钥别名下,从下拉菜单中选择你为密钥创建的别名名称,并在提示时输入密码。选择浏览按钮来选择应用程序构建的位置。完成后选择构建按钮:行动时间——为 Google Play 商店签名你的应用

刚才发生了什么?

keytool命令会生成一个名为my-release-key.keystore的密钥库文件。密钥库和密钥由你输入的密码保护。密钥库包含一个单一密钥,有效期为 999999 天。别名是你在签名应用程序时稍后用来指代此密钥库的名称。

你的密钥库密码是你在 Corona 中构建应用程序时创建并必须记住的内容。如果你想要为别名名称使用不同的密码,将有一个选项。当你在终端或命令提示符中时,可以按Enter使用相同的密码。

当你在 Corona 中创建构建时,请确保你的版本号是一个没有特殊字符的整数。此外,你还需要确保你的build.settings文件中包含了versionCode。这个数字将与你的版本号相同。更多信息请参考第九章,处理多设备和网络应用

你的构建中的 Java 方案是域名反转,加上你的产品/公司名称,再加上你的应用名称,例如,com.mycompany.games.mygame

当你使用你的私钥构建应用程序,并选择了一个别名名称后,.apk文件将被创建,并准备好发布到 Google Play 商店。

行动时间——向 Google Play 商店提交应用

我们将使用开发者控制台。这是创建开发者资料以发布到 Google Play 商店的地方。

  1. 登录到开发者控制台后,点击 Android 图标并选择标有添加新应用的按钮。你将看到一个弹出窗口,允许你上传你的构建版本。从下拉菜单中选择你的默认语言,并在标题下输入你的应用名称。点击上传 APK按钮进入下一页。行动时间 – 向 Google Play 商店提交应用

  2. 点击将你的第一个 APK 上传到生产环境,然后点击浏览文件以找到你的应用的.apk文件。选择打开按钮以上传你的文件。行动时间 – 向 Google Play 商店提交应用

  3. 上传.apk文件后,选择商店列表标签。填写你应用的相关信息,包括标题简短描述完整描述:行动时间 – 向 Google Play 商店提交应用

  4. 在图形资产部分,添加你的应用屏幕截图。至少需要两张截图才能提交你的应用。其他需要的强制性图形包括高分辨率图标功能图形

  5. 分类、联系详情和隐私政策部分需要处理。确保你完成这些部分,并在转到下一个标签之前点击页面顶部的保存按钮。

  6. 选择定价与分销标签。选择与应用相关的信息。定价默认设置为免费。如果你想制作付费版本,你必须与 Google Checkout 设置一个商家账户。完成后点击保存:行动时间 – 向 Google Play 商店提交应用

  7. 填写完所有与应用相关的信息后,请确保 APK、商店列表和定价与分销标签旁边有绿色的勾选标记。

  8. 最后,点击准备发布按钮,并在下拉菜单中选择发布此应用。恭喜你!你刚刚将你的应用发布到了 Google Play 商店!!行动时间 – 向 Google Play 商店提交应用

刚才发生了什么?

开发者控制台页面展示了一个简单的分步流程,指导你如何发布.apk文件。

发布应用所需的资产在每部分旁边显示了可接受的分辨率和图像类型。包括促销图形、功能图形和促销视频是可选的,但为了你的最佳利益,最好为你的应用页面添加足够的实质内容。这将使它吸引潜在客户。

完成所有与应用相关的信息后,确保保存你的进度。选择发布此应用菜单后,你就完成了!你应该能在你发布后的小时内看到你的应用在 Google Play 商店中。

尝试英雄 – 添加更多促销信息

Google Play 商店为你提供了许多推广应用程序的方式。可以从开发者控制台添加额外的资源。尝试以下方法:

  • 添加宣传图像作为展示你应用程序的市场推广工具。

  • 添加一个功能图像。

  • 创建你的应用程序的宣传视频。像 YouTube 这样的网站是分享你的游戏预告片的好方式。

小测验 - 发布应用程序

Q1. 创建 iOS 分发配置文件时,你需要使用哪种分发方法?

  1. 开发

  2. 应用商店

  3. Ad hoc

  4. 以上都不是

Q2. 你在哪里查看提交的 iOS 应用程序的状态?

  1. iTunes Connect

  2. iOS 配置门户

  3. 应用程序加载器

  4. 以上都不是

Q3. 为 Google Play 商店构建应用程序需要什么?

  1. 使用 keytool 命令创建一个私钥

  2. 使用调试密钥为你的应用程序签名

  3. 使用你的私钥为应用程序签名

  4. a 和 c

总结

通过本章的学习,我们已经完成了一个巨大的里程碑。我们不仅学会了如何提交到一个,而是两个主要的应用市场!最终,将你的应用程序发布到 App Store 和 Google Play 商店并不那么可怕。

我们已经涵盖了以下主题:

  • 内存效率的重要性

  • 创建用于向 App Store 分发的配置文件

  • 管理 iTunes Connect

  • 向应用程序加载器提交二进制文件

  • 为 Android 应用程序签署发布构建

  • 向 Google Play 商店提交 .apk 文件

在下一章中,我们将看看 iOS 平台的 Apple iTunes 商店中的应用内购买。

第十一章:实现 应用内购买

应用内购买是开发者可以选择使用的一个功能,可以直接在应用中嵌入商店。有时,你可能希望扩展当前游戏的一些功能,以保持玩家的兴趣。现在就是你的机会,也许还能让你的口袋里收入更多!

本章仅关注 iOS 平台上 Apple iTunes Store 的应用内购买。希望在应用中实现应用内购买的 Android 开发者可以参考相关内容。iOS 和 Android 的应用内购买设置方式类似。但是,在 build.settings 文件和代码中需要设置一些不同之处。

注意

这里使用的应用程序界面经常更新。但无论你使用的是哪种界面,你都能完成所有步骤。

我们将在本章介绍以下内容:

  • 消耗性、非消耗性和订阅购买

  • 进行交易

  • 恢复已购项目

  • 初始化 Corona 的商店模块

  • 在设备上创建和测试应用内购买

准备,设定,出发!

应用内购买的奇妙之处

实施应用内购买的目的在于为应用添加应用内支付功能,以收取增强功能或游戏内可使用的额外内容的费用。以下是将此功能融入应用的选择:

  • 提供除默认内容之外的全新关卡包进行游戏的应用

  • 允许你通过购买虚拟货币在游戏过程中创建或建立新资产的高级游戏

  • 添加额外的角色或特殊能力提升以增强游戏元素

以下是一些可以使用应用内购买实现的示例。

应用内购买允许用户在应用程序内购买额外内容。App Store 只管理交易信息。开发者不能使用 App Store 传送内容。因此,你可以在发布应用时捆绑内容(购买后即可解锁),或者如果你希望传送内容,需要自己设计下载数据的系统。

应用内购买的类型

你可以在应用中使用几种不同的应用内购买类型。

注意

你可以在 Apple 网站上找到更多关于应用内购买的信息,地址为developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnectInAppPurchase_Guide/Chapters/CreatingInAppPurchaseProducts.html

  • 消耗性:这些是用户每次需要该物品时都必须购买的产品。它们通常是单次服务,如在需要支付建造建筑物的供应品的游戏中使用的货币。

  • 非消耗性:这些是用户只需购买一次的产品。这些可能是游戏中的附加关卡包。

  • 自动续订订阅:这些产品允许用户购买一定时间内的应用内内容。一个自动续订订阅的例子是一份利用 iOS 内置的自动续订功能的杂志或报纸。

  • 免费订阅:这些用于在 Newsstand 中放置免费订阅内容。一旦用户注册了免费订阅,它将在与该用户 Apple ID 相关联的所有设备上可用。请注意,免费订阅不会过期,并且只能在启用 Newsstand 的应用中提供。

  • 非续订订阅:与自动续订订阅类似,这些是非续订订阅,要求用户在订阅到期时每次都进行续订。你的应用必须包含识别到期发生的代码。还必须提示用户购买新的订阅。自动续订订阅则省略了这些步骤。

Corona 的商店模块

在你的应用程序中应用应用内购买可能是一个相当令人困惑和繁琐的过程。与 Corona 集成需要调用商店模块:

store = require("store")

商店模块已经整合到 Corona API 中,类似于 Facebook 和游戏网络。你可以在 docs.coronalabs.com/daily/guide/monetization/IAP/index.html 了解更多关于 Corona 商店模块的信息。

store.init()

在处理应用程序中的商店交易时,必须调用 store.init() 函数。它激活了应用内购买,并允许你使用指定的监听函数接收回调:

store.init( listener )

这里唯一的参数是 listener。它是一个处理交易回调事件的功能函数。

以下代码块确定了在应用内购买过程中可能发生的交易状态。四种不同的状态分别是:购买、恢复、取消和失败:

function transactionCallback( event )
  local transaction = event.transaction
  if transaction.state == "purchased" then
    print("Transaction successful!")
    print("productIdentifier", transaction.productIdentifier)
    print("receipt", transaction.receipt)
    print("transactionIdentifier", transaction.identifier)
    print("date", transaction.date)

    elseif  transaction.state == "restored" then
    print("Transaction restored (from previous session)")
    print("productIdentifier", transaction.productIdentifier)
    print("receipt", transaction.receipt)
    print("transactionIdentifier", transaction.identifier)
    print("date", transaction.date)
    print("originalReceipt", transaction.originalReceipt)
    print("originalTransactionIdentifier", transaction.originalIdentifier)
    print("originalDate", transaction.originalDate)

    elseif transaction.state == "cancelled" then
    print("User cancelled transaction")

    elseif transaction.state == "failed" then
    print("Transaction failed, type:", transaction.errorType, transaction.errorString)

    else
    print("unknown event")
    end

    -- Once we are done with a transaction, call this to tell the store
    -- we are done with the transaction.
    -- If you are providing downloadable content, wait to call this until
    -- after the download completes.
    store.finishTransaction( transaction )
end

store.init( "apple", transactionCallback )

event.transaction

event.transaction 对象包含了交易信息。

交易对象支持以下只读属性:

  • "state":这是一个字符串,包含交易的状态。有效的值有 "purchased""restored""cancelled""failed"

  • "productIdentifier":这是与交易关联的产品标识符。

  • "receipt":这是从 App Store 返回的唯一收据。它以十六进制字符串的形式返回。

  • "signature":这是一个用于验证购买的有效字符串。对于 Google Play,它由 "inapp_signature" 返回。在 iOS 中,它返回 nil

  • "identifier":这是从 App Store 返回的唯一交易标识符。它是一个字符串。

  • "date":这是交易发生的日期。

  • "originalReceipt":这是从 App Store 原始购买尝试返回的唯一收据。它主要在恢复的情况下相关。它以十六进制字符串的形式返回。

  • "originalIdentifier":这是从商店原始购买尝试返回的唯一交易标识符。这在恢复的情况下最为相关。它是一个字符串。

  • "originalDate":这是原始交易的日期。这在恢复的情况下最为相关。

  • "errorType":这是状态为"failed"时发生的错误类型(一个字符串)。

  • "errorString":这是在"failed"情况下出现问题的描述性错误信息。

store.loadProducts()

store.loadProducts()函数获取有关待售商品的信息。这包括每件商品的价格、名称和描述:

store.loadProducts( arrayOfProductIdentifiers, listener )

它的参数如下:

  • arrayOfProductIdentifiers:这是一个数组,每个元素包含你想要了解的应用内产品产品 ID 的字符串。

  • listener:这是一个回调函数,当商店完成获取产品信息时被调用

以下代码块显示了应用中可用的产品列表。可以通过loadProductsCallback()函数获取产品信息,并判断其有效或无效:

-- Contains your Product ID's set in iTunes Connect
local listOfProducts = 
{
  "com.mycompany.InAppPurchaseExample.Consumable",
  "com.mycompany.InAppPurchaseExample.NonConsumable",
  "com.mycompany.InAppPurchaseExample.Subscription",
}

function loadProductsCallback ( event )
  print("showing valid products", #event.products)
  for i=1, #event.products do
    print(event.products[i].title)
    print(event.products[i].description)
    print(event.products[i].price)
    print(event.products[i].productIdentifier)
  end

  print("showing invalidProducts", #event.invalidProducts)
    for i=1, #event.invalidProducts do
      print(event.invalidProducts[i])
end
end

store.loadProducts( listOfProducts, loadProductsCallback )

event.products

store.loadProducts()返回请求的产品列表时,可以通过event.products属性访问产品信息数组。

产品信息,如标题、描述、价格和产品标识符,包含在表格中:

event.products

event.products数组中的每个条目支持以下字段:

  • title:这是项目的本地化名称

  • description:这是项目的本地化描述

  • price:这是项目的价格(作为一个数字)

  • productIdentifier:这是产品标识符

event.invalidProducts

store.loadProducts()返回其请求的产品列表时,任何你请求的不可售产品将以数组形式返回。你可以通过event.invalidProducts属性访问无效产品的数组。

这是一个 Lua 数组,包含从store.loadProducts()请求的产品标识符字符串:

event.invalidProducts

store.canMakePurchases

store.canMakePurchases函数如果允许购买则返回 true,否则返回 false。Corona 的 API 可以检查是否可以进行购买。iOS 设备提供了一个禁用购买的设置。这可以用来避免意外购买应用。

    if store.canMakePurchases then
      store.purchase( listOfProducts )
    else
      print("Store purchases are not available")
    end

store.purchase()

store.purchase()函数启动对提供的产品列表的购买交易。

这个函数将向商店发送购买请求。当商店处理完交易后,将在store.init()中指定的监听器将被调用:

store.purchase( arrayOfProducts )

它唯一的参数是arrayOfProducts,一个指定你想要购买的产品数组:

store.purchase{ "com.mycompany.InAppPurchaseExample.Consumable"}

store.finishTransaction()

这个函数通知应用商店交易已完成。

在你完成事务处理后,必须在该事务对象上调用store.finishTransaction()。如果你不这样做,App Store 会认为你的事务被中断,并会在下次应用程序启动时尝试恢复它。

语法:

store.finishTransaction( transaction )

参数:

事务:属于你想标记为完成的事务的transaction对象。

示例:

store.finishTransaction( transaction )

store.restore()

任何之前购买的项目,如果从设备上清除或升级到新设备,都可以在用户的账户上恢复,无需再次为产品付费。store.restore() API 会启动这个过程。通过使用store.init()注册的transactionCallback监听器,可以恢复事务。事务状态将是"restored",然后你的应用程序可以使用事务对象的"originalReceipt""originalIdentifier""originalDate"字段。

store.restore()

该代码块将通过transactionCallback()函数运行,并确定之前是否从应用程序购买过产品。如果结果为真,store.restore()将启动获取产品的过程,而无需让用户再次付费:

function transactionCallback( event )
  local transaction = event.transaction
  if transaction.state == "purchased" then
    print("Transaction successful!")
    print("productIdentifier", transaction.productIdentifier)
    print("receipt", transaction.receipt)
    print("transactionIdentifier", transaction.identifier)
    print("date", transaction.date)

  elseif  transaction.state == "restored" then
    print("Transaction restored (from previous session)")
    print("productIdentifier", transaction.productIdentifier)
    print("receipt", transaction.receipt)
    print("transactionIdentifier", transaction.identifier)
    print("date", transaction.date)
    print("originalReceipt", transaction.originalReceipt)
    print("originalTransactionIdentifier", transaction.originalIdentifier)
    print("originalDate", transaction.originalDate)

  elseif transaction.state == "cancelled" then
      print("User cancelled transaction")

  elseif transaction.state == "failed" then
    print("Transaction failed, type:", transaction.errorType, transaction.errorString)

  else
    print("unknown event")
  end

  -- Once we are done with a transaction, call this to tell the store
  -- we are done with the transaction.
  -- If you are providing downloadable content, wait to call this until
  -- after the download completes.
  store.finishTransaction( transaction )
end

store.init( transactionCallback )
store.restore()

创建应用内购买

在继续之前,请确保你知道如何从 iOS 配置门户创建 App ID 和分发配置文件。还要确保你知道如何在 iTunes Connect 中管理新应用程序。如果你不确定,请参考第十章,优化、测试和发布你的游戏,了解更多信息。在创建应用内购买之前,以下是你应用中需要准备的事项:

  • 为你的应用已经制作好的分发证书。

  • 为你的应用程序指定一个显式的 App ID,例如,com.companyname.appname。不要使用通配符(星号:"*")。为了使用应用内购买功能,捆绑 ID 需要完全唯一。

  • 一个临时分发配置文件(用于测试应用内购买)。当你准备提交带有应用内购买的应用程序时,需要一个 App Store 分发配置文件。创建应用内购买

  • 你的应用程序信息必须在 iTunes Connect 中设置。在创建或测试应用内购买时,你不需要上传你的二进制文件。

  • 确保你已经与苹果公司签订了有效的 iOS 付费应用程序合同。如果没有,你需要在 iTunes Connect 主页上的合同、税务和银行信息中申请。你需要提供你的银行和税务信息,以便在应用中提供应用内购买。

动手操作——在 iTunes Connect 中创建应用内购买

我们将通过 iTunes Connect 实现应用内购买,并在示例应用程序中创建一个将调用事务的场景。让我们创建将在应用内购买中使用的产品 ID:

  1. 登录到 iTunes Connect。在首页上,选择管理您的应用程序。选择您计划添加应用内购买的应用程序。

  2. 当您在应用概览页面时,点击管理应用内购买按钮,然后在左上角点击创建新购买项目按钮。![行动时间——在 iTunes Connect 中创建应用内购买]

  3. 您将看到一个页面,该页面显示了您可以创建的应用内购买类型概览。在本例中,选择了非消耗性。我们将创建一个只需购买一次的产品。

  4. 在下一个页面,您需要填写有关产品的信息。这些信息适用于消耗性、非消耗性和非续订订阅的应用内购买。为您的产品填写参考名称产品 ID字段。产品 ID 需要是一个唯一的标识符,可以是字母和数字的任意组合(例如,com.companyname.appname.productid)。

    注意

    自动续订订阅需要您生成一个共享密钥。如果您要在应用中使用自动续订订阅,请在管理应用内购买页面上,点击查看或生成共享密钥链接。您将被带到生成共享密钥的页面。点击生成按钮。共享密钥将显示 32 个随机生成的字母数字字符。当您选择自动续订订阅时,与其他应用内购买类型的不同之处在于,您必须选择产品之间自动续订的持续时间。有关自动续订订阅的更多信息,请访问developer.apple.com/library/ios/iTunesConnectGuide

    行动时间——在 iTunes Connect 中创建应用内购买

  5. 点击添加语言按钮。选择将用于应用内购买的语言。为您的产品添加一个显示名称和简短描述。完成后,点击保存按钮。![行动时间——在 iTunes Connect 中创建应用内购买]

  6. 定价和可用性部分,确保已清除销售选项选择为。在价格层级下拉菜单中,选择您计划销售应用内购买的价格。在本例中,选择了层级 1。在审核截图部分,您需要上传应用内购买的截图。如果您在临时版本上进行测试,则无需截图。当您准备分发时,需要上传截图以便在提交审核时对应用内购买进行审查。完成后点击保存按钮。![行动时间——在 iTunes Connect 中创建应用内购买]

  7. 你将在下一页看到你创建的应用内购买的摘要。如果所有信息看起来都正确,请点击完成按钮。动手时间——在 iTunes Connect 中创建应用内购买

刚才发生了什么?

添加新的应用内购买是一个非常简单的过程。交易过程中将调用产品 ID 中包含的信息。管理应用内购买类型完全取决于你想在游戏中销售的产品类型。这个例子展示了购买/解锁游戏中一个新级别的非消耗性产品的目的。这对于想要销售关卡包的用户来说是一个常见场景。

你的应用程序不需要完成就可以测试应用内购买。需要做的是在 iTunes Connect 中设置你的应用程序信息,这样你就可以管理应用内购买的功能。

动手时间——使用 Corona 商店模块创建应用内购买

既然我们在 iTunes Connect 中为应用内购买设置了产品 ID,我们就可以在应用中实现它,以购买我们将要销售的产品。创建了一个 Breakout 的示例菜单应用,以演示如何在应用程序内购买关卡。该应用在关卡选择屏幕上包含两个级别。第一个默认可用。第二个被锁定,只能通过支付 0.99 美元来解锁。我们将创建一个关卡选择屏幕,使其按此方式操作:

  1. 第十一章文件夹中,将Breakout 应用内购买演示项目文件夹复制到你的桌面。你可以从 Packt Publishing 网站下载伴随这本书的项目文件。你会注意到,配置、库、资源和.lua文件都已包含。

  2. 创建一个新的levelselect.lua文件并将其保存到项目文件夹中。

  3. 使用以下变量和保存/加载函数设置场景。最重要的变量是local store = require("store"),它调用应用内购买的商店模块:

    local composer = require( "composer" )
    local scene = composer.newScene()
    
    local ui = require("ui")
    local movieclip = require( "movieclip" )
    local store = require("store")
    
    ---------------------------------------------------------------------------------
    -- BEGINNING OF YOUR IMPLEMENTATION
    ---------------------------------------------------------------------------------
    
    local menuTimer
    
    -- AUDIO
    local tapSound = audio.loadSound( "tapsound.wav" )
    
    --***************************************************
    
    -- saveValue() --> used for saving high score, etc.
    
    --***************************************************
    local saveValue = function( strFilename, strValue )
      -- will save specified value to specified file
      local theFile = strFilename
      local theValue = strValue
    
      local path = system.pathForFile( theFile, system.DocumentsDirectory )
    
      -- io.open opens a file at path. returns nil if no file found
      local file = io.open( path, "w+" )
      if file then
        -- write game score to the text file
        file:write( theValue )
        io.close( file )
      end
    end
    
    --***************************************************
    
    -- loadValue() --> load saved value from file (returns loaded value as string)
    
    --***************************************************
    local loadValue = function( strFilename )
      -- will load specified file, or create new file if it doesn't exist
    
      local theFile = strFilename
    
      local path = system.pathForFile( theFile, system.DocumentsDirectory )
    
      -- io.open opens a file at path. returns nil if no file found
      local file = io.open( path, "r" )
      if file then
        -- read all contents of file into a string
        local contents = file:read( "*a" )
        io.close( file )
        return contents
      else
        -- create file b/c it doesn't exist yet
        file = io.open( path, "w" )
        file:write( "0" )
        io.close( file )
        return "0"
      end
    end
    
    -- DATA SAVING
    local level2Unlocked = 1
    local level2Filename = "level2.data"
    local loadedLevel2Unlocked = loadValue( level2Filename )
    
  4. 创建一个create()事件,并移除"mainmenu""level1""level2"场景:

    -- Called when the scene's view does not exist:
    function scene:create( event )
      local sceneGroup = self.view
    
      -- completely remove maingame and options
      composer.removeScene( "mainmenu" )
      composer.removeScene( "level1" )
      composer.removeScene( "level2" )
    
      print( "\nlevelselect: create event" )
    end
    
  5. 接下来,创建一个show()事件和一个数组,其中包含设置为 iTunes Connect 中应用内购买的产品 ID的字符串:

    function scene:show( event )
      local sceneGroup = self.view
    
      print( "levelselect: show event" )
    
      local listOfProducts = 
      {
        -- These Product IDs must already be set up in your store
        -- Replace Product ID with a valid one from iTunes Connect
        "com.companyname.appname.NonConsumable", -- Non Consumable In-App Purchase
      }
    
  6. validProductsinvalidProducts添加一个本地空表。创建一个名为unpackValidProducts()的本地函数,检查有效的和无效的产品 ID:

      local validProducts = {} 
        local invalidProducts = {}
    
        local unpackValidProducts = function()
            print ("Loading product list")
            if not validProducts then
                native.showAlert( "In-App features not available", "initStore() failed", { "OK" } )
            else
              print( "Found " .. #validProducts .. " valid items ")
                for i=1, #invalidProducts do
                  -- Debug:  display the product info 
                    native.showAlert( "Item " .. invalidProducts[i] .. " is invalid.",{ "OK" } )
                    print("Item " .. invalidProducts[i] .. " is invalid.")
                end
    
            end
        end
    
  7. 创建一个名为loadProductsCallback()的本地函数,带有一个event参数。设置处理程序以使用打印语句接收产品信息:

      local loadProductsCallback = function( event )
        -- Debug info for testing
            print("loadProductsCallback()")
            print("event, event.name", event, event.name)
            print(event.products)
            print("#event.products", #event.products)
    
            validProducts = event.products
            invalidProducts = event.invalidProducts    
            unpackValidProducts ()
        end
    
  8. 创建一个名为 transactionCallback() 的局部函数,带有 event 参数。为每个 transaction.state 事件可能发生的结果添加几种情况。当商店完成交易时,在函数结束前调用 store.finishTransaction(event.transaction)。设置另一个名为 setUpStore() 的局部函数,带有 event 参数,以调用 store.loadProducts(listOfProducts, loadProductsCallback)

      local transactionCallback = function( event )
        if event.transaction.state == "purchased" then 
          print("Transaction successful!")
            saveValue( level2Filename, tostring(level2Unlocked) 
        elseif event.transcation.state == "restored" then 
          print("productIdentifier", event.transaction.productIdentifier)
          print("receipt", event.transaction.receipt)
          print("transactionIdentifier", event.transaction.transactionIdentifier)
          print("date", event.transaction.date)
          print("originalReceipt", event.transaction.originalReceipt)
        elseif event.transaction.state == "cancelled" then
          print("Transaction cancelled by user.")
        elseif event.transaction.state == "failed" then
          print("Transaction failed, type: ", event.transaction.errorType, event.transaction.errorString)
          local alert = native.showAlert("Failed ", infoString,{ "OK" })
        else
          print("Unknown event")
          local alert = native.showAlert("Unknown ", infoString,{ "OK" })
        end
        -- Tell the store we are done with the transaction.
        store.finishTransaction( event.transaction )
        end
    
        local setupMyStore = function(event)
          store.loadProducts( listOfProducts, loadProductsCallback)
          print ("After store.loadProducts(), waiting for callback")
        end
    
  9. 设置背景和关卡1按钮的显示对象:

      local backgroundImage = display.newImageRect( "levelSelectScreen.png", 480, 320 )
      backgroundImage.x = 240; backgroundImage.y = 160
      sceneGroup:insert( backgroundImage )
    
      local level1Btn = movieclip.newAnim({"level1btn.png"}, 200, 60)
      level1Btn.x = 240; level1Btn.y = 100
      sceneGroup:insert( level1Btn )
    
      local function level1touch( event )
        if event.phase == "ended" then
          audio.play( tapSound )
          composer.gotoScene( "loadlevel1", "fade", 300  )
        end
      end
      level1Btn:addEventListener( "touch", level1touch )
      level1Btn:stopAtFrame(1)
    
  10. 设置关卡2按钮的位置:

      -- LEVEL 2
      local level2Btn = movieclip.newAnim({"levelLocked.png","level2btn.png"}, 200, 60)
      level2Btn.x = 240; level2Btn.y = 180
      sceneGroup:insert( level2Btn )
    
  11. 使用局部函数 onBuyLevel2Touch(event) 并创建一个 if 语句,检查 event.phase == ended and level2Unlocked ~= tonumber(loadedLevel2Unlocked),以便场景切换到 mainmenu.lua

      local onBuyLevel2Touch = function( event )
        if event.phase == "ended" and level2Unlocked ~= tonumber(loadedLevel2Unlocked) then
          audio.play( tapSound )
          composer.gotoScene( "mainmenu", "fade", 300  )
    
  12. 在同一个 if 语句中,创建一个名为 buyLevel2() 的局部函数,带有 product 参数,以调用 store.purchase() 函数:

        local buyLevel2 = function ( product ) 
          print ("Congrats! Purchasing " ..product)
    
         -- Purchase the item
          if store.canMakePurchases then 
            store.purchase( {validProducts[1]} ) 
          else
            native.showAlert("Store purchases are not available, please try again later",  { "OK" } ) – Will occur only due to phone setting/account restrictions
          end 
        end 
        -- Enter your product ID here
         -- Replace Product ID with a valid one from iTunes Connect
     buyLevel2("com.companyname.appname.NonConsumable")
    
    
  13. 添加一个 elseif 语句,以检查在交易完成后,是否已购买并解锁了关卡 2:

        elseif event.phase == "ended" and level2Unlocked == tonumber(loadedLevel2Unlocked) then
          audio.play( tapSound )
          composer.gotoScene( "loadlevel2", "fade", 300  )
        end
      end
      level2Btn:addEventListener( "touch", onBuyLevel2Touch )
    
      if level2Unlocked == tonumber(loadedLevel2Unlocked) then
        level2Btn:stopAtFrame(2)
      end
    
  14. 使用 store.init() 激活应用内购买,并将 transactionCallback() 作为参数调用。同时以 500 毫秒的定时器调用 setupMyStore()

      store.init( "apple", transactionCallback) 
        timer.performWithDelay (500, setupMyStore)
    
  15. 创建一个关闭的 UI 按钮,以及一个名为 onCloseTouch() 的局部函数,带有事件参数。让该函数在释放关闭按钮时,切换到 loadmainmenu.lua 场景。使用 end 结束 enterScene() 事件:

      local closeBtn
    
      local onCloseTouch = function( event )
        if event.phase == "release" then
    
          audio.play( tapSound )
          composer.gotoScene( "loadmainmenu", "fade", 300  )
    
        end
      end
    
      closeBtn = ui.newButton{
        defaultSrc = "closebtn.png",
        defaultX = 100,
        defaultY = 30,
        overSrc = "closebtn.png",
        overX = 105,
        overY = 35,
        onEvent = onCloseTouch,
        id = "CloseButton",
        text = "",
        font = "Helvetica",
        textColor = { 255, 255, 255, 255 },
        size = 16,
        emboss = false
      }
    
      closeBtn.x = 80; closeBtn.y = 280
      closeBtn.isVisible = false
      sceneGroup:insert( closeBtn )
    
      menuTimer = timer.performWithDelay( 200, function() closeBtn.isVisible = true; end, 1 )
    
    end
    
  16. 创建 hide()destroy() 事件。在 hide() 事件中,取消 menuTimer 定时器。为场景事件添加所有事件监听器并 return scene

    -- Called when scene is about to move offscreen:
    function scene:hide()
    
      if menuTimer then timer.cancel( menuTimer ); end
    
        print( "levelselect: hide event" )
    
      end
    
    -- Called prior to the removal of scene's "view" (display group)
    function scene:destroy( event )
    
      print( "destroying levelselect's view" )
    end
    
    -- "create" event is dispatched if scene's view does not exist
    scene:addEventListener( "create", scene )
    
    -- "show" event is dispatched whenever scene transition has finished
    scene:addEventListener( "show", scene )
    
    -- "hide" event is dispatched before next scene's transition begins
    scene:addEventListener( "hide", scene )
    
    -- "destroy" event is dispatched before view is unloaded, which can be
    scene:addEventListener( "destroy", scene )
    
    return scene
    
  17. 保存文件,并在 Corona 模拟器中运行项目。当你点击播放按钮时,你会在关卡选择屏幕上注意到一个1按钮和一个锁定按钮。当你按下锁定按钮时,它会调用商店进行交易。你会在终端中注意到一条打印语句,显示正在参考哪个产品 ID进行购买。完整的内购功能无法在模拟器中测试。你将需要创建一个发行版本,并在 iOS 设备上上传以在商店中发起购买。行动时间 – 使用 Corona 商店模块创建应用内购买

刚才发生了什么?

在此示例中,我们使用了 BeebeGames 类中的 saveValue()loadValue() 函数,来实现如何通过电影剪辑作为按钮,使我们的锁定关卡从锁定模式转变为解锁模式。local listOfProducts 中的数组以字符串格式显示产品 ID。在此示例中,产品 ID 需要是一种非消耗性应用内购买类型,并且必须在 iTunes Connect 中已存在。

unpackValidProducts()函数检查应用内购买中有多少有效和无效的商品。loadProductsCallback()函数接收商店中的产品信息。transactionCallback(event)函数检查每种状态:"purchased""restored""cancelled""failed"。在应用内购买中实现"purchased"状态时,会调用saveValue()函数来更改level2.data的值。交易完成后,需要调用store.finishTransaction(event.transaction)来告诉商店你的购买已经完成。

setupMyStore(event)函数调用store.loadProducts(listOfProducts, loadProductsCallback)并检查应用程序中可用的产品 ID(或 IDs)。一旦store.init(transactionCallback)初始化并调用setupMyStore(),事件就会被处理。

onBuyLevel2Touch(event)函数允许我们检查是否已为锁定级别进行了应用内购买。当用户能够购买并接受应用内购买时,将处理交易,level2Unlocked的值将与tonumber(loadedLevel2Unlocked)相匹配。buyLevel2(product)函数一旦产品 ID 返回有效,就会使用store.purchase()验证购买的商品。

应用内购买完成后,屏幕会过渡到主菜单,允许锁定按钮变为级别2的按钮。一旦按钮变为帧 2,级别 2 就可以访问了。

尝试英雄——处理多个产品 ID

既然你知道如何为单一产品创建应用内购买,尝试为同一应用程序添加多个产品。场景是开放式的。

你可以添加以下内容:

  • 更多可供购买的级别

  • 如果你的游戏有主角,可以设置多种角色供用户扮演。

  • 为你的应用程序添加新的背景场景

你如何处理商店的新产品完全由你决定。

测试应用内购买

你需要确保购买能够正确进行。苹果提供了一个沙盒环境,允许你测试应用内购买。沙盒环境与 App Store 使用相同的模型,但不会处理实际支付。交易会返回,就像支付已经成功处理一样。在提交给苹果审核之前,测试应用内购买在沙盒环境中是必须的。

在沙盒环境中测试时,你需要创建一个与当前 iTunes Connect 账户不同的独立用户测试账户。在沙盒环境中测试你的商店时,不允许使用你的当前账户。

用户测试账户

当您登录到您的 iTunes Connect 账户时,您需要从主页选择管理用户链接。在选择用户类型页面选择测试用户。添加一个新用户,并确保测试账户使用的电子邮件地址没有与其他任何 Apple 账户关联。所有测试账户在测试应用内购买时只应在测试环境中使用。当所有信息填写完毕后,点击保存按钮。

创建用户测试账户后,您需要确保在设备的商店设置中已登出您的 Apple 账户。这将防止在测试应用内购买时使用非测试账户。当应用内购买沙盒提示时,您只能登录到您的用户测试账户以测试应用程序。在启动应用程序之前,不要登录到您的测试账户。这将防止它使您的测试账户无效。

行动时间 – 使用 Breakout 应用内购买演示测试应用内购买

在您可以在 iOS 设备上测试应用内购买之前,请确保您在 iTunes Connect 中有一个测试用户账户。同时,请确保您使用临时分发配置文件为要测试应用内购买功能的应用创建了一个分发构建。如果您按照本章前面的所有步骤操作,通过商店进行购买测试将相应地顺利进行:

  1. 在 Corona 模拟器中,创建 Breakout 应用内购买演示的分发构建。一旦构建完成编译,将构建上传到您的 iOS 设备。

  2. 保持设备与您的机器连接,并启动 Xcode。从工具栏中,转到窗口 | 组织者。一旦进入组织者,在设备部分选择已连接的设备,然后选择控制台。这将允许您检查设备上的控制台输出,以捕获代码中的调试信息(即打印语句)以及任何应用程序崩溃。

  3. 在启动应用程序之前,您需要在设备上选择设置图标。向上滚动直到看到商店图标并选择它。行动时间 – 使用 Breakout 应用内购买演示测试应用内购买

  4. 如果您已登录 iTunes 商店账户,请登出,这样您就可以在沙盒环境中测试应用内购买。行动时间 – 使用 Breakout 应用内购买演示测试应用内购买

  5. 从您的设备上启动 Breakout 应用内购买演示。选择播放按钮,然后选择锁定按钮。屏幕将转回主菜单,并弹出一个窗口以确认您的应用内购买。按下确定继续购买。行动时间 – 使用 Breakout 应用内购买演示测试应用内购买

  6. 接下来,你将看到一个窗口,提示你使用 Apple ID 登录。在这里,你需要使用在 iTunes Connect 中创建的测试用户账户登录。不要使用用于登录 iTunes Connect 的实际 Apple 账户。行动时间——使用 Breakout In-App Purchase Demo 测试应用内购买

  7. 登录后,再次选择播放按钮。你会注意到2按钮已经被解锁。选择它后,你将可以访问那个场景。行动时间——使用 Breakout In-App Purchase Demo 测试应用内购买

  8. 退出应用程序并参考控制台。你会注意到来自设备的输出和你的代码中一些熟悉的打印语句。控制台日志显示了用于应用内购买的产品 ID,并通知你它是否有效以及交易是否成功。行动时间——使用 Breakout In-App Purchase Demo 测试应用内购买

  9. 如果你想要确保应用内购买确实有效,请从你的设备上删除应用程序,并退出你的测试用户账户。上传同样的版本到你的设备上——无需创建新的版本。启动应用程序并重新运行应用内购买。使用同样的测试用户账户登录。你应该会看到一个弹出窗口,提示你已经购买了该产品,并询问你是否希望再次免费下载。收到通知意味着你的应用内购买成功了。行动时间——使用 Breakout In-App Purchase Demo 测试应用内购买

刚才发生了什么?

正确遵循应用内购买测试步骤非常重要。为了确保你在沙盒环境中获得准确的结果,从商店设置中退出你的 Apple 账户是整个流程的关键。

启动应用程序并通过按下锁定按钮调用商店功能后,你会注意到应用内购买的商品的显示名称和价格。如果你正确实现,它应该与你在 iTunes Connect 中创建的内容相匹配。

使用在 iTunes Connect 中创建的测试用户账户登录后,假设苹果服务器端没有问题或设备连接没有问题,交易应该能够顺利进行,不会出现任何错误。在关卡选择屏幕上的第 2 级将会被解锁并可以访问。恭喜你!你已经创建了一个应用内购买(In-App Purchase)。

动手试试——使用其他类型的应用内购买英雄(注:此处"Have a go hero"可能指的是一种鼓励尝试的挑战,直译可能不太通顺,故保留原文,仅将"using other In-App Purchase types"翻译为“使用其他类型的应用内购买”)

在 Breakout In-App Purchase Demo 中,我们更关注非消耗性应用内购买。尝试将消耗性、自动续订或非续订订阅与你自己应用整合。

那些包含消耗性产品的应用是那些在免费游戏环境中需要货币购买或建造物品的游戏。订阅产品可以针对那些永不结束且不断更新新关卡的游戏,或者可能需要在线服务器在多人环境中交互的游戏。看看您能想出什么!

关于应用内购买的小测验。

Q1. 非消耗性购买是什么?

  1. 用户只需购买一次的产品。

  2. 用户每次需要该项物品时都需要购买的产品。

  3. 允许用户购买一定时间期限内容的产品。

  4. 用户每次到期都需要续订的订阅。

Q2. 关于测试应用内购买,以下哪个是正确的?

  1. 您需要始终登录到您的账户。

  2. 您的 Apple 账户用于测试应用内购买。

  3. 当在应用内购买沙盒中提示时,登录您的用户测试账户。

  4. 以上都不是。

Q3. 测试应用内购买必须使用哪种配置文件?

  1. 开发配置文件。

  2. Ad Hoc 分发配置文件。

  3. App Store 分发配置文件。

  4. 以上都不是。

总结。

我们终于看到了隧道尽头的光明。至此,您应该对如何在您的游戏中实现应用内购买有了初步了解。这是一个非常耗时的过程,需要组织、设置代码,并在沙盒环境中测试准确的购买。

本章节中讲解了以下内容:

  • 如何在 iTunes Connect 中为应用内购买设置产品 ID。

  • 使用 Corona 的商店模块实现购买项目。

  • 在 iTunes Connect 中添加测试用户账户。

  • 在设备上测试应用内购买。

掌握应用内购买的概念可能需要一些时间。最好研究示例代码,并查看与 Corona 的商店模块相关的功能。

请查看苹果的应用内购买编程指南developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StoreKitGuide/StoreKitGuide.pdf,以及 Corona Labs 网站 API 参考部分中的应用内购买,了解更多关于此话题的参考资料。

经过 11 章的学习,我们已经到了这本书的结尾。现在,您已经获得了足够的知识来创建自己的应用程序,并在 Apple App Store 或 Google Play Store 中销售。希望您获得的所有信息都能有所帮助。我期待听到您使用 Corona SDK 开发的游戏!

附录 A. 小测验答案

第一章:– 开始使用 Corona SDK

小测验 – 了解 Corona

Q1 使用 Corona 模拟器有哪些正确之处? 1
Q2 在 iPhone 开发者计划中,你可以使用多少个 iOS 设备进行开发? 4
Q3 使用 Corona SDK 为 Android 构建时,版本代码需要是什么? 2

第二章:– Lua 速成课程和 Corona 框架

小测验 – Lua 基础

Q1 以下哪些是值? 4
Q2 哪个关系运算符是错误的? 3
Q3 正确缩放对象在x方向的方法是什么? 4

第三章:– 建立我们的第一个游戏 – Breakout

小测验 – 构建游戏

Q1 在代码中添加物理引擎时,哪些函数可以添加到你的应用程序中? 4
Q2 添加事件监听器时以下哪个是正确的? 4
Q3 以下显示对象正确过渡到x = 300, y = 150并将 alpha 改为 0.5,需要 2 秒的方法是什么? 1

第四章:– 游戏控制

小测验 – 使用游戏控制

Q1 正确从舞台移除显示对象的方法是什么? 3

| Q2 将以下显示对象正确转换为物理对象的方法是什么? |

local ball = display.newImage("ball.png")
3

| Q3 在以下函数中,"began"最好表示什么意思? |

local function onCollision( event )
  if event.phase == "began" and event.object1.myName == "Box 1" then

    print( "Collision made." )

  end
end
4

第五章:– 让我们的游戏动起来

小测验 – 动画图形

Q1 正确暂停图像表动画的方法是什么? 1
Q2 如何使动画序列无限循环? 3
Q3 如何创建一个新的图像表? 4

第六章:– 播放声音和音乐

小测验 – 关于音频的一切

Q1 正确清除内存中音频文件的方法是什么? 3
Q2 应用程序中可以同时播放多少个音频通道? 4
Q3 如何使音频文件无限循环? 1

第七章:– 物理 – 下落物体

小测验 – 动画图形

Q1 有哪个功能可以获取或设置文本对象的文本字符串? 1
Q2 有哪个函数能将任何参数转换成字符串? 3
Q3 哪种体型受到重力和其他体型碰撞的影响? 1

第八章:– 操作 Composer

小测验 – 游戏过渡和场景

Q1 使用 Composer 改变场景时需要调用哪个函数? 2
Q2 有哪个函数能将任何参数转换成数字或 nil? 1
Q3 如何暂停一个计时器? 3
Q4. 如何恢复一个计时器? 2

第九章:– 处理多设备和网络应用

小测验 – 处理社交网络

Q1 缩放高分辨率精灵表的特定 API 是什么? 2
Q2 在 Facebook 上允许在用户墙发布内容的发布权限叫什么? 2
Q3 facebook.login()需要哪些参数? 4

第十章:– 优化、测试和发布你的游戏

小测验 – 发布应用

Q1 创建 iOS Distribution Provisioning 文件时,需要使用哪种分发方法? 2
Q2 提交的 iOS 应用程序的状态应在哪里查询? 1
Q3 在 Google Play 商店中构建应用需要什么? 4

第十一章:– 实现应用内购买

突击测验 – 关于应用内购买的一切

Q1 非消耗性购买是什么? 1
Q2 关于测试应用内购买,以下哪项是正确的? 3
Q3 测试应用内购买必须使用哪种类型的 Provisioning Profile? 2
posted @ 2024-05-23 11:06  绝不原创的飞龙  阅读(14)  评论(0编辑  收藏  举报