Corona-SDK-移动游戏开发初学者指南-全-
Corona SDK 移动游戏开发初学者指南(全)
原文:
zh.annas-archive.org/md5/a062c0acf1c6eb24d4dce7039ad45f82
译者:飞龙
前言
这本书旨在介绍你在 iOS 和 Android 平台上使用 Corona SDK 的基本标准。通过构建三个独特的游戏,你将增强你的学习体验。除了开发游戏,你还将深入了解社交网络集成、内购、盈利以及将你的应用程序发布到 App Store 和/或 Google Play Store。
本书涵盖内容
第一章, 开始使用 Corona SDK,首先教用户如何在 Mac OSX 和 Windows 操作系统上安装 Corona SDK。你将学习如何仅用两行代码创建你的第一个程序。最后,我们将介绍构建和加载应用程序到 iOS 或 Android 设备的过程。
第二章, Lua 快速入门与 Corona 框架,深入探讨了用于在 Corona SDK 中开发的 Lua 编程语言。我们将介绍 Lua 中变量、函数和数据结构的基础。本章还将介绍如何在 Corona 框架中实现各种显示对象。
第三章, 构建我们的第一个游戏:Breakout,讨论了构建第一个游戏 Breakout 的前半部分。我们将学习如何在 Corona 项目中结构化游戏文件并创建将在屏幕上显示的游戏对象。
第四章, 游戏控制,继续介绍构建第一个游戏 Breakout 的后半部分。我们将涵盖游戏对象的移动以及场景中对象的碰撞检测。你还将学习如何创建一个计分系统,该系统将实现游戏的胜利和失败条件。
第五章, 为我们的游戏添加动画,解释了如何使用电影剪辑和精灵表来为游戏添加动画。本章将深入探讨在创建新游戏框架时如何管理运动和过渡。
第六章, 播放声音和音乐,提供了如何将声音效果和音乐应用到你的应用程序中的信息。在增强我们游戏开发的感觉体验方面,包含某种类型的音频是至关重要的。你将学习如何通过加载、执行和循环技术使用 Corona 音频系统来集成音频。
第七章, 物理:下落物体,介绍了如何在 Corona SDK 中使用显示对象实现 Box2D 引擎。你将能够自定义身体构造并处理下落物体的物理行为。在本章中,我们将应用动态/静态身体的用途并解释后碰撞的目的。
第八章, 操作 Storyboard,讨论了如何使用 Storyboard API 管理你所有的游戏场景。我们还将详细介绍菜单设计,例如创建暂停菜单和主菜单。此外,你还将学习如何在游戏中保存高分。
第九章, 处理多个设备和网络应用,提供了关于将你的应用程序与社交网络如 Twitter 或 Facebook 集成的信息。还将讨论的其他社交功能包括使用 OpenFeint 集成成就和排行榜。这将使你的应用能够触及全球更广泛的受众。
第十章, 优化、测试和发布你的游戏,解释了 iOS 和 Android 设备的应用程序提交流程。本章将指导你如何设置 App Store 的发布配置文件,并在 iTunes Connect 中管理你的应用信息。Android 开发者将学习如何为发布签名他们的应用程序,以便提交到 Google Play Store。
第十一章, 实现应用内购买,介绍了通过创建消耗品、非消耗品或订阅购买来使你的游戏盈利。你将使用 Corona 的商店模块在 App Store 中应用应用内购买。我们将查看在设备上测试购买情况,以查看是否已使用沙盒环境应用了交易。
你需要为此书准备以下内容
在你开始使用 Corona SDK 为 Mac 开发游戏之前,你需要以下物品:
-
如果你正在安装 Corona for Mac OS X,请确保你的系统如下:
-
Mac OS^® X 10.6 或更高版本
-
运行 Snow Leopard 或 Lion 的基于 Intel 的系统
-
64 位 CPU(Core 2 Duo)
-
OpenGL 1.4 或更高版本的图形系统
-
-
你必须注册为苹果开发者计划成员
-
XCode
-
一个文本编辑器,如 TextWrangler、BBEdit 或 TextMate
在你开始使用 Corona SDK 为 Windows 开发游戏之前,你需要以下物品:
-
如果你正在运行 Microsoft Windows,请确保你的系统如下:
-
Windows 7、Vista 或 XP 操作系统
-
推荐使用 1 GHz 的处理器
-
至少 38 MB 的磁盘空间(最小)
-
至少 1 GB 的 RAM(最小)
-
OpenGL 1.3 或更高版本的图形系统(大多数现代 Windows 系统都可用)
-
-
Java 6 SDK
-
一个文本编辑器,如 Notepad++或 Crimson Editor
如果你想要为 Android 设备提交和发布应用,你必须注册为 Google Play 开发者。
游戏教程需要与本书一起提供的资源文件,可以从 Packt 网站下载。
最后,你需要 Corona SDK 的最新稳定版本:版本 2011.704。这适用于测试驱动器和订阅者。
本书面向的对象
这本书是为任何想要尝试为 Android 和 iOS 创建商业成功游戏的人而写的。你不需要游戏开发或编程经验。
术语
在这本书中,你会发现一些经常出现的标题。
为了清楚地说明如何完成一个程序或任务,我们使用:
行动时间——标题
-
动作 1
-
动作 2
-
动作 3
指示通常需要一些额外的解释,以便它们有意义,因此它们后面跟着:
刚才发生了什么?
这个标题解释了你刚刚完成的任务或指令的工作原理。
你还会在书中找到一些其他的学习辅助工具,包括:
快速问答——标题
这些是旨在帮助你测试自己理解的简短多项选择题。
尝试英雄——标题
这些设置了一些实际挑战,并为你提供了实验你所学内容的想法。
你还会发现许多不同风格的文本,以区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。
文本中的代码词汇如下所示:“继续更改当前显示对象的值,textObject."
。
代码块以如下方式设置:
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
当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
textObject = display.newText( "Hello World!", 50, 40, native.systemFont, 36 )
textObject:setTextColor( 124,252,0 )
任何命令行输入或输出都如下所示:
--This is number 4
--This is number 5
新术语和重要词汇以粗体显示。你会在屏幕上看到这些词汇,例如在菜单或对话框中,文本中显示如下:“一旦你这样做,在Corona 模拟器菜单栏下,选择窗口 | 查看 | iPhone 4"。
注意
警告或重要注意事项以如下方式显示。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们读者的反馈总是受欢迎的。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>
,并在邮件主题中提及书名。
如果有一本书你需要并且希望我们出版,请通过www.packtpub.com上的建议书名表单或发送电子邮件至<suggest@packtpub.com>
联系我们。
如果你在一个主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们关于www.packtpub.com/authors的作者指南。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。
下载示例代码
您可以从www.PacktPub.com
的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
错误更正
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何错误更正,请通过访问www.packtpub.com/support
,选择您的书籍,点击错误提交表单链接,并输入您的错误更正详情。一旦您的错误更正得到验证,您的提交将被接受,错误更正将被上传到我们的网站,或添加到该标题的“错误更正”部分下的现有错误更正列表中。您可以通过从www.packtpub.com/support
选择您的标题来查看任何现有的错误更正。
盗版
在互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何我们作品的非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过发送电子邮件到<copyright@packtpub.com>
并提供涉嫌盗版材料的链接与我们联系。
我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过发送电子邮件到<questions@packtpub.com>
与我们联系,我们将尽力解决。
第一章. 使用 Corona SDK 入门
在我们直接开始编写一些简单的游戏代码之前,我们需要安装和运行使我们的应用程序变得生动的必要程序。Corona SDK主要是一个 2D 开发引擎。如果你有 iOS 或 Android 开发的经验,你会发现使用 Corona SDK 的感觉很清新,使用起来也很简单。很快,你就能创建出可以在 iPhone App Store 和 Google Play Store 上分发的成品。
在本章中,我们将:
-
在 Mac OS X 和 Windows 上设置 Corona SDK
-
为 Mac OS X 安装 Xcode
-
用两行代码创建一个 Hello World 程序
-
在 iOS 配置文件门户中添加设备
-
将应用程序加载到 iOS 设备上
-
将应用程序加载到 Android 设备上
下载和安装 Corona
你可以选择在 Mac OS X 或 Microsoft Windows 操作系统上开发。请记住运行程序所需的以下系统要求。
如果你正在为 Mac OS X 安装 Corona,请确保你的系统如下:
-
Mac OS^® X 10.6 或更高版本
-
运行 Snow Leopard 或 Lion 的基于 Intel 的系统
-
64 位 CPU(Core 2 Duo)
-
OpenGL 1.4 或更高版本的图形系统
如果你正在运行 Microsoft Windows,请确保你的系统如下:
-
Windows 7、Vista 或 XP 操作系统
-
推荐的 1 GHz 处理器
-
至少 38MB 的磁盘空间
-
至少 1GB 的 RAM
-
OpenGL 1.3 或更高版本的图形系统(大多数现代 Windows 系统都可用)
行动时间——在 Mac OS X 上设置和激活 Corona
让我们从在桌面设置 Corona SDK 开始。
-
如果你还没有下载 SDK,请在此下载:
www.anscamobile.com/corona/
。在你可以访问 SDK 之前,你必须注册为用户。 -
任何 Mac 程序的文件扩展名应以
.dmg
结尾,也称为Apple 磁盘映像。一旦你下载了磁盘映像,双击磁盘映像文件以挂载它。名称应类似于CoronaSDK.dmg
。一旦加载,你应该会看到如下截图所示的挂载的磁盘映像文件夹:
-
接下来,将
CoronaSDK
文件夹拖到Applications
文件夹中。这将把 Corona 文件夹的内容复制到/Applications
。如果你不是账户的主要管理员,你将被提示输入管理员密码。一旦成功安装,你将在/Applications
中看到CoronaSDK
文件夹。为了方便访问文件夹内容,通过将
CoronaSDK
文件夹拖到 Mac 桌面上的 Dock 创建一个别名。第一次使用 Corona SDK 的用户在可以访问之前必须执行一个快速且简单的一次性授权过程。
注意
你必须连接到互联网才能完成授权过程。
-
在 SDK 文件夹中启动 Corona 模拟器。
-
假设这是您第一次使用,您将看到一个最终用户许可协议(EULA)。一旦您接受协议,输入您用于注册 Corona 的电子邮件和密码以激活 SDK。否则,点击注册以创建账户。
Note
如果您以试用用户身份注册 Corona,在 iOS 和/或 Android 设备上进行开发无需付费。希望将应用程序发布到 App Store 或 Google Play 的开发者需要在网站上购买 Corona SDK 订阅,具体操作如下:
www.anscamobile.com/corona/
。 -
登录成功后,您将看到一个确认对话框,显示 SDK 已准备好使用,如下截图所示:
-
点击继续按钮,您将看到欢迎使用 Corona屏幕,如下截图所示:
刚才发生了什么?
在您的 Mac 操作系统上设置 Corona SDK 与安装任何其他专用 Mac 程序一样简单。在您的机器上授权 SDK 并使用电子邮件和密码登录后,它将准备好使用。从现在开始,每次您启动 Corona 时,它将自动登录到您的账户。您会注意到,当这种情况发生时,您会看到一个Corona SDK屏幕。
Time for action—setting up and activating Corona on Windows
让我们在桌面上设置 Corona SDK。
-
从以下 URL 下载 Corona SDK:
www.anscamobile.com/corona/
。在您能够访问 SDK 之前,您必须注册为用户。 -
Corona 的 Windows 版本文件扩展名应以
.msi
结尾,也称为Windows Installer,这是微软为安装程序而制作的 Windows 组件。双击该文件。文件名应类似于CoronaSDK.msi
。 -
按照屏幕上的安装说明进行操作。
-
Corona 默认会直接安装到您的
Programs
文件夹中。在 Microsoft Windows 上,从开始菜单中选择Corona程序列表中的Corona,或在桌面上双击 Corona 图标。激活成功后,您应该会看到以下屏幕:Note
一旦您第一次启动 Corona,激活 SDK 的过程应该与 Mac 操作过程相同。
Tip
如果您遇到图像无法正确显示的问题,请检查您是否使用的是 1.3 或更高版本的最新 OpenGL 图形驱动程序。
Note
注意,Windows 上的 Corona SDK 只能为 Android 设备构建,不能为 iOS 设备(iPhone、iPad 或 iPod Touch)构建。Mac 可以在 Corona 中为 iOS 设备构建,也可以为 Android 构建应用程序。
-
要创建设备构建,需要在你的 PC 上安装 Java 6 SDK。你需要访问 JDK 下载页面:
jdk6.java.net/download.html
并点击Java SE 6选项下的下载按钮。 -
在下一页,选择接受许可协议单选按钮,然后点击Windows x86链接下载安装程序。如果你还没有,你将被要求在 Oracle 网站上登录或创建一个用户账户。
-
一旦下载了 JDK,就运行安装程序。安装完成后,你将能够在你的 PC 上创建 Android 的设备构建。
刚才发生了什么?
在 Windows 上安装 SDK 的设置与 Mac OS X 不同。在执行安装文件时,Windows 将自动提供一个指定的位置来安装应用程序,例如Programs
文件夹,这样你就不必手动选择目的地。安装成功后,你将在桌面上看到 Corona SDK 图标,方便访问,或者在开始菜单的程序列表中突出显示,假设你是第一次访问。当你授权 Corona 在你的机器上,并使用登录信息登录时,它就准备好供你使用了,并且每次启动时都会自动登录。
在 Mac 和 Windows 上使用模拟器
在 Mac OS X 上,通过从“应用程序目录”中选择Corona Terminal
或Corona Simulator
来启动 Corona SDK。这两个选择都将访问 SDK。Corona Simulator
只会打开模拟器。Corona Terminal
将打开模拟器和终端窗口。终端有助于调试你的程序,并显示模拟器错误/警告和print()
消息。
在 Microsoft Windows 上,选择Corona SDK
文件夹,然后从你的开始菜单中的程序列表中选择Corona Simulator,或者在桌面上双击 Corona 图标。(如果你使用 Windows,模拟器和终端总是同时打开)。
让我们来看看Corona SDK
文件夹中包含的有用内容(位于 Mac 上的Applications/Corona SDK
和 Windows 上的Start/All Programs/Corona SDK
):
-
调试器(Mac)/Corona Debugger(Windows)—用于查找和隔离代码中的问题的工具。
-
Corona Simulator—用于测试你的应用程序的环境。它模拟你在本地计算机上开发的移动设备。(在 Windows 上,它将打开模拟器和终端)。
-
Corona Terminal—启动Corona Simulator并打开一个终端窗口来显示错误/警告消息和
print()
语句。这对于调试你的代码非常有帮助(仅在 Mac 上)。 -
模拟器——具有与
Corona Terminal
相同的属性,但仅在 Mac 上从命令行调用。 -
SampleCode——一组示例应用程序,帮助你开始使用 Corona。包含与代码和艺术资源一起工作的代码。
当你启动模拟器时,Corona SDK窗口默认打开。你可以在模拟器中打开一个 Corona 项目,创建用于测试或分发的设备构建,并查看一些示例游戏和应用,以便熟悉 SDK。
行动时间——在模拟器中查看示例项目
让我们来看看模拟器中的HelloPhysics
示例项目:
-
在Corona SDK窗口中点击模拟器。
-
在出现的打开对话框中,导航到
Applications/CoronaSDK/SampleCode/Physics/HelloPhysics
(Mac)或C:\Program Files\Ansca\Corona SDK\Sample Code\Physics\HelloPhysics
(Windows)。在 Mac 上,点击打开,它将自动打开main.lua
。在 Windows 上,双击main.lua
以打开文件。HelloPhysics
应用程序在模拟器中打开并运行。
刚才发生了什么?
通过Corona Terminal
或Corona Simulator
访问 SDK 取决于你的操作偏好。许多 Mac 用户更喜欢使用Corona Terminal
,这样他们可以跟踪终端输出的消息,特别是用于调试目的。当你通过Corona Simulator
启动 SDK 时,模拟器会显示,但不会显示终端窗口。当 Windows 用户启动Corona Simulator时,它将显示模拟器和终端窗口。当你想尝试 Corona 提供的任何示例应用程序时,这是一个很好的选择。
main.lua
文件是一个特殊的文件名,它告诉 Corona 在项目文件夹中的起始位置。此文件还可以加载其他代码文件或其他程序资源,如声音或图形。
当你在 Corona 中启动HelloPhysics
应用程序时,你会在模拟器中观察到从屏幕顶部落下的盒子对象与地面对象发生碰撞。从启动main.lua
文件到在模拟器中查看结果的过程几乎是即时的。
尝试不同的设备外壳
当你开始熟悉Corona Simulator
时,无论是在 Windows 还是 Mac OSX 上,当你启动一个应用程序时,总是使用默认设备。Windows 使用 Droid 作为默认设备,而 Mac OS X 使用标准的 iPhone。尝试在不同的设备外壳上启动示例代码,以查看模拟器提供的所有设备上的屏幕分辨率差异。
当将构建移植到多个平台时,你必须考虑 iOS 和 Android 设备上的各种屏幕分辨率。构建是所有源代码编译成的一个文件。将你的游戏构建配置为多个平台可以扩大你应用程序的受众范围。
选择文本编辑器
Corona 没有指定的程序编辑器来编写代码,因此您需要找到适合您需求的编辑器。
对于 Mac OS,TextWrangler是一个不错的选择,而且也是免费的!你可以在www.barebones.com/products/textwrangler/download.html
下载它。其他文本编辑器,如www.barebones.com/thedeck
的BBEdit和macromates.com/
的TextMate也非常好,但使用这些编辑器需要购买。TextMate 还兼容 Corona TextMate Bundle:www.ludicroussoftware.com/corona-textmate-bundle/index.html
。
对于 Microsoft Windows,推荐使用Notepad++,可以在notepad-plus-plus.org/
或www.crimsoneditor.com/
下载Crimson Editor。这两个编辑器都是免费的。
任何操作系统已经包含的文本编辑器,如 Mac 的 TextEdit 或 Windows 的 Notepad,都可以使用,但使用专为编程设计的编辑器会更容易。对于 Corona,使用支持 Lua 语法高亮的编辑器在编码时效果最佳。语法高亮通过为关键字和标点符号添加格式属性,使读者更容易区分代码和文本。
在设备上开发
如果你只想使用 Corona 模拟器,不需要下载 Apple 的Xcode或Android SDK。为了在 iOS 设备(iPhone、iPod Touch 和 iPad)上构建和测试你的代码,你需要注册为 Apple 开发者并创建和下载配置文件。如果你想开发 Android 应用,除非你想使用 ADB 工具来帮助安装构建和查看调试信息,否则不需要下载 Android SDK。
Corona 模拟器的试用版允许构建 Adhoc(用于 iOS)和调试构建(Android)以在您的设备上进行测试。当应用在设备上运行时,构建将包含一个试用用户消息框。如果您想为 Apple 的 App Store 或 Android 的 Google Play Store 构建,则需要购买 Corona 订阅。Corona 订阅者还可以享受仅订阅者可用的功能,如访问每日构建、Corona 论坛上的仅订阅者区域和 LaunchPad。
行动时间——下载和安装 Xcode
为了开发任何 iOS 应用,你需要注册 Apple 开发者计划,每年费用为 99 美元,并在 Apple 网站上developer.apple.com/programs/ios/
创建账户。
-
点击立即注册按钮,按照 Apple 的说明完成流程。
-
当你完成注册后,点击标记为Dev Centers部分下的 iOS 链接。
-
如果您使用的是 Snow Leopard,请向下滚动到下载部分并下载当前的 Xcode。如果您使用的是 Lion,您可以在 Mac App Store 中下载 Xcode。
-
一旦您完全下载了 Xcode,双击
.mpkg
安装包(图标是一个打开的棕色盒子)。默认情况下,Xcode 将安装到/Developer
文件夹中。您将被要求以管理员用户身份进行身份验证。 -
在您输入凭证后,点击确定按钮以完成安装。
-
当您安装了 Xcode 开发者工具后,可以通过启动 Xcode 并选择帮助菜单中的任何项目来访问文档。Xcode 和 Instruments 等开发者应用程序安装在
/Developer/Applications
中。您可以将以下应用程序图标拖到 Dock 中以方便访问。
刚才发生了什么?
我们刚刚介绍了如何在 Mac OS X 上安装 Xcode 的步骤。通过加入 Apple 开发者计划,您将能够访问网站上的最新开发工具。请记住,为了继续成为 Apple 开发者,您必须每年支付 99 美元的费用以保持您的订阅。
Xcode 文件相当大,所以下载所需的时间取决于您的互联网连接速度。当您的安装完成时,Xcode 将准备好使用。
行动时间——用两行代码创建 Hello World 应用程序
现在我们已经设置了模拟器和文本编辑器,让我们开始编写我们的第一个 Corona 程序!我们将要编写的第一个程序叫做Hello World。这是一个许多人在开始学习一门新编程语言时都会学习的传统程序。
-
打开您首选的文本编辑器并输入以下行:
textObject = display.newText( "Hello World!", 50, 40, native.systemFont, 36 ) textObject:setTextColor( 255,255,255)
-
接下来,在您的桌面上创建一个名为
Hello World
的文件夹。将前面的文本保存为名为main.lua
的文件到您的项目文件夹位置。 -
启动 Corona。您将看到Corona SDK界面。点击模拟器并导航到您刚刚创建的
Hello World
文件夹。您应该在这个文件夹中看到您的main.lua
文件,如下面的截图所示: -
在 Mac 上,点击打开按钮。在 Windows 上,选择
main.lua
文件并点击打开按钮。您将看到您的新程序在 Corona 模拟器中运行:
行动时间——修改我们的应用程序
在我们深入研究更复杂的示例之前,让我们修改一下您程序中的某些小细节。
-
让我们修改
main.lua
的第二行,使其显示如下:textObject = display.newText( "Hello World!", 50, 40, native.systemFont, 36 ) textObject:setTextColor( 124,252,0 )
-
保存你的文件,然后回到Corona 模拟器。模拟器会检测到文件的变化,并自动重新启动以应用新的更改。如果你在保存文件后模拟器没有自动重新启动,请按Command + R(Mac)/Ctrl + R(Windows)。
注意
随着你继续学习更多的 Corona 函数,你会发现一些文本值将是可选的。在这种情况下,我们需要使用所有五个值。
动手实践—给你的应用程序应用一个新的字体名称
现在我们来尝试一下字体名称。
-
将第一行更改为以下代码:
textObject = display.newText( "Hello World!", 50, 40, "Times New Roman", 36 )
-
在进行任何更改后,务必保存你的
main.lua
文件,然后在 Corona 中按Command+R(Mac)/Ctrl+R(Windows)重新启动模拟器以查看新的字体。如果你使用的是 Mac,通常在保存文件后模拟器会自动重新启动,或者它可能会询问你是否想要重新启动程序。
刚才发生了什么?
现在你已经制作了你的第一个完整的移动应用程序!更令人惊讶的是,这是一个完成的 iPhone、iPad 和 Android 应用程序。如果你创建一个构建,这个两行程序实际上会安装并在你的 iOS/Android 设备上运行。你现在已经看到了 Corona 的基本工作流程是什么样的。
如果你查看main.lua
文件中的第 2 行,你会注意到setTextColor
改变了Hello World!
的文本颜色。
颜色由三组RGB数字组成,表示颜色中包含的红、绿和蓝色的量。它们以三个从 0 到 255 的数值显示。例如,黑色是(0,0,0),蓝色是(0,0,255),白色是(255,255,255)。
继续尝试不同的颜色值,看看不同的结果。当你保存main.lua
文件并重新启动 Corona 时,你可以在模拟器中看到代码的更改。
当你查看main.lua
文件的第一行时,你会注意到newText()
是通过textObject
调用的,这是一个随后用来引用显示文本的名称。newText()
函数返回一个对象,它将代表屏幕上的文本。newText
是显示库的一部分。
当你想访问newText
的显示属性时,输入display.newText
。在Hello World!
后面的两个数字控制文本在屏幕上的水平和垂直位置,单位是像素。下一项指定了字体。我们使用了native.systemFont
这个名字,它默认指的是当前设备上的标准字体。例如,iPhone 的默认字体是 Helvetica。你可以使用任何标准字体名称,例如Times New Roman
(在先前的例子中使用过)。最后一个数字是字体大小。
尝试一下英雄—添加更多的文本对象
现在你开始对编码有所体会,尝试在你的当前项目文件中应用以下内容:
-
使用不同的字体和文本颜色创建一个新的显示对象。将其显示在“Hello World!”文本下方。提示:确保你的新文本对象有一个不同的对象名称。
-
继续更改当前显示对象
textObject
的值。改变 x 和 y 坐标,字符串文本,字体名称,甚至字体大小。 -
当对象
setTextColor(r,g,b)
设置文本颜色时,你可以添加一个可选参数来控制文本的不透明度。尝试使用对象setTextColor(r, g, b [, a])
。a
的值范围也在 0 到 255 之间(255 是不透明的,这是默认值)。观察你的文本颜色的结果。
在 iOS 设备上测试我们的应用程序
如果你只想在 Android 设备上测试应用程序,请跳过本章的“在 Android 设备上测试我们的应用程序”部分。在我们能够将第一个 Hello World 应用程序上传到 iOS 设备之前,我们需要登录到我们的 Apple 开发者账户,以便在我们的开发机器上创建和安装我们的签名证书。如果你还没有创建开发者账户,请访问developer.apple.com/programs/ios/
进行创建。请记住,成为苹果开发者每年需要支付 99 美元的费用。
注意
Apple 开发者账户仅适用于在 Mac OS X 上开发的用户。确保你的 Xcode 版本与你的手机上的操作系统版本相同或更高。例如,如果你安装了 iPhone OS 的 5.0 版本,你需要 Xcode,它包含 iOS SDK 的 5.0 或更高版本。
行动时间——获取 iOS 开发者证书
确保你已经注册了开发者计划;你需要使用位于/Applications/Utilities
的密钥链访问工具来创建证书请求。一个有效的证书必须在 iOS 应用程序运行在苹果设备上进行任何类型的测试之前对其进行签名。
-
打开密钥链访问 | 证书助手 | 从证书颁发机构请求证书。
-
在用户电子邮件地址字段中,输入您注册为 iOS 开发者时使用的电子邮件地址。对于通用名称,输入您的姓名或团队名称。确保输入的名称与您注册为 iOS 开发者时提交的信息相匹配。CA 电子邮件地址字段无需填写,因此您可以将其留空。我们不会将证书通过电子邮件发送给证书颁发机构(CA)。勾选保存到磁盘和让我指定密钥对信息。当您点击继续时,您将被要求选择保存位置。将您的文件保存在您容易找到的位置,例如桌面。
-
在以下窗口中,确保选择2048 位作为密钥大小,以及RSA作为算法,然后点击继续。这将生成密钥并将其保存到您指定的位置。在下一个窗口中点击完成。
-
接下来,访问 Apple 开发者网站:
developer.apple.com/
,点击iOS 开发中心,并登录您的开发者账户。选择iOS 配置文件门户选项卡,并在左侧列中导航到证书。如果尚未选择,请点击开发选项卡,然后点击页面右侧的添加证书按钮。 -
点击选择文件按钮,找到您保存在桌面的证书文件,然后点击提交按钮。
-
在按下提交按钮后,您将收到从密钥链访问中指定的证书颁发机构(CA)请求表单的电子邮件通知。创建证书的人将收到此电子邮件,并可以通过点击批准按钮来批准请求。一旦批准,您就可以下载证书。
-
点击下载按钮,并将证书保存到易于找到的位置。完成此操作后,双击文件,您将看到以下对话框:
-
点击确定。这将安装您的 iPhone 开发证书到您的密钥链中。
刚才发生了什么?
现在我们有了有效的 iOS 设备证书。iOS 开发证书仅用于开发目的,有效期为大约一年。密钥对由您的公钥和私钥组成。私钥是允许 Xcode 为 iOS 应用程序签名的关键。私钥仅对密钥对创建者可用,并存储在创建者的机器的系统密钥链中。以下是一些您可以参考以创建有效证书的网站:
-
在 Ansca 网站上为设备构建:
developer.anscamobile.com/content/building-devices-iphoneipad
-
iPhone 配置设置和 App Store 提交(由 Ansca 工程师创建):
www.authorstream.com/Presentation/anscamobile-509082-iphone-provisioning-setup-and-app-store-submission/
-
AppCode 博客:
www.theappcodeblog.com/2011/04/28/getting-an-apple-developer-certificate/
添加 iOS 设备
在 iPhone 开发者计划中,您可以为开发和测试目的分配多达 100 台设备。要注册设备,您需要唯一设备标识符(UDID)号码。您可以在 iTunes 和 Xcode 中找到它。
Xcode
要找出您的设备 UDID,将您的设备连接到您的 Mac 并打开 Xcode。在 Xcode 中,导航到菜单栏并选择窗口,然后点击组织者。在标识符字段中的 40 个十六进制字符字符串是您的设备 UDID。一旦组织者窗口打开,您应该在左侧的设备列表中看到您的设备名称。点击它,并用鼠标选择标识符,将其复制到剪贴板:
通常,当您第一次将设备连接到组织者时,您会收到一个按钮通知,提示用于开发。选择它,Xcode 将在 iOS 配置文件门户中为您设备完成大部分配置工作。
iTunes
将您的设备连接后,打开iTunes并点击设备列表中的您的设备。选择摘要选项卡。单击序列号标签以显示标识符字段和 40 个字符的 UDID。按Command + C将 UDID 复制到您的剪贴板。
行动时间——添加/注册您的 iOS 设备
要添加用于开发和测试的设备:
-
在 iOS 配置文件门户中选择设备,然后点击添加设备。
-
在设备名称字段中为您的设备创建一个名称,并通过按Command + V粘贴您已保存到剪贴板上的 UDID 到设备 ID。
-
完成后,点击提交
行动时间——创建 App ID
现在您已将设备添加到门户,您需要创建一个App ID。App ID 有一个由 Apple 生成的唯一 10 字符包种子 ID前缀和一个由配置文件门户中的团队管理员创建的包标识符后缀。App ID 可能看起来类似于以下示例:7R456G1254.com.companyname.YourApplication
。要创建新的 App ID,请按照以下步骤操作:
-
在门户的App ID部分点击新建 App ID按钮。
-
在描述字段中填写您的应用程序名称。
-
您已经分配了一个包种子 ID(也称为团队 ID)。
-
在包标识符(App ID 后缀)字段中,指定您应用程序的唯一标识符。您可以根据自己的意愿来识别您的应用程序,但建议您使用反向域名风格的字符串,即
com.domainname.appname
。注意
您可以在包标识符中创建一个通配符字符,您可以在使用相同密钥链访问权限的一组应用程序之间共享。为此,只需创建一个以星号(*)结尾的单个 App ID。您可以将此放在包标识符字段中,无论是单独使用还是放在字符串的末尾:
com.domainname.*
。有关此主题的更多信息,请参阅 iOS 配置文件门户的 App IDs 部分:developer.apple.com/ios/manage/bundles/howto.action
。
刚才发生了什么?
每个设备的 UDID 都是唯一的,我们可以在 Xcode 和 iTunes 中找到它们。当我们在 iOS 配置文件门户中添加设备时,我们取了 UDID,它由 40 个十六进制字符组成,并确保我们创建了一个设备名称,这样我们就可以识别我们用于开发的内容。
我们现在有一个用于在设备上安装的应用程序的 App ID。App ID是一个 iOS 用来允许您的应用程序连接到 Apple 推送通知服务、在应用程序之间共享密钥链数据以及与您希望将 iOS 应用程序与其配对的任何外部硬件配件进行通信的唯一标识符。
配置文件
配置文件是一组数字实体,它将开发者和设备与授权的 iOS 开发团队唯一关联,并允许设备用于测试。这些配置文件将您的设备与您的开发团队关联起来,用于测试和分发。
操作时间——创建配置文件
要创建配置文件,请转到 iOS 配置文件门户的配置部分,并在开发选项卡上点击新建配置文件。
-
为配置文件输入一个名称。它可以与您的应用程序名称相同。
-
在证书旁边的框中勾选。
-
在下拉菜单中选择为您的应用程序创建的App ID。
-
检查您希望为此配置文件授权的设备。
-
完成后点击提交按钮。
-
您将被返回到开发选项卡,状态可能显示为挂起。在您的浏览器上点击刷新,它应该显示您的配置文件状态为活动。
-
点击下载按钮。当文件正在下载时,如果 Xcode 还未打开,请启动它,并在键盘上按Shift + Command + 2以打开组织者。
-
在库下,选择配置文件部分。将下载的
.mobileprovision
文件拖放到组织者窗口。这将自动将.mobileprovision
文件复制到正确的目录。
刚才发生了什么?
在配置文件中拥有权限的设备可以用于测试,只要证书包含在配置文件中。一个设备可以安装多个配置文件。
应用程序图标
目前我们的应用程序没有图标图像在设备上显示。默认情况下,如果没有为应用程序设置图标图像,则在构建加载到您的设备后,您将看到一个浅灰色框以及下面的应用程序名称。因此,启动您首选的创意开发工具,让我们创建一个简单的图像。
iPhone/iTouch 的应用程序图标应为 57 x 57 PNG 图像文件或 72 x 72 的 iPad。图像应始终保存为Icon.png
,并且必须位于您的当前项目文件夹中。对于支持Retina显示的 iPhone / iPod touch 设备,我们还需要一个额外的 114 x 114 的高分辨率图标,命名为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 需要一个 512 x 512 像素的图标版本。最好首先以更高的分辨率创建您的图标。请参阅Apple iOS 人机界面指南以获取最新的官方 App Store 要求:
创建应用程序图标是您应用程序名称的视觉表示。一旦您将构建编译在一起,您就能在设备上查看该图标。该图标也是启动您应用程序的图像。
创建 iOS 的 Hello World 构建
我们现在准备为我们的设备构建“Hello World”应用程序。由于我们已经设置了配置文件,因此从现在开始的构建过程相当简单。在创建设备构建之前,请确保您已连接到互联网。您可以在Xcode 模拟器或设备上测试构建您的应用程序。
创建 iOS 构建的行动时间
按照以下步骤在 Corona SDK 中创建新的 iOS 构建:
-
打开 Corona 模拟器并选择模拟器。
-
导航到您的“Hello World”应用程序并选择您的
main.lua
文件。 -
一旦在模拟器上启动应用程序,请转到 Corona 模拟器的菜单栏,并选择文件 | 构建 | iOS,或在键盘上按Command + B。将出现以下对话框:
-
在应用程序名称字段中为您的应用创建一个名称。我们可以保持相同的名称Hello World。在版本字段中,保持数字为1.0。为了在 Xcode 模拟器中测试应用程序,从构建目标下拉菜单中选择Xcode 模拟器。如果您想为设备构建,请选择设备以构建应用程序包。接下来,从支持设备下拉菜单中选择目标设备(iPhone 或 iPad)。在代码签名标识下拉菜单下,选择为构建指定的设备创建的配置文件。它与 Apple 开发者网站上 iOS 配置门户中的配置文件名称相同。在保存到文件夹部分,点击浏览并选择您希望应用程序保存的位置。
-
如果所有信息已在对话框中确认,请点击构建按钮。
小贴士
将您的应用程序设置为保存在
桌面
上会更方便;这样更容易找到。
刚才发生了什么?
恭喜!您现在已成功创建了第一个 iOS 应用程序文件,可以上传到您的设备。随着您开始为分发开发应用程序,您将想要创建应用程序的新版本,以便您可以跟踪每次构建中的更改。您的配置文件中的所有信息都是在 iOS 配置门户中创建的,并应用于构建。一旦 Corona 完成编译构建,应用程序应位于您保存它的首选文件夹中。
操作时间——在您的 iOS 设备上加载应用
选择您创建的 Hello World 构建,并选择以下任一选项来将您的应用加载到 iOS 设备上。可以使用 iTunes、Xcode 或 iPhone 配置实用程序来传输应用程序文件。
如果使用 iTunes,将您的构建拖入 iTunes 库,然后正常同步您的设备。
将您的应用安装到设备上的另一种方法是使用 Xcode,因为它提供了方便的方法来安装 iOS 设备应用。
-
连接设备后,从菜单栏打开 Xcode 的组织者,窗口 | 组织者,然后在左侧的设备列表下导航到您的连接设备。
-
如果建立了正确的连接,您将看到一个绿色的指示器。如果几分钟后它变成黄色,尝试关闭并重新启动设备或断开连接并再次连接。这通常会建立正确的连接。
-
简单地将您的构建文件拖放到组织者窗口的应用程序区域,它将自动安装到您的设备上。
最后,您可以使用从 Apple 网站支持标签页下作为单独下载的 iPhone 配置实用程序:
www.apple.com/support/iphone/enterprise/
。它允许您管理配置配置文件,跟踪和安装配置文件和授权应用程序,并捕获包括控制台日志在内的设备信息。 -
点击下载按钮,按照如何安装程序的说明进行操作。
-
确保您的设备连接到您的计算机,启动 iPhone 配置实用程序,然后点击窗口左上角的添加按钮。导航到您桌面上的 Hello World 构建或您保存它的任何位置,然后点击打开。
-
当您的设备被识别后,在设备部分下选择您的设备。您将看到当前已安装或尚未安装的应用程序列表。点击您添加到实用程序中的 Hello World 应用程序旁边的安装按钮。
小贴士
如果您正在重复测试相同版本号的应用程序,确保在新的安装之前从设备中删除应用程序的先前版本,以删除任何缓存的或相关数据。或者在使用构建选项时使用不同的版本号。
刚才发生了什么?
我们刚刚学习了三种不同的方法,使用 iTunes、Xcode 和 iPhone 配置实用程序将应用程序构建加载到 iOS 设备上。
使用 iTunes 提供简单地将构建拖放到您的库中的功能,然后允许您在设备同步的情况下传输构建。
Xcode 方法可能是最简单且最常见的方法来将构建加载到设备上。只要您的设备连接正确且准备在组织者中使用,将构建拖放到应用程序中,它就会自动加载。
最后,iPhone 配置实用程序是一个逐步的工具,您可以通过它轻松区分哪些应用程序已安装到您的设备上。当您在主库中加载您的构建时,设备区域将列出您想要安装或卸载的应用程序。找到您的构建后,您只需点击安装按钮,文件就会被加载。
在 Android 设备上测试我们的应用程序
在 Android 设备上创建和测试我们的构建不需要像苹果对 iOS 设备那样需要一个开发者账户。您只需要为 Android 构建所需的工具是一个 PC 或 Mac、Corona SDK、JDK6 安装和一个 Android 设备。如果您计划将应用程序提交到 Google Play 商店,您需要在play.google.com/apps/publish
注册为 Google Play 开发者。如果您想在 Google Play 商店发布软件,您需要支付一次性的 25 美元注册费。
创建 Android 的 Hello World 构建
由于我们不需要为调试构建创建唯一的密钥库或密钥别名,因此构建我们的 Hello World 应用程序相当简单。当您准备好将应用程序提交到 Google Play 商店时,您需要创建一个发布构建并生成您自己的私钥来签名您的应用程序。我们将在本书的后面部分更详细地讨论发布构建和私钥。
创建 Android 构建的行动时间
按照以下步骤在 Corona SDK 中创建新的 Android 构建:
-
启动 Corona 模拟器并选择模拟器。
-
导航到您的 Hello World 应用程序并选择您的
main.lua
文件。 -
一旦您的应用程序在模拟器上运行,请转到Corona 模拟器菜单栏,选择文件 | 构建为 | Android(Windows)/Shift + Command + B(Mac)。将出现以下对话框:
-
在应用程序名称字段中为您的应用程序创建一个名称。我们可以保持相同的名称
Hello World
。在版本代码字段中,如果默认值不是数字1
,请将其设置为1
。这个特定的字段必须始终是整数,并且对用户不可见。在版本名称字段中,保持数字为1.0
。此属性是显示给用户的字符串。在包字段中,您需要指定一个使用传统 Java 方案的名称,这基本上是您域名名称的反向格式。例如,com.mycompany.app.helloworld
可以作为包名。项目路径显示了您的项目文件夹的位置。目标操作系统兼容性目前支持运行 ArmV7 处理器的 Android 2.2 及更高版本设备。在密钥库字段中,您将使用 Corona 中已提供的Debug
密钥库对构建进行签名。在密钥别名字段中,如果未选择,请从下拉菜单中选择androiddebugkey
。在保存到文件夹部分,点击浏览并选择您希望应用程序保存的位置。 -
如果对话框中所有信息都已确认,请点击构建按钮。
注意
有关 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。以下是一些简单的方法:
将.apk
文件上传到设备的最基本方法是通过 USB 接口将其传输到 SD 卡。如果您的设备没有某种文件管理器应用程序,您可以从 Google Play Store 下载一个很好的应用程序,即 ASTRO 文件管理器,网址为:play.google.com/store/apps/details?id=com.metago.astro
。还有许多其他安装程序,如 AppInstaller,网址为:play.google.com/store/apps/details?id=com.funtrigger.appinstaller
。AppInstaller 允许您从 SD 卡安装.apk
文件。您始终可以在设备上的 Google Play Store 应用程序中搜索前面的应用程序。
-
在您的设备设置中,选择应用程序然后选择开发。如果该模式未激活,请触摸USB 调试。
-
返回几屏到应用程序部分。如果尚未激活,请启用未知来源。这将允许您安装任何非市场应用程序(即调试构建)。完成后,在您的设备上按主页按钮。
-
使用 USB 电缆将设备连接到您的计算机。您将看到一个新通知,表明已连接到您的 PC 或 Mac 的新驱动器。访问 SD 驱动器并创建一个新文件夹。为文件夹命名,以便您能够轻松识别您的 Android 构建。将
Hello World.apk
文件从桌面拖放到文件夹中。 -
将驱动器从您的桌面弹出,并从 USB 电缆断开您的设备。启动 ASTRO 文件管理器或 AppInstaller,无论您决定从 Google Play Store 下载哪个应用程序。在 ASTRO 中,选择文件管理器,搜索您在 SD 卡上添加的文件夹并选择它。在 AppInstaller 中,搜索您新命名的文件夹并选择它。在这两个应用程序中,您都会看到您的
Hello World.apk
文件。选择文件,将出现一个提示要求您安装它。选择安装按钮,您应该会在设备的应用文件夹中看到 Hello World 应用程序。通过 Dropbox 是一种方便的方法。您可以在:
www.dropbox.com/
创建账户。Dropbox 是一项免费服务,允许您在 PC/Mac 和移动设备上上传/下载文件。 -
下载 Dropbox 安装程序并在您的计算机上安装它。同时,从 Google Play 商店(也是免费的)下载移动应用程序到您的设备并安装它。
-
在您的计算机和移动设备上登录 Dropbox 账户。从您的计算机上传
Hello World.apk
文件。 -
一旦上传完成,请前往您设备上的 Dropbox 应用并选择您的
Hello World.apk
文件。您将看到一个屏幕,它会询问您是否想要安装应用程序。选择安装按钮。假设安装正确,另一个屏幕将出现,显示应用程序已安装,您可以通过按下可用的打开按钮来启动您的 Hello World 应用程序。其中一种最简单的方法是通过 Gmail。如果您还没有 Gmail 账户,请在此处创建一个:
mail.google.com/
。 -
登录您的账户,编写一封新电子邮件并将您的
Hello World.apk
文件附加到消息中。 -
将消息的收件人地址设为您自己的电子邮件地址并发送。
-
在您的 Android 设备上,确保您已经将电子邮件账户链接到那里。一旦收到消息,打开电子邮件,您将有一个选项将应用程序安装到您的设备上。将显示一个安装按钮或类似按钮。
刚才发生了什么?
我们刚刚学习了如何将 .apk
文件加载到 Android 设备上的几种方法。前面提到的方法是一些快速加载应用程序且不会遇到任何问题的最简单方法。
使用文件管理器方法允许您轻松访问 .apk
文件,无需任何运营商数据或 Wi-Fi 连接。通过使用与您的设备兼容的 USB 线缆并将其连接到计算机,这是一个简单的拖放过程。
Dropbox 方法是在您的计算机和移动设备上设置后最方便的方法之一。您所要做的就是将 .apk
文件拖放到您的账户文件夹中,它将立即对安装了 Dropbox 应用的任何设备可访问。您还可以通过下载链接共享您的文件——这是 Dropbox 提供的另一个出色功能。
如果您不想在您的设备或计算机上下载任何文件管理器和其他程序,设置 Gmail 账户并将您的 .apk
文件作为附件发送给自己是一个简单的过程。您唯一需要记住的是,在 Gmail 中,您不能发送超过 25 MB 大小的附件。
快速问答——理解 Corona
-
当使用 Corona 时,以下哪个陈述是正确的?
-
a. 您需要一个
main.lua
文件来启动您的应用程序。 -
b. Corona SDK 只在 Mac OSX 上运行。
-
c. Corona 终端不会启动模拟器。
-
d. 以上都不是。
-
-
在 iPhone 开发者计划中,您可以使用多少个 iOS 设备进行开发?
-
a. 50。
-
b. 75。
-
c. 5.
-
d. 100.
-
-
在 Corona SDK 中为 Android 构建时,版本代码应该是什么?
-
a. 一个字符串。
-
b. 一个整数。
-
c. 必须遵循 Java 方案格式。
-
d. 以上皆非。
-
摘要
在本章中,我们介绍了开始为 Corona SDK 开发应用程序所需的必要工具。无论您是在 Mac OS X 还是 Microsoft Windows 上工作,您都会注意到在两个操作系统上工作的相似性,以及运行 Corona SDK 的简单性。
为了更好地熟悉 Corona,尝试以下操作:
-
抽时间查看 Corona 提供的示例代码,以查看 SDK 的功能。
-
随意修改任何示例代码,以适应您的喜好,从而更好地理解 Lua 编程。
-
无论您是在 iOS 上工作(如果您是注册的 Apple 开发者)还是 Android,尝试将任何示例代码安装到您的设备上,以查看应用程序在模拟器环境之外的运行情况。
-
查看 Ansca 论坛
developer.anscamobile.com/forum/
,浏览由 Corona SDK 开发者和员工发起的最新关于 Corona 开发的讨论。
既然您已经了解了在 Corona 中显示对象的过程,我们将深入探讨其他有助于创建可运行移动游戏的功能。
在下一章中,我们将更深入地探讨 Lua 编程语言,并学习与 Corona 中示例代码类似的简单编码技巧。您将更好地理解 Lua 语法,并注意到与其他编程语言相比,学习 Lua 的速度和容易程度。
第二章:Lua 快速入门和 Corona 框架
Lua 是开发 Corona SDK 所使用的编程语言。到目前为止,我们已经学习了如何使用主要资源来运行 SDK 和其他开发工具来创建移动设备的应用程序。现在我们已经尝试编写了一些使程序工作的代码行,让我们深入了解 Lua 的基本功能,这将让您更好地理解 Lua 的能力。
在本章中,您将学习如何:
-
将变量应用于脚本
-
使用数据结构来形成表
-
与显示对象一起工作
-
使用对象方法和参数实现函数
-
优化你的工作流程
所以让我们直接进入正题。
Lua 来帮忙
Lua 是游戏编程的行业标准。它与 JavaScript 和 Flash 的 ActionScript 类似。任何在这些语言中编写过脚本的人都会很快过渡到 Lua。
Lua 在创建各种应用程序和游戏中非常有用。许多游戏程序员发现 Lua 是一种方便的脚本语言,因为它易于嵌入、执行速度快,学习曲线平缓。它在《魔兽世界》中无处不在。它也被电子艺界、Rovio、ngmoco 和 Tapulous 等公司用于愤怒的小鸟、Tap Tap Revenge、Diner Dash 等游戏。
如需了解更多关于 Lua 的信息,请参阅 www.lua.org
。
有价值的变量
就像在许多脚本语言中一样,Lua 有变量。您可以将它视为存储值的东西。当您将值应用于变量时,您可以使用相同的变量来引用它。
一个应用程序由语句和变量组成。语句提供关于需要执行的操作和计算指令。变量存储这些计算的结果。将值赋给变量称为赋值。
Lua 使用三种类型的变量:全局、局部和表字段。
全局变量
全局变量可以在任何范围内访问,并且可以从任何地方修改。术语范围用于描述一组变量存在的区域。您不必声明全局变量。一旦您给它赋值,它就会创建。
myVariable = 10
print( myVariable ) -- prints the number 10
局部变量
局部变量是从局部范围访问的,通常是从函数或代码块中调用。当我们创建一个块时,我们正在创建一个变量可以存在或一系列按顺序执行的语句的范围。当引用变量时,Lua 必须找到该变量。局部化变量有助于加快查找过程并提高代码的性能。通过使用局部语句,它声明了一个局部变量。
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
更多有关表的信息将在名为Tables的部分中讨论。
你可能已经注意到了前面示例中某些代码行的附加文本。那些就是所谓的注释。注释可以从任何位置开始,使用双破折号--
,除了字符串内部。它们一直运行到行尾。还有块注释。注释一个块的一个常见技巧是将其包围在--[[
。
这里是如何注释一行:
a = 2
--print(a) -- 2
以下是一个块注释:
--[[
k = 50
print(k) -- 50
]]--
赋值约定
变量名有一些规则。变量以字母或下划线开头。它不能包含除了字母、下划线或数字之外的其他任何内容。它也不能是 Lua 保留的以下单词之一:
-
which
-
are
-
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 是一种动态类型语言。语言中没有定义的类型。每个值都携带自己的类型。
正如你所注意到的,值可以存储在变量中。它们可以被操作以给出任何类型的值。这也允许你将参数传递给其他函数,并作为结果返回。
你将处理的基本值类型如下:
-
nil
——它是唯一一个值是nil
的类型。任何未初始化的变量都有nil
作为其值。像全局变量一样,它默认是nil
,并且可以将nil
赋给它来删除它。 -
Boolean
——布尔类型有两个值,false
和true
。你会注意到,条件表达式将false
和nil
视为假,将其他任何内容视为真。 -
Numbers
——表示实数(双精度浮点数)。 -
String
——字符串是一系列字符。允许 8 位字符和嵌入的零。 -
Tables
——Lua 中的数据结构。它通过关联数组实现,这是一种不仅可以使用数字索引,还可以使用字符串或任何其他值(除了nil
)索引的数组。(我们将在本章后面的Tables部分中进一步讨论这一点)。 -
Functions
——称为 Lua 的第一类值。通常,函数可以存储在变量中,作为参数传递给其他函数,并作为结果返回。
行动时间——使用块打印值
让我们试一试,看看 Lua 语言有多强大。我们开始了解变量是如何工作的,以及当你给它赋值时会发生什么。如果你有一个具有多个值的变量怎么办?Lua 如何区分它们?我们将使用Corona Terminal
,这样我们就可以在终端框中看到输出的值。在这个过程中,你将随着本节的进展学习其他编程技巧。我们还将在这个练习中引用代码块。Lua 的执行单元被称为代码块。代码块是一系列语句,它们按顺序执行。
如果你记得上一章的内容,我们学习了如何为 Hello World 应用程序创建自己的项目文件夹和main.lua
文件。
-
在你的桌面上创建一个新的项目文件夹,并将其命名为
Variables
。 -
打开你喜欢的文本编辑器,并将其保存为
Variables
项目文件夹中的main.lua
。 -
创建以下变量:
local x = 10 -- Local to the chunk local i = 1 -- Local to the chunk
-
在
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
-
创建一个
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
-
保存你的脚本。
-
启动
Corona Terminal
。确保你看到Corona SDK屏幕和一个终端窗口弹出。 -
导航到你的
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 = 10
不同。while
循环运行十次,每次递增 1 打印出一个值。
if
语句比较i < 20
,其中i
等于 1,并使用另一个局部于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 中的 逻辑运算符 是 and, or
, 和 not
。所有逻辑运算符都将 false
和 nil
视为假,将其他任何内容视为真。
运算符 and
如果其值是 false
或 nil
,则返回其第一个参数;否则返回其第二个参数。运算符 or
如果其值不同于 nil
和 false
,则返回其第一个参数;否则返回其第二个参数。and
和 or
都使用短路评估,这意味着第二个操作数只有在必要时才会被评估。
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
长度运算符
长度运算符 #
测量字符串的长度。字符串的长度简单地是其字符数。一个字符被视为 1 字节。
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?"
是时候动手实践,让我们充分了解字符串
我们开始熟悉几个代码块及其相互之间的交互。让我们看看当我们添加一些使用字符串的表达式时会发生什么,以及它们与在终端中打印的普通字符串相比有何不同。
-
在你的桌面创建一个新的项目文件夹,并将其命名为
Working With Strings
。 -
在你的文本编辑器中创建一个新的
main.lua
文件,并将其保存在你的文件夹中。 -
输入以下行(代码中不要包含行号,它仅用于行参考):
1 print("This is a string!") -- This is a string! 2 print("15" + 1) -- Returns the value 16
-
添加以下变量。注意它使用了相同的变量名:
3 myVar = 28 4 print(myVar) -- Returns 28 5 myVar = "twenty-eight" 6 print(myVar) -- Returns twenty-eight
-
让我们添加一些具有字符串值的变量,并使用不同的运算符进行比较。
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
-
保存你的脚本并在 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 = 28
和 myVar = "twenty-eight"
)。
在代码的最后一段(第 7-12 行),我们使用了关系运算符比较不同的变量名。首先,我们打印了Name1
和Phone
的字符串。接下来的几行是关于Name1, Name2
和Phone
之间的比较。当两个字符串具有完全相同的字符顺序时,它们被认为是相同的字符串,彼此相等。当你查看print(Name1 == Phone)
和print(Name1 <= Phone)
时,字符之间没有关联,因此返回false
。在print(Name1 == Name2)
中,两个变量包含相同的字符,因此返回true
。
尝试英雄——拉一些更多的字符串
字符串处理起来相当简单,因为它们只是字符序列。尝试使用以下修改与前面的示例类似地创建自己的表达式:
-
创建一些具有数值的变量和另一组具有数值字符串的变量。使用关系运算符比较值,然后打印出结果。
-
使用连接运算符将几个字符串或数字组合在一起,并均匀地间隔它们。在终端窗口中打印出结果。
表格
表格是 Lua 中的专用数据结构。它们代表数组、列表、集合、记录、图等。Lua 中的表格类似于关联数组。关联数组可以用任何类型的值索引,而不仅仅是数字。表格有效地实现了所有这些结构。例如,可以通过用整数索引表格来实现数组。数组没有固定的大小,而是根据需要增长。当初始化数组时,其大小是间接定义的。
以下是如何构建表格的示例:
1 a = {} -- create a table with reference to "a"
2 b = "y"
3 a[b] = 10 -- new entry, with key="b" 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 = nil
用nil
覆盖了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
循环,并从数字 1 开始调用myNumbers[i]
五次。每次调用时,它增加 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
是一个包含属性xMin, xMax, yMin
和yMax
的表格,这些属性在屏幕坐标系中。通常用于将组中的对象映射到屏幕坐标系。 -
object.contentHeight
是屏幕坐标系中的高度。 -
object.contentWidth
是屏幕坐标系中的宽度。 -
object.width
表示对象在局部坐标系中的宽度。 -
object.x
指定了对象相对于父对象的 x 位置(在局部坐标系中)——更确切地说,是父对象的坐标原点。它提供了对象参考点相对于父对象的 x 位置。更改此值将在 x 方向上移动对象。 -
object.xOrigin
指定了对象原点相对于父原点的 x 位置。它位于对象的局部坐标系中。更改此属性的值将在 x 方向上移动对象。 -
object.xReference
定义了参考点相对于对象局部原点的 x 位置。对于大多数显示对象,默认值为 0,意味着原点和参考点的 x 位置相同。 -
object.xScale
获取或设置 X 缩放因子。值为 0.5 将对象在 X 方向上缩放至 50%。缩放是围绕对象的参考点进行的。大多数显示对象的默认参考点是中心。 -
object.y
指定了对象相对于父对象的 y 位置(在局部坐标系中)——更确切地说,是父对象的坐标原点。 -
object.yOrigin
指定了对象原点相对于父原点的 y 位置。它位于对象的局部坐标系中。更改此属性的值将在 y 方向上移动对象。 -
object.yReference
定义了参考点相对于对象局部原点的 y 位置。对于大多数显示对象,默认值为 0,意味着原点和参考点的 y 位置相同。 -
object.yScale
获取或设置 Y 缩放因子。值为 0.5 将对象在 Y 方向上缩放至 50%。缩放是围绕对象的参考点进行的。大多数显示对象的默认参考点是中心。
对象方法
Corona 可以创建显示对象以将对象方法作为属性存储。这可以通过两种方式完成,点操作符(.)和冒号操作符(:)。两者都是创建对象方法的有效方式。
使用点操作符调用对象方法时,如果它是第一个参数,则传递给对象:
object = display.newRect(110, 100, 50, 50)
object:setFillColor(255, 255, 255)
object.translate( object, 10, 10 )
冒号操作符方法只是一个简写,涉及更少的输入来创建函数:
object = display.newRect(110, 100, 50, 50)
object:setFillColor(255, 255, 255)
object:translate( 10, 10 )
显示对象共享以下方法:
-
object:rotate( deltaAngle )
或object.rotate( object, deltaAngle )
——实际上是将deltaAngle
(以度为单位)添加到当前的旋转属性。 -
object:scale( sx, sy )
或object.scale(object, sx, sy )
— 分别将xScale
和yScale
属性乘以sx
和sy
。如果当前的xScale
和yScale
值是 0.5,而sx
和sy
也是 0.5,则结果缩放将是xScale
和yScale
的 0.25。这将对象从原始大小的 50% 缩放到 25%。 -
object:setReferencePoint( referencePoint )
或object.setReferencePoint( object, referencePoint )
— 将参考点设置为对象的中心(默认)或对象边界框上的几个方便点之一。参数referencePoint
应该是以下之一:-
display.CenterReferencePoint
-
display.TopLeftReferencePoint
-
display.TopCenterReferencePoint
-
display.TopRightReferencePoint
-
display.CenterRightReferencePoint
-
display.BottomRightReferencePoint
-
display.BottomCenterReferencePoint
-
display.BottomLeftReferencePoint
-
display.CenterLeftReferencePoint
-
-
object:translate( deltaX, deltaY )
或object.translate( object, deltaX, deltaY )
— 分别将deltaX
和deltaY
添加到x
和y
属性。这将移动对象到其当前位置。 -
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。
-
如果图像的大小大于设备的最大纹理尺寸,则图像仍会自动缩放。这通常为 1024x1024(iPhone 3G)或 2048x2048(iPhone 3GS 和 iPad)。
-
如果你多次重新加载同一张图片,后续对
display.newImage
的调用将忽略isFullResolution
参数,并采用第一次传递的值。换句话说,你第一次加载图像文件的方式会影响下一次加载同一文件时的自动缩放设置。这是因为 Corona 通过自动重用已加载的纹理来节省纹理内存。因此,你可以多次使用相同的图像而不会消耗额外的纹理内存。
关于 Corona SDK 文档的更多信息可以在 Ansca 的网站上找到:www.anscamobile.com
。
开始行动时间——在屏幕上放置图像
我们终于开始添加图像显示对象,使本章的视觉效果变得吸引人。现在我们不需要参考终端窗口。所以让我们专注于模拟器屏幕。我们将首先创建一个背景图像和一些艺术资源。
-
首先,在你的桌面上创建一个新的项目文件夹,并将其命名为
Display Objects
。 -
在
Chapter 2 Resources
文件夹中,将glassbg.png
和moon.png
图像文件复制到你的Display Objects
项目文件夹中。你可以从 Packt 网站下载本书附带的项目文件。 -
启动你的文本编辑器,为你的当前项目创建一个新的
main.lua
文件。 -
输出以下代码行:
local background = display.newImage( "glassbg.png", true ) local image01 = display.newImage( "moon.png", 110, 30 ) 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", true )
使用
image02.x = 160; image02.y = 200
与以下方式相同:image02.x = 160 image02.y = 200
分号 (😉 表示语句的结束,是可选的。它使得在一行中分隔两个或多个语句变得更容易,并且可以节省在代码中添加额外行的时间。
-
保存你的脚本并在模拟器中启动你的项目。
注意
如果你使用的是 Mac OSX 上的 Corona SDK,默认设备是 iPhone。如果你使用的是 Windows,默认设备是 Droid。
-
你应该会看到一个背景图像和三个其他与以下截图所示的图像相同的显示对象。显示结果将取决于你用于模拟的设备。
变量 image01, image02
和 image03
的显示对象应包含 moon.png
文件名。你代码中的文件名是区分大小写的,所以请确保你按照在项目文件夹中显示的格式准确书写。
刚才发生了什么?
目前,background
设置为全分辨率,因为我们已在显示对象中指定了true
。我们还使图像围绕其局部原点居中,因为没有应用top
或left
坐标。
当你在模拟器中观察image01, image02
和image03
的放置时,它们在垂直方向上实际上是对齐的,尽管image01
与image02/image03
的脚本风格书写不同。这是因为image01
的坐标是基于显示对象的(left, top)
。你可以选择指定图像的左上角位于坐标(left, top)
;如果你不提供两个坐标,图像将围绕其局部原点居中。
image02
和image03
的放置是从显示对象的局部原点指定的,并通过设备屏幕 x 和 y 属性的局部值定位。局部原点是图像的中心;参考点初始化于此点。由于我们没有对image02
和image03
应用(left, top)
值,因此进一步访问 x 或 y 属性将参照图像的中心。
现在,你可能已经注意到 iPhone 3G 的输出看起来很好,但 Droid 的输出显示显示对象没有居中,背景图像甚至没有填满整个屏幕。我们看到我们指定的所有对象都在那里,但缩放不正确。这是因为每个 iOS 和 Android 设备的屏幕分辨率都不同。iPhone 3G 的屏幕分辨率为 320 x 480 像素,Droid 的屏幕分辨率为 480 x 854 像素。在一种类型的设备上看起来很好的内容,在另一种设备上可能不会完全相同。不用担心,我们可以通过使用将在下一节讨论的config.lua
文件来简单地解决这个问题。
尝试一下英雄般的操作——调整显示对象属性
现在你已经知道如何将图像添加到设备屏幕上,尝试测试其他显示属性。尝试以下任何一项:
-
更改
image01, image02
和image03
显示对象的所有 x 和 y 坐标 -
选择任何显示对象并更改其旋转
-
更改单个显示对象的可见性
如果你对如何进行前面的调整不确定,请参考本章前面提到的显示属性。
运行时配置
所有项目文件不仅包含一个main.lua
文件,还包括根据项目需要的其他.lua
和相关资源。一些 Corona 项目使用config.lua
文件进行配置,该文件编译到项目中并在运行时访问。这允许你同时指定动态内容缩放、动态内容对齐、动态图像分辨率、帧率控制和抗锯齿,以便在每种类型的设备上显示的输出相似。
动态内容缩放
您可以指定 Corona 您的内容的原始屏幕大小。然后允许它缩放您的应用程序以在具有与原始屏幕大小不同的设备上运行。
应使用以下值来缩放内容:
-
width
(数字)—原始目标设备的屏幕分辨率宽度(在纵向方向) -
height
(数字)—原始目标设备的屏幕分辨率高度(在纵向方向) -
scale
(字符串)—以下自动缩放类型之一:-
none
—关闭动态内容缩放 -
letterbox
—尽可能均匀地放大内容 -
zoomEven
—均匀放大内容以填充屏幕,同时保持宽高比 -
zoomStretch
—非均匀地放大内容以填充屏幕,并可能垂直或水平拉伸
注意
zoomStretch
与 Android 设备缩放配合良好,因为许多设备具有不同的屏幕分辨率。 -
动态内容对齐
动态缩放的内容默认情况下已经居中。您可能会遇到不需要内容居中的情况。例如,iPhone 3G 和 Droid 这样的设备具有完全不同的屏幕分辨率。为了使 Droid 上显示的内容与 iPhone 3G 匹配,需要对齐进行调整,以便内容填充整个屏幕,不留任何空白的黑色屏幕空间。
xAlign:
一个指定 x 方向对齐的字符串。以下值可以使用:
-
left
-
center
(默认) -
right
yAlign:
一个指定 y 方向对齐的字符串。以下值可以使用:
-
top
-
center
(默认) -
bottom
动态图像分辨率
Corona 允许您在不更改布局代码的情况下,将更高分辨率的图像版本交换到更高分辨率的设备上。如果您正在为具有不同屏幕分辨率的多个设备构建,这是一个需要考虑的案例。
在您想要显示高分辨率图像的例子中,比如在分辨率为 640 x 960 像素的 iPhone 4 上。这个分辨率是早期 iOS 设备(如 iPod Touch 2G 或 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 可以使您的应用程序运行得更平滑。当涉及到运行动画或碰撞检测时,您可以在运动中轻松检测到逼真的流畅性。
Corona 为矢量对象使用软件抗锯齿。默认情况下,它是关闭的,以提高矢量对象的表现性能。您可以通过将 antialias
键设置为 true
来开启它。
请看以下示例:
application =
{
content =
{
fps = 60,
antialias = true,
},
}
操作时间——在多个设备上缩放显示对象
在我们的 Display Objects
项目中,我们在模拟器中省略了显示背景图像和三个类似的显示对象。当在不同的设备上运行项目时,坐标和分辨率大小与 iPhone 最为兼容。当在 iOS 和 Android 平台的多设备上构建应用程序时,我们可以使用一个编译到项目中并在运行时访问的 config.lua
文件来配置它。所以,让我们开始吧!
-
在您的文本编辑器中,创建一个新文件并写下以下行:
application = { content = { width = 320, height = 480, scale = "letterbox", xAlign = "left", yAlign = "top" }, }
-
将您的脚本保存为
config.lua
到您的Display Objects
项目文件夹中。 -
Mac 用户,在 Corona 中模拟 iPhone 设备运行您的应用程序。一旦完成,在 Corona Simulator 菜单栏中,选择 Window | View As | iPhone 4。您会注意到显示对象完美地适应屏幕,并且没有显示任何空白的黑色区域。
-
Windows 用户,在 Corona 中模拟 Droid 设备运行您的应用程序。您会注意到所有内容都进行了适当的缩放和对齐。在 Corona Simulator 菜单栏中,选择 Window | View As | NexusOne。观察内容放置与 Droid 的相似性。从左到右:iPhone 3G、iPhone 4、Droid 和 NexusOne。
刚才发生了什么?
我们现在已经学会了一种方法,可以在 iOS 和 Android 的多种设备上轻松配置显示我们的内容。内容缩放功能对多屏开发很有用。如果您查看我们创建的 config.lua
文件,内容 width = 320
和 height = 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,] left, top, width, height )
使用width
乘以height
创建一个矩形。位置从设备屏幕的左上角开始,使用left
和top
作为您的放置坐标。 -
display.newRoundedRect( [parentGroup,] left, top, width, height, cornerRadius )
使用width
乘以height
创建一个圆角矩形。位置从设备屏幕的左上角开始,使用left
和top
作为您的放置坐标。通过cornerRadius
进行圆角处理。 -
display.newCircle( [parentGroup,] xCenter, yCenter, radius )
使用radius
创建一个以xCenter, yCenter
为中心的圆。
应用笔触宽度、填充颜色和笔触颜色
所有矢量对象都可以使用笔触进行轮廓绘制。您可以设置笔触宽度、填充颜色和笔触颜色。
-
object.strokeWidth
—以像素为单位创建笔触宽度。 -
object:setFillColor( r, g, b [, a] )
—使用介于 0 到 255 之间的r,g,b
代码。a
表示透明度,是可选的,默认为 255。 -
object:setStrokeColor( r, g, b [, a] )
—使用介于 0 到 255 之间的r,g,b
代码。a
表示透明度,是可选的,默认为 255。
以下是一个使用笔触显示矢量对象的示例:
local rect = display.newRect(110, 100, 250, 250)
rect:setFillColor(255, 255, 255)
rect:setStrokeColor(45, 180, 100)
rect.strokeWidth = 10
文本,文本,文本
在第一章,使用 Corona SDK 入门中,我们使用文本显示对象创建了 Hello World 应用程序。让我们详细了解一下如何在屏幕上实现文本。
display.newText( [parentGroup,] string, x, y, font, size )
使用x
和y
值创建一个文本对象。默认情况下没有文本颜色。在font
参数中,应用库中的任何字体名称。size
参数显示文本的大小。如果您不想应用字体名称,可以使用一些默认常量:
-
native.systemFont
-
native.systemFontBold
应用颜色和字符串值
文本显示对象中的大小、颜色和文本字段可以设置或检索。
-
object.size
—文本的大小 -
object:setTextColor( r, g, b [, a] )
—使用介于 0 到 255 之间的r,g,b
代码。a
表示透明度,是可选的,默认为 255 -
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 设备不受支持。
参数 mode
应该是以下之一:
-
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 或 Google Play Store 销售的应用程序,你需要意识到你的设计选择以及它们如何影响你应用程序的性能。这意味着要考虑你的移动设备处理应用程序时使用的内存量。以下是一些如果你刚开始使用 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
不在屏幕上显示,它引用的显示对象也不会被释放。
与全局变量不同,局部化变量有助于加快查找显示对象的过程。此外,它只存在于定义它的代码块或代码段中。在以下代码中使用局部变量将完全删除对象并释放内存:
-- 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
end
-- Stop further propagation of touch event
return true
end
myImage:addEventListener( "touch", removeBody )
优化你的显示图像
优化你的图像文件大小非常重要。使用全屏图像可能会影响你应用程序的性能。它们在设备上加载需要更多时间,并且消耗大量的纹理内存。当应用程序消耗大量内存时,在大多数情况下,它将被强制退出。
iOS 设备的可用内存因设备而异:
-
iPhone 3G, iTouch 2G—128 MB RAM
-
iPhone 3GS, iPad, iTouch 3G/4G—256 MB RAM
-
iPhone 4/4S, iPad 2—512 MB RAM
例如,iPhone 3GS 上的纹理内存应该在性能问题开始出现之前保持在 25 MB 以下,这些问题可能包括减慢你的应用程序或甚至强制它退出。iPad 2 由于有更多的可用内存,可以进一步降低这个界限。
有关为 iOS 设备应用内存警告的信息,请参阅以下链接:developer.anscamobile.com/reference/index/memorywarning-ios
。
对于 Android 设备,大约有 24 MB 的内存限制。因此,了解您的场景中有多少显示对象以及如何管理它们在应用程序中不再需要时非常重要。
在您不再需要在屏幕上显示图像的情况下,使用以下代码:
image.parent:remove( image ) -- remove image from hierarchy
-- or --
image:removeSelf( ) -- same as above
如果您想在应用程序的生命周期内完全从场景中删除一个图像,请在您的 image.parent:remove( image )
或 image:removeSelf()
代码之后包含以下行:
image = nil
在您的应用程序中保持低内存使用量可以防止崩溃并提高性能。有关优化的更多信息,请访问以下 URL:developer.anscamobile.com/content/performance-and-optimization
。
快速问答——Lua 的基础知识
-
以下哪些是值?
-
a. 数字
-
b. 空值
-
c. 字符串
-
d. 以上所有
-
-
哪个关系运算符是错误的?
-
a.
print(0 == 0)
-
b.
print(3 >= 2)
-
c.
print(2 ~= 2)
-
d.
print(0 ~= 2)
-
-
正确的缩放对象 x 方向的方法是什么?
-
a.
object.scaleX
-
b.
object.xscale
-
c.
object.Xscale
-
d.
object.xScale
-
摘要
本章讨论了 Lua 编程的某些部分,这将帮助您开始创建自己的 Corona 应用程序。随着您继续使用 Lua,您将更好地理解术语。最终,您会发现新的编程解决方案,这将有助于您的开发过程。
以下是一些您已经学到的技能:
-
创建变量并将值分配给它们
-
使用运算符建立表达式
-
如何使用 Corona 终端输出或打印结果
-
使用表格来构建列表、数组、集合等
-
在模拟器中添加显示对象
-
配置您的应用程序构建以在不同的移动设备上工作
-
实现动态分辨率图像
-
创建函数以运行代码块
在本节中确实有很多东西要学习。关于 Lua 的信息还有一些我们没有触及,但您已经学到了足够的知识来开始。有关 Lua 编程的更多信息,您可以参考 www.lua.org/pil/index.html
或 Corona 网站上的资源部分 www.anscamobile.com/resources/
。
在下一章中,我们将开始制作我们的第一个游戏,名为 Breakout!您将在 Corona 中创建游戏框架并获得一些实际操作经验,并将所有必要的资源应用到移动游戏开发中。您会对创建一个游戏如此之快和简单感到惊讶。
第三章. 构建我们的第一个游戏:Breakout
到目前为止,我们已经了解了 Lua 编程的一些重要基础知识,并在 Corona 模拟器中应用了一些代码。了解术语只是学习如何制作应用程序的一部分。我们需要更进一步,亲身体验从开始到结束构建项目的感觉。我们将通过从头开始创建我们的第一个游戏来实现这一点。这将帮助你更深入地理解代码块,并应用一些游戏逻辑来制作一个功能性的游戏。
到本章结束时,你将理解以下内容:
-
在 Corona 项目中组织游戏文件
-
为游戏创建变量
-
将游戏对象添加到屏幕上
-
创建一个警告消息
-
显示得分和关卡编号
让乐趣开始吧!
Breakout——重温经典游戏
你可能在过去几十年中看到过许多形式的 Breakout 游戏,尤其是在雅达利时代。为了给你一个关于游戏内容的良好印象,这里有一篇 Big Fish Games 关于 Breakout 历史的简要评论:www.bigfishgames.com/blog/the-history-of-breakout/
。
在游戏屏幕上,有几列和几行砖块放置在屏幕顶部附近。一个球在屏幕上滚动,反弹到屏幕的顶部和侧面墙壁。当球击中砖块时,球会弹开,砖块被摧毁。当球触碰到屏幕底部时,玩家就会输掉这一轮。为了防止这种情况发生,玩家有一个可移动的球拍来将球弹回上方,保持游戏进行。
我们将创建一个克隆版本,使用触摸事件和加速度计来控制球拍移动,这将由玩家控制。我们将为球添加一些物理属性,使其可以在整个屏幕上弹跳。
在下一章中,我们将添加游戏对象的移动、碰撞检测、得分和胜负条件。现在,我们将专注于如何设置 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( "hybrid" ):
此模式在正常 Corona 对象上叠加碰撞轮廓 -
physics.setDrawMode( "normal" ):
此模式是默认的 Corona 渲染器,没有碰撞轮廓
物理数据使用彩色矢量图形显示,反映了不同对象类型和属性:
-
橙色——动态物理物体(默认物体类型)
-
深蓝色——运动学物理物体
-
绿色——静态物理物体,如地面或墙壁
-
灰色——由于缺乏活动而处于休眠状态的物体
-
浅蓝色——关节
physics.setPositionIterations
此函数设置引擎位置计算的精度。默认值为 8
,意味着引擎将每帧迭代八个位置近似值,每个对象,但会增加处理器占用,因此应谨慎处理,因为它可能会减慢应用程序的运行速度。
语法:physics.setPositionIterations( value )
physics.setPositionIterations( 16 )
physics.setVelocityIterations
此函数设置引擎速度计算的精度。默认值是 3
,这意味着引擎将为每个对象在每个帧中迭代三个速度近似值,但这会增加处理器的占用,因此应该小心处理,因为它可能会减慢应用程序的运行速度。
语法:physics.setVelocityIterations( value )
physics.setVelocityIterations( 6 )
配置应用程序
本教程适用于 iOS 和 Android 设备。图形已被设计以适应这两个平台的屏幕尺寸变化。
构建配置
默认情况下,所有设备屏幕上显示的所有项目都以竖屏模式显示。我们将专门在横屏模式下创建这款游戏,因此我们需要修改一些构建设置并配置屏幕上所有项目的显示方式。在横屏模式下玩游戏实际上会增加玩家的交互性,因为挡板将有更多的屏幕空间移动,而球体将拥有更少的时间在空中。
行动时间——添加 build.settings 文件
构建时属性可以通过可选的 build.settings
文件提供,该文件使用 Lua 语法。build.settings
文件用于设置应用程序的方向和自动旋转行为,以及各种平台特定的构建参数。
-
在您的桌面上创建一个新的项目文件夹,命名为
Breakout
。 -
在您首选的文本编辑器中,创建一个名为
build.settings
的新文件,并将其保存到您的项目文件夹中。 -
输入以下行:
settings = { orientation = { default = "landscapeRight", } }
-
保存并关闭。
build.settings
文件已完成。
刚才发生了什么?
默认方向设置决定了设备上的初始启动方向,以及 Corona 模拟器的初始方向。
默认方向不会影响 Android 设备。方向初始化为设备的实际方向(除非只指定了一个方向)。此外,唯一支持的旋转方向是 landscapeRight
和竖屏。在设备上,您可以切换到 landscapeRight
或 landscapeLeft
,但操作系统只会报告一种横屏,Corona 的方向事件选择 landscapeRight
。
我们已创建此应用程序以支持横屏方向,支持 landscapeRight
。我们已将其设置为默认方向,因此它不会切换到 landscapeLeft
或任何竖屏模式。当在 iOS 设备上工作时,如果在启动应用程序之前未设置 build.settings
,它将进入默认的竖屏模式。
运行时配置
目前我们的项目尚未缩放以均匀显示跨平台开发。内容仍将在设备上显示,但很可能会不在正确的宽高比中。
例如,iPhone 项目是为 320 像素宽的屏幕设计的,可以升级到 480 像素宽的屏幕以适应 Android 设备。这使得移植变得更容易,因为代码和艺术资源不需要修改。
Corona 可以针对为 iPhone 4 和其他当前 iOS 设备制作的构建,这些设备显示双倍分辨率的艺术资源,同时保持 iOS 3.0 兼容性。这意味着 iPhone 4 之前的旧内容将不再自动放大到更大的 iPhone 4 屏幕分辨率。随着 iOS 开发的进步,内容缩放通常在新 iPhone 应用中是必需的,以兼容 iPhone 3GS 和更低的设备。
动手实践——添加 config.lua 文件
如果未指定内容大小,则返回的内容宽度和高度将与设备的物理屏幕宽度和高度相同。如果您在config.lua
中指定不同的内容宽度和高度,则内容宽度和高度将采用这些值。
-
在您的文本编辑器中,创建一个名为
config.lua
的新文件,并将其保存到您的项目文件夹中。 -
输入以下行:
application = { content = { width = 320, height = 480, scale = "letterbox", fps = 60, }, }
-
保存并关闭你的文件。
刚才发生了什么?
内容宽度和高度允许您选择一个虚拟屏幕大小,该大小独立于物理设备屏幕大小。我们已将大小设置为针对原始 iPhone,因为它在 iOS 和 Android 平台上所有设备中显示的最小尺寸。原始 iPhone 的尺寸为 320 x 480。使用此配置,它仍然会均匀缩放到 iPad,其尺寸为 768 x 1024。
在此应用中使用的缩放比例设置为letterbox
。它将尽可能均匀地放大内容,同时仍然在屏幕上显示所有内容。这将创建一个宽屏外观,与屏幕比 iPhone 3GS 更长的 Droid 兼容。
我们将fps
设置为60
。默认情况下,帧率为 30 fps。在此应用中,这将使球的移动看起来更快,并允许我们方便地提高速度。我们可以将帧率扩展到 Corona 允许的最大 60 fps。
构建应用
现在我们已经将应用配置为横幅模式,并将显示内容设置为在多台设备上缩放,我们准备开始设计游戏。在我们开始编写游戏代码之前,我们需要添加一些将在屏幕上显示的艺术资源。您可以在第三章资源
文件夹中找到它们。您可以从 Packt 网站下载本书附带的项目文件。以下是需要添加到您的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(255, 255, 255)
local myGroup = display.newGroup()
myGroup:insert( rect )
使用系统函数
在本章中我们将要介绍的系统函数将返回有关系统(获取设备信息、当前方向)和控制系统函数(启用多点触控、控制空闲时间、加速度计、GPS)的信息。我们将使用以下系统函数来返回应用程序运行的环境信息以及加速度计事件的响应频率。
system.getInfo()
此函数返回有关应用程序运行在的系统信息。
语法:system.getInfo( param )
print( system.getInfo( "name" ) ) -- display the deviceID
参数的有效值如下:
-
"name"
—返回名称。例如,在 iTouch 上,这将是在 iTunes 中显示的电话名称,Pat 的 iTouch。 -
"model"
—返回设备类型。这些包括:-
"iPhone"
-
"iPad"
-
"iPhone Simulator"
-
"Nexus One"
-
"Droid"
-
"myTouch"
-
"Galaxy Tab"
-
-
"deviceID"
—返回设备的唯一 ID。 -
"environment"
—返回应用程序运行的环境。这些包括:-
"simulator"
:Corona 模拟器 -
"device"
:iOS、Android 设备和 Xcode 模拟器
-
-
"platformName"
—返回平台名称(操作系统名称),即以下之一:-
Mac OS X(Mac 上的 Corona 模拟器)
-
Win(Windows 上的 Corona 模拟器)
-
iPhone OS(所有 iOS 设备)
-
Android(所有 Android 设备)
-
-
"platformVersion"
—返回平台版本的字符串表示。 -
"version"
—返回使用的 Corona 版本。 -
"build"
—返回 Corona 构建字符串。 -
"textureMemoryUsed"
—以字节为单位返回纹理内存使用情况。 -
"maxTextureSize"
—返回设备支持的纹理最大宽度或高度。 -
"architectureInfo"
—返回描述您正在运行的设备底层 CPU 架构的字符串。
system.setAccelerometerInterval()
此函数设置加速度计事件的频率。iPhone 上的最小频率是 10 Hz,最大频率是 100 Hz。加速度计事件对电池的消耗很大;因此,只有在需要更快响应的游戏中才增加频率。始终尽可能降低频率以节省电池寿命。
语法:system.setAccelerometerInterval( frequency )
system.setAccelerometerInterval( 75 )
函数设置样本间隔(赫兹)。赫兹是每秒的周期数,每秒要进行的测量次数。如果你将频率设置为 75,则系统将每秒进行 75 次测量。
在你将第三章资源文件夹中的资产添加到你的项目文件夹后,让我们开始编写一些代码!
行动时间——为游戏创建变量
要启动任何应用程序,我们需要创建一个main.lua
文件。这在上一章中已经讨论过,当时我们使用了一些示例代码并在模拟器中运行它。
当游戏完成时,代码将根据你的main.lua
文件进行相应地结构化:
必要的类
(例如:物理或 ui)
变量和常量
主函数
对象方法
调用主函数
(这总是必须调用,否则你的应用程序将无法运行)
格式化你的代码以使其看起来像前面的结构是保持事物组织良好并高效运行应用程序的良好实践。
在本节中,我们将介绍将显示主菜单屏幕和一个用户可以与之交互以进入主游戏屏幕的Play按钮的显示组。所有游戏元素,如桨、球、砖块对象和抬头显示元素,都在玩家点击Play按钮后跟随。我们还将介绍将被称为alertDisplayGroup
的赢和输条件。所有这些游戏元素将在我们代码的开始部分初始化。
-
在你的文本编辑器中创建一个新的
main.lua
文件并将其保存到你的项目文件夹中。 -
我们将隐藏状态栏(特别是针对 iOS 设备)并加载物理引擎。Corona 使用已内置到 SDK 中的 Box2D 引擎。
display.setStatusBar(display.HiddenStatusBar) local physics = require "physics" physics.start() physics.setGravity(0, 0) system.setAccelerometerInterval( 100 )
注意
更多关于 Corona 物理 API 的信息可以在 Corona 网站上找到:
developer.anscamobile.com/content/game-edition-box2d-physics-engine
。在 Corona SDK 中使用的 Box2D 物理引擎是由暴雪娱乐的 Erin Catto 编写的。更多关于 Box2D 的信息可以在
box2d.org/manual.pdf
找到。 -
添加菜单屏幕对象。
local menuScreenGroup -- display.newGroup() local mmScreen local playBtn
-
添加游戏屏幕对象。
local background local paddle local brick local ball
-
添加分数和等级的 HUD 元素。
local scoreText local scoreNum local levelText local levelNum
注意
HUD 也称为抬头显示。它是一种在游戏屏幕上视觉表示角色信息的方法。
-
接下来,我们将添加用于赢/输条件的警告显示组。
local alertDisplayGroup -- display.newGroup() local alertBox local conditionDisplay local messageText
-
以下变量包含砖块显示组、分数、球的速度和在游戏事件中的值。
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 = ""
-
加速度计事件只能在设备上测试,因此我们将通过调用
"simulator"
环境为挡板添加一个触摸事件变量。这样我们就可以在 Corona 模拟器中测试挡板移动。如果你要在设备上测试应用程序,挡板上的触摸和加速度计事件监听器不会冲突。local isSimulator = "simulator" == system.getInfo("environment")
-
最后,添加
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 menuScreenGroup
、local alertDisplayGroup
和local bricks
都是我们可以分离和组织显示对象的显示组形式。例如,local menuScreenGroup
是为显示在主菜单屏幕上的对象指定的;这样它们就可以作为一个组而不是单个对象被移除。
已经添加的一些变量已经有了应用于某些游戏对象的值。球已经使用local vx = 3
和local vy = -3
设置了速度。x 和 y 速度决定了球在游戏屏幕上的移动方式。根据球与对象的碰撞位置,球将沿着连续的路径移动。brickWidth
和brickHeight
有一个值将在整个应用程序中保持不变,这样我们就可以在屏幕上均匀地排列砖块对象。
local gameEvent = " "
将存储游戏事件,如"win"
、"lose"
和"finished"
。当函数检查任何这些事件的任何游戏状态时,它将在屏幕上显示正确的条件。
我们还添加了一些系统函数。我们创建了local isSimulator = "simulator" == system.getInfo("environment")
,以便它返回有关应用程序运行系统的信息。这将用于挡板触摸事件,以便我们可以在模拟器中测试应用程序。如果构建要移植到设备上,你将只能使用加速度计来移动挡板。模拟器无法测试加速度计事件。另一个系统函数是system.setAccelerometerInterval( 100 )
。它设置加速度计事件的频率。在 iPhone 上,最小频率是 10 Hz,最大频率是 100 Hz。
空函数main()
集合将开始显示层次结构。把它想象成一个剧本。你首先看到的是介绍,然后中间发生一些动作,告诉你主要内容。在这种情况下,主要内容是游戏玩法。最后你看到的是某种结局或闭合,以将整个故事串联起来。结局是在关卡结束时显示胜负条件。
理解事件和监听器
事件被发送到监听器。函数或对象可以是事件监听器。当事件发生时,监听器通过表示事件的表被调用。所有事件都将有一个属性名称来标识事件的类型。
注册事件
显示对象和全局 Runtime 对象可以是事件监听器。您可以使用以下对象方法添加和删除事件监听器:
-
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 )
运行时事件
我们正在创建的应用程序使用运行时事件。运行时事件没有特定的目标,并且只发送到全局 Runtime。它们向所有已注册的监听器广播。以下所有事件都具有字符串名称,并将应用于 Breakout 游戏:
enterFrame
enterFrame
事件发生在应用程序的帧间隔中。它们只发送到全局 Runtime 对象。例如,如果帧率为 30fps,那么它将每秒大约发生 30 次。
此事件中可用属性:
-
event.name
是字符串"enterFrame"
。 -
event.time
是自应用程序开始以来的时间(以毫秒为单位)。
加速度计
加速度计事件允许您检测运动并确定设备相对于重力的方向。这些事件仅发送到支持加速度计的设备。它们只发送到全局 Runtime 对象。
此事件有以下属性可用:
-
event.name
是字符串"accelerometer"
。 -
event.xGravity
是 x 方向的重力加速度。 -
event.yGravity
是 y 方向的重力加速度。 -
event.zGravity
是 z 方向的重力加速度。 -
event.xInstant
是 x 方向的瞬时加速度。 -
event.yInstant
是 y 方向的瞬时加速度。 -
event.zInstant
是 z 方向的瞬时加速度。 -
event.isShake
为真时,用户正在摇动设备。
触摸事件
当用户的指尖触摸屏幕时,会生成一个击中事件并将其派发到显示层次结构中的显示对象。只有与屏幕上指尖位置相交的对象才会接收到该事件。
触摸(单点触摸)
触摸事件是一种特殊类型的命中事件。当用户的指尖触摸屏幕时,他们开始了一系列的触摸事件,每个事件都有不同的阶段。
-
event.name
是字符串"touch"
。 -
event.x
是触摸在屏幕坐标中的 x 位置。 -
event.y
是触摸在屏幕坐标中的 y 位置。 -
event.xStart
是触摸从触摸序列的"began"
阶段开始的 x 位置。 -
event.yStart
是触摸从触摸序列的"began"
阶段开始的 y 位置。 -
event.phase
是一个字符串,用于标识事件在触摸序列中的位置:-
"began"
指的是手指触摸了屏幕。 -
"moved"
指的是手指在屏幕上移动。 -
"ended"
指的是手指从屏幕上抬起。 -
"cancelled"
系统取消了触摸跟踪。
-
触摸
触摸在用户触摸屏幕时生成一个命中事件。事件被分发到显示层次结构中的显示对象。这与 touch
事件类似,除了事件回调中可用的是命中计数(触摸次数)。
-
event.name
是字符串"tap"
。 -
event.numTaps
返回屏幕上的触摸次数。 -
event.x
是触摸的 x 位置在屏幕坐标中。 -
event.y
是触摸的 y 位置在屏幕坐标中。
过渡
在本章中,我们将探讨 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
,表示 false。 -
params.onStart:
它是在补间动画开始前调用的函数或表监听器。 -
params.onComplete:
它是在补间动画完成后调用的函数或表监听器。
-
例如:
_W = display.contentWidth
_H = display.contentHeight
local square = display.newRect( 0, 0, 100, 100 )
square:setFillColor( 255,255,255 )
square.x = _W/2; square.y = _H/2
local square2 = display.newRect( 0, 0, 50, 50 )
square2:setFillColor( 255,255,255 )
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
显示对象将从当前位置移动到 x = 250 和 y = 400 的新位置,耗时 1500 毫秒。square2
显示对象将从 x = 275 和 y = 0 的位置过渡到其初始位置,耗时 1500 毫秒。
创建菜单屏幕
拥有菜单屏幕可以让玩家在不同的应用程序部分之间进行切换。通常,游戏开始时会显示一个带有交互式用户界面按钮的屏幕,按钮上标有Play或Start,以便玩家可以选择玩游戏。在任何移动应用程序中,在切换到主要内容之前都有一个菜单屏幕是标准的。
行动时间——添加主菜单屏幕
主菜单屏幕将是玩家在应用程序启动后与菜单系统交互的第一个东西。这是介绍游戏标题的好方法,同时也让玩家对应该期待的游戏环境有一个概念。我们不想让玩家在没有适当通知的情况下突然进入应用程序。当玩家启动应用程序时,让他们为即将发生的事情做好准备是很重要的。
-
我们将创建一个名为
mainMenu()
的函数来介绍标题屏幕。所以,在function main()
结束之后,添加以下行:function mainMenu() end
-
我们将在这个函数中添加一个显示组和两个显示对象。一个显示对象将代表主菜单屏幕的图像,另一个将是一个名为Play的 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:setReferencePoint(display.CenterReferencePoint) playBtn.x = _W; playBtn.y = _H + 50 playBtn.name = "playbutton" menuScreenGroup:insert(mmScreen) menuScreenGroup:insert(playBtn)
-
记得那个空的
main()
函数吗?我们需要在它里面调用mainMenu()
。整个函数应该看起来像这样:function main() mainMenu() end
-
在
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
-
接下来,我们需要为
playBtn
添加一个事件监听器,以便在它被点击时,将调用loadGame()
函数。在mainMenu()
函数的最后一种方法之后添加以下行:playBtn:addEventListener("tap", loadGame)
-
在模拟器中运行项目。你应该会看到主菜单屏幕显示Breakout和Play按钮。
刚才发生了什么?
创建一个主菜单屏幕只需要几块代码。对于loadGame(event)
,我们设置了一个名为event
的参数。当调用if
语句时,它将playbutton
作为参数,该参数引用显示对象playBtn
并检查它是否为真。由于它是真的,menuScreenGroup
将从舞台中移除,并调用addGameScreen()
函数。同时,playBtn
的事件监听器也将从场景中移除。
尝试创建一个帮助屏幕
目前菜单系统的设计是这样的,它从主菜单屏幕开始,然后过渡到游戏玩法屏幕。你可以选择扩展菜单屏幕而不立即进入游戏。在主菜单屏幕之后,我们可以添加一个额外的帮助菜单屏幕,该屏幕将向玩家解释如何玩游戏。
在你喜欢的图像编辑程序中创建一个新图像,并写出如何玩游戏的步骤。然后创建一个名为下一步的新按钮,并将这两个艺术资源添加到你的项目文件夹中。在你的代码中,你需要创建一个新的函数和下一步按钮的事件监听器,以便过渡到游戏玩法屏幕。
创建游戏玩法场景
现在我们已经建立了菜单系统,我们可以开始构建应用程序的游戏玩法元素。我们将开始添加玩家将与之交互的所有主要游戏对象。在添加游戏对象时,需要考虑的是它们在屏幕上的位置。鉴于这款游戏将以横屏模式进行,我们必须记住在 x 方向上有足够的空间,而在 y 方向上空间较小。根据游戏的原设计,屏幕的底部墙壁会导致玩家在球落在该区域时失去关卡或转向。所以如果你要确定放置挡板对象的位置,我们不会将其设置在屏幕顶部。将挡板设置在屏幕底部更合理,这样可以更好地保护球。
行动时间——添加游戏对象
让我们添加玩家在游戏过程中将看到的显示对象。
-
在
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"
-
接下来,我们将添加在游戏过程中显示的分数和关卡编号的文本。
scoreText = display.newText("Score:", 5, 2, "Arial", 14) scoreText:setTextColor(255, 255, 255, 255) scoreNum = display.newText("0", 54, 2, "Arial", 14) scoreNum:setTextColor(255, 255, 255, 255) levelText = display.newText("Level:", 420, 2, "Arial", 14) levelText:setTextColor(255, 255, 255, 255) levelNum = display.newText("1", 460, 2, "Arial", 14) levelNum:setTextColor(255, 255, 255, 255)
-
要构建第一个游戏关卡,我们需要调用
gameLevel1()
函数,这个函数将在本章后面进行解释。别忘了用end
关闭addGameScreen()
函数。gameLevel1() end
刚才发生了什么?
addGameScreen()
函数显示了游戏过程中显示的所有游戏对象。我们添加了本章提供的艺术资源中的background
、paddle
和ball
。
我们已经在游戏屏幕顶部添加了分数和级别的文本。scoreNum
初始设置为 0
。我们将在下一章讨论如何在砖块碰撞时更新分数数字。levelNum
从 1
开始,并在完成级别并进入下一个级别时更新。
我们通过调用 gameLevel1()
结束了函数,该函数将在下一节中实现,以开始第一级。
行动时间——构建砖块
砖块是我们需要添加到这个应用程序中的最后一个游戏对象。我们将为这个游戏创建两个不同的级别。每个级别都将具有与其他级别不同的砖块布局。
-
我们将创建第一级的函数。让我们创建一个新的函数
gameLevel1()
。我们还将设置currentLevel = 1
,因为应用程序从 Level 1 开始。然后我们将添加bricks
显示组并将其设置为toFront()
,使其出现在游戏背景之前。function gameLevel1() currentLevel = 1 bricks:toFront()
注意
方法
object:toFront( )
将目标对象移动到其父组(object.parent)的可视前端。在这种情况下,我们将bricks
组设置为在游戏过程中显示为最前面的显示组,因此它出现在背景图像之前。 -
接下来,添加一些局部变量,以显示将在屏幕上显示多少行和列的砖块以及每个砖块在游戏场中的放置位置。
local numOfRows = 4 local numOfColumns = 4 local brickPlacement = {x = (_W) - (brickWidth * numOfColumns ) / 2 + 20, y = 50}
-
创建双重
for
循环,一个用于numOfRows
,另一个用于numOfColumns
。创建一个根据其宽度、高度和numOfRows
和numOfColumns
对应的数字放置的砖块实例。砖块显示对象的美术资源由本章提供。之后,使用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
-
Level 2 的设置与 Level 1 的排列方式相似。代码几乎相同,除了我们新的函数被命名为
gameLevel2()
,currentLevel = 2
,以及numOfRows
和numOfColumns
的值不同。在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
-
保存您的文件并重新启动模拟器。您将能够与 Play 按钮交互,并看到从 主菜单 屏幕到游戏屏幕的过渡。您将在屏幕上看到 Level 1 的游戏布局。
刚才发生了什么?
bricks
显示组被设置为 bricks:toFront()
。这意味着除了 background
、paddle
和 ball
显示对象外,该组将始终位于显示层次结构的前面。
gameLevel1()
为游戏场中显示的砖块对象数量设置了值。它们将根据设备外壳的 contentWidth
居中,并在 y 方向上设置为 50。砖块组通过 brickPlacement
放置在右上角附近,占据屏幕中间,并减去所有砖块对象总宽度的一半。然后我们在 x 方向上再添加 20 个像素,使其与桨居中对齐。
我们为numOfRows
和numOfColumns
创建了双重for
循环,从屏幕的左上角开始创建砖块对象。
注意,brick
显示对象被命名为"brick"
。只需记住,当调用对象时"brick"
不能像brick
那样使用。"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
brick
的物理体被设置为"static"
,因此它不受重力下拉的影响。然后它被添加到bricks
组下的bricks.insert(bricks, brick)
。
尝试一下英雄—专注的平台游戏
完成这一章和下一章后,您可以自由地重新设计显示图像,以专注于特定的平台。例如,您可以轻松地将代码转换为与所有 iOS 设备兼容。这可以通过将显示对象转换为display.newImageRect( [parentGroup,] filename [, baseDirectory] w, h )
来实现,这样您就可以在具有视网膜显示屏的设备上替换更高分辨率的图像(即 iPhone 4/iPod Touch 4G)。请记住,您必须调整配置设置以应用更改。这涉及到在config.lua
文件中添加@2x
图像后缀(或您首选的命名约定)。
红色警报!
在每一场游戏中,都有些消息告诉你主要行动结束后你的进度状态。对于这个应用程序,我们需要一种方式让玩家知道他们是否赢得或输掉了一轮,他们如何再次玩游戏,或者游戏何时正式结束。
行动时间—显示游戏消息
让我们设置一些胜负通知,以便我们可以在游戏中显示这些事件发生:
-
创建一个名为
alertScreen()
的新函数,并传递两个名为title
和message
的参数。添加一个新的显示对象alertbox
,并使用easing.outExpo
将其从xScale
和yScale
的 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})
-
将
title
参数存储在名为conditionDisplay
的文本对象中。conditionDisplay = display.newText(title, 0, 0, "Arial", 38) conditionDisplay:setTextColor(255,255,255,255) conditionDisplay.xScale = 0.5 conditionDisplay.yScale = 0.5 conditionDisplay:setReferencePoint(display.CenterReferencePoint) conditionDisplay.x = display.contentCenterX conditionDisplay.y = display.contentCenterY - 15
-
将
message
参数存储在名为messageText
的文本对象中。messageText = display.newText(message, 0, 0, "Arial", 24) messageText:setTextColor(255,255,255,255) messageText.xScale = 0.5 messageText.yScale = 0.5 messageText:setReferencePoint(display.CenterReferencePoint) messageText.x = display.contentCenterX messageText.y = display.contentCenterY + 15
-
创建一个新的显示组
alertDisplayGroup
,并将所有对象插入到该组中。关闭函数。alertDisplayGroup = display.newGroup() alertDisplayGroup:insert(alertBox) alertDisplayGroup:insert(conditionDisplay) alertDisplayGroup:insert(messageText) end
-
保存你的文件并在模拟器中运行项目。播放按钮的功能仍然会跳转到第 1 级的游戏屏幕。目前没有任何对象有移动。我们将在下一章中添加触摸事件、球体移动和碰撞。所有游戏对象都应该像以下截图所示排列:
刚才发生了什么?
我们已经设置了游戏警报系统,但目前尚未启用,直到我们添加更多游戏功能来设置游戏对象。下一章将演示alertScreen()
函数传递两个参数,title
和message
。当条件发生时,alertBox
显示对象作为警报文本的背景出现。当alertBox
弹出时,它从xScale
和yScale
的 0.5 过渡到完整的图像缩放,这基本上相当于半秒。
conditionDisplay
对象传递了 title
参数。这将是要显示的文本,你赢了或你输了。
messageText
对象传递了 message
参数。带有此参数的文本会在条件满足后显示消息,例如再玩一次或继续。
此函数中的所有对象随后被插入到alertDisplayGroup = display.newGroup()
中。当它出现在舞台上或消失时,将作为一个整体而不是单个对象。
在模拟器中运行代码时;如果你的终端窗口中出现错误,请确保检查导致错误的行。有时一个简单的大小写错误,甚至是一个缺失的逗号或引号,都可能使你的应用程序在模拟器中无法运行。确保你了解这些常见的错误。它们很容易被忽视。
你可以参考Chapter 3
文件夹中的Breakout - Part 1
文件夹,看看这个教程代码的前半部分是如何设置的。
快速问答——构建游戏
-
当你在代码中添加物理引擎时,哪些函数是有效的,可以添加到你的应用程序中?
-
a.
physics.start()
-
b.
physics.pause()
-
c.
physics.stop()
-
d. 以上皆非
-
-
添加事件监听器时,哪个是正确的?
-
a.
button:addeventlistener("touch", listener )
-
b.
button:AddEventListener("touch", listener )
-
c.
button:addEventListener(touch, listener )
-
d.
button:addEventListener("touch", listener )
-
-
正确的方式是将以下显示对象过渡到 x = 300, y = 150,alpha 变为 0.5,需要 2 秒吗?
local square = display.newRect( 0, 0, 50, 50 ) square:setFillColor( 255,255,255 ) square.x = 100 square2.y = 300
-
a.
transition.to( square, { time=2000, x=300, y=150, alpha=0.5 } )
-
b.
transition.from( square, { time=2000, x=300, y=150, alpha=0.5 } )
-
c.
transition.to( square, { time=2, x=300, y=150, alpha=0.5 } )
-
d. 以上皆非
-
摘要
我们已经完成了这个游戏教程的前半部分。正确地构建 Corona 项目结构可以使你的代码组织更清晰,更好地跟踪你的资源。我们已经体验了与涉及游戏逻辑的小块代码一起工作的感觉,这是允许应用程序运行所必需的。
到目前为止我们有:
-
指定了在显示 Android 和 iOS 设备内容时的构建配置。
-
介绍了将在应用程序中运行的变量和常量。
-
实例化了物理引擎,并开始将其应用于需要物理体的游戏对象。
-
创建了菜单与游戏屏幕之间的过渡
-
向屏幕添加了显示对象和游戏信息
到目前为止,我们已经取得了很多成就,包括在编写应用程序的过程中学习了一些新的 API。在游戏完全功能化之前,我们还有很多东西要添加。在游戏完全功能化之前,我们还有很多东西要添加。
在下一章中,我们将完成这个游戏教程的最后半部分。我们将处理挡板、球、砖块和墙壁对象的碰撞检测。此外,我们还将学习如何在场景中移除砖块时更新分数,并激活我们的胜负条件。我们已经进入冲刺阶段。让我们继续前进!
第四章:游戏控制
到目前为止,我们在上一章中完成了游戏的一半。我们通过将游戏对象引入屏幕来开始开发项目的初始结构。目前,拍子和球的移动是无效的,但模拟器中显示的所有内容都相应地缩放到了原始游戏设计。完成本教程的最后阶段是添加游戏中将发生的所有动作,包括对象移动和更新分数。
在本章中,我们将涵盖:
-
使用触摸事件和加速度计移动拍子
-
场景中所有游戏对象的碰撞检测
-
碰撞检测后移除对象
-
球在屏幕边界内的移动
-
计算分数
-
胜利和失败条件
最后冲刺!我们可以做到!
向上移动方向
如果让物体出现在屏幕上让你兴奋,那么等你看到它们移动!Breakout 游戏的主要目标是保持球在拍子位置上方,以保持游戏状态,并使其与所有砖块碰撞以完成关卡。保持悬念流动的是对整个游戏屏幕中球移动的期待。如果没有在游戏对象上添加物理边界以进行碰撞检测,这是不可能的。
让我们更加物理化
在上一章中,我们讨论了如何将物理引擎集成到代码中。我们还开始将物理体实现到砖块对象中,现在我们需要将相同的操作应用于其他活动游戏对象,如拍子和球。让我们继续本教程的最后部分。我们将继续使用Breakout
项目文件夹中的main.lua
文件。
physics.addBody()
使用一行代码,Corona 显示对象可以被转换成模拟物理对象。
-
如果没有指定形状信息,显示对象将采用原始图像的实际矩形边界来创建物理体。例如,如果显示对象是 100 x 100 像素,那么这将物理体的实际大小。
-
如果指定了形状,则物体边界将遵循形状提供的多边形。形状坐标必须按顺时针顺序定义,并且生成的形状必须是凸形状。
-
如果指定了半径,则物体边界将是圆形的,并且位于创建物理体的显示对象中间。
物体形状是一个相对于显示对象中心的局部(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 } )
参数:
-
Object
(对象): 一个显示对象。 -
bodyType
(字符串): 指定身体类型是可选的。它使用一个字符串参数在第一个身体元素之前。可能的类型是"static", "dynamic"
和"kinematic"
。如果没有指定值,默认类型是"dynamic"
。-
静态身体不会移动,也不会相互交互;静态对象的例子包括地面或弹球机的墙壁。
-
动态身体受到重力和其他身体类型碰撞的影响。
-
动力学对象受到力的作用,但不受到重力的影响,因此你应该通常将可拖动对象设置为
"kinematic"
,至少在拖动事件期间。
-
-
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 位于对象中心的顶部)。
开始物理操作——启动挡板和球的物理属性
目前,我们的显示对象相当静止。为了开始游戏,我们必须激活挡板和球体的物理属性,以便在游戏中发生任何类型的移动。
-
在
gameLevel1()
函数上方创建一个名为startGame()
的新函数。function startGame()
-
添加以下行以实例化挡板和球的物理属性:
physics.addBody(paddle, "static", {density = 1, friction = 0, bounce = 0}) physics.addBody(ball, "dynamic", {density = 1, friction = 0, bounce = 0})
-
创建一个事件监听器,使用背景显示对象来移除
startGame()
的"tap"
事件。使用end
结束函数。background:removeEventListener("tap", startGame) end
-
在上一章中我们创建的
addGameScreen()
函数中,必须在调用gameLevel1()
函数之后添加以下行。这样,当背景被触摸时,实际上开始游戏。background:addEventListener("tap", startGame)
刚才发生了什么?
挡板对象具有"static"
类型的身体,因此它不会受到任何对其发生的碰撞的影响。
球对象具有"dynamic"
类型的身体,因为我们希望它能够受到由于墙壁边缘、砖块和挡板方向变化而产生的屏幕碰撞的影响。
在startGame()
函数中移除背景上的事件监听器,这样它就不会影响游戏中应用的任何其他触摸事件。
挡板移动
让桨在两侧移动是必须完成的关键动作之一。游戏设计的一部分是保护球不达到屏幕底部。我们将在模拟器和加速度计中分离桨的移动。模拟器中的移动使我们能够通过触摸事件进行测试,因为加速度计动作无法在模拟器中测试。
行动时间——在模拟器中拖动桨
目前,桨没有任何移动。屏幕上没有设置用于转换的坐标,所以让我们创建它。
-
在
addGameScreen()
函数下方,创建一个名为dragPaddle(event)
的新函数。function dragPaddle(event)
-
接下来,我们将专注于在游戏屏幕的边界内移动桨的左右移动。
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.x
在 x 方向上不会越过< 0
。当桨滑到屏幕的右侧时,paddle.x
在 x 方向上不会越过> display.contentWidth
。
屏幕右侧没有指定的坐标,因为代码应该是适用于 iOS 和 Android 设备上所有屏幕尺寸的通用代码。这两个平台具有不同的屏幕分辨率,因此display.contentWidth
考虑了它们。
行动时间——使用加速度计移动桨
如前所述,加速度计事件无法在模拟器中测试。它们只有在将游戏构建上传到设备上才能看到结果时才起作用。桨的移动将在水平轴上的关卡墙壁边界内。
-
在
dragPaddle()
函数下方,创建一个名为movePaddle(event)
的新函数。function movePaddle(event)
-
使用
yGravity
添加加速度计运动。它提供了 y 方向上的重力加速度。paddle.x = display.contentCenterX - (display.contentCenterX * (event.yGravity*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
。
注意
当使用xGravity
和yGravity
时,加速度计事件基于纵向比例。当为横向模式指定显示对象时,xGravity
和yGravity
值会切换以补偿事件以正确工作。
我们已经为桨应用了与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
这仍然使桨不会越过任何墙壁边界。
球与桨的碰撞
每次球与挡板碰撞时,球的运动必须流畅。这意味着在游戏场地的所有方向上都要进行适当的方向改变。
行动时间——使球反弹到挡板上
我们将检查球击中挡板的哪一侧,以选择球下一次移动的方向。在现实环境中,确保运动轨迹跟随任何方向的击打是很重要的。在每次挡板碰撞中,我们都要确保球向上移动。
-
在
movePaddle()
函数之后为球创建一个名为bounce()
的新函数。function bounce()
-
在 y 方向的速度中添加一个-3 的值。这将使球向上移动:
vy = -3
-
检查与
paddle
和ball
的碰撞,并关闭函数: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 )
-- 从层次结构中移除myImage
或者
myImage:removeSelf( )
-- 与上面相同
这并没有从显示对象中释放所有内存。为了确保显示对象被正确移除,我们需要消除所有对该对象的变量引用。
变量引用
即使显示对象已经从层次结构中移除,也存在一些情况下对象仍然存在。为了做到这一点,我们将属性设置为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 )
一块接一块
游戏中的砖块是主要的障碍,因为必须清除它们才能进入下一轮。在这个版本的 Breakout 中,玩家必须在一轮内摧毁所有砖块。未能做到这一点将导致从当前级别的开始重新开始。
行动时间——移除砖块
当球与砖块碰撞时,我们将使用应用于桨的相同技术来确定球将跟随的侧面。当砖块被击中时,我们需要找出哪个砖块被触摸,然后从舞台和砖块组中移除它。每次移除砖块都会将 100 分加到得分上。得分将从得分常量中取出并添加到当前得分作为文本。
-
在
gameLevel2()
函数下方创建一个名为removeBrick(event):
的函数。function removeBrick(event)
-
使用
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
-
添加以下
if
语句,当球与一个砖块碰撞时从场景中移除砖块。在碰撞发生后,将score
增加 1。启动scoreNum
以获取score
的值并乘以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:setReferencePoint(display.CenterLeftReferencePoint) scoreNum.x = 54 end
-
当所有砖块从关卡中销毁后,创建一个在Alert屏幕上弹出的
if
语句,以显示胜利条件,并将gameEvent
字符串设置为"win"
。if bricks.numChildren < 0 then alertScreen("YOU WIN!", "Continue") gameEvent = "win" end
-
使用
end
关闭函数。end
刚才发生了什么?
如果你记得上一章,我们给brick
对象起了一个叫"brick"
的名字。
当球击中任何单个砖块的左侧时,球会向左移动。当球击中砖块的右侧时,它会向右移动。每个物体的宽度作为一个整体来计算球移动的方向。
当砖块被击中时,球会向上弹起(y 方向)。在每次碰撞后,球与砖块接触;物体从场景中移除并从记忆中销毁。
bricks.numChildren -1
从最初的总砖块数中减去计数。当移除砖块时,每次得分增加 100 分。scoreNum
文本对象在每次击中砖块时更新得分。
当所有砖块消失后,Alert屏幕会弹出通知玩家已赢得关卡。我们还设置了gameEvent = "win"
,这将在另一个函数中使用,以将事件过渡到新场景。
方向变化
除了球与桨的运动之外,其他因素还包括与墙边界的碰撞状态。当发生碰撞时,球会改变方向。每个动作都有一个反应,就像现实世界的物理一样。
行动时间——更新球
球需要在没有重力影响的情况下连续移动。我们必须考虑侧墙、顶部和底部墙壁。当任何边界发生碰撞时,x 和 y 方向的速度必须反映相反的方向。我们需要设置坐标,球只能通过这些坐标移动,并且当它通过桨区域下方的区域时发出警报。
-
在
removeBrick(event)
函数下方创建一个名为updateBall()
的新函数。function updateBall()
-
添加球体运动:
ball.x = ball.x + vx ball.y = ball.y + vy
-
添加 x 方向的球体运动:
if ball.x < 0 or ball.x + ball.width > display.contentWidth then vx = -vx end
-
添加 y 方向的球体运动:
if ball.y < 0 then vy = -vy end
-
在游戏界面底部碰撞时添加球体运动。创建一个失去警报屏幕和一个名为
"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
。球体不反射相反方向的情况只有当它击中屏幕底部时。
警报屏幕显示失败条件,强调玩家再次游戏。在另一个if
语句中使用gameEvent = "lose"
来重置当前级别。
级别过渡
当出现胜利或失败条件时,游戏需要一种方式来过渡到下一级或重复当前级别。主要游戏对象必须重置到起始位置,并重新绘制砖块。当你第一次开始游戏时,基本上有相同的概念。
行动时间——重置和更改级别
我们需要创建设置游戏第一级和第二级的函数。如果需要重玩游戏级别,只能访问用户失去的当前级别。
-
创建一个名为
changeLevel1()
的新函数。这将放置在updateBall()
函数下方:function changeLevel1()
-
当玩家输掉回合时清除
砖块
组并重置它们:bricks:removeSelf() bricks.numChildren = 0 bricks = display.newGroup()
-
移除
alertDisplayGroup
: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
-
重新绘制当前级别的
砖块
:gameLevel1()
-
为
背景
对象添加一个事件监听器以调用startGame()
。关闭函数。background:addEventListener("tap", startGame) end
-
接下来创建一个名为
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()
移除了砖块
组。
当任何警报屏幕弹出时,无论是胜利还是失败,在重置过程中也会移除整个alertDisplayGroup
。球体
和挡板
被设置回起始游戏位置。
调用gameLevel1()
重新绘制第一级的砖块。该函数包含砖块
显示对象和砖块
组的初始设置。
再次使用背景
对象调用startGame()
函数,并添加事件监听器。当需要设置第二级时,使用与changeLevel1()
函数中相同的程序,但调用changeLevel2()
,并使用gameLevel2()
重新绘制砖块。
英雄尝试——添加更多级别
目前,游戏只有两个级别。要扩展这个游戏,可以添加更多级别。它们可以使用为gameLevel1()
和gameLevel2()
创建的逻辑创建,通过调整用于创建砖块行和列的数字。你必须创建一个新的函数来重置级别。我们可以使用在changeLevel1()
和changeLevel2()
上执行的方法来重新创建级别并重置它。
有赢有输
没有什么比赢得胜利的期待更令人兴奋的了。直到你犯了一个小小的错误,导致你不得不从头开始。别担心,这并不是世界末日,你总是可以再次尝试并从击败级别的错误中学习。
程序中发生的游戏事件,如胜利或失败条件,会通知玩家他们的进度。游戏必须有一种方法来指导玩家进行哪些操作以重新播放级别或进入下一个级别。
行动时间——创建胜利和失败条件
为了让游戏在游戏中出现任何警报,我们需要为每个级别中可能出现的每个场景创建一些if
语句。当这种情况发生时,分数需要重置为零。
-
在
alertScreen()
函数下方,创建一个新的函数,称为restart():
function restart()
-
创建一个
if
语句以处理第一级完成时的"win"
游戏事件,并过渡到第 2 级。if gameEvent == "win" and currentLevel == 1 then currentLevel = currentLevel + 1 changeLevel2() levelNum.text = tostring(currentLevel)
注意
tostring()
函数可以将任何参数转换为字符串。在上面的例子中,当发生"win"
游戏事件时,currentLevel
的值从 1 变为 2。该值将转换为字符串格式,levelNum
文本对象可以在屏幕上显示第 2 级。 -
当第二级完成时,添加一个
elseif
语句以处理"win"
游戏事件,并通知玩家游戏已完成。elseif gameEvent == "win" and currentLevel == 2 then alertScreen(" Game Over", " Congratulations!") gameEvent = "completed"
-
在第一级添加另一个
elseif
语句以处理"lose"
游戏事件。将分数重置为零并重新播放第 1 级。elseif gameEvent == "lose" and currentLevel == 1 then score = 0 scoreNum.text = "0" changeLevel1()
-
在第二级添加另一个
elseif
语句以处理"lose"
游戏事件。将分数重置为零并重新播放第 2 级。elseif gameEvent == "lose" and currentLevel == 2 then score = 0 scoreNum.text = "0" changeLevel2()
-
最后,添加另一个
elseif
语句以处理gameEvent = "completed"
。使用end
关闭函数。elseif gameEvent == "completed" then alertBox:removeEventListener("tap", restart) end end
-
现在,我们需要回溯并使用
alertBox
对象为alertScreen()
函数添加一个事件监听器。我们将将其添加到函数的底部。这将激活restart()
函数。alertBox:addEventListener("tap", restart)
刚才发生了什么?
restart()
函数检查游戏过程中发生的所有gameEvent
和currentLevel
变量。当游戏事件检查字符串"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 )
行动时间——添加游戏监听器
对于我们为游戏对象创建的许多函数,我们需要激活事件监听器,以便它们可以运行代码,并在游戏停止时禁用它们。
-
为了完成这个游戏,我们需要创建的最后一个函数是
gameListeners()
,它也将有一个名为event
的参数。这应该紧接在gameLevel2()
函数之后。function gameListeners(event)
-
使用
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)
-
接下来,我们将为事件监听器添加一个
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
-
为了使
function gameListeners()
正常工作,我们需要在startGame()
函数中使用参数中的"add"
字符串来实例化它。将其放置在函数末尾之前。gameListeners("add")
-
在
alertScreen()
函数中,将参数中的"remove"
字符串添加到函数的开始处。gameListeners("remove")
-
所有代码都已编写!现在请在模拟器中运行游戏。应用程序也已准备好适配设备。制作一个适合你正在开发的设备所需尺寸的简单图标图像。编译构建并在你的设备上运行它。
刚才发生了什么?
对于event
参数有两个if
语句集,"add"
和"remove"
。
此函数中的所有事件监听器在使游戏运行方面都发挥着重要作用。"accelerometer"
和"enterframe"
作为运行时事件使用,因为它们没有特定的目标。
paddle
和ball
都有"collision"
事件,它们将在任何对象接触时执行其功能。
"touch"
事件允许用户触摸并拖动挡板,使其可以在模拟器中来回移动。
注意event == "remove"
会移除游戏中所有活跃的事件监听器。当游戏开始时,gameListeners("add")
被激活。当达到胜利或失败条件时,gameListeners("remove")
被激活。
来吧,英雄——让我们把一切都颠倒过来。
如果我们决定将游戏上下颠倒呢?换句话说,将挡板放置在屏幕顶部附近,球在挡板下方,砖块组靠近屏幕底部。
你需要考虑的事情:
-
现在顶部墙壁是你必须阻止球进入的区域。
-
球与砖块碰撞时移动的 y 方向。
-
当球与底部墙壁碰撞时,它必须从底部墙壁反弹。
正如你所见,在从负值切换到正值以及反之亦然之前,有几件事情需要考虑。确保验证你的逻辑,并确保在创建这个新变体时它是有意义的。
结果出来了!
让我们逐块重申,以确保我们已经将所有内容添加到我们的游戏中。你也可以参考第四章文件夹中的Breakout Final
文件夹,查看最终代码。我们确保介绍了游戏中使用的变量。我们还初始化了main()
函数,该函数启动游戏。实现了主菜单屏幕,包含游戏标题和开始游戏按钮。
-- Hide Status Bar
display.setStatusBar(display.HiddenStatusBar)
-- Physics Engine
local physics = require "physics"
physics.start()
physics.setGravity(0, 0)
-- Accelerometer
system.setAccelerometerInterval( 100 )
-- Menu Screen
local menuScreenGroup -- display.newGroup()
local mmScreen
local playBtn
-- Game Screen
local background
local paddle
local brick
local ball
-- Score/Level Text
local scoreText
local scoreNum
local levelText
local levelNum
-- alertDisplayGroup
local alertDisplayGroup -- display.newGroup()
local alertBox
local conditionDisplay
local messageText
-- Variables
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 = ""
local isSimulator = "simulator" == system.getInfo("environment")
-- Main Function
function main()
mainMenu()
end
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:setReferencePoint(display.CenterReferencePoint)
playBtn.x = _W; playBtn.y = _H + 50
playBtn.name = "playbutton"
menuScreenGroup:insert(mmScreen)
menuScreenGroup:insert(playBtn)
-- Button Listeners
playBtn:addEventListener("tap", loadGame)
end
接下来,我们将menuScreenGroup
从舞台移除以加载主游戏区域。游戏的主要显示对象,如挡板、球和砖块被添加。分数和关卡数字作为 UI 元素显示,并在整个游戏过程中更新。同时添加了模拟器和加速度计中的挡板移动以及挡板和球的碰撞检测。
function loadGame(event)
if event.target.name == "playbutton" then
-- Start Game
transition.to(menuScreenGroup,{time = 0, alpha=0, onComplete = addGameScreen})
playBtn:removeEventListener("tap", loadGame)
end
end
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"
-- Text
scoreText = display.newText("Score:", 5, 2, "Arial", 14)
scoreText:setTextColor(255, 255, 255, 255)
scoreNum = display.newText("0", 54, 2, "Arial", 14)
scoreNum:setTextColor(255, 255, 255, 255)
levelText = display.newText("Level:", 420, 2, "Arial", 14)
levelText:setTextColor(255, 255, 255, 255)
levelNum = display.newText("1", 460, 2, "Arial", 14)
levelNum:setTextColor(255, 255, 255, 255)
-- Build Level Bricks
gameLevel1()
-- Start Listener
background:addEventListener("tap", startGame)
end
-- Used to drag the paddle on the simulator
function dragPaddle(event)
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
function movePaddle(event)
-- Accelerometer Movement
--must be yGravity since it's landscape
paddle.x = display.contentCenterX - (display.contentCenterX * (event.yGravity*3))
-- Wall Borders
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
function bounce()
vy = -3
-- Paddle Collision, check the which side of the paddle the ball hits, left, right
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
挡板和球的物理属性被添加到游戏开始时。为两个关卡各自创建了砖块布局。我们从游戏对象需要激活时开始添加事件监听器,并在游戏结束后移除。
function startGame()
-- Physics
physics.addBody(paddle, "static", {density = 1, friction = 0, bounce = 0})
physics.addBody(ball, "dynamic", {density = 1, friction = 0, bounce = 0})
background:removeEventListener("tap", startGame)
gameListeners("add")
end
-- HOW TO BUILD BLOCKS
function gameLevel1()
currentLevel = 1
bricks:toFront()
local numOfRows = 4
local numOfColumns = 4
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
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
function gameListeners(event)
if event == "add" then
Runtime:addEventListener("accelerometer", movePaddle)
Runtime:addEventListener("enterFrame", updateBall)
paddle:addEventListener("collision", bounce)
ball:addEventListener("collision", removeBrick)
-- Used to drag the paddle on the simulator
paddle:addEventListener("touch", dragPaddle)
elseif event == "remove" then
Runtime:removeEventListener("accelerometer", movePaddle)
Runtime:removeEventListener("enterFrame", updateBall)
paddle:removeEventListener("collision", bounce)
ball:removeEventListener("collision", removeBrick)
-- Used to drag the paddle on the simulator
paddle:removeEventListener("touch", dragPaddle)
end
end
每当球与砖块碰撞时,砖块就会从场景中移除。对于每个墙壁、挡板或砖块的碰撞,都会更新球的方向变化。每次发生胜负条件时,所有游戏对象都会重置,以开始当前或新关卡。
--BRICK REMOVAL
function removeBrick(event)
-- Check the which side of the brick the ball hits, left, right
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
-- Bounce, Remove
if event.other.name == "brick" then
vy = vy * -1
event.other:removeSelf()
event.other = nil
bricks.numChildren = bricks.numChildren - 1
-- Score
score = score + 1
scoreNum.text = score * scoreIncrease
scoreNum:setReferencePoint(display.CenterLeftReferencePoint)
scoreNum.x = 54
end
-- Check if all bricks are destroyed
if bricks.numChildren < 0 then
alertScreen("YOU WIN!", "Continue")
gameEvent = "win"
end
end
-- BALL FUNCTION
function updateBall()
-- Ball Movement
ball.x = ball.x + vx
ball.y = ball.y + vy
-- Wall Collision
if ball.x < 0 or ball.x + ball.width > display.contentWidth then
vx = -vx
end--Left
if ball.y < 0 then
vy = -vy
end--Up
if ball.y + ball.height > paddle.y + paddle.height then
alertScreen("YOU LOSE!", "Play Again") gameEvent = "lose"
end--down/lose
end
-- RESET LEVEL
function changeLevel1()
-- Clear Level Bricks
bricks:removeSelf()
bricks.numChildren = 0
bricks = display.newGroup()
-- Remove Alert
alertBox:removeEventListener("tap", restart)
alertDisplayGroup:removeSelf()
alertDisplayGroup = nil
-- Reset Ball and Paddle position
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
-- Redraw Bricks
gameLevel1()
-- Start
background:addEventListener("tap", startGame)
end
function changeLevel2()
-- Clear Level Bricks
bricks:removeSelf()
bricks.numChildren = 0
bricks = display.newGroup()
-- Remove Alert
alertBox:removeEventListener("tap", restart)
alertDisplayGroup:removeSelf()
alertDisplayGroup = nil
-- Reset Ball and Paddle position
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
-- Redraw Bricks
gameLevel2()
-- Start
background:addEventListener("tap", startGame)
end
当发生条件时,一个警报屏幕弹出,通知玩家发生了什么。创建触发警报的显示对象为一个函数。最后,创建胜负参数以确定当前关卡是否需要重玩、进入下一关卡,或者游戏是否已经完成。
function alertScreen(title, message)
gameListeners("remove")
alertBox = display.newImage("alertBox.png")
alertBox.x = 240; alertBox.y = 160
transition.from(alertBox, {time = 300, xScale = 0.5, yScale = 0.5, transition = easing.outExpo})
conditionDisplay = display.newText(title, 0, 0, "Arial", 38)
conditionDisplay:setTextColor(255,255,255,255)
conditionDisplay.xScale = 0.5
conditionDisplay.yScale = 0.5
conditionDisplay:setReferencePoint(display.CenterReferencePoint)
conditionDisplay.x = display.contentCenterX
conditionDisplay.y = display.contentCenterY - 15
messageText = display.newText(message, 0, 0, "Arial", 24)
messageText:setTextColor(255,255,255,255)
messageText.xScale = 0.5
messageText.yScale = 0.5
messageText:setReferencePoint(display.CenterReferencePoint)
messageText.x = display.contentCenterX
messageText.y = display.contentCenterY + 15
alertDisplayGroup = display.newGroup()
alertDisplayGroup:insert(alertBox)
alertDisplayGroup:insert(conditionDisplay)
alertDisplayGroup:insert(messageText)
alertBox:addEventListener("tap", restart)
end
-- WIN/LOSE ARGUMENT
function restart()
if gameEvent == "win" and currentLevel == 1 then
currentLevel = currentLevel + 1
changeLevel2()--next level
levelNum.text = tostring(currentLevel)
elseif gameEvent == "win" and currentLevel == 2 then
alertScreen(" Game Over", " Congratulations!")
gameEvent = "completed"
elseif gameEvent == "lose" and currentLevel == 1 then
score = 0
scoreNum.text = "0"
changeLevel1()--same level
elseif gameEvent == "lose" and currentLevel == 2 then
score = 0
scoreNum.text = "0"
changeLevel2()--same level
elseif gameEvent == "completed" then
alertBox:removeEventListener("tap", restart)
end
end
main()
注意变量和函数的大小写敏感,以防遇到错误。同时,确保检查代码中是否缺少任何必要的标点符号。这些很容易被忽略。请参考模拟器中的终端窗口以获取任何错误参考。
突击测验——与游戏控制操作
-
如何正确地从舞台中移除显示对象?
-
a.
remove()
-
b.
object:remove()
-
c.
object:removeSelf(); object = nil
-
d. 以上都不是
-
-
正确的方式是将以下显示对象转换为物理对象是什么?
local ball = display.newImage("ball.png")
-
a.
physics.addBody( circle, { density=2.0, friction=0.5, bounce=0.2, radius = 25 } )
-
b.
physics.addBody( circle, "dynamic", { density=2.0, friction=0.5, bounce=0.2, radius = 15 } )
-
c. a 和 b
-
d. 以上都不是
-
-
以下函数中,
"began"
一词的最佳代表是什么?local function onCollision( event ) if event.phase == "began" and event.object1.myName == "Box 1" then print( "Collision made." ) end end
-
a. 手指在屏幕上移动
-
b. 手指从屏幕上抬起
-
c. 系统取消了跟踪起始触摸
-
d. 手指触摸了屏幕
-
摘要
恭喜!你已经完成了你的第一个游戏!你应该为自己感到非常自豪。现在你已经体验到了使用 Corona 制作应用程序的简单性。制作一个应用程序可能只需要几百行代码。
在本章中,我们涵盖了以下内容:
-
通过触摸事件为挡板添加了移动
-
介绍了加速度计功能
-
为所有受影响的游戏对象实现了碰撞事件监听器
-
当游戏屏幕上不再需要时,从内存中移除对象
-
实现了将球作为物理对象的移动
-
更新了每次砖块碰撞的得分板
-
学会了如何处理胜负条件
过去的两章现在看起来并不那么糟糕了,不是吗?随着我们在 Lua 中继续编程,我们正在熟悉工作流程。只要你继续使用不同的游戏框架,理解起来一定会更容易。
下一章将包含另一个肯定会吸引你注意力的游戏。我们将为显示对象创建动画精灵表。这难道不是视觉盛宴吗?
第五章。为我们的游戏添加动画
在我们的移动游戏开发之旅中,我们取得了良好的开端。我们已经经历了大量的编程,从在屏幕上显示对象到编写用户可以看到的游戏逻辑。Corona SDK 最强大的功能之一是任何显示对象都可以被动画化。这是对 Corona 提供的灵活图形模型的证明。
动画为游戏中的用户体验增添了众多特色。这是通过生成一系列帧来实现的,这些帧从一帧平滑地过渡到另一帧。我们将亲身体验并应用这些知识到我们将要创建的新游戏中。
在本章中,我们将:
-
使用运动和过渡工作
-
使用电影剪辑动画
-
使用精灵表动画
-
为显示对象创建游戏循环
-
构建我们的下一个游戏框架
让我们开始动画吧!
熊猫星捕手
本节涉及创建我们的第二个游戏,名为《熊猫星捕手》。主要前提是一只名叫玲玲的熊猫,它需要在计时器耗尽之前向天空发射,尽可能多地捕捉星星。熊猫将被动画化,并且对于每个应用的动作(如发射前的设置和它在空中时)都会有不同的动作。弹射机制也将被应用于将玲玲发射到空中。您可能在《愤怒的小鸟》和《城堡粉碎》等游戏中见过类似的功能。
让我们开始让一切动起来
我们在第三章中介绍了过渡,构建我们的第一个游戏:Breakout,并对它进行了简要的介绍。让我们更深入地探讨它们。
过渡
过渡库允许您通过允许您对显示对象的一个或多个属性进行缓动,仅用一行代码创建动画。我们之前在第三章讨论了过渡的基础,
这可以通过transition.to
方法完成,该方法接受一个显示对象和一个包含控制参数的表。控制参数指定动画的持续时间和显示对象属性的最终值。属性的中间值由一个也作为控制参数指定的缓动函数确定。
transition.to():
使用easing
过渡在一段时间内动画化显示对象的属性。
语法: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
库是过渡库使用的插值函数集合:
-
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:
是值的改变(最终值 = 起始值 + delta)
例如:
local square = display.newRect( 0, 0, 50, 50 )
square:setFillColor( 255,255,255 )
square.x = 50; square.y = 100
local square2 = display.newRect( 0, 0, 50, 50 )
square2:setFillColor( 255,255,255 )
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
函数使您能够触发在您选择的特定延迟(以毫秒为单位)的事件。
-
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 myTimerID = timer.performWithDelay( 1000, myEvent, 0 )
-
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 包含一个用于构建动画精灵的 精灵图 功能。有关精灵图的更多信息,请参阅以下链接:developer.anscamobile.com/reference/sprite-sheets
。
精灵图 是节省纹理内存的有效方式。它们在涉及复杂角色动画或多种动画类型时推荐使用。
精灵图需要更多的编码和更高级的设置。它们需要构建一个大的动画帧图。电影剪辑库更容易上手,并且可以更快地将 Flash 内容移植过来,因为电影剪辑帧可以作为 PNG 序列从 Flash 中导出。
电影剪辑
外部电影剪辑库允许你从一系列图片中创建动画精灵,可以使用与其他 Corona 显示对象相同的技巧在屏幕上移动它们。
电影剪辑库是一个外部模块,movieclip.lua
,可以与你的项目一起包含,并使用 require
命令加载。
电影剪辑库可以在 Corona SDK 内的 SampleCode/Graphics
文件夹中的 Movieclip
示例项目中找到。
电影剪辑函数
使用一系列图片返回一个动画对象。你可以使用返回的动画对象的方法来控制其播放,例如 play()
, stop()
和 reverse()
。
-
movieclip.newAnim( frames ):
使用在帧表中提供的图像文件名数组创建一个动画精灵:myAnimation = movieclip.newAnim{ "1.png", "2.png", "3.png", "4.png", "5.png"}
-
object:play():
以正向方向开始播放动画精灵。当达到序列的末尾时,它将从开始处重复播放。 -
object:play{ startFrame=a, endFrame=b, loop=c, remove=shouldRemove }:
以正向运动开始动画精灵。当达到由endFrame
给出的帧号时,它将循环回到由startFrame
给出的帧号并继续播放:myAnimation:play{ startFrame=1, endFrame=4, loop=5, remove=true }
动画可以根据你指定的次数循环播放。使用
0
将使动画无限循环。移除参数是一个布尔标志,如果设置为 true,当给定序列完成后,电影剪辑将自动删除自己。默认值是
false
。 -
object:reverse():
以反向方向播放动画精灵。当达到图像集的开始时,它将循环回到最后一张图像并继续反向播放。 -
object:reverse{ startFrame=a, endFrame=b, loop=c, remove=shouldRemove }:
以反向顺序开始动画精灵。当达到由endFrame
给出的帧号时,它将循环回到由startFrame
给出的帧号并继续播放。动画可以根据你指定的次数循环播放。使用
0
将使动画无限循环。移除参数是一个布尔标志,如果设置为 true,当给定序列完成后,电影剪辑将自动删除自己。默认值是
false
。 -
object:nextFrame():
重置任何正在进行的动画序列,将动画移动到总序列中的下一张图像,并停止。 -
object:previousFrame():
重置任何正在进行的动画序列,将动画移动到总序列中的上一张图像,并停止。 -
object:setLabels( labels ):
向先前创建的对象添加可选标签,使用表将标签名称分配给选定的帧号:语法:
object:setLabels{ frameLabel1=num1, frameLabel2=num2, ..., frameLabelN = numN }
示例:
myAnimation:setLabels{ main=1, end=30 }
-
object:stop():
在当前帧停止精灵的动画。 -
object:stopAtFrame( frame ):
将动画跳转到指定的帧,可以是帧号或可选的帧标签。myAnimation:stopAtFrame(2) myAnimation:play() myAnimation:stopAtFrame("label") myAnimation:reverse()
-
object:setDrag:
当将拖动设置为 true 时,将任何电影剪辑转换为可拖动的对象。limitX
和limitY
参数限制拖动到 x 或 y 轴,并且可以使用边界参数指定对象的拖动边界,格式为{left, top, width, height}
。onPress, onDrag
和onRelease
参数接受在发生这些事件时要调用的函数名称。所有参数都是可选的。myAnimation:setDrag{ drag=true, limitX=false, limitY=false, onPress=myPressFunction, onDrag=myDragFunction, onRelease=myReleaseFunction, bounds={ 20, 20, 100, 25 }}
要再次关闭可拖动属性,将拖动设置为 false:
myAnimation:setDrag{ drag=false }
这是精灵狂热!
精灵图是 2D 动画,编译成多个帧,合并成一个纹理图像。这是一种节省纹理内存的有效方法。对移动设备有益,并最小化了加载时间。
精灵 API
以下行使精灵功能在精灵命名空间下可用:
require "sprite"
-
sprite.newSpriteSheet:
该函数创建一个新的精灵图。spriteSheet = sprite.newSpriteSheet("myImage.png", frameWidth, frameHeight) -- the width/height of each animation in the sprite sheet
例如,假设精灵图中的帧数是
floor(imageWidth/frameWidth) * floor(imageHeight/frameHeight)
。第一帧放置在左上角,从左到右读取,如果适用,则继续下一行。以下精灵图有 5 帧,每帧 128 x 128 像素。整个精灵图图像是 384 x 256 像素。如果要在 Corona 中集成,一个示例方法如下所示:spriteSheet = sprite.newSpriteSheet("pandaSheet.png", 128, 128)
-
spriteSet = sprite.newSpriteSet(spriteSheet, startFrame, frameCount):
从精灵图中创建一个新的精灵集。精灵集定义了属于同一角色或另一个移动资源的帧集合,这些帧可以进一步细分为不同的播放动画序列。精灵集是一个 Lua 表,包含一个或多个动画序列的键,用于特定角色。 -
sprite.add( spriteSet, "sequenceName", startFrame, frameCount, time, [loopCount]):
将名为 "sequenceName" 的序列添加到指定的精灵集中。该序列有frameCount
帧,并且它将播放指定毫秒数的时间。每个序列的帧率可以通过修改time
参数单独控制。 -
spriteSheet:dispose():
释放精灵图集及其纹理内存。它还会对使用该图集的所有精灵实例调用removeSelf()
,将它们从舞台中移除。所有属于已移除精灵图集的精灵、序列和集合都不可访问。local sprite = require("sprite") local spriteSheet = sprite.newSpriteSheet("mySprite.png") spriteSheet:dispose()
-
si = sprite.newSprite( spriteSet ):
创建一个新的精灵实例。 -
si:prepare([sequence]):
停止播放当前动画序列,设置新的当前序列,并移动到该序列的第一帧。 -
si:play():
播放动画序列,从当前帧开始。 -
si:pause():
停止动画,但帧保持在最后显示的帧上。稍后可以通过play()
继续播放。 -
si:addEventListener("sprite", listener):
当精灵实例动画有事件时通知监听器。传递给监听器的事件具有以下字段:event.sprite
: 触发事件的精灵;其当前属性也可以通过事件访问。event.phase
: 阶段可以是以下之一:-
"end":
精灵停止播放 -
"loop":
精灵循环(从最后一帧到第一帧,或反向方向) -
"next":
播放精灵的下一帧
-
游戏时间!
现在我们已经学会了如何设置对象移动、电影剪辑和精灵图集,让我们尝试将它们整合到 Panda Star Catcher
中!您可以从 Packt 网站下载本书附带的配套项目文件。在第五章文件文件夹中有一个名为 Panda Star Catcher
的项目文件夹。它已经为您设置了 config.lua
和 build.settings
文件。艺术资源也包含在该文件夹中。您会注意到构建和运行时配置与 第三章,构建我们的第一个游戏:Breakout 和 第四章,游戏控制 中的设置相似。本教程与 iOS 和 Android 设备兼容。
项目文件夹中包含的图形已设计为在两个平台上正确显示。
行动时间——设置变量
让我们从介绍运行游戏所需的所有变量开始。
-
在
Panda Star Catcher
项目文件夹中创建一个新的main.lua
文件并将其添加进去。 -
让我们从隐藏设备上的状态栏并设置游戏所需的所有变量开始:
display.setStatusBar( display.HiddenStatusBar ) -- Hides the status bar -- 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 sprite = require("sprite") 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
。这是回合从秒开始倒计时的时长。starWidth
和starHeight
描述了对象的总体尺寸。
让我们开始这一轮
在熊猫可以发射之前,我们需要将其加载到游戏屏幕上。熊猫将从屏幕底部过渡并向上移动,在发生任何触摸事件之前。
行动时间——开始游戏
目前我们需要设置熊猫的离屏位置,并让它过渡到起始发射位置,以便用户与之交互。
-
在添加变量后,创建一个新的局部函数
startNewRound()
,并添加一个if
语句以将panda
对象初始化到场景中。local startNewRound = function() if panda then
-
在
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 panda.isBodyActive = true
-
创建另一个名为
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
-
在 1000 毫秒内将熊猫过渡到
y=225
的位置。当补间动画完成后,使用onComplete
命令调用pandaLoaded()
函数。使用end
关闭activateRound()
函数并调用它。关闭panda
的if
语句和startNewRound()
函数的end
。transition.to( panda, { time=1000, y=225, onComplete=pandaLoaded } ) end activateRound() end end
刚才发生了什么?
当关卡激活时,熊猫在玩家可见之前被放置在地面下方。对于pandaLoaded()
,通过gameIsActive = true
激活游戏,熊猫准备好由玩家发射。熊猫从地面水平过渡到屏幕上可以访问的区域。
呼啸而去!
熊猫在完成转身后需要从舞台上消失。我们不会让它消失得无影无踪,而是当它与屏幕上的任何物体碰撞时,会添加一个呼啸效果。
行动时间——在舞台上重新加载熊猫
当熊猫在空中停留一定时间或撞击屏幕外的区域时,它将变成一团烟雾。当熊猫与屏幕边缘或地面发生碰撞事件时,它将被一个呼啸图像所取代。为了使呼啸效果生效,熊猫的可见属性必须关闭。当碰撞发生后,熊猫需要在游戏仍然激活的情况下重新加载到屏幕上。
-
创建一个名为
callNewRound()
的局部函数。包括一个名为isGameOver
的局部变量,并将其设置为false
。local callNewRound = function() local isGameOver = false
-
在当前函数中,创建一个名为
pandaGone()
的新局部函数。为熊猫添加新属性,使其不再显示在游戏舞台上。local pandaGone = function() panda:setLinearVelocity( 0, 0 ) panda.bodyType = "static" panda.isVisible = false panda.isBodyActive = false panda.rotation = 0 poof.x = panda.x; poof.y = panda.y poof.alpha = 0 poof.isVisible = true
-
为
poof
对象添加一个名为fadePoof()
的新函数。使用onComplete
命令,过渡time=50
和alpha=1
。使poof
对象在time=100
和alpha=0
时淡出。关闭pandaGone()
函数,并使用timer.peformWithDelay
调用它。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 )
-
当
isGameOver
仍然为false
时,为startNewRound()
添加一个timer.peformWithDelay
。关闭callNewRound()
函数。if isGameOver == false then restartTimer = timer.performWithDelay( 1500, startNewRound, 1 ) end end
刚才发生了什么?
当熊猫不再显示在屏幕上,时钟仍在倒计时,会开始新一轮。当isGameOver
仍然为false
时,熊猫通过调用startNewRound()
重新加载。
熊猫碰撞通过pandaGone()
发生。所有物理属性通过应用panda.isVisible = false
和pandaisBodyActive = false
变为不活跃。
熊猫消失的确切位置,烟雾会出现。当poof.x = panda.x; poof.y = panda.y. poof
通过fadePoof()
变得可见一段时间时,这就会发生。一旦它消失,新一轮等待,这使得waitingForNewRound = true
。
赢得一些分数
当熊猫捕捉到天空中的任何星星时,就会获得分数。游戏由计时器运行,因此玩家的任务是尽可能多地捕捉星星,直到时间用完。让我们积累一些分数吧!
显示游戏分数的时间到了
分数通过名为scoreNum
的参数更新,并在游戏过程中显示。分数数字通过gameScore
接收。
-
下一个要创建的函数是名为
setScore()
的函数,带有名为scoreNum
的参数。local setScore = function( scoreNum )
-
使用一个名为
newScore
的局部变量,并将其设置为scoreNum
。设置gameScore = newScore
。为gameScore
提供一个if
语句,以便在游戏过程中设置分数为0
。local newScore = scoreNum gameScore = newScore if gameScore < 0 then gameScore = 0; end
-
在
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
的值。
当游戏结束时
这款游戏中没有失败者。每个人都赢了!你仍然可以通过在计时器用完之前尽可能多地捕捉星星来保持你的肾上腺素激增。当一切结束时,我们仍然需要在时间用完时得到通知。
显示游戏结束屏幕的时间到了
我们需要设置游戏结束屏幕,并在回合结束时显示玩家所达到的最终分数。
-
创建一个名为
callGameOver()
的新局部函数。local callGameOver = function()
-
将
gameIsActive
设置为false
并暂停物理引擎。从舞台中移除panda
和stars
对象。gameIsActive = false physics.pause() panda:removeSelf() panda = nil stars:removeSelf() stars = nil
-
显示游戏结束对象并将它们插入到
hudGroup
组中。使用transition.to
方法在屏幕上显示游戏结束对象。local shade = display.newRect( 0, 0, 480, 320 ) shade:setFillColor( 0, 0, 0, 255 ) shade.alpha = 0 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, alpha=0.65 } ) transition.to( gameOverDisplay, { time=500, alpha=1 } )
-
使用名为
newScore
的局部变量更新最终得分。将counter
和scoreText
的isVisible
设置为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
并暂停所有物理,这样熊猫就不能通过任何其他屏幕触摸来移动。然后从场景中移除熊猫和星星。
shade
和gameOverDisplay
通过transition.to
变得可见,因此通知玩家回合已结束。最终得分将在回合结束时在gameOverDisplay
对象前显示。
背景显示
熊猫需要在游戏中有一个通用的位置设置。让我们设置背景和地面对象。
开始行动——添加背景元素
-
将
background
和ground
显示对象添加到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
刚才发生了什么?
background
和ground
显示对象放置在名为drawBackground()
的函数中。ground
有一个定制的物理形状,其大小与原始显示对象不同。如果熊猫不小心碰到地面,它将能够与之碰撞而不会掉下去。
头上显示!
在游戏可以开始之前,我们需要一个关于如何操作游戏控制的一般想法。幸运的是,我们只需添加一个解释如何玩的游戏帮助屏幕。HUD(抬头显示)也需要显示,以便玩家可以了解剩余时间以及累积了多少分数。
开始行动——显示计时器和得分
让我们设置需要显示的游戏中的帮助屏幕和 HUD 元素。
-
创建一个新的局部函数
hud()
。local hud = function()
-
在游戏开始时显示
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 })
-
在屏幕顶部附近显示
counter
和scoreText
。也将scoreText
添加到hudGroup
组中。使用end
关闭函数。counter = display.newText( "Time: " .. tostring( numSeconds ), 0, 0, "Helvetica-Bold", counterSize ) counter:setTextColor( 255, 255, 255, 255 ) 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:setTextColor( 255, 255, 255, 255 ) --> 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 秒时停止倒计时。
-
使用一个名为
myTimer()
的局部函数设置游戏计时器的倒计时。local myTimer = function()
-
计时器的秒数以 1 的增量增加。使用
counter
文本对象,使用numSeconds
显示时间。打印出numSeconds
以在 终端 窗口中查看倒计时。numSeconds = numSeconds - 1 counter.text = "Time: " .. tostring( numSeconds ) print(numSeconds)
-
创建一个
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
-
使用一个名为
startTimer()
的新局部函数启动myTimer()
函数。这将从游戏开始时的开始倒计时。local startTimer = function() print("Start Timer") timerInfo = timer.performWithDelay( 1000, myTimer, 0 ) end
刚才发生了什么?
主要计时器函数在 myTimer()
中。我们使用 numSeconds = numSeconds -1
来倒计时秒数。秒数将在 counter
显示对象中更新。print(numSeconds)
将在终端窗口中更新,以查看代码中倒计时的速度。
当时间耗尽或所有星星都被收集时,创建一个 if
语句来检查是否有任何参数为 true
。当任何语句评估为 true
时,计时器停止倒计时,熊猫动画暂停,并调用 callGameOver()
函数。这将调用显示游戏结束界面的函数。
计时器通过 local startTimer = function()
以每 1000 毫秒(相当于 1 秒)的速率启动倒计时。
它如此发光
熊猫需要另一个元素来显示发射到天空之前添加了多少力量。我们将添加一个微妙的类似发光的显示对象来表示这一点。
行动时间——制作强力射击
我们需要为 powerShot
创建一个单独的函数,以便在熊猫准备发射时调用。
-
通过一个名为
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
对象,并在熊猫设置发射时调用。
熊猫!
一旦屏幕上出现一些动画,将会非常激动人心。我们的主要角色将在游戏过程中应用每个动作时都有指定的动画。
是时候行动了——创建熊猫角色
我们需要设置熊猫的碰撞事件,并使用精灵表相应地动画化它。
-
我们需要创建一个本地函数,用于引入熊猫的碰撞和触摸事件。我们将称之为
createPanda()
。local createPanda = function()
-
当熊猫与星星碰撞时,使用
onPandaCollision()
函数,参数为self
和event
。每次与星星或屏幕边缘发生碰撞时,通过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
-
创建一个方向箭头,允许用户瞄准发射熊猫的区域。将其插入到
gameGroup
组中。arrow = display.newImage( "arrow.png" ) arrow.x = 240; arrow.y = 225 arrow.isVisible = false gameGroup:insert( arrow )
-
为
panda
显示对象创建精灵表,该对象有三个不同的动画序列,分别称为"set"
、"crouch"
和"air"
。local pandaSheet = sprite.newSpriteSheet( "pandaSprite.png", 128, 128 ) local spriteSet = sprite.newSpriteSet(pandaSheet, 1, 5) sprite.add( spriteSet, "set", 1, 2, 200, 0 ) sprite.add( spriteSet, "crouch", 3, 1, 1, 0 ) sprite.add( spriteSet, "air", 4, 2, 100, 0 ) panda = sprite.newSprite( spriteSet ) panda:prepare("set") panda:play()
-
在熊猫起飞前添加
panda
的属性。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
-
使用
"collision"
为panda
设置碰撞,并应用事件监听器。panda.collision = onPandaCollision panda:addEventListener( "collision", panda )
-
创建
poof
对象。poof = display.newImage( "poof.png" ) poof.alpha = 1.0 poof.isVisible = false
-
将
panda
和poof
插入到gameGroup
组中。结束函数。gameGroup:insert( panda ) gameGroup:insert( poof ) end
-
我们需要滚动到
activateRound()
函数,并添加熊猫的"set"
动画序列。panda:prepare("set") panda:play()
发生了什么?
熊猫发生的碰撞事件从 if event.phase == "began"
开始。熊猫通过多个 if
语句在屏幕上重新加载。event.other.myName == "star"
将调用新的一轮,以及当熊猫向舞台的右侧、左侧或顶部边缘飞出时。
熊猫的精灵表有三组动画。它们被称为 "set"
、"air"
和 "crouch"
。精灵表中总共有五帧。
在起飞前设置熊猫的物理属性。身体类型设置为 "static"
,在空中时将改变。
熊猫的碰撞事件通过 panda:addEventListener( "collision", panda )
调用。
现在精灵表已设置,需要在 activateRound()
函数中添加 "set"
动画以启动移动。
星空
星星在游戏中扮演着重要角色。它是熊猫必须越过以在时钟用完前获得分数的主要障碍。
是时候行动了——创建星星碰撞
星星碰撞需要被创建并从舞台移除,以便为玩家累积分数。
-
为星星碰撞创建一个名为
onStarCollision()
的函数,并具有self
和event
参数。local onStarCollision = function( self, event )
-
在发生碰撞时,添加
if
语句从游戏屏幕中移除stars
子对象。每次屏幕上移除一个星星时,分数增加 500。使用end.
结束函数。if event.phase == "began" and self.isHit == false then self.isHit = true print( "star destroyed!") self.isVisible = false self.isBodyActive = 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
增加分数,并更新到 setScore = (scoreNum)
。
英雄尝试——跟踪星星计数
尝试跟踪熊猫在游戏过程中捕获了多少星星。逻辑与创建游戏分数的方式相似。每个捕获的星星都需要增加 1,作为每次碰撞的计数。星星计数放置在onStarCollision()
函数中。需要创建一个新的函数和方法来显示星星计数文本,并且每次计数变化时都需要更新。
屏幕触摸
熊猫将通过创建类似于弹弓的发射机制来跨越比赛场地以到达星星。力量将在推动熊猫向上运动中扮演重要角色。
行动时间——发射熊猫
让我们为熊猫添加一个触摸事件,使其向星星发射。powerShot
对象将发挥作用,帮助玩家可视化熊猫发射到空中之前施加的力量。
-
实现熊猫的触摸事件。创建一个名为
onScreenTouch()
的本地函数,带有事件参数。local onScreenTouch = function( event )
-
当
gameIsActive
被启动时,通过使用event.phase == "began"
添加一个if
语句,以处理触摸事件开始时的情况。在此事件期间,使用"crouch"
动画来准备熊猫发射。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:prepare("crouch") panda:play()
-
使用
event.phase == "ended"
添加一个elseif
语句,以处理触摸事件结束时的情况。创建一个新的本地函数fling()
,它将包含熊猫向star
对象发射时的属性。应用与触摸事件拖动方向相反的力量。当触摸事件从角色拉远时,将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:prepare("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
刚才发生了什么?
一旦游戏激活并且熊猫被加载到屏幕上,就可以开始触摸事件以发射熊猫。熊猫将从"static"
物理状态变为"dynamic"
物理状态。随着熊猫被事件触摸拉得越远,powerShot
显示对象的大小会增加。
熊猫发射的力量是通过local fling = function()
应用的。通过xForce
和yForce
创建力量发射。通过panda:applyForce( xForce, yForce, panda.x, panda.y )
推动熊猫对象。请注意,身体类型变为"dynamic"
,这样重力就可以影响该对象了。
组织显示对象
当回合被设置后,游戏对象的显示层次结构需要重新排列。最重要的对象显示在屏幕的前方。
行动时间——重新排列层次
-
需要创建一个新的本地函数
reorderLayers()
,以在游戏过程中组织屏幕上对象的显示层次结构。local reorderLayers = function() gameGroup:insert( levelGroup ) ground:toFront() panda:toFront() poof:toFront() hudGroup:toFront() end
刚才发生了什么?
gameGroup, hudGroup
和其他显示对象在游戏屏幕的显示层次结构中进行了重新组织。最重要的对象被置于最前方,而最不重要的对象则位于后方。
创建星星
星星将填满游戏,以便熊猫可以尽可能多地捕捉到它们。天空肯定会被它们填满。
行动时间——在关卡中创建星星
我们需要在游戏中添加星星的布局,并让它们移动以增加它们活跃的视觉效果。需要应用一个碰撞事件,这样当 panda 碰到它们时,它们就会被移除。
-
创建一个名为
createStars()
的新函数,并通过for
循环布局star
对象。添加一个"collision"
事件,该事件将由onStarCollision()
调用,当星星被熊猫击中时移除星星。星星在 10 秒内向前和向后旋转,每次旋转 1080 度和-1080 度。这将允许星星向前和向后旋转三个完整的间隔。为屏幕的左右两侧创建墙壁。local createStars = function() starsstarscreating, in levellocal 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}) 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
刚才发生了什么?
屏幕上显示的星星数量由 numOfRows
和 numOfColumns
设置。通过一个 for
循环来显示每个单独的星星对象,并将其放置在 stars
组中。star
的碰撞通过事件监听器通过 onStarCollision()
来检测。
leftWall
和 rightWall
也有物理属性,并且会考虑到与 panda 的碰撞检测。
星星通过 starAnimation()
和 starRotation()
动画化。每个函数都会在每个星星对象上旋转 10 秒(10000 毫秒),并在 1080 度和-1080 度之间交替。
英雄行动——创建 movieclip
目前星星通过同时旋转来获得一些运动。尝试通过让它们以各种大小变化来给图像添加更多特性。这可以通过制作一系列相同的图像并改变资产本身的大小(而不是图像大小)来实现。需要使用图像处理软件程序来完成这项工作。创建尽可能多的图像,并将它们设置到 movieclip 函数中。在游戏过程中运行它。
开始游戏
游戏在时钟开始倒计时并且熊猫加载到屏幕上时开始。一旦熊猫被设置在屏幕上,玩家需要快速瞄准并发射它,以便可以立即重新加载熊猫。
行动时间——初始化游戏
需要初始化物理和剩余的游戏功能以运行游戏。所有游戏动作都需要延迟,直到 帮助 屏幕离开舞台。
-
通过创建一个名为
gameInit()
的新函数来开始游戏,该函数将包含physics
属性并激活舞台上的显示对象。local gameInit = function() physics.start( true ) physics.setGravity( 0, 9.8 ) drawBackground() createPowerShot() createPanda() createStars() hud()
-
使用
"touch"
为onScreenTouch()
添加一个Runtime
事件监听器。Runtime:addEventListener( "touch", onScreenTouch )
-
让关卡和计时器晚 10 秒开始,这样用户就有时间阅读帮助文本。关闭函数,并通过
gameInit()
开始游戏。local gameTimer = timer.performWithDelay( 10000, function() startNewRound(); end, 1 ) local gameTimer = timer.performWithDelay( 10000, function() startTimer(); end, 1 ) end gameInit()
所有代码都已完成!在模拟器中运行游戏,亲自看看它是如何工作的。
刚才发生了什么?
通过gameInit()
初始化回合。此时运行物理引擎和剩余的功能。同时添加了onScreenTouch()
的事件监听器。startNewRound()
和startTimer()
函数在启动应用程序后 10 秒通过timer.performWithDelay()
启动。
突击测验——图形动画
-
正确停止精灵图集动画的方法是什么?
-
a.
sprite:stop()
-
b.
sprite:pause()
-
c.
sprite:dispose()
-
d. 以上都不是
-
-
你如何使电影剪辑动画无限循环?
-
a.
myAnimation:play{ startFrame=1, endFrame=4, loop=1, remove=true }
-
b.
myAnimation:play{ startFrame=1, endFrame=4, loop=-1, remove=true }
-
c.
myAnimation:play{ startFrame=1, endFrame=4, loop=0, remove=true }
-
d.
myAnimation:play{ startFrame=1, endFrame=4, loop=100, remove=true }
-
-
你如何创建一个新的精灵图集?
-
a.
spriteSheet = sprite.newSpriteSheetFromData( "myImage.png", spriteData )
-
b.
spriteSheet = sprite.newSpriteSheetFromData( "myImage.png", spriteSet )
-
c.
spriteSheet = sprite.newSpriteSheet("myImage.png", frameWidth, frameHeight)
-
d. 以上都不是
-
概述
我们的第二款游戏,熊猫星捕手终于完成了!我们现在对编写更多函数和不同类型的游戏逻辑有了很好的掌握。现在我们有了动画经验!做得好!
我们研究了:
-
深入探讨过渡和应用缓动技术
-
理解电影剪辑和精灵图集之间的区别
-
为需要不断在屏幕上重新加载的显示对象创建游戏循环
-
对显示对象施加力量,使其朝向指定的方向推进
-
添加一个碰撞事件,从显示对象切换到另一个显示对象
我们在整章中完成制作了另一款游戏!在 Corona SDK 中工作非常简单且易于学习。它甚至不需要数千行代码来创建一个简单的游戏。
在下一章,我们将学习创建游戏中的另一个重要元素,音效和音乐!你将有一个美好的体验。
第六章. 播放声音和音乐
我们在日常接触的几乎所有类型的媒体中都能听到声音效果和音乐。许多著名的游戏,如 PAC-MAN、愤怒的小鸟和水果忍者,仅凭其主题曲或声音效果就可以被识别。除了我们在游戏中看到的外观图像外,声音有助于影响故事情节和/或游戏过程中的氛围。与您的游戏主题相关的优质声音效果和音乐有助于让您的观众感受到真实的体验。
在本章中,您将学习如何应用可以添加到您应用程序中的声音效果和音乐。我们已经在创建 Breakout 和 Panda Star Catcher 的前几章中实现了视觉吸引力。现在让我们通过耳朵来增强感官体验!
我们将要讨论的主要要点是:
-
加载、播放和循环音频
-
了解如何播放、暂停、继续、倒带和停止
-
内存管理(释放音频)
-
音量控制
-
性能和编码技巧
让我们再创造一些奇迹!
冠状音频系统
Corona 音频系统具有先进的 OpenAL (Open Audio Library) 功能。OpenAL 是为高效渲染多通道三维定位音频而设计的。OpenAL 的一般功能编码在源对象、音频缓冲区和单个听者中。源对象包含指向缓冲区的指针、声音的速度、位置和方向以及声音的强度。缓冲区包含以 PCM 格式的音频数据,可以是 8 位或 16 位,可以是单声道或立体声格式。听者对象包含听者的速度、位置和方向以及应用于所有声音的一般增益。
注意
如需了解有关 Corona 音频系统的更多信息,请访问:developer.anscamobile.com/partner/audionotes
。有关 OpenAL 的一般信息可在:www.openal.com.
找到。
声音格式
以下是与 iOS 和 Android 平台兼容的声音格式:
-
所有平台都支持 16 位、小端、线性、
.wav
文件 -
iOS 和 Mac 模拟器支持
.mp3, .caf
和.aac
-
Windows 模拟器支持
.mp3
和.wav
-
Android 支持
.mp3
和.ogg
格式
Android 上声音文件名的限制
在 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(audiofileName [, baseDir ])
参数:
-
audiofileName
- 指定要加载的音频文件名称。支持的文件格式取决于您在哪个平台上运行音频文件。 -
baseDir
- 默认情况下,预期声音文件位于应用程序资源目录中。如果声音文件位于应用程序文档目录中,请使用system.DocumentsDirectory
。
示例:
-
tapSound = audio.loadSound("tap.wav")
-
smokeSound = audio.loadSound("smoke.mp3")
audio.loadStream()
这将加载一个文件作为流来读取。流式文件将分批读取小部分数据,以最小化内存使用。大型且持续时间长的文件适用于此。这些文件不能在多个通道上同时共享。如果需要,您必须加载文件的多实例。
语法:
audio.loadStream(audioFileName [, baseDir ])
参数:
-
audiofileName
- 指定要加载的音频文件名称。支持的文件格式取决于您在哪个平台上运行音频文件。 -
baseDir
- 默认情况下,预期声音文件位于应用程序资源目录中。如果声音文件位于应用程序文档目录中,请使用system.DocumentsDirectory
。
示例:
-
music1 = audio.loadStream("song1.mp3")
-
music2 = audio.loadStream("song2.wav")
audio.play()
在指定通道上播放由音频句柄指定的音频。如果没有指定通道,系统将自动为您选择一个可用通道。
语法:
audio.play(audioHandle [, {[channel=c] [, loops=l] [, duration=d] [, fadein=f] [, onComplete=o]}])
参数:
-
audioHandle
- 这是您想要播放的音频数据。 -
channel
- 您想要播放的通道号。通道号从 1 到最大通道数(32),都是有效的通道。指定 0 或省略此参数,系统将自动为您选择一个通道。 -
loops
- 您想要音频循环的次数。0 表示音频将循环 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()
(最多可达通道数上限)。
audio.play(bellSound)
audio.play(bellSound)
audio.play(bellSound)
行动时间——播放音频
我们将倾听在 Corona 中如何实现声音效果和音乐,以了解它实际上是如何工作的:
-
在你的桌面创建一个名为
Playing Audio
的新项目文件夹。 -
在
Chapter 6 Resources
文件夹中,将ring.wav
和song1.mp3
音频文件复制到你的项目文件夹中,并创建一个新的main.lua
文件。你可以从 Packt 网站下载本书的配套项目文件。 -
使用
loadSound()
和loadStream()
预加载以下音频:ringSound = audio.loadSound("ring.wav") backgroundSound = audio.loadStream("song1.mp3")
-
通过将
backgroundSound
设置为通道 1,无限循环,并在 3 秒后淡入来播放backgroundSound
:mySong = audio.play(backgroundSound, {channel=1, loops=-1, fadein=3000})
-
添加
ringSound
并播放一次:myRingSound = audio.play(ringSound)
-
在 Corona 模拟器中保存并运行项目以听取结果。
发生了什么?
对于仅仅是短声音效果的音频,我们使用了 audio.loadSound()
来准备声音。对于大尺寸或长时间段的音频,使用 audio.loadStream()
。
backgroundSound
文件设置为通道 1,在开始播放时淡入 3 秒。loops = -1
表示文件从头到尾无限循环。
英雄尝试——带延迟重复音频
如你所见,加载和播放音频非常简单。播放一个简单的声音只需要 2 行代码。让我们看看你能否提高一个档次。
使用 ring.wav
文件并通过 loadSound()
加载它。创建一个播放音频的函数。每隔 2 秒播放声音五次。
控制时间
现在我们可以控制我们的声音了,因为它们可以在模拟器中播放。如果你回想起磁带播放机的日子,它们有使用暂停、停止和倒带等功能的能力。Corona 的音频 API 库可以做到这一点。
audio.stop()
在通道上停止播放并清除通道,以便可以再次播放。
语法:
audio.stop([channel])
或者:
audio.stop([{channel = c}])
参数:
-
没有参数将停止所有活动通道。
-
channel
- 要停止的通道。指定 0 停止所有通道。
audio.pause()
这将在通道上暂停播放,对未播放的通道没有影响。
语法:
audio.pause([channel])
或者:
audio.pause([{channel = c}])
参数:
-
没有参数将暂停所有活动通道。
-
channel
- 要暂停的通道。指定 0 暂停所有通道。
audio.resume()
这将在暂停的通道上恢复播放。对未暂停的通道没有影响。
语法:
audio.pause([channel])
或者:
audio.pause([{channel = c}])
参数:
-
没有参数将恢复所有暂停的通道。
-
channel
- 要恢复的通道。指定 0 恢复所有通道。
audio.rewind()
这将音频回退到活动通道或音频句柄的起始位置。
语法:
audio.rewind([, audioHandle ] [, {channel=c} ])
参数:
-
audioHandle
- 你想要回退的数据的音频句柄。最适合使用audio.loadStream()
加载的音频。不要在同一调用中尝试与通道参数一起使用此参数。 -
channel
- 要应用回退操作的通道。最适合使用audio.loadSound()
加载的音频。不要在同一调用中与audioHandle
参数一起使用此参数。
操作时间——控制音频
让我们通过创建控制音频调用的用户界面按钮来模拟我们自己的小型音乐播放器:
-
在
第六章
文件夹中,将Controlling Audio
项目文件夹复制到你的桌面。你将注意到其中包含几个艺术资产、一个ui.lua
库和一个song2.mp3
文件。你可以从 Packt 网站下载本书的配套项目文件: -
在同一个项目文件夹中,创建一个全新的
main.lua
文件。 -
通过
loadStream()
加载音频文件,命名为backgroundSound
,并调用 UI 库。还添加一个名为myMusic:
的局部变量:local ui = require("ui") local backgroundSound = audio.loadStream("song2.mp3") local myMusic
-
创建一个名为
onPlayTouch()
的局部函数,并带有事件参数以播放音频文件。添加一个包含event.phase == "release"
的if
语句,以便在按钮释放时开始播放音乐。将playBtn
显示对象应用为新 UI 按钮:local onPlayTouch = function( event ) if event.phase == "release" then myMusic = audio.play(backgroundSound, {channel=1, 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", textColor = {255, 255, 255, 255}, size = 16, emboss = false } playBtn.x = 160; playBtn.y = 100
-
创建一个名为
onPauseTouch()
的局部函数,并带有事件参数以暂停音频文件。当event.phase == "release"
时添加一个if
语句,以便音乐暂停。将pauseBtn
显示对象应用为新 UI 按钮:local onPauseTouch = function(event) if event.phase == "release" then myMusic = audio.pause(backgroundSound) 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", textColor = {255, 255, 255, 255}, size = 16, emboss = false } pauseBtn.x = 160; pauseBtn.y = 160
-
添加一个名为
onResumeTouch()
的局部函数,并带有事件参数以恢复音频文件。当event.phase == "release"
时添加一个if
语句,以便音乐恢复播放。将resumeBtn
显示对象应用为新 UI 按钮:local onResumeTouch = function(event) if event.phase == "release" then myMusic = audio.resume(backgroundSound) 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", textColor = {255, 255, 255, 255}, size = 16, emboss = false } resumeBtn.x = 160; resumeBtn.y = 220
-
添加一个名为
onStopTouch()
的局部函数,并带有事件参数以停止音频文件。当event.phase == "release"
时创建一个if
语句,以便音乐停止。将stopBtn
显示对象应用为新 UI 按钮:local onStopTouch = function(event) if event.phase == "release" then myMusic = audio.stop(backgroundSound) 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", textColor = { 255, 255, 255, 255 }, size = 16, emboss = false } stopBtn.x = 160; stopBtn.y = 280
-
添加一个名为
onRewindTouch()
的局部函数,并带有event
参数以回退音频文件。当event.phase == "release"
时创建一个if
语句,以便音乐回退到曲目开头。将rewindBtn
显示对象应用为新 UI 按钮:local onRewindTouch = function(event) if event.phase == "release" then myMusic = audio.rewind(backgroundSound) 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", textColor = { 255, 255, 255, 255 }, size = 16, emboss = false } rewindBtn.x = 160; rewindBtn.y = 340
-
保存你的项目并在模拟器中运行它。你现在已经创建了一个功能齐全的媒体播放器!!操作时间——控制音频
发生了什么?
我们通过调用require("ui")
为用户界面按钮添加了一个 UI 库。当按钮被按下时,它会产生“按下”的外观。
创建了各种函数来运行每个按钮:
-
当用户按下按钮触发事件时,
onPlayTouch()
调用myMusic = audio.play(backgroundSound, {channel=1, loops=-1})
-
onPauseTouch()
调用myMusic = audio.pause(backgroundSound)
以在按钮按下时暂停歌曲 -
onResumeTouch()
调用myMusic = audio.resume(backgroundSound)
以恢复已暂停的歌曲 -
如果歌曲正在播放,
onStopTouch()
将调用myMusic = audio.stop(backgroundSound)
并停止音频 -
onRewindTouch()
调用myMusic = audio.rewind(backgroundSound)
将歌曲倒回轨道开头注意:
当歌曲暂停时,只有通过按下恢复按钮才能继续播放。当按下暂停按钮时,播放按钮将没有任何效果。
内存管理
当您完全完成音频文件时,重要的一步是调用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
命令时,将销毁文件。 -
在
Controlling Audio
项目文件中,在onStopTouch()
函数中处理音频。
音频更改
音频系统还具有在您的应用程序中需要时更改音频音量最小和最大状态以及音频淡入淡出的能力。
音量控制
音频音量可以用 0 到 1.0 的值设置。此设置可以在扩展声音播放之前或期间进行调整。
audio.setVolume()
语法:
audio.setVolume( volume [, {[channel=c]} ] )
参数:
-
volume
- 您想要应用的新音量级别。有效数字范围从 0.0 到 1.0,其中 1.0 是最大音量值。默认音量基于您的设备铃声音量,可能会有所不同。 -
channel
- 您想要设置音量的通道号。通道号从 1 到最大通道数。指定 0 将音量应用于所有通道。完全省略此参数将设置主音量,这与通道音量不同。
示例:
-
audio.setVolume(0.75) -- 设置主音量
-
audio.setVolume(0.5, {channel=2}) -- 在通道 2 上设置音量
audio.setMinVolume()
将最小音量限制在设置值。任何低于最小音量的音量将以最小音量级别播放。
语法:
audio.setMinVolume(volume, {channel=c})
参数:
-
volume
- 你想要应用的新最小音量级别。有效数字范围从 0.0 到 1.0,其中 1.0 是最大音量值。 -
channel
- 你想要设置最小音量的通道号。通道号 1 到最小通道数都是有效的通道。指定 0 将应用最小音量到所有通道。
示例:
audio.setMinVolume(0.10, {channel=1}) -- set the min volume on channel 1
audio.setMaxVolume()
将最大音量限制在设置值。任何超过最大音量的音量将以最大音量级别播放。
语法:
audio.setMaxVolume(volume, {channel=c})
参数:
-
volume
- 你想要应用的新最大音量级别。有效数字范围从 0.0 到 1.0,其中 1.0 是最大值。 -
channel
- 你想要设置最大音量的通道号。1 到最大通道数都是有效的通道。指定 0 将应用最大音量到所有通道。
示例:
audio.setMaxVolume(0.90, {channel=1}) -- set the max volume on channel 1
audio.getVolume()
这将获取特定通道的音量或获取主音量。
语法:
audio.getVolume([{[channel=c]}])
参数:
channel
- 你想要获取音量的通道号。最多可以有 32 个有效的通道。指定 0 将返回所有通道的平均音量。完全省略此参数将获取主音量,这与通道音量不同。
示例:
-
masterVolume = audio.getVolume() -- 获取主音量
-
channel1Volume = audio.getVolume({channel=1}) -- 获取 1 通道的音量
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({channel=c})
参数:
channel
- 你想要获取最大音量的通道号。最多可以有 32 个有效的通道。指定 0 将返回所有通道的平均音量。
示例:
channel1MaxVolume = audio.getMaxVolume({channel=1}) -- get the max volume on channel 1
淡入音频
你可以在任何音频开始播放时淡入音量,但还有其他控制它的方法。
audio.fade()
这将在指定的量将播放的声音淡入到指定的音量。淡入完成后,音频将继续播放。
语法:
audio.fade([{ [channel=c] [, time=t] [, volume=v] }])
参数:
-
channel
- 你想要淡入的通道号。1 到最大通道数都是有效的通道。指定 0 将应用淡入到所有通道。 -
time
- 你想要音频淡入并停止的时间。省略此参数将调用默认的淡入时间,为 1000 毫秒。 -
volume
- 你想要更改淡入的音量目标。有效数字是 0.0 到 1.0,其中 1.0 是最大音量。如果省略此参数,默认值是 0.0。
示例:
audio.fade({channel=1, time=3000, volume=0.5})
audio.fadeOut()
这将在指定的时间内停止播放声音并淡至最小音量。音频将在时间结束时停止,并且通道将被释放。
语法:
audio.fadeOut([{ [channel=c] [, time=t] }])
参数:
-
channel
- 你想要淡出的通道号。1 到最大通道数都是有效的通道。指定 0 将fadeOut
应用于所有通道。 -
time
- 从现在开始,你希望音频淡出并停止的时间量。省略此参数将调用默认的淡出时间,为 1000 毫秒。
示例:
audio.fadeOut({ channel=1, time=5000 })
性能技巧
在为你的游戏创建高质量音频时,以下是一些有用的提示。
预加载阶段
在你的应用程序启动时最好预加载所有文件。虽然loadStream()
通常很快,但loadSound()
可能需要一段时间,因为它必须立即加载和解码整个文件。通常,你不想在用户期望事件发生时调用loadSound()
的部分,例如在游戏过程中。
audioPlayFrequency
在你的config.lua
文件中,你可以指定一个名为audioPlayFrequency
的字段。
application =
{
content =
{
width = 320,
height = 480,
scale = "letterbox",
audioPlayFrequency = 22050
},
}
这告诉 OpenAL 系统在混音和回放时使用什么采样率。为了获得最佳效果,请将其设置为您实际需要的最高值。所以如果您不需要超过 22050 Hz 的回放,请将其设置为 22050。它产生高质量的语音录音或中等质量的音乐录音。如果您确实需要高质量,请将其设置为 44100 以在回放时产生音频 CD 质量。
当你设置此选项时,最好将所有音频文件编码在同一频率下。支持的值有 11025、22050 和 44100。
专利和版税
对于高度压缩的格式,如 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
。
音频知识小测验
-
清除音频文件从内存中的正确方法是什么?
-
a.
audio.pause()
-
b.
audio.stop()
-
c.
audio.dispose()
-
d.
audio.fadeOut()
-
-
在应用程序中可以同时播放多少个音频通道?
-
a. 10
-
b. 18
-
c. 25
-
d. 32
-
-
你如何使你的音频文件无限循环?
-
a. loops = -1
-
b. loops = 0
-
c. loops = 1
-
d. 以上皆非
-
摘要
现在我们已经了解了在 Corona SDK 中使用音频文件的重要方面。现在你可以去添加自己的音效和音乐到你的游戏中,甚至可以将它们添加到我们在前几章中制作的任何示例中。这样做,你为用户体验添加了另一层,这将吸引玩家进入你创建的环境。
到目前为止,你已经学习了如何:
-
使用
loadSound()
和loadStream()
预加载并播放音效和音乐 -
在音频系统 API 下控制暂停、恢复、停止和倒退音乐轨道的音频功能
-
当音频不再使用时,从内存中释放音频
-
调整音频文件中的音量
在下一章中,我们将把迄今为止所学的一切结合起来,创建本书中的最终游戏。我们还将介绍其他实现物理对象和碰撞机制的方法,这些方法在目前市场上的移动游戏中很受欢迎。更多激动人心的信息等待我们去学习。让我们全力以赴吧!
第七章。物理:下落物体
有许多方法可以将物理引擎与显示对象结合使用。到目前为止,我们已经处理了移除具有碰撞的对象、通过舞台区域移动对象以及通过施加对抗重力的力来发射对象,仅举几个例子。现在我们将探索另一种机制,允许重力控制环境。我们将要创建的下一个游戏将涉及下落的物理物体。
本章将讨论的要点是:
-
与更多物理体一起工作
-
定制身体构造
-
跟踪捕获的物体
-
与后碰撞一起工作
-
创建下落物体
在这个部分,让我们再创建一个有趣简单的游戏。让我们开始吧!
创建我们的新游戏:鸡蛋掉落
到目前为止的每一步都让我们对 iOS/Android 设备上的游戏开发有了更多的了解。在这个新的部分中,我们的游戏将包括音效,这将增强我们游戏中的感官体验。
小贴士
确保您使用的是 Corona SDK 版本 2011.704 的最新稳定版本。
我们将要创建的新游戏被称为鸡蛋掉落。玩家控制主要角色,即一个拿着平底锅的伐木工。在游戏过程中,鸡蛋从天空中掉落,伐木工的任务是用手中的平底锅接住鸡蛋,不让它们掉到地上。每个接住的鸡蛋获得 500 分。玩家开始时有 3 条生命。当一个鸡蛋未能击中平底锅而击中地面时,就会失去一条生命。当所有 3 条生命都用完时,游戏结束。
在开始新的游戏项目时,请确保从“第七章”文件夹中获取“鸡蛋掉落”文件。您可以从 Packt 网站 www.packt.com 下载本书附带的项目文件。它包含所有为您构建的必要文件,例如 build.settings, config.lua, ui.lua
,音频文件和游戏所需的美术资源。在开始编码之前,您必须在项目文件夹中创建一个新的 main.lua
文件。
初始变量
这将是我们的第一个完整游戏设置,其中包含许多 Corona SDK 的特点。我们将结合到目前为止所学的基础知识,包括变量、显示对象、物理引擎、触摸/加速度计事件和音频。Corona 的许多 API 都易于使用和理解。这显示了使用 Corona 进行快速学习曲线,即使基本没有编程知识。
行动时间——设置变量
让我们开始介绍我们将使用来创建游戏的变量。这将包括显示对象、用于计数的整数以及在游戏过程中预加载的主要音效。
-
隐藏状态栏并添加名为
gameGroup
的display.newGroup()
。display.setStatusBar( display.HiddenStatusBar ) local gameGroup = display.newGroup()
-
在游戏中包含外部模块。
local sprite = require "sprite" local physics = require "physics" local ui = require "ui"
-
添加显示对象。
local background local ground local charObject local friedEgg local scoreText local eggText local livesText local shade local gameOverScreen
-
添加变量。
local gameIsActive = false local startDrop local gameLives = 3 local gameScore = 0 local eggCount = 0 local mRand = math.random
-
创建鸡蛋边界和密度。
local eggDensity = 1.0 local eggShape = { -12,-13, 12,-13, 12,13, -12,13 } local panShape = { 15,-13, 65,-13, 65,13, 15,13 }
-
设置加速度计和音频。
system.setAccelerometerInterval( 100 ) local eggCaughtSound = audio.loadSound( "friedEgg.wav" ) local gameOverSound = audio.loadSound( "gameover.wav" )
刚才发生了什么?
我们继续创建与 Panda Star Catcher 游戏类似的变量设置。通过将它们分组、显示对象、音频等来组织它们更有效率。
显示的许多变量都有指定的整数,以满足游戏目标。这包括gameLives = 3
和eggCount = 0
等值。
控制主角
加速度计事件与游戏的主范围配合得最好。它使你能够在不触摸屏幕的情况下查看游戏环境的全部区域。必要的触摸事件对于用户界面按钮,如暂停、菜单、播放等是有意义的。
行动时间——移动角色
鸡蛋将从屏幕的各个不同区域从天空落下。让我们为主角准备移动通过屏幕上所有潜在区域的能力。
-
设置一个新的局部函数
moveChar()
,带有event
参数。local moveChar = function(event)
-
为角色添加加速度计移动。
charObject.x = display.contentCenterX - (display.contentCenterX * (event.yGravity * 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
。
注意
当使用xGravity
和yGravity
时,加速度计事件基于纵向比例。当指定显示对象为横向模式时,xGravity
和yGravity
的值会切换以补偿事件以正确工作。
你会注意到第 3 步中的代码阻止了charObject
越过任何墙壁边界。
来吧,英雄——添加触摸事件
目前角色由加速度计控制。另一种控制角色的方法是触摸事件。尝试用"touch"
替换事件监听器并使用事件参数,以便触摸事件正常工作。
如果你记得我们如何在第三章中结合 Breakout 的挡板移动,Building our First Game: Breakout 和第四章的Game Controls,Game Controls 对于模拟器,应该非常相似。
更新分数
当分数更新时,它指的是我们的文本显示对象,并将数值转换为字符串。
例如:
gameScore = 100
scoreText = display.newText( "Score: " .. gameScore, 0, 0, "Arial",
45 )
scoreText:setTextColor( 255, 255, 255, 255 )
scoreText.x = 160; scoreText.y = 100
在前面的例子中,你会注意到我们将gameScore
的值设置为100
。在下面的scoreText
行中,使用gameScore
来连接字符串"Score: "
和gameScore
的值。这样做通过scoreText
以字符串格式显示gameScore
的值。
行动时间——设置分数
谁不喜欢一些友好的竞争呢?我们对之前章节中制作的游戏的计分板很熟悉。因此,我们并不陌生于如何跟踪分数。
-
创建一个名为
setScore()
的局部函数,带有名为scoreNum
的参数。local setScore = function( scoreNum )
-
设置变量来计算分数。
local newScore = scoreNum gameScore = newScore if gameScore < 0 then gameScore = 0; end
-
当在游戏过程中获得分数时更新分数,并关闭函数。
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: "
中,"Score: "
是在游戏过程中显示在设备上的字符串。gameScore
获取变量当前赋予的值并将其显示为字符串。
显示环境
合理设置显示对象有助于玩家想象主要角色与环境之间的关系。由于我们的主要角色是伐木工,所以他置身于森林或专注于自然的区域中是有意义的。
行动时间——绘制背景
在本节中,我们将填充屏幕上的环境显示对象。这包括我们的背景和地面对象,并添加物理元素到我们的地面,以便我们可以为它指定碰撞事件。
-
创建一个名为
drawBackground()
的局部函数。local drawBackground = function()
-
添加背景图像。
background = display.newImageRect( "bg.png", 480, 320 ) background.x = 240; background.y = 160 gameGroup:insert( background )
-
添加地面元素并创建地面物理边界。关闭函数。
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
刚才发生了什么?
background
和 ground
显示对象放置在名为 drawBackground()
的函数中。由于我们的一些图像正在采用动态缩放,因此使用了 display.newImageRect()
函数。ground
显示对象有一个定制的物理形状,其大小与原始显示对象不同。
我们的 background
对象被放置在设备屏幕区域的中心,并插入到 gameGroup
中。
ground
显示对象放置在显示区域的底部附近。它通过 ground.myName = "ground"
被赋予一个名称。我们稍后会使用 "ground"
名称来确定碰撞事件。通过 groundShape
为地面创建了一个定制的物理形状,这允许地面的身体影响分配给显示对象的尺寸。当 physics.addBody()
被初始化时,我们使用 groundShape
作为形状参数。接下来,将 ground
设置为 gameGroup
。
显示抬头显示
在游戏中,抬头显示(HUD) 是用来向玩家视觉传达信息的方法。在许多游戏中,常见的显示功能包括健康/生命值、时间、武器、菜单、地图等。这使玩家能够保持警觉,了解游戏过程中正在发生的事情。当涉及到跟踪生命值时,你希望了解在角色耗尽继续游戏的机会之前还剩下多少。
行动时间——设计 HUD
在尝试让玩家的游戏体验变得愉快的同时,显示的信息与游戏相关并且放置得战略性地,这样就不会干扰主要游戏区域。
-
创建一个名为
hud()
的新局部函数。local hud = function()
-
显示在游戏过程中捕获的鸡蛋的文本。
eggText = display.newText( "Caught: " .. eggCount, 0, 0, "Arial", 45 ) eggText:setTextColor( 255, 255, 255, 255 ) eggText.xScale = 0.5; eggText.yScale = 0.5 eggText.x = (480 - (eggText.contentWidth * 0.5)) - 15 eggText.y = 305 gameGroup:insert( eggText )
-
添加跟踪生命值的文本。
livesText = display.newText( "Lives: " .. gameLives, 0, 0, "Arial", 45 ) livesText:setTextColor( 255, 255, 255, 255 ) --> 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 )
-
添加分数文本并关闭函数。
scoreText = display.newText( "Score: " .. gameScore, 0, 0, "Arial", 45 ) scoreText:setTextColor( 255, 255, 255, 255 ) --> 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
刚才发生了什么?
eggText
显示对象将在屏幕的右下角找到。在游戏过程中,它仍然对用户可见,同时又不干扰主要焦点。注意eggText = display.newText( "Caught: " .. eggCount, 0, 0, "Arial", 45 )
将在值更新时引用eggCount
。
livesText
显示对象的设置与eggText
类似。它位于屏幕的右上角。由于其在游戏中的重要性,这个对象的放置相当突出。它位于一个从背景中容易注意到的地方,允许玩家在游戏中参考。当gameLives
更新时,livesText
会减少数字。
scoreText
的初始设置从hud()
函数开始。它位于屏幕的左上角,与livesText
相对。
创建游戏生命值
如果游戏中没有后果,那么就没有紧迫感去完成主要目标。为了在游戏过程中保持玩家的参与度,引入一些具有挑战性的元素将保持竞争性和兴奋感。在游戏中添加后果会给玩家带来紧张感,并给予他们更多生存下去的动力。
行动时间——计算生命值
跟踪剩余生命值使玩家了解游戏何时结束。
-
设置名为
livesCount()
的函数。local livesCount = function()
-
每次数字减少时显示生命值的文本。
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
。
需要创建一个方法,以便当游戏生命值减少到0
时,所有三个显示对象都会受到影响。
介绍主要角色
我们的主要角色将在游戏过程中应用每个动作时进行动画。我们还将创建一个复杂身体构造,因为对其碰撞点的关注将指向他持有的对象,而不是他的整个身体。
复杂身体构造
也可能从多个元素中构建一个身体。每个身体元素都指定为一个具有自己物理属性的独立多边形形状。
由于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"
。
行动时间——创建角色
主要角色是通过精灵表创建的,需要设置以查看它提供的动画。其他将出现的显示图像包括碰撞物理对象时的裂缝鸡蛋。
-
创建一个名为
createChar()
的新局部函数。local createChar = function()
-
创建主要角色的精灵表。
local characterSheet = sprite.newSpriteSheet ( "charSprite.png",128, 128 ) local spriteSet = sprite.newSpriteSet(characterSheet, 1, 4) sprite.add( spriteSet, "move", 1, 4, 400, 0 ) charObject = sprite.newSprite( spriteSet ) charObject:prepare("move") charObject:play()
-
设置主要角色的起始位置和物理属性。
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 charObject.myName = "character"
-
在鸡蛋发生碰撞后添加过渡图像。
friedEgg = display.newImageRect( "friedEgg.png", 40, 23 ) friedEgg.alpha = 1.0 friedEgg.isVisible = false gameGroup:insert( charObject ) gameGroup:insert( friedEgg ) end
刚才发生了什么?
所指的精灵表称为spriteSet
,并从"charSprite.png"
中获取动画的前4
帧。我们通过sprite.add( spriteSet, "move", 1, 4, 400, 0 )
创建了一个动画集。每次调用"move"
时,它都会从帧1
开始动画,并在400
毫秒内播放4
帧。
主要显示对象称为charObject
,它具有spriteSet
的特性。当它调用prepare("move")
时,在执行play()
命令时,该动画序列会播放。
对角色物理身体的一个重要更改是,其主要碰撞点将指向动画中使用的煎锅。对角色身体的任何碰撞检测都不会被读取。charObject
被赋予一个名为"character"
的名称,这将用于检测包括掉落的鸡蛋在内的碰撞。
我们还在这个函数中放置了煎蛋,以准备碰撞。
添加后碰撞
我们想确保在对象与另一个对象交互后立即发生事件类型。在后碰撞的瞬间,我们可以确认两个物体之间的碰撞力。这有助于我们确定被摧毁的物体是以一定的力量完成的。
碰撞处理
在处理 Box2D 物理引擎时要小心。如果 Corona 代码尝试修改仍在碰撞中的对象,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
这是控制物体线性运动阻尼的值。这是角速度随时间减少的速率。默认值为 0
newBody.linearDamping = 5
local object = newBody.linearDamping
body.angularDamping
这是控制物体旋转阻尼的值。默认值为 0.
newBody.angularDamping = 5
local object = newBody.angularDamping
body.bodyType
这是一个表示正在模拟的物理身体类型的字符串值。可用的值有 "static"
、"dynamic"
和 "kinematic"
:
-
static
物体不会移动或相互交互。静态物体的例子包括地面或迷宫的墙壁。 -
dynamic
物体受到重力和其他物体类型碰撞的影响。 -
kinematic
对象受到力的作用,但不受重力的影响。对于可拖动的对象,应在拖动事件期间将其设置为"kinematic"
。
默认身体类型是"dynamic"
。
newBody.bodyType = "kinematic"
local currentBodyType = newBody.bodyType
行动时间——创建鸡蛋碰撞
我们在之前创建的示例游戏中处理了碰撞。处理碰撞后需要引入力来执行碰撞后事件的完成。
-
创建一个名为
onEggCollision()
的新局部函数,带有两个参数self
和event
。local onEggCollision = function( self, event )
-
当力大于
1
且not self.isHit
时创建一个if
语句。添加eggCaughtSound
声音效果。if event.force > 1 and not self.isHit then audio.play( eggCaughtSound )
-
使
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
-
创建一个函数,使用
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
-
使用
if event.other.myName == "character"
,当主要角色抓住鸡蛋时更新eggCount
。对于每次碰撞,通过500
分更新gameScore
。如果鸡蛋碰到地面,使用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 > 1 and not 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
减去生命值。
使显示对象下落
我们将通过学习如何将物理对象添加到场景中,并让它们在游戏中的随机区域落下,来应用主要资产。物理引擎将考虑我们为鸡蛋显示对象创建的动态物理体。
行动时间——添加鸡蛋对象
想象一个充满下落鸡蛋的世界。这并不完全真实,但在这个游戏中,我们正在创建这个元素。至少我们将确保应用重力和现实世界的物理。
-
创建一个名为
eggDrop()
的新局部函数。local eggDrop = function()
-
添加
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 )
-
为
egg
显示对象添加postCollision
事件。egg.postCollision = onEggCollision egg:addEventListener( "postCollision", egg ) end
刚才发生了什么?
我们将设置egg
的x
值为240 + mRand( 120 )
。mRand
函数等于math.random
,这将允许egg
在以 50 为 x 方向的120
像素区域内随机出现。
确保将egg.isHit = false
设置为对碰撞事件正确应用至关重要。物理体被设置为"dynamic"
,因此它会响应重力并使物体下落。我们为创建的egg
定制了密度和形状,这已经在代码的开始部分完成。
碰撞要正常工作,最后一个重要细节是将egg
添加到onEggCollision()
函数中,使用egg.postCollision = onEggCollision
,然后使事件监听器使用"postCollision"
事件,通过egg:addEventListener( "postCollision", egg )
。
行动时间——制作鸡蛋下落
我们将执行鸡蛋的计时器,这样他们就可以开始在屏幕上放下鸡蛋。
-
创建一个名为
eggTimer()
的本地函数,并使用timer.performWithDelay
每秒(1000 毫秒)重复地放下一个鸡蛋。使用eggDrop()
来激活下落。local eggTimer = function() startDrop = timer.performWithDelay( 1000, eggDrop, 0 ) end
-
在
onEggCollision()
函数的第一个if
语句中,使用timerID, startDrop
取消计时器。添加语句if gameLives < 1
以停止鸡蛋的下落。if gameLives < 1 then timer.cancel( startDrop ) print("timer cancelled") end
刚才发生了什么?
为了让鸡蛋从天空开始下落,我们创建了一个名为eggTimer()
的函数。它通过每次1000
毫秒(1 秒)后无限期地让鸡蛋下落来激活eggDrop()
函数,使用startDrop = timer.performWithDelay( 1000, eggDrop, 0 )
。
返回到onEggCollision()
,我们希望检查gameLives
是否小于1
。当这个语句为真时,鸡蛋将停止下落。这是通过timer.cancel( startDrop ). startDrop
完成的,startDrop
是我们在eggTimer()
中设置的timerID
。
结束游戏玩法
每一场游戏的开始总会有一个结束。无论是简单的你赢了、你输了,还是仅仅的游戏结束,它都为玩家提供了一个结局。通知玩家这些事件是很重要的,这样他们可以反思所获得的成就。
行动时间——调用游戏结束
我们将确保当游戏结束显示屏幕弹出时,我们当前正在运动的任何显示对象都会停止移动,事件监听器也会被禁用。除了我们游戏结束屏幕的视觉显示外,我们还将添加一个声音通知,这也有助于触发事件。
-
创建一个新的本地函数
callGameOver()
,并将其放置在setScore()
函数之后和drawBackground()
函数之前。local callGameOver = function()
-
当游戏结束显示弹出时,引入声音效果。将
gameIsActive
设置为false
并暂停游戏中的物理。audio.play( gameOverSound ) gameIsActive = false physics.pause()
-
创建一个覆盖当前背景的阴影。
shade = display.newRect( 0, 0, 570, 320 ) shade:setFillColor( 0, 0, 0, 255 ) shade.x = 240; shade.y = 160 shade.alpha = 0
-
显示游戏结束窗口并重复最终得分。
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 } )
-
在 游戏结束 屏幕上显示 得分。
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() timer.performWithDelay( 0, function() scoreText.isVisible = true; end, 1 ) end
刚才发生了什么?
我们的 gameOver()
函数触发了我们在代码开始时预加载的 gameOverSound
声音效果。我们确保通过 gameIsActive = false
没有禁用任何事件,例如加速度计的运动。
我们显示对象的元素在此时以 shade
, gameOverScreen
, 和 scoreText
的形式出现。
如果你注意到了,当游戏通过 scoreText.isVisible = false
结束时,scoreText
会消失,然后使用 timer.performWithDelay( 0, function() scoreText.isVisible = true; end, 1 )
在屏幕的不同区域重新出现。
开始游戏
我们将激活所有剩余的函数,并让它们相应地运行。
到时候——激活游戏
在所有游戏元素设置到位后,现在是时候启动应用程序了。
-
创建一个名为
gameActivate()
的新局部函数,并插入gameIsActive = true
。将函数放在moveChar()
函数之上。local gameActivate = function() gameIsActive = true end
-
通过创建一个名为
gameStart()
的新函数来初始化所有游戏动作。local gameStart = function()
-
启动物理属性并设置下落物体的重力。
physics.start( true ) physics.setGravity( 0, 9.8 )
-
激活所有实例化的函数。为
charObject
使用"touch"
添加事件监听器给moveChar()
函数。drawBackground() createChar() eggTimer() hud() gameActivate() Runtime:addEventListener("accelerometer", moveChar) end
-
实例化
gameStart()
函数并返回gameGroup
组。gameStart() return gameGroup
刚才发生了什么?
如果你记得,在代码的开始部分,我们设置了 gameIsActive = false
。然后我们通过 gameActivate()
函数更改了状态,使 gameIsActive = true
。我们使 gameStart()
函数应用所有初始游戏元素。这包括物理引擎和重力的开始。同时,我们初始化了所有剩余的函数。
一旦所有函数都被激活,gameGroup
需要被返回,这样所有显示对象在游戏过程中都会出现。
为了确保你的显示对象的物理对象边界在正确的位置,请在 gameStart()
函数中使用 physics.setDrawMode( "hybrid" )
。
快速问答——图形动画
-
它是用来检索或设置文本对象的文本字符串的吗?
-
a.
object.text
-
b.
object.size
-
c.
object:setTextColor()
-
d. 以上都不是
-
-
哪个函数可以将任何参数转换为字符串?
-
a.
tonumber()
-
b.
print()
-
c.
tostring()
-
d.
nil
-
-
哪种类型的物体受到重力和其他物体类型碰撞的影响?
-
a. 动态
-
b. 运动学
-
c. 静态
-
d. 以上都不是
-
摘要
我们应用程序的游戏构建现在完成了。既然我们已经熟悉了使用物理引擎的各种方法,这也显示了使用 Box2D 和设计涉及物理体的其他游戏的简便性。
我们对以下内容有了更清晰的认识:
-
应用动态和静态物理体的用法
-
为我们显示对象的物理属性构造一个定制的形状
-
通过变量给出的值跟踪捕获到的对象数量
-
使用后碰撞来切换图像
在下一章中,我们将通过使用Storyboard API创建多功能菜单屏幕来完善游戏体验。我们还将学习如何添加暂停动作、保存高分,以及了解更多关于数据保存和卸载文件的知识。
使用 Corona SDK 帮助我们以最短的时间设计和开发游戏。让我们继续为我们的游戏添加最后的修饰吧!
第八章:操作 Storyboard
我们已经将我们的游戏“鸡蛋掉落”进行了扩展,探索了创建游戏物理的方法,以便与碰撞检测反应,并跟踪其他有用的数据,如生命值和得分系统。我们还与自定义物理体合作,并为我们的显示对象创建名称,这些名称适用于游戏得分计数。
接下来,我们将添加一个菜单系统,该系统包括对游戏的介绍,并在游戏过程中应用暂停菜单,以及在游戏结束后保存高分。
我们正在完成一个具有必要元素的应用程序,使其准备好发布到 App Store 或 Google Play Store。
在本章中,我们将学习以下主题:
-
保存高分
-
添加暂停菜单
-
使用 Storyboard API 更改场景
-
添加加载屏幕
-
添加主菜单和选项菜单
让我们继续前进!
“鸡蛋掉落”的继续
我们已经完成了“鸡蛋掉落”游戏的主游戏部分,作为我们应用程序的基础。现在是我们包括如何在游戏中途暂停动作以及如何保存高分的时候了。我们还将添加一些新的场景,这将帮助我们以简单快捷的方式介绍和过渡到游戏。
在第八章的“资源”文件夹中,获取所有图像和文件资源,并将它们复制到您当前的“鸡蛋掉落”项目文件夹中。您可以从 Packt 网站下载与本书配套的项目文件。我们将使用这些文件为我们的游戏添加最后的修饰。
数据保存
保存文件信息在游戏开发的许多方面都有应用。我们用它来保存高分、游戏设置,如声音开/关、锁定/解锁关卡等。它们不是必需的,但如果您想在应用程序中包含这些功能,那么它们是很好的。
在 Corona SDK 中,应用程序是沙盒化的,这意味着您的文件(应用程序图像、数据和首选项)存储在一个其他应用程序无法访问的位置。您的文件将驻留在特定于应用程序的目录中,用于文档、资源或临时文件。这种限制与您的设备上的文件有关,而与您在 Mac 或 PC 上编码无关。
BeebeGames 类用于保存和加载数值
我们将使用由乔纳森·比比(Jonathan Beebe)创建的 BeebeGames 类。它提供了许多简单且有用的功能,可用于游戏。其中一些值得注意的功能包括一种简单的保存和加载数据的方式,我们将将其添加到我们的游戏中。有关 BeebeGames 类的更多信息,请参阅:developer.anscamobile.com/code/beebegames-class
。您可以从链接下载文件,并查看与动画、过渡、计时器等相关的方法,以防您将来需要使用它们。目前,我们将专注于为我们的游戏轻松保存和加载值的方法。
保存和加载数值的示例:
-- 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
是所有应用程序资源存在的目录。请注意,你不应该在此目录中创建、修改或添加文件。注意
更多关于文件的信息可以在:
developer.anscamobile.com/content/files
。
读取文件
要读取文件,使用io
库。这个库允许你根据绝对路径打开文件。
写入文件
要写入文件,你遵循与读取文件相同的许多步骤。而不是使用读取方法,你将数据(字符串或数字)写入文件。
是时候行动了——保存和加载高分
当游戏结束屏幕显示时,我们将保存和加载最终得分和最高分的值。
-
打开我们为 Egg Drop 创建的
main.lua
文件。我们将继续使用相同的文件,并添加更多代码,以对游戏进行新的修改。 -
在所有其他初始化变量附近的位置添加两个新变量,
local highScoreText
和local highScore
。local highScoreText local highScore
-
在预加载的声音文件之后介绍
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
-
添加
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
-
在
callGameOver()
函数的末尾,创建一个if
语句来比较gameScore
和highScore
。使用saveValue()
函数保存最高分。if gameScore > highScore then highScore = gameScore local highScoreFilename = "highScore.data" saveValue( highScoreFilename, tostring(highScore) ) end
-
接下来,在相同的
callGameOver()
函数中添加highScoreText
显示文本,以在游戏结束时显示最高分。highScoreText = display.newText( "Best Game Score: " .. tostring( highScore ), 0, 0, "Arial", 30 ) highScoreText:setTextColor( 255, 255, 255, 255 ) highScoreText.xScale = 0.5; highScoreText.yScale = 0.5 highScoreText.x = 240 highScoreText.y = 120 gameGroup:insert( highScoreText )
-
在
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)
将被转换为字符串。
当游戏结束屏幕可见时,highScoreText
显示从highScore
保存的值,位于获得的gameScore
上方。添加高分可以给玩家一个挑战最高分的动力,并为游戏增加重玩价值。
在gameStart()
函数中,在游戏开始时加载highScore.data
中的值非常重要。通过使用我们创建的保存highScore
的相同数据文件,我们也可以在整个游戏中使用它来加载值。要加载值,local highScore
调用loadValue(highScoreFileName)
。这将从highScore.data
中获取信息。为了获取值,tonumber(loadedHighScore)
将其从字符串转换为整数,并可以用来显示highScore
的值。
暂停游戏
你是否曾在玩游戏的过程中突然需要去洗手间或者手抽筋?显然,任何这些情况都需要你将注意力从游戏进度中移开,并且你需要暂时停止当前动作来满足这些需求。这时暂停按钮就派上用场了,你可以停止那一刻的动作,在你准备好再次玩游戏时继续。
行动时间——暂停游戏
这不仅仅是创建一个按钮,还包括暂停屏幕上的所有动作,包括物理效果和计时器。
-
在代码开始附近初始化所有其他变量的位置添加变量
local pauseBtn
和local pauseBG
。在脚本顶部附近的gameOverSound
之后预加载btnSound
音频。-- Place near other game variables local pauseBtn local pauseBG -- Place after gameOverSound local btnSound = audio.loadSound( "btnSound.wav" )
-
在
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, 255 ) 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()
-
当游戏暂停后,让物理效果再次激活并移除所有暂停显示对象。
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
-
在
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 )
-
为了在游戏过程中显示
pauseBtn
,需要在gameActivate()
函数中将其设置为可见和激活状态。pauseBtn.isVisible = true pauseBtn.isActive = true
-
当游戏结束时,在
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. pauseBG.isVisible
和pauseBG.isActive
都将设置为false
。
记得我们设置gameIsActive
为false
的时候吗?嗯,现在是时候将其设置回true
了。这也意味着通过physics.start()
恢复物理。计时器通过局部函数resumeGame()
恢复,并在函数内部调用timer.resume(startDrop)
。
pauseBtn
和pauseBG
显示对象被插入到if
语句块的末尾。一旦游戏可玩,pauseBtn
就会显示为可见和激活。当游戏结束屏幕出现时,它是不可见和无效的。这是因为当游戏结束时没有其他触摸事件会干扰。
Storyboard API
Storyboard API 为开发者提供了一个简单的方法来控制带有或没有过渡的场景。这是一个用于显示菜单系统,甚至管理游戏中多个级别的优秀场景管理库。Storyboard 还附带了一系列过渡效果。它们可以在 storyboard.gotoScene()
API 参考页面找到:developer.anscamobile.com/reference/index/storyboardgotoscene
。
更多关于 Storyboard API 的信息可以在 Anscamobile 网站上找到:developer.anscamobile.com/content/storyboard
。
我们的场景管理将类似于在developer.anscamobile.com/reference/index/scene-template
显示的场景模板。
您还可以从github.com/ansca/Storyboard-Sample
下载Storyboard 示例代码,并在 Corona 模拟器中运行项目文件,以熟悉其工作方式。
使用 Storyboard API 进行游戏开发
您可能会想知道我们如何将 Storyboard 应用于 Egg Drop。实际上,这非常简单。我们需要修改一些游戏代码以使其与 Storyboard 兼容,并为在游戏开始前应用的菜单系统创建一些新场景。
行动时间——修改游戏文件
我们将把当前的main.lua
文件重命名为maingame.lua
,并将一些额外的行添加到我们的游戏代码中。
小贴士
请确保更改 Egg Drop 项目文件夹内的文件名。
-
在代码顶部附近删除以下行。我们将在本章后面创建的另一个场景中隐藏状态栏。
gameGroup
显示组将被调整以适应 Storyboard 参数。display.setStatusBar( display.HiddenStatusBar ) local gameGroup = display.newGroup()
-
在代码的最顶部,通过添加
local storyboard = require( "storyboard" )
和local scene = storyboard.newScene()
来实现 Storyboard,这样我们就可以调用场景事件。local storyboard = require( "storyboard" ) local scene = storyboard.newScene()
-
在
local loadValue = function( strFilename )
之后,添加createScene()
事件。我们还将把我们的gameGroup
显示组添加回来,但放在场景的视图属性下。同时,添加in storyboard.removeScene( "loadgame" )
。"loadgame"
场景将在本章后面介绍。-- Called when the scene's view does not exist: function scene:createScene( event ) local gameGroup = self.view -- completely remove loadgame's view storyboard.removeScene( "loadgame" ) print( "\nmaingame: createScene event") end
-
在
createScene()
事件之后,创建enterScene()
事件,并在gameActivate()
函数之前添加它。enterScene()
将过渡所有屏幕上的游戏功能。同时,在场景的视图属性中包含gameGroup
。-- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local gameGroup = self.view
-
在
gameStart()
函数之后,删除return gameGroup
行。return gameGroup -- Code will not run if this line is not removed
-
接下来,使用
end
关闭function scene: enterScene( event )
。print( "maingame: enterScene event" ) end
-
创建
exitScene()
和destroyScene()
事件。-- Called when scene is about to move offscreen: function scene:exitScene( event ) print( "maingame: exitScene event" ) end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) print( "((destroying maingame's view))" ) end
-
最后,为所有场景事件创建事件监听器,并在代码末尾添加
return scene
。-- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be -- automatically unloaded in low memory situations, or explicitly via a call to -- storyboard.purgeScene() or storyboard.removeScene(). scene:addEventListener( "destroyScene", scene ) return scene
刚才发生了什么?
使用 Storyboard API 将帮助我们更容易、更快地切换场景。每次你想将新场景加载到视图中时,都需要添加 require("storyboard")
。local scene = storyboard.newScene()
将允许我们调用场景事件:createScene()
、enterScene()
、exitScene()
和 destroyScene()
。
在游戏代码的末尾,我们添加了所有场景事件的事件监听器,并添加了 return scene
。
使用 Storyboard 管理每个场景的格式将类似于前面的代码。大部分游戏代码将在 createScene()
和 enterScene()
事件触发场景显示时被分发。当你想要清理或卸载监听器、音频、资源等时,将使用 exitScene()
和 destroyScene()
事件。
组织游戏
我们已经习惯了将 main.lua
作为我们的主要源文件来展示游戏代码的每一个细节。现在是时候借助 Storyboard API 高效地组织它了。
是时候添加新的 main.lua 文件了
在使用 Storyboard 的同时,我们的 main.lua
文件仍然至关重要,因为它是 Corona SDK 在模拟器中启动应用程序时首先查看的文件。我们将添加一些代码来更改游戏中的场景。
-
创建一个全新的文件名为
main.lua
,并让我们把状态栏添加回来。display.setStatusBar( display.HiddenStatusBar )
-
导入 Storyboard 并加载名为
loadmainmenu
的第一个场景。我们将在接下来的几节中创建这个场景。-- require controller module local storyboard = require ( "storyboard" ) -- load first screen storyboard.gotoScene( "loadmainmenu" )
刚才发生了什么?
为了在整个应用程序中整合 Storyboard,我们调用了 local storyboard = require ( "storyboard" )
模块。场景将通过 storyboard.gotoScene( "loadmainmenu" )
进行更改,这是一个引导用户到主菜单屏幕的加载屏幕。
新的游戏过渡
现在我们已经介绍了 Storyboard API,我们可以应用一些期待已久的过渡效果,这将有助于我们的游戏。一种方法是在游戏结束后过渡出游戏。
操作时间——游戏结束后切换屏幕
现在我们已经重命名了我们的游戏文件,让我们添加一个场景过渡,这样我们的游戏在游戏结束后就不会卡在 Game Over 屏幕上了。
在我们的 maingame.lua
文件中,添加一个名为 local menuBtn
的新变量,其中所有其他变量都在代码的开始部分初始化。在 callGameOver()
函数内部,在 highScoreText
代码之后添加以下行:
local onMenuTouch = function( event )
if event.phase == "release" then
audio.play( btnSound )
storyboard.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 = "",
font = "Helvetica",
textColor = { 255, 255, 255, 255 },
size = 16,
emboss = false
}
menuBtn.x = 100; menuBtn.y = 260
gameGroup:insert( menuBtn )
刚才发生了什么?
为了从 Game Over 屏幕过渡出来,创建了一个菜单按钮来改变场景。在 onMenuTouch()
函数中,当按钮 release
时,我们调用了 storyboard.gotoScene( "mainmenu", "fade", 500 )
。这将允许应用程序过渡到主菜单,我们将在本章后面创建它。
英雄试炼——重新开始游戏
现在你已经了解了 Storyboard API 如何与场景切换以及使用 UI 按钮在它们之间进行过渡,那么在 Game Over 屏幕出现后创建一个重新开始游戏的按钮如何?到目前为止,应用程序允许用户在游戏结束时返回到菜单屏幕。
在 callGameOver()
函数内,需要创建一个新的本地函数,该函数将使用 UI 按钮系统运行一个事件来使用 Storyboard 改变场景。提示:如果你当前正在该场景中,你不能调用相同的场景。
创建加载界面
加载界面提供了程序正在加载的反馈。这通过通知用户下一个屏幕正在加载,因此他们不会假设应用程序崩溃,特别是如果下一个屏幕正在加载大量数据。
操作时间——添加加载界面
当应用程序启动以及游戏关卡开始之前,我们将放置加载界面。这告诉用户还有更多内容或信息正在到来。
-
在你的项目文件夹中创建一个名为
loadmainmenu.lua
的新文件。 -
导入 Storyboard 并添加
storyboard.newScene()
函数。local storyboard = require( "storyboard" ) local scene = storyboard.newScene()
-
创建两个名为
myTimer
和loadingImage
的本地变量。在createScene()
事件和screenGroup
显示组中添加。local myTimer local loadingImage -- Called when the scene's view does not exist: function scene:createScene( event ) local screenGroup = self.view print( "\nloadmainmenu: createScene event" ) end
-
创建
enterScene()
事件并添加screenGroup
显示组。-- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local screenGroup = self.view print( "loadmainmenu: enterScene event" )
-
介绍
loadingImage
显示对象。loadingImage = display.newImageRect( "loading.png", 480, 320 ) loadingImage.x = 240; loadingImage.y = 160 screenGroup:insert( loadingImage )
-
创建另一个名为
goToMenu()
的本地函数,并调用storyboard.gotoScene( "mainmenu", "zoomOutInFadeRotate", 500 )
来将场景更改为"mainmenu"
。local goToMenu = function() storyboard.gotoScene( "mainmenu", "zoomOutInFadeRotate", 500 ) end
-
使用计时器函数,并在 1000 毫秒后调用
goToMenu()
一次。使用计时器 IDmyTimer
定义它。使用end
关闭enterScene()
事件。myTimer = timer.performWithDelay( 1000, goToMenu, 1 ) end
-
调用
exitScene()
和destroyScene()
事件。在exitScene()
事件中,取消myTimer
。-- Called when scene is about to move offscreen: function scene:exitScene() if myTimer then timer.cancel( myTimer ); end print( "loadmainmenu: exitScene event" ) end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) print( "((destroying loadmainmenu's view))" ) end
-
为所有场景事件添加事件监听器并
return scene
。保存并关闭文件。-- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be scene:addEventListener( "destroyScene", scene ) return scene
-
在你的项目文件夹中创建一个名为
loadgame.lua
的新文件。我们将创建另一个加载屏幕,该屏幕出现在游戏场景maingame.lua
之前。使用storyboard.gotoScene( "maingame", "flipFadeOutIn", 500 )
来切换场景。保存并关闭文件。local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local myTimer local loadingImage -- Called when the scene's view does not exist: function scene:createScene( event ) local screenGroup = self.view -- completely remove mainmenu storyboard.removeScene( "mainmenu" ) print( "\nloadgame: createScene event" ) end -- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local screenGroup = self.view print( "loadgame: enterScene event" ) loadingImage = display.newImageRect( "loading.png", 480, 320 ) loadingImage.x = 240; loadingImage.y = 160 screenGroup:insert( loadingImage ) local changeScene = function() storyboard.gotoScene( "maingame", "flipFadeOutIn", 500 ) end myTimer = timer.performWithDelay( 1000, changeScene, 1 ) end -- Called when scene is about to move offscreen: function scene:exitScene() if myTimer then timer.cancel( myTimer ); end print( "loadgame: exitScene event" ) end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) print( "((destroying loadgame's view))" ) end -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be scene:addEventListener( "destroyScene", scene ) return scene
刚才发生了什么?
在loadmainmenu.lua
文件中,一旦loadingImage
被添加到屏幕上,我们创建了goToMenu()
函数来将场景切换到"mainmenu"
,并使用过渡" zoomOutInFadeRotate"
,该过渡在淡入背景时放大并旋转加载屏幕图像。myTimer = timer.performWithDelay( 1000, goToMenu, 1 )
在 1000 毫秒(一秒)后执行函数,并只运行一次。这足以查看图像并使其淡出。
所有显示对象都通过function scene:enterScene( event ). loadingImage
进入场景。为了确保场景变化后没有定时器在运行,myTimer
通过使用timer.cancel(myTimer)
在function scene:exitScene()
中停止运行。
loadgame.lua
的代码与loadmainmenu.lua
类似。对于此文件,Storyboard 将场景过渡到maingame.lua
,即游戏文件。
创建主菜单
主菜单或标题屏幕是玩家在玩游戏之前看到的第一个印象之一。它通常会显示与实际游戏相关的小图像或风景片段,并显示应用程序的标题。
有如Start或Play之类的按钮,鼓励玩家在愿意的情况下进入游戏,以及一些次要按钮如Options,用于查看设置和其他相关信息,这些信息可能与应用程序相关。
操作时间——添加主菜单
我们将通过引入游戏标题、Play按钮和Options按钮来创建游戏的前端,这些按钮可以轻松地在应用程序的不同场景之间切换。
-
创建一个名为
mainmenu.lua
的新文件,并导入 Storyboard 和 UI 模块,storyboard.newScene()
函数以及计时器和音频的变量。local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local ui = require("ui") local btnAnim local btnSound = audio.loadSound( "btnSound.wav" )
-
创建
createScene()
事件。添加以下行,storyboard.removeScene( "maingame" )
和storyboard.removeScene( "options" )
,这将移除"maingame"
和"options"
场景。移除"maingame"
将在玩家从主游戏屏幕过渡到主菜单屏幕后发生。移除"options"
将在玩家从设置屏幕过渡到主菜单屏幕后发生。-- Called when the scene's view does not exist: function scene:createScene( event ) local screenGroup = self.view -- completely remove maingame and options storyboard.removeScene( "maingame" ) storyboard.removeScene( "options" ) print( "\nmainmenu: createScene event" ) end
-
添加
enterScene()
事件和backgroundImage
显示对象。-- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local screenGroup = self.view print( "mainmenu: enterScene event" ) local backgroundImage = display.newImageRect( "mainMenuBG.png", 480, 320 ) backgroundImage.x = 240; backgroundImage.y = 160 screenGroup:insert( backgroundImage )
-
引入
playBtn
显示对象,并创建一个名为onPlayTouch(event)
的函数,该函数使用storyboard.gotoScene()
将场景切换到"loadgame"
。使用"fade"
效果来切换场景。local playBtn local onPlayTouch = function( event ) if event.phase == "release" then audio.play( btnSound ) storyboard.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 screenGroup:insert( playBtn )
-
使用
easing.inOutExpo
过渡,将playBtn
显示对象在 500 毫秒内过渡到y = 260
。通过btnAnim
初始化。btnAnim = transition.to( playBtn, { time=500, y=260, transition=easing.inOutExpo } )
-
介绍
optBtn
显示对象并创建一个名为onOptionsTouch(event)
的函数。使用storybook.gotoScene()
通过"crossFade"
效果将场景过渡到"options"
。local optBtn local onOptionsTouch = function( event ) if event.phase == "release" then audio.play( btnSound ) storyboard.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 screenGroup:insert( optBtn )
-
使用
easing.inOutExpo
过渡,将optBtn
显示对象在 500 毫秒内过渡到y = 280
。通过btnAnim
初始化。使用end
关闭scene:enterScene( event )
函数。btnAnim = transition.to( optBtn, { time=500, y=280, transition=easing.inOutExpo } ) end
-
创建
exitScene()
事件并取消btnAnim
过渡。同时,创建destroyScene()
事件。-- Called when scene is about to move offscreen: function scene:exitScene() if btnAnim then transition.cancel( btnAnim ); end print( "mainmenu: exitScene event" ) end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) print( "((destroying mainmenu's view))" ) end
-
为所有场景事件添加事件监听器,并使用
return scene
保存并关闭文件。-- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be scene:addEventListener( "destroyScene", scene ) return scene
刚才发生了什么?
在主菜单屏幕上,我们添加了一个显示游戏标题、Play 按钮和选项按钮的图像。选项按钮目前尚未启用。onPlayTouch()
函数将场景过渡到 "loadgame"
。这将场景更改为 loadgame.lua
。Play 按钮放置在 x = 240; y = 440(中间和屏幕外)。当场景加载时,playBtn
过渡到 y = 260,使其从屏幕底部弹出,耗时 500 毫秒。
选项按钮做类似的事情。optBtn
放置在舞台的右侧,并在 500 毫秒内弹出至 y = 280。
通过 scene:exitScene()
函数,使用 transition.cancel( btnAnim )
取消 btnAnim
过渡。清理计时器、过渡和事件监听器在每次更改场景时都很重要,以防止在应用程序中发生潜在的内存泄漏。
创建一个选项菜单
选项菜单允许用户更改游戏中的各种设置或包含无法在主菜单中显示的其他信息。游戏可以有多个选项,也可以只有一个。有时选项菜单也可以称为设置菜单,它为玩家提供相同类型的定制体验。
是时候添加选项菜单了
我们将添加一个可以通过主菜单访问的选项菜单。我们将添加一个名为 Credits 的新 UI 按钮,当按下时,将用户引导到信用屏幕。
-
创建一个名为
options.lua
的新文件,并导入 Storyboard 和 UI 模块、storybook.newScene()
函数以及计时器和音频变量。local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local ui = require("ui") local btnAnim local btnSound = audio.loadSound( "btnSound.wav" )
-
创建
createScene()
事件。添加storyboard.removeScene ( "mainmenu" )
,这将移除"mainmenu"
场景。这将在玩家从主菜单屏幕过渡到选项屏幕后发生。接下来,添加storyboard.removeScene( "creditsScreen" )
。这将在玩家从信用屏幕过渡回选项屏幕后移除"creditsScreen"
。-- Called when the scene's view does not exist: function scene:createScene( event ) local screenGroup = self.view -- completely remove mainmenu and creditsScreen storyboard.removeScene( "mainmenu" ) storyboard.removeScene( "creditsScreen" ) print( "\noptions: createScene event" ) end
-
添加
enterScene()
事件和backgroundImage
显示对象。-- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local screenGroup = self.view print( "options: enterScene event" ) local backgroundImage = display.newImageRect( "optionsBG.png", 480, 320 ) backgroundImage.x = 240; backgroundImage.y = 160 screenGroup:insert( backgroundImage )
-
为致谢屏幕创建一个按钮。使用
easing.inOutExpo
过渡将creditsBtn
显示对象过渡到 y = 260,持续 500 毫秒。通过btnAnim
初始化它。local creditsBtn local onCreditsTouch = function( event ) if event.phase == "release" then audio.play( btnSound ) storyboard.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 screenGroup:insert( creditsBtn ) btnAnim = transition.to( creditsBtn, { time=500, y=260, transition=easing.inOutExpo } )
-
创建一个加载主菜单的关闭按钮。使用
end
关闭scene:enterScene ( event )
。local closeBtn local onCloseTouch = function( event ) if event.phase == "release" then audio.play( tapSound ) storyboard.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 screenGroup:insert( closeBtn ) end
-
创建
exitScene()
事件并取消btnAnim
过渡。同时,创建destroyScene()
事件。将事件监听器添加到所有场景事件和return scene
。保存并关闭你的文件。-- Called when scene is about to move offscreen: function scene:exitScene() if btnAnim then transition.cancel( btnAnim ); end print( "options: exitScene event" ) end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) print( "((destroying options's view))" ) end -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished options menuoptions menuaddingscene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be scene:addEventListener( "destroyScene", scene ) return scene
刚才发生了什么?
在这个场景中,creditsBtn
将以类似于创建主菜单的方式操作。致谢按钮目前仍然不可用。在 onCreditsTouch()
函数中,场景过渡到 "creditsScreen"
,并使用 "crossFade"
作为效果。当场景加载时,从屏幕外位置,creditsBtn
在 500 毫秒内过渡到 y=260。
为此场景创建了一个关闭按钮,以便用户有返回上一个屏幕的方法。使用 onCloseTouch()
函数,当 closeBtn
被释放时,Storyboard 将场景切换到 "mainmenu"
。按下关闭按钮时将显示主菜单屏幕。通过 scene:exitScene()
函数取消 btnAnim
过渡。
创建一个致谢屏幕
致谢屏幕通常会显示并列出参与游戏制作的所有人。它还可以以感谢某些个人和用于创建最终项目的程序的形式包含其他信息。
是时候添加一个致谢屏幕了
我们将要创建的致谢屏幕将基于一个从引入它的上一个屏幕过渡到该屏幕的触摸事件。
-
创建一个名为
creditsScreen.lua
的新文件,并导入 Storyboard、storyboard.newScene()
函数和backgroundImage
变量。local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local backgroundImage
-
创建
createScene()
事件。添加storyboard.removeScene ( "options" )
行,这将删除"options"
场景。这将在玩家从选项屏幕过渡到致谢屏幕后发生。-- Called when the scene's view does not exist: function scene:createScene( event ) local screenGroup = self.view -- completely remove options storyboard.removeScene( "options" ) print( "\ncreditsScreen: createScene event" ) end
-
添加
enterScene()
事件和backgroundImage
显示对象。-- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local screenGroup = self.view print( "creditsScreen: enterScene event" ) backgroundImage = display.newImageRect( "creditsScreen.png", 480, 320 ) backgroundImage.x = 240; backgroundImage.y = 160 screenGroup:insert( backgroundImage )
-
创建一个名为
changeToOptions()
的局部函数,带有事件参数。让该函数通过在backgroundImage
上的触摸事件使用 Storyboard 将场景切换回选项屏幕。使用end
关闭scene:enterScene( event )
函数。local changeToOptions = function( event ) if event.phase == "began" then storyboard.gotoScene( "options", "crossFade", 300 ) end end backgroundImage:addEventListener( "touch", changeToOptions) end
-
创建
exitScene()
和destroyScene()
事件。将事件监听器添加到所有场景事件和return scene
。保存并关闭你的文件。-- Called when scene is about to move offscreen: function scene:exitScene() print( "creditsScreen: exitScene event" ) end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) print( "((destroying creditsScreen's view))" ) end -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be scene:addEventListener( "destroyScene", scene ) return scene
刚才发生了什么?
信用屏幕使用事件监听器。changeToOptions(event)
函数将告诉 Storyboard 将场景更改为 "options"
,使用 storyboard.gotoScene ( "options", "crossFade", 500 )
。在函数的末尾,backgroundImage
将在屏幕被触摸时激活事件监听器。backgroundImage
被插入到 screenGroup
中,在 scene:enterScene( event )
函数之下。现在鸡蛋掉落游戏已经完全可以使用 Storyboard 操作。在模拟器中运行游戏。你将能够过渡到本章中创建的所有场景,并玩这个游戏。
尝试英雄——添加更多关卡
现在鸡蛋掉落游戏已经完成,并且拥有一个工作的菜单系统,通过创建更多关卡来挑战自己。为了加入额外的关卡,需要添加一些位置上的小改动。记得在改变场景时应用 Storyboard。
尝试创建以下内容:
-
级别选择屏幕
-
添加额外关卡的关卡编号按钮
在创建新关卡时,参考 maingame.lua
中的格式。可以通过改变鸡蛋从天空掉落的速度间隔或添加其他需要避免的游戏资源来改变新关卡。在这个游戏框架中,有如此多的可能性来添加自己的特色。试试看吧!
突击测验——游戏过渡和场景
-
你调用哪个函数来使用 Storyboard 改变场景?
-
a.
storybook()
-
b.
storybook.gotoScene()
-
c.
storyboard(changeScene)
-
d. 以上都不是
-
-
哪个函数可以将任何参数转换为数字?
-
a.
tonumber()
-
b.
print()
-
c.
tostring()
-
d. nil
-
-
你如何暂停计时器?
-
a.
timer.cancel()
-
b.
physics.pause()
-
c.
timer.pause( timerID )
-
d. 以上都不是
-
-
你如何恢复计时器?
-
a.
resume()
-
b.
timer.resume( timerID )
-
c.
timer.performWithDelay()
-
d. 以上都不是
-
摘要
恭喜!我们有一个足够完整的游戏可以进入 App Store 或 Google Play Store。当然,不是使用这个确切的游戏,但我们已经学到了足够的材料来创建一个。完成游戏框架是一项伟大的成就,尤其是在如此短的时间内完成这样简单的事情。
这里有一些你学到的技能:
-
使用
saveValue()
和loadValue()
保存高分 -
理解如何暂停物理/计时器
-
显示暂停菜单
-
使用 Storyboard API 改变场景
-
使用加载屏幕创建场景之间的过渡
-
使用主菜单介绍游戏标题和子菜单
本章是一个重要的里程碑。我们在前几章中学到的所有内容都应用到了这个示例游戏中。最棒的是,它只用了不到一天的开发时间来编写代码。另一方面,艺术资源则是另一回事。
我们还有许多关于 Corona SDK 功能需要学习。在下一章中,我们将更详细地介绍如何优化我们的游戏资源以适应高分辨率设备。我们还将介绍如何在 Facebook 和 Twitter 上发布消息,以及如何将我们的应用程序与 Openfeint 同步!
第九章. 处理多个设备和应用的网络连接
允许您的应用程序集成到社交网络中是推广您最终产品的一个好方法。许多游戏允许玩家上传他们的高分,并与玩相同游戏的用户分享。一些游戏提供需要成功完成的挑战,以解锁成就。社交网络增强了游戏体验,并为开发者提供了很好的曝光。
我们还将更详细地介绍构建配置,因为我们越来越习惯于编程。了解配置您的设备构建的重要性对于跨平台开发是强制性的。这是 Corona SDK 可以轻松处理 iOS 和 Android 设备的能力。
在本章中,我们将学习以下主题:
-
重新访问配置设置
-
发布 Twitter 消息
-
发布 Facebook 消息
-
使用 OpenFeint 添加成就和排行榜
让我们添加这些最后的修饰吧!
返回配置
在第二章中简要讨论了构建设置和运行时配置,即《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",
},
},
}
方向支持(Android)
Android 平台支持两种方向:portrait
和 landscapeRight
。方向 landscapeLeft
和 portraitUpsideDown
对 Android 没有影响。此外,Android 目前不支持自动旋转。默认方向不影响 Android 设备。方向初始化为设备的实际方向(除非只指定了一个方向)。
这里是一个 Android 特定的 build.settings
文件示例(您也可以在同一文件中结合 Android 和 iPhone 设置):
settings =
{
android =
{
versionCode = "2",
versionName = "2.0"
},
androidPermissions =
{
"android.permission.INTERNET"
},
orientation =
{
default = "portrait"
},
}
版本代码和版本名称(Android)
versionCode
和 versionName
可以在 build.settings
中的可选 "android"
表中设置。
versionCode
字段默认为"1"
,而versionName
字段在build.settings
文件中未设置时默认为"1.0"
。当将应用程序的更新版本提交到 Google Play Store 时,versionCode
和versionName
也必须更新。所有versionCode
的版本号都必须是整数。versionCode
不能包含任何小数。versionName
可以包含小数。
更多信息,请参阅android:versionCode和android:versionName:http://developer.android.com/guide/topics/manifest/manifest-element.html#vcode.
注意
versionCode
是一个内部数字,用于区分 Google Play Store 中的应用程序发布。它不同于 Corona 构建对话框中提供的版本。versionName
是显示给用户的版本号。
应用权限(Android)
可以使用可选的"androidPermissions"
表来指定权限,使用与AndroidManifest Reference: developer.android.com/reference/android/Manifest.permission.html
中给出的字符串值。
开发者应使用符合其应用程序要求的权限。例如,如果需要网络访问,则需要设置 Internet 权限。
在更简单的层面上进行内容缩放
如果您以前从未处理过这些问题,那么在多台设备上进行内容缩放有时可能会令人沮丧。虽然 iPhone 和 iPhone 4 可以轻松地在均匀的范围内进行缩放,但需要一些其他个别尺寸来使用 iPad 调整屏幕尺寸,其分辨率为 768 x 1024。Droid 的分辨率为 480 x 854,三星 Galaxy 平板电脑的分辨率为 600 x 1024,仅举几个例子。
当设置您的config.lua
,就像我们在前面的章节中所做的那样,我们将内容设置为width = 320, height = 480
,和scale = "letterbox"
。如果为 Android 设备构建,"zoomStretch"
最适合适应平台上的不同屏幕尺寸。这有助于在 iOS/Android 设备之间构建,并展示足够大的显示图像,以适应各种屏幕尺寸。
如果您想要适应更大的屏幕尺寸然后缩小,请使用 iPad 的屏幕尺寸。您的config.lua
将类似于以下代码:
application =
{
content =
{
width = 768,
height = 1024,
scale = "letterbox"
}
}
虽然前面的例子是另一种缩放内容的方法,但重要的是要记住,与较大的(高分辨率)图像相关的纹理内存限制。虽然 iPad、iPhone 4 和三星 Galaxy 平板电脑可以很好地处理这个问题,但 iPhone 3GS 和更老设备将拥有远少于纹理内存来处理大型图形。
解决这个潜在问题的方法之一是使用动态图像分辨率来替换更适合低端设备和高端设备的资源。我们将在本节稍后更详细地讨论这个话题。
两者之最佳
如您可能已注意到,我们在示例应用中使用的某些背景图片的尺寸为 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.contentWidth / 2
backgroundImage.y = display.contentHeight / 2
任何大小为 320 x 480 的内容都被认为是焦点区域。任何在该区域之外的内容将被裁剪,但在任何设备上都会用内容填充整个屏幕。
动态图像分辨率的深层含义
我们知道我们可以交换用于 iOS 设备的基图像"image.png"
(用于 3GS 及以下)和双分辨率图像"image@2x.png"
(用于具有视网膜显示屏的 iPhone 4)。这仅发生在苹果设备能够判断屏幕尺寸是原始内容分辨率的两倍时。
可用命名方案来处理 iPad 和 Android 手机等设备。了解如何处理针对提议的设备受影响的资源缩放是成功的一半。我们将不得不定义 Corona 需要解决的分辨率缩放级别。
使用以下行:display.newImageRect( [parentGroup,] filename [, baseDirectory] w, h )
将调用您的动态分辨率图片。
通常,我们使用["@2x"] = 2
来调用 iOS 设备项目中可用的更高分辨率图片:
application =
{
content =
{
width = 320,
height = 480,
scale = "letterbox",
imageSuffix =
{
["@2x"] = 2,
},
},
}
前面的示例仅适用于 iPhone 4 和 iPad,因为它超过了这两个设备上的基本尺寸 320 x 480。如果我们想让它对 Droid 可用,缩放阈值将是 1.5。对于三星 Galaxy 平板等 Android 平板来说,缩放阈值是 1.875。那么我们如何得出这些数字呢?很简单。取高端设备的宽度除以 320(基本尺寸)。例如:
Droid 的尺寸为 480 x 854。将 480 除以 320 等于 1.5。
三星 Galaxy 平板的尺寸为 600 x 1024。将 600 除以 320 等于 1.875。
如果试图在同一个项目中管理 iOS 和 Android 设备,您可以在config.lua
中更改您的imageSuffix
如下:
imageSuffix =
{
["@2x"] = 1.5, -- this will handle most Android devices such as the Droid, Nexus, Galaxy Tablet, etc...
}
-- 或者
imageSuffix =
{
["@2x"] = 1.8, -- this will handle the Galaxy Tablet and similar sized devices
}
使用上述任一示例将触发提议的 Android 设备显示更高分辨率的图片。
imageSuffix
不一定必须是"@2x"
,它可以像"@2", "_lrg"
或甚至"-2x"
这样的任何东西。只要您的更高分辨率图片在主图片名称后有预期的后缀,它就能正常工作。
高分辨率精灵图集
高分辨率精灵表的处理方式与动态图像分辨率不同。虽然您可以使用相同的命名约定来区分您的高分辨率图像和基本图像,但精灵表将无法使用display.newImageRect()
来引用。
如果您当前的内容缩放比例在config.lua
文件中设置为width = 320, height = 480
,并且scale = "letterbox"
,那么以下设备的缩放输出将展示以下内容:
-
iPhone = 1
-
iPhone 4 = 0.5
-
Droid = 0.666666668653488
-
iPad = 0.46875
应用一个与 iPhone 缩放匹配的基本精灵表,将显示清晰、干净的图像。当相同的精灵表应用于 iPhone 4 时,显示将匹配设备的内容缩放,但精灵表在边缘会略显像素化和模糊。使用display.contentScaleX
和调用一些方法可以为您解决这个问题。请注意,displayScale < 1
将根据前述设备缩放访问高分辨率精灵表。
local spriteSheet
local myObject
local displayScale = display.contentScaleX - scales sprite sheets down
if displayScale < 1 then - pertains to all high-res devices
spriteSheet = sprite.newSpriteSheet( "mySprite@2x.png", 512, 512 )
else
spriteSheet = sprite.newSpriteSheet( "mySprite.png", 256, 256 )
end
local spriteSet = sprite.newSpriteSet(spriteSheet, 1, 4)
sprite.add( spriteSet, "walk", 1, 4, 300, 0 ) -- play 4 frames every 300 ms
myObject = sprite.newSprite( spriteSet )
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:prepare("walk")
myObject:play()
网络化您的应用
当您完成主要游戏框架的开发后,如果您决定进行网络化,考虑一下如何实现它是很好的。
我们在生活的某个时刻都使用过某种网络工具,比如推特和 Facebook。也许您目前正在使用这些应用程序,但重点是您已经阅读了其他用户关于新游戏发布的更新,或者有人正在传播下载游戏并与他们竞争的消息。您可以成为那个开发者,开发他们正在谈论的游戏!
在您的游戏中加入网络机制并不麻烦。只需几行代码就可以让它工作。
发布到推特
推特,推特,推特……推特是一种连接您与最新感兴趣信息的网络工具。它也是一个分享您业务和游戏信息的优秀工具。通过推广您的应用程序来接触游戏开发社区。
为了在推特上发布内容,您需要在以下网址创建一个推特账户:twitter.com/
并确保您已登录。
实践时间——在您的应用中添加推特
我们将通过 UI 按钮访问一个网络服务,在我们的应用中实现推特功能。
-
在
Chapter 9
文件夹中,将Twitter Web Pop-Up
项目文件夹复制到您的桌面。所有需要的配置、库和资源都已经包含在内。您可以从 Packt 网站下载本书附带的项目文件。 -
创建一个新的
main.lua
文件并将其保存到项目文件夹中。 -
在代码开头设置以下变量:
display.setStatusBar( display.HiddenStatusBar ) local ui = require("ui") local openBtn local closeBtn local score = 100
-
创建一个名为
onOpenTouch()
的局部函数,并带有事件参数。添加一个if
语句,以便事件接收一个"release"
动作。local onOpenTouch = function( event ) if event.phase == "release" then
-
使用名为
message
的局部变量,添加以下字符串语句并将score
连接起来。local message = "Posting to Twitter from Corona SDK and got a final score of " ..score.. "."
-
添加
local myString
并对message
应用string.gsub()
以替换空格实例。local myString = string.gsub(message, "( )", "%%20")
-
介绍
native.showWebPopup()
函数,该函数链接到 Twitter 账户。将myString
拼接到预加载的消息中。关闭函数。native.showWebPopup(0, 0, 320, 300, "http://twitter.com/intent/tweet?text="..myString) end end
-
设置
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
-
创建一个名为
onCloseTouch()
的本地函数,带有event
参数。添加一个if
语句,其中event.phase == "release"
以激活native.cancelWebPopup()
。local onCloseTouch = function( event ) if event.phase == "release" then native.cancelWebPopup() end end
-
设置
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
-
保存文件并在模拟器中运行项目。确保你已连接到互联网以查看结果。
注意
如果你目前未登录到你的 Twitter 账户,你将需要在看到我们代码中推文的成果之前登录。
刚才发生了什么?
在代码的顶部附近,我们设置了一个变量 local score = 100
。这将在我们的 Twitter 消息中使用。
在 onOpenTouch(event)
函数中,当释放 openBtn
时,将加载一个网页弹出窗口。将要发布的文本以字符串格式显示在变量 local message
下。你会注意到我们将 score
拼接到字符串中,以便在消息发布中显示其值。
local myString, string.gsub()
用于替换字符串中模式指示的所有实例。在这种情况下,它将消息中的字符串内部搜索每个单词之间的每个空格,并将其替换为 %20\. %20
。%20
编码 URL 参数以表示空格。额外的 %
作为转义字符。
native.showWebPopup()
函数以 320 x 300 的尺寸显示;大约占设备屏幕尺寸的一半。要显示的 Twitter 消息对话框的 URL 被添加并拼接 myString
。
当网页弹出窗口不再需要使用并需要关闭时,onCloseTouch(event)
由 closeBtn
调用。这将获取 event
参数 "release"
并调用 native.cancelWebPopup()
。这个特定的函数将关闭当前的网页弹出窗口。
发布到 Facebook
另一个可以用来分享你的游戏信息的社交网络工具是 Facebook。你可以轻松自定义帖子以链接有关你的游戏的信息或分享关于高分的信息,并坚持要求其他用户下载它。
为了发布到 Facebook,你需要登录到你的 Facebook 账户或在此处创建一个:www.facebook.com/
。你将需要从 Facebook 开发者 网站获取一个 App ID:developers.facebook.com/
。App ID 是你网站的唯一标识符,它决定了用户和应用程序页面/网站之间应有的安全级别。
一旦您创建了 App ID,您还需要编辑应用信息并选择您希望如何与 Facebook 集成。您有几种选择,例如:网站、原生 iOS 应用和原生 Android 应用,仅举几例。必须选择网站集成并填写有效的 URL,以便 Facebook 将重定向到处理网页弹窗的指定 URL。
操作时间——将 Facebook 添加到您的应用中
与我们的 Twitter 示例类似,我们还将结合使用带有网页弹窗的 Facebook 帖子。
-
在
第九章
文件夹中,将Facebook Web Pop-Up
项目文件夹复制到您的桌面。所有必要的配置、库和资源都已包含在内。您可以从 Packt 网站下载本书附带的项目文件。 -
创建一个新的
main.lua
文件并将其保存到项目文件夹中。 -
在代码开头设置以下变量:
display.setStatusBar( display.HiddenStatusBar ) local ui = require("ui") local openBtn local closeBtn local score = 100
-
创建一个名为
onOpenTouch()
的本地函数,并带有事件参数。当事件接收到"release"
动作时,添加一个if
语句。local onOpenTouch = function( event ) if event.phase == "release" then
-
添加以下本地变量,包括我们将要实现到 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")
-
介绍链接到 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/
。 -
创建一个名为
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
-
保存文件并在模拟器中运行项目。确保您已连接到互联网,并且您的 Facebook 账户,以便查看结果。
刚才发生了什么?
在 onOpenTouch(event)
函数中,当按下并释放 openBtn
时,会调用几个变量。请注意,local appId
指示的是在 Facebook 开发者网站上创建应用后可以获得的数字字符串。
message1, message2
和 message3
是显示消息帖子的字符串。myString1, myString2
和 myString3
帮助替换 message1, message2
和 message3
中的空格。
native.showWebPopup()
函数以 320 x 300 的尺寸显示,并将对话框 URL 显示给 Facebook。以下参数相应显示:
-
app_id
— 示例:"1234567"
(这是你在 Facebook 开发者网站上创建的唯一 ID)。 -
redirect_uri
— 用户点击对话框中的按钮后要重定向到的 URL。这是参数中必需的。 -
display
— 显示用于渲染对话框的模式。touch
— 用于智能手机设备,如 iPhone 和 Android。使对话框屏幕适应小尺寸。
-
link
— 附带在帖子中的链接。 -
picture
— 发布帖子的图片的 URL。 -
name—name
— 链接附件的名称。 -
caption
— 链接的标题(出现在链接名称下方)。 -
description
— 链接的描述(出现在链接标题下方)。
当不再需要使用网页弹出窗口并需要关闭时,closeBtn
会调用 onCloseTouch(event)
。这将获取事件参数 "release"
并调用 native.cancelWebPopup()
。这个特定的函数将关闭当前的网页弹出窗口。
Facebook Connect
该库提供了一系列函数,通过官方的 Facebook Connect 接口与 www.facebook.com
进行交互。
行动时间 — 使用 Facebook Connect 发布分数
Facebook Connect 是通过使用本地的 Facebook UI 功能在墙面上发表帖子的另一种方式。我们将创建一种不同的方式来发布消息和分数到新闻源。为了了解 Facebook Connect 的工作原理,你需要将构建加载到设备上以查看结果。它不会在模拟器中运行。
-
在
第九章
文件夹中,将Facebook Connect
项目文件夹复制到你的桌面。所有需要的配置、库和资源都已经包含在内。你可以从 Packt 网站下载本书附带的项目文件。 -
创建一个新的
main.lua
文件并将其保存到项目文件夹中。 -
在代码开头设置以下变量:
display.setStatusBar( display.HiddenStatusBar ) local ui = require("ui") local facebook = require "facebook" local fbBtn local score = 100
-
创建一个带有事件参数的局部函数
onFBTouch()
。添加一个包含event.phase == release
的if
语句。同时包括你的 Facebook 应用 ID,以字符串格式。local onFBTouch = function( event ) if event.phase == "release" then local fbAppID = "0123456789" -- Your FB App ID from facebook developer's panel
-
在
onFBTouch(event)
中创建另一个局部函数facebookListener()
,它也有一个事件参数。包括一个引用"session"
等于event.type
的if
语句。local facebookListener = function( event ) if ( "session" == event.type ) then
-
在
if
语句中添加另一个条件,其中"login"
等于event.phase
。包含一个名为theMessage
的局部变量来显示你想与其他 Facebook 用户分享的消息。if ( "login" == event.phase ) then local theMessage = "Got a score of " .. score .. " on Your App Name Here!"
-
添加
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 指向一个类似play.google.com/store/apps/details?id=com.yourcompany.yourappname
的 Android 应用,或者选择一个通用的网站 URL。 -
调用包含你的 appID、监听器和权限以在用户的 Facebook 墙上发布消息的
facebook.login()
。关闭onFBTouch(event)
函数的其余部分。facebook.login( fbAppID, facebookListener, { "publish_stream" } ) end end
-
启用
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
-
为 iOS 或 Android 创建一个新的设备构建。将构建加载到你的设备上并运行应用程序。在你可以看到应用程序的结果之前,你将被要求登录到你的 Facebook 账户。
刚才发生了什么?
需要完成的重要事情之一是require "facebook"
,以便使 Facebook API 能够工作。我们还创建了一个名为score
的局部变量,其值为 100。
onFBTouch(event)
函数会在fbBtn
的"release"
事件发生时启动事件参数。在函数内部,fbAppID
以字符串格式包含字符。这将是一组唯一的数字,你必须从 Facebook 开发者网站获取。当你在这个网站上创建一个应用页面时,App ID 会为你创建。
另一个函数facebookListener(event)
被创建,它将启动所有fbConnect
事件。在包含("login" == event.phase)
的if
语句中,将请求通过"me/feed, "POST"
发布消息。该 feed 包含以下内容:
-
message=theMessage
—指的是属于变量的字符串。它还会连接分数,以便显示其值。 -
name
—包含你的应用名称或主题的消息。 -
caption
—一个简短的具有说服力的消息,以吸引其他用户注意玩游戏。 -
link
—从 App Store 或 Google Play Store 下载游戏的 URL。 -
picture
—包含你的图像的 URL,用于显示你的应用图标或游戏的视觉表示。
在设置好参数后,facebook.login()
将引用fbAppID
,facebookListener()
将检查是否正在使用有效的应用程序 ID 在 Facebook 上发布。成功后,将通过"publish_stream"
发布帖子。
尝试英雄——创建一个对话框
看看你是否能找出如何使用 Facebook Connect 和与前面示例相同的设置来显示对话框。以下行将显示为:
facebook.showDialog( {action="stream.publish"} )
现在看看代码中facebook.showDialog()
可以访问的位置。这是另一种向 Facebook 发布消息的方法。
OpenFeint 的奇妙之处
OpenFeint 是一个第三方库,它使社交游戏功能,如公开排行榜和成就成为可能。更多信息,请参阅www.openfeint.com/
。
一旦你准备好将你的移动游戏公开联网,实现排行榜和成就系统非常简单。
你需要在 OpenFeint 开发者网站上创建一个用户账户:www.openfeint.com/developers
,以将它的功能集成到你的应用程序中。
游戏网络 API
游戏网络允许访问第三方库,如 OpenFeint、Papaya 和 Game Center,这些库可以启用社交游戏功能,如公开排行榜和成就。一旦你在应用程序中实现它们,API 将能够访问这些库。我们将重点关注易于使用的 OpenFeint 库,该库将实现游戏网络 API 的以下功能。
以下行使游戏网络功能在 OpenFeint 命名空间下可用:
local gameNetwork = require "gameNetwork"
gameNetwork.init()
初始化游戏网络提供者所需的产品密钥、密钥、显示名称等。登录到你的 OpenFeint 开发者账户后,你需要选择“添加新游戏”按钮并创建一个新的应用程序名称。App ID、产品密钥和产品密钥可以在应用程序名称的应用程序信息部分找到。
语法:
gameNetwork.init( providerName [, parms ...] )
参数:
-
providerName
—游戏网络提供者的字符串 ("openfeint") -
parms
—"openfeint" 提供商所需的附加参数-
Product Key
—应用程序 OpenFeint 产品密钥的字符串(由 OpenFeint 提供) -
Product Secret
—应用程序产品密钥的字符串(由 OpenFeint 提供) -
Display Name
—在 OpenFeint 排行榜和其他视图中显示的名称字符串 -
App ID
—应用程序 ID 的字符串(由 OpenFeint 提供)
-
示例:
gameNetwork.init( "openfeint", "<OpenFeint Product Key>", "<OpenFeint Product Secret>", "Display Name", "<App ID>" )
使用指定的产品密钥、密钥和显示名称初始化应用程序。这应该只调用一次。
gameNetwork.show()
在屏幕上显示来自游戏网络提供者的信息。
对于 OpenFeint 提供商,以下配置中启动 OpenFeint 仪表板:排行榜、挑战、成就、好友、游戏、高分。
语法:
gameNetwork.show( name [, data] )
参数:
-
name
—OpenFeint 提供商支持的字符串:-
gameNetwork.show()
—启动 OpenFeint 仪表板 -
gameNetwork.show( "leaderboards")
—打开用户的排行榜仪表板 -
gameNetwork.show( "challenges")
—打开用户的挑战仪表板(在 Android 上不支持) -
gameNetwork.show( "achievements")
—打开用户的成就仪表板 -
gameNetwork.show( "friends")
—打开用户的好友仪表板(在 Android 上不支持) -
gameNetwork.show( "playing ")
—打开用户的游戏仪表板(在 Android 上不支持) -
gameNetwork.show( "highscore", "LeaderboardID")
—打开特定项目的用户高分仪表板
-
-
data-String
—当 OpenFeint 仪表板视图为"highscore"
时,字符串应包含"leaderboardID"
属性,其值是对应的 OpenFeint 排行榜 ID。
gameNetwork.request()
向/从游戏网络提供者发送或请求信息。
语法:
gameNetwork.request( command [, parms ...] )
参数:
-
command
—OpenFeint 提供商支持的字符串:-
setHighScore
-
unlockAchievement
-
uploadBlob
—在 Android 上不支持 -
downloadBlob
—在 Android 上不支持
-
-
parms
—在先前的 OpenFeint 命令中使用的参数:-
setHighScore: { leaderboardID="123456", score=50 }
-
unlockAchievement: "achievementId"
-
uploadBlob: "uploadBlob", key, data
-
downloadBlob: key, [listener]
—监听"completion"
事件,其中"blob"
键被设置
-
示例:
以下解锁指定的成就:
gameNetwork.request( "unlockAchievement", "achievementId" )
以下在排行榜上设置高分:
gameNetwork.request( "setHighScore", { leaderboardID="1234567", score=50, displayText="50 points" } )
前面的函数接受一个包含以下元素的表作为参数:
-
leaderboardID:
字符串。OpenFeint 排行榜的 ID,高分应在此排行榜上发布。 -
score:
数字。要发布到指定排行榜的新高分值。 -
displayText:
字符串。一个可选的字符串,用于显示在由分数参数分配的数值分数所在的位置。
以下是在云中使用 OpenFeint 的 网络保存卡 功能保存游戏数据:
gameNetwork.request( "uploadBlob", key, data )
以下下载之前保存并上传的 blob 数据:
gameNetwork.request( "downloadBlob", key, listener ) -- listener for "completion" event with "blob" key set.
立刻使用 OpenFeint
在这本书中我们已经制作的游戏中实现 OpenFeint 有什么更好的方法呢!
操作时间——在 Egg Drop 中设置 OpenFeint 排行榜和成就
我们将把我们的 Egg Drop 游戏与到目前为止讨论过的 OpenFeint 功能结合起来。
-
在 OpenFeint 开发者网站上创建一个账户:
openfeint.com/developers
。您可以使用 Egg Drop 作为游戏名称进行测试,然后稍后删除它。您可以选择使其适用于 iOS 或 Android 或两者都适用。选择权在您手中。登录后,点击 账户信息 选项卡以查看 产品密钥、产品密钥 和 客户端应用程序 ID。这些需要添加到您的应用程序中。 -
从
第九章
文件夹中,将Egg Drop with OF
项目文件夹复制到您的桌面。您可以从 Packt 网站下载本书附带的项目文件。打开mainmenu.lua
文件。在 UI 模块之后插入游戏网络功能,并使用从 OpenFeint 开发者账户获得的信息进行初始化。local gameNetwork = require("gameNetwork") gameNetwork.init( "openfeint", "<OpenFeint Product Key>", "<OpenFeint Product Secret>", "Display Name", "<App ID>" )
-
在与
optBtn
相关的代码块下方,创建一个名为local ofBtn
的新 UI 按钮,该按钮在 500 毫秒内从屏幕外过渡到 y=280。添加一个名为onOFTouch()
的本地函数,带有event
参数。当event.phase == "release"
时,让游戏网络显示排行榜。保存并关闭您的文件。local ofBtn local onOFTouch = function( event ) if event.phase == "release" then audio.play( btnSound ) gameNetwork.show( "leaderboards" ) end end ofBtn = ui.newButton{ defaultSrc = "ofbtn.png", defaultX = 60, defaultY = 60, overSrc = "ofbtn-over.png", overX = 60, overY = 60, onEvent = onOFTouch, id = "OFButton", text = "", font = "Helvetica", textColor = { 255, 255, 255, 255 }, size = 16, emboss = false } ofBtn.x = 50; ofBtn.y = 440 screenGroup:insert( ofBtn ) btnAnim = transition.to( ofBtn, { time=500, y=280, transition=easing.inOutExpo } )
-
返回 OpenFeint 开发者门户,点击 功能 选项卡,然后点击 成就 选项卡。点击橙色按钮,上面写着 添加成就。
-
我们将创建的特定成就是当角色第一次接住鸡蛋时。为成就创建一个标题,确定您想要从 1000 个 Feint 积分中确定的点值,并写一个简短的订阅说明。如果您愿意,可以添加成就图标,但在这个教程中不是必需的。完成操作后,点击保存成就。
-
在下一屏,您将看到添加的成就以及一个唯一标识符,我们将使用它来在游戏中创建一个成就。您将使用为您在 OpenFeint 中生成的唯一标识符,而不是以下截图显示的那个:
-
在项目文件夹中打开
maingame.lua
文件。在onEggCollision()
函数中,找到包含event.force > 1.0 and self.isHit == false
的if
语句。在函数底部附近创建另一个if
语句,当接住一个鸡蛋时,请求gameNetwork()
、成就和您的唯一标识符。if eggCount == 1 then gameNetwork.request( "unlockAchievement", "1315272" ) -- replace the Unique ID with the one you created in OpenFeint end
-
返回 OpenFeint 开发者门户,点击排行榜标签。点击显示添加排行榜的橙色按钮。您可以创建任何与关卡相关的名称。您可以保留复选框不变。点击保存排行榜按钮。
-
在下一屏,您将看到排行榜的新唯一标识符。
-
在
maingame.lua
文件中,找到callGameOver()
函数。在说gameScore > highScore
的if
语句中,添加请求将游戏最高分发布到 OpenFeint 的gameNetwork()
请求。使用唯一标识符填写排行榜 ID
。您将使用在 OpenFeint 中为您生成的唯一标识符来确定最高分,score=gamescore
。保存您的文件。gameNetwork.request( "setHighScore", { leaderboardID="957686", score=gameScore } ) -- replace the Unique ID with the one you created in OpenFeint
-
成就排行榜的添加已完成!OpenFeint 的功能不能在模拟器上运行。您需要将游戏构建加载到设备上才能查看结果。
刚才发生了什么?
gameNetwork.init()
使用游戏网络提供者所需的参数初始化一个应用,这些参数包括("openfeint","<OpenFeint 产品密钥>", "<OpenFeint 产品密钥>", "显示名称", "<应用 ID>")
。
主菜单显示ofBtn
并建立到gameNetwork.show ( "leaderboards" )
的连接,并打开用户的排行榜仪表板。
当添加成就时,我们在onEggCollision()
函数中添加了if eggCount == 1 then gameNetwork.request( "unlockAchievement", "1315272" ) end
,因为我们知道eggCount
是通过主要角色的每一次碰撞来跟踪的。这将解锁 OpenFeint 中的成就,因为我们通过我们创建的唯一 ID 指定了它。
排行榜跟踪每个级别中实现的最高分。我们在callGameOver()
函数和比较gameScore
和highScore
的if
语句中添加了gameNetwork.request( "setHighScore", { leaderboardID="957686", score=gameScore } )
。如果达到了该级别的最高分,它将被发送到 OpenFeint 中的排行榜,在开发者门户中设置的唯一 ID 下。
注意
当你在开发者门户下注册时,拥有 OpenFeint 会员不需要支付任何费用。当你完成你的游戏时,请确保注册它,这样它就可以在公众中完全访问 OpenFeint 功能。
尝试一下英雄——添加更多 OpenFeint 成就
通过使用当前的游戏,尝试想出其他可以解锁的成就,并找出它们如何通过 OpenFeint 访问。以下是一些帮助你开始的想法:
-
达到一定数量的积分
-
捕捉一定数量的鸡蛋
-
在所有三个生命都可用的情况下,捕捉一定数量的鸡蛋
有许多可能性可以创造。试试看!
快速问答——处理社交网络
-
什么是指定降低高分辨率精灵图集的特定 API?
-
a.
object.xScale
-
b.
display.contentScaleX
-
c.
object.xReference
-
d. 以上都不是
-
-
在整个应用程序中,可以向用户授予的最大 Feint 积分是多少?
-
a. 200
-
b. 750
-
c. 10000
-
d. 1000
-
-
以下哪个是
gameNetwork.init()
所需的有效参数?-
a. App ID
-
b. 产品密钥
-
c. 产品密钥
-
d. 所有以上选项
-
摘要
我们在我们的应用程序中涵盖了更多关于增强配置设置和集成当今媒体中最受欢迎的三个社交网络的内容。
我们深入研究了以下内容:
-
构建设置
-
动态内容缩放和动态图像分辨率
-
高分辨率精灵图集
-
将消息推送到 Twitter 和 Facebook
-
在 OpenFeint 中实现成就和排行榜
在下一章中,我们将介绍如何将我们的游戏提交到 App Store 和 Google Play Store 的过程。你绝对不想错过这个机会!
第十章。优化、测试和发布你的游戏
将游戏开发到完成是一个巨大的成就。这是向世界分享它并让其他人能够玩你的新应用迈出的一步。使用 Corona SDK 创建你的游戏的好处是,你有选择为 iOS 和/或 Android 构建应用。你想要确保你的应用已准备好提交,以便可以在你正在开发的移动平台上分发。我们将介绍准备你的游戏以发布状态所需的过程。
在本章中,我们将学习以下主题:
-
提高你应用的性能
-
为 App Store 设置分发配置文件
-
在 iTunes Connect 中管理应用信息
-
学习如何提交应用至应用加载器
-
为 Android 签署应用
-
学习如何提交应用至 Google Play 商店
理解内存效率
在开发你的应用时,你应该始终考虑你的设计选择如何影响你应用的性能。尽管计算能力和内存都有所提升,但设备内存仍然有其限制。在设备内部进行性能和优化不仅能够实现更快的响应时间,还能帮助最小化内存使用并最大化电池寿命。
内存是移动设备上的重要资源。当消耗过多内存时,设备可能会在你最意想不到的时候强制退出你的应用。以下是在开发过程中需要注意的一些事项:
-
消除内存泄漏:允许内存泄漏存在意味着你的应用中会有额外的已使用内存,这会占用宝贵的空间。尽管 Lua 具有自动内存管理功能,但你的代码中仍然可能发生内存泄漏。例如,当你将全局变量引入你的应用中时,你有责任告诉 Lua 当它们不再需要时,以便释放内存。这可以通过在代码中使用
nil
来实现(myVariable = nil)
。 -
显示的图像应尽可能小:你可能想在场景中有许多显示图像,但这可能会占用过多的纹理内存。精灵表单可能会在你的应用中占用大量内存。它们应该尽可能以最小的尺寸创建,并具有适当数量的帧来清晰地展示动画。对于你显示的所有项目,规划出哪些元素始终在你的背景和前景中。如果有可能将几个图像组合在一起而不移动,请这样做。当添加多个显示图像时,这将节省一些内存。
-
不要一次性加载所有资源:避免在资源实际需要之前加载资源文件。这将有助于节省内存,并防止你的应用在尝试一次性加载过多内容时崩溃。
-
从显示层次结构中移除对象:当创建显示对象时,它会被隐式地添加到显示层次结构中。当你不再需要显示对象时,你应该从显示层次结构中移除它,特别是当对象包含图像时。这可以通过使用
display.remove( myImage ); myImage = nil
来完成。例如:
local box = display.newRect( 0, 50, 100, 100) box:setFillColor( 255, 255, 255, 255 ) 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 或 Pngcrush 这样的图像编辑工具尽可能多地压缩文件大小。这将有助于减小文件大小并减轻应用程序卡顿的痛苦。压缩大图像尺寸从长远来看会对你有益。
分发 iOS 应用程序
一旦你的游戏最终调试并完成,接下来是什么?假设你已经注册了 iOS 开发者计划,在应用程序可以提交到 App Store 之前,有一些指南必须遵循。
准备你的应用程序图标
根据你的应用程序为哪些 iOS 设备开发,应用程序图标需要各种图像大小和命名约定。你可以在 Apple 开发者网站上 iOS 人类界面指南的应用程序图标部分找到最新信息:developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/MobileHIG.pdf
。
以下是为应用程序图标的要求,它们也需要是.png
格式:
-
iTunesArtwork
—512x512 像素图像。此图像需要移除 .png 扩展名。 -
Icon.png
—57x57 像素图像。用于 App Store 和 iPhone 3G/iPod Touch 的主屏幕。 -
Icon@2x.png
—114x114 像素图像。用于 iPhone 4/4S 主屏幕。 -
Icon-72.png
—72x72 像素图像。用于 iPad 主屏幕。 -
Icon-72@2x.png
—144x144 像素图像。用于 iPad(高分辨率)主屏幕。 -
Icon-Small.png
—29x29 像素图像。用于 Spotlight 和设置。 -
Icon-Small@2x.png
—58x58 像素图像。用于 iPhone 4/4S Spotlight 和设置。 -
Icon-Small-50.png
—50x50 像素图像。用于 iPad Spotlight 和设置。 -
Icon-Small-50@2x.png
—100x100 像素图像。用于 iPad(高分辨率)Spotlight 和设置。
在您的 build.settings
文件中,您需要包含您在应用程序中为所有构建的设备提供的图标引用。以下是在创建通用构建时设置文件的示例:
settings =
{
orientation =
{
default = "landscapeRight",
},
iphone =
{
plist =
{
CFBundleIconFile = "Icon.png",
CFBundleIconFiles = {
"Icon.png",
"Icon@2x.png",
"Icon-72.png",
"Icon-72@2x.png",
"Icon-Small.png",
"Icon-Small@2x.png",
"Icon-Small-50.png",
"Icon-Small-50@2x.png",
},
},
},
}
您不需要在 plist 中包含 iTunesArtwork 图像,但请确保将其插入到您的构建项目文件夹中。
行动时间—设置 App Store 的分发证书和配置文件
我们专注于创建开发证书和配置文件,以便在设备上测试和调试我们的应用程序。现在我们必须创建它们的分发版本,以便提交 iOS 应用程序。请注意,苹果公司可以随时更改其网站的设计。因此,如果步骤和截图不匹配,请不要沮丧。
-
登录您的 Apple 开发者账户,并前往 iOS 配置文件门户。选择 App ID。创建一个新的 App ID,使其与您的应用程序相关联,以便您可以识别它。如果您在开发过程中已经使用了一个现有的 App ID,您可以忽略此步骤。如果您选择使用现有的 Bundle Seed ID,否则请选择生成新,如果这是您在包中的第一次,或者如果您只是想创建一个新的独立应用程序。
在包标识符(App ID 后缀)字段中,指定您应用程序的唯一标识符。建议您使用反向域名样式字符串,即
com.domainname.appname
。您需要将domainname替换为您自己的域名,将appname替换为您自己的应用程序名称。 -
导航到配置文件门户的配置文件部分,并选择分发选项卡。选择分发方法旁边的App Store单选按钮。创建分发配置文件名称。检查以确保您的 iOS 分发证书已显示。最后,选择您计划用于创建分发配置文件的App ID。点击提交按钮。
-
刷新您的浏览器并下载您的新分发配置文件。
-
如果 Xcode 尚未打开,请启动它,然后在键盘上按Shift + Command + 2以打开组织者。在库下,选择配置文件部分。将您下载的
.mobileprovision
文件拖到组织者窗口中。这将自动将您的.mobileprovision
文件复制到正确的目录,或者双击文件以添加它。
刚才发生了什么?
您使用的App ID对于识别您将要提交的应用至关重要。最好使用一个独特的反向域名风格字符串。至于您的包标识符(App ID 前缀),您可以生成一个新的,也可以使用现有的。如果您在应用程序中实现 Game Center 或内购功能,请不要使用通配符(星号*)。包 ID需要完全唯一。
为了在 App Store 分发,您需要创建一个 App Store 分发配置文件。任何其他配置文件,无论是开发还是临时配置文件,都将不被接受。这个过程与创建开发配置文件类似。
您可以在 Apple 开发者网站上找到有关分发配置文件的更多信息:developer.apple.com/ios/manage/distribution/index.action
(如果您尚未这样做,您将被要求登录到您的 Apple 开发者账户)。Anscamobile 的网站:developer.anscamobile.com/content/building-devices-iphoneipad
。
iTunes Connect
iTunes Connect 是一套基于 Web 的工具,允许您提交和管理您在 App Store 上分发的应用程序。在 iTunes Connect 中,您将能够检查合同的状态,设置您的税务和银行信息,获取销售和财务报告,请求促销代码,管理用户、应用程序、元数据和您的内购目录。
合同、税务和银行
如果您计划销售您的应用程序,您需要有一个付费的商业协议,以便它可以在 App Store 上发布。您将必须请求一个与 iOS 付费应用程序相关的合同。所有这些操作都通过 iTunes Connect 下的合同、税务和银行链接完成。
在请求合同时,请注意可能发生的问题,例如,当苹果第一次处理您的信息时的延误以及/或在 iTunes Connect 中更改您的当前联系信息(即,如果您搬到了不同的地点,地址变更)。确保您的合同信息始终是最新的责任在于您,您需要持续联系苹果以获得支持。
操作时间——在 iTunes Connect 中管理您的应用
我们将介绍如何在 iTunes Connect 中设置您的应用信息。有关用户账户、合同和银行等其他您想要设置的信息,可以在以下 URL 找到:developer.apple.com/appstore/resources/submission/
。
-
在
itunesconnect.apple.com/
登录 iTunes Connect。您的登录信息与您的 iOS 开发者账户相同。登录后,选择管理您的应用。点击添加新应用按钮。应用名称是您应用的名称。SKU 编号是应用的唯一字母数字标识符。您的包标识符是在 iOS 配置文件门户中创建的。填写信息并点击继续。![操作时间——在 iTunes Connect 中管理您的应用] -
下一步是选择您希望应用在 App Store 上上线的时间和您想要收取的价格层级。有一个可选的复选框用于教育机构折扣。这仅适用于您希望为同时购买多份副本的教育机构提供折扣的情况。完成后点击继续。![操作时间——在 iTunes Connect 中管理您的应用]
-
接下来,填写关于您应用的元数据部分。这包括版本号、您游戏的描述、类别、与您的应用相关的关键词、版权信息、联系信息和支持网址。![操作时间——在 iTunes Connect 中管理您的应用]
-
评分部分基于您应用的内容。对于每个描述,选择最能描述您应用的频率级别。某些内容类型会导致自动拒绝,例如,在您的应用中展示的现实主义暴力或针对个人或群体的攻击等。您可以在以下 URL 了解更多关于App Store 审查指南的信息:
developer.apple.com/appstore/resources/approval/guidelines.html
。![操作时间——在 iTunes Connect 中管理您的应用] -
如前文在上传部分所述,您需要一个较大版本的应用程序图标、iPhone/iPod Touch 截图和 iPad 截图(如果您的应用程序在 iPad 上运行)的内容。
-
您将看到一个关于应用程序信息的页面摘要。检查确保一切显示正确,然后点击完成。
-
您将被返回到您的版本详情页面。注意一个写着准备上传二进制文件的按钮。点击该按钮,您将需要回答一些关于出口合规性的问题。一旦完成,您将通过应用程序加载器上传二进制文件的权限。
刚才发生了什么?
从现在开始,当您在 App Store 中分发应用程序时,您将在 iTunes Connect 中管理您的应用程序。您想要显示的关于您应用程序的每一条信息都是在 iTunes Connect 中完成的。
一旦您进入关于应用程序信息的部分,请确保您的SKU 编号是唯一的,并且它与您的应用程序相关,以便您稍后可以识别它。此外,请确保您为应用程序指定的捆绑 ID是正确的。
权利和定价部分中的应用程序可用性控制着一旦批准,您希望应用程序何时上线。从提交之日起设置在未来几周内的日期是个不错的选择。只要提交没有问题,从审查中到准备销售的审查过程可能需要几天到几周的时间。价格层级是您设置应用程序价格的地方,或者它可以设置为免费。您可以点击查看定价矩阵以确定您希望销售应用程序的价格。
在元数据部分填写信息是客户在 App Store 中能看到的内容。评分部分与苹果内容描述相关。请确保频率级别尽可能接近您应用程序的内容。
上传部分是您包含 512 x 512 像素的应用程序图标和最适合您应用程序视觉的截图的地方。请确保您提供正确的图像大小。一旦您返回到应用程序信息屏幕,您会注意到状态显示为准备上传。当您在版本详情页上点击准备上传二进制文件按钮时,您将回答关于出口合规性的问题。不久之后,状态将变为等待上传。
更多关于 iTunes Connect 的信息可以在以下网址找到:itunesconnect.apple.com/docs/iTunesConnect_DeveloperGuide.pdf
。
在 Corona 中构建用于分发的 iOS 应用程序
我们已经到了将您的 iOS 应用程序提交到 App Store 的最后阶段。假设您已经测试了您的应用程序,并使用您的开发配置文件进行了调试,您现在可以创建一个分发构建,这将创建您的应用程序的二进制 ZIP 文件。
操作时间——构建您的应用程序并将其上传到应用加载器
是时候创建用于 iOS 分发的最终游戏构建版本,并将其上传到应用加载器,以便在苹果的审核委员会下进行审核。
-
启动 Corona 模拟器,导航到应用程序项目文件夹,并运行它。转到 Corona 模拟器菜单栏,选择文件 | 构建 | iOS。填写所有应用程序详细信息。确保您的应用程序名称和版本与您在 iTunes Connect 账户中显示的一致。选择设备以构建应用程序包。接下来,从支持设备下拉菜单中选择您的应用程序是为哪个目标设备(iPhone 或 iPad)创建的。
在代码签名标识下拉菜单下,选择您在 iOS 配置文件门户中创建的分发配置文件。在保存到文件夹部分,点击浏览并选择您希望应用程序保存的位置。完成后,点击构建按钮。
-
当构建编译完成后,您将看到一个显示您的应用程序已准备好分发的界面。选择上传到 App Store按钮。操作时间——构建您的应用程序并将其上传到应用加载器
-
当欢迎使用应用加载器窗口弹出时,使用您的 iTunes Connect 信息登录。然后您将被带到另一个窗口,您可以选择交付您的应用程序或创建新包。选择交付您的应用程序。下一个窗口显示一个下拉菜单;选择您将要提交的应用程序的名称,然后点击下一步按钮。操作时间——构建您的应用程序并将其上传到应用加载器
-
在 iTunes Connect 中找到的可用应用程序信息将显示出来。验证其正确性,然后点击选择按钮。操作时间——构建您的应用程序并将其上传到应用加载器
-
点击省略号 (...) 按钮在提交前替换当前文件,然后选择发送按钮。操作时间——构建您的应用程序并将其上传到应用加载器
-
应用加载器将开始提交您的应用程序二进制文件到 App Store。操作时间——构建您的应用程序并将其上传到应用加载器
-
如果二进制文件上传成功,你将收到确认你的二进制文件已交付到 App Store 的消息。当你的应用程序进入审核、准备销售、上线等状态时,你可以在 iTunes Connect 中检查应用程序的状态。每当应用程序状态发生变化时,都会向你发送一封电子邮件。就是这样!这就是你提交应用程序到 App Store 的方法!!行动时间——构建你的应用程序并将其上传到应用程序加载器
-
当你的应用程序在 App Store 中被审核并批准后,你可以进入 iTunes Connect 并调整可用日期,如果它在你建议的发布日期之前被批准。你的应用程序将立即在 App Store 中上线。
刚才发生了什么?
在构建你的应用程序时,在代码签名标识下,请选择为你创建的发布构建的发布配置文件。在构建编译完成后,你可以启动应用程序加载器。确保你已经安装了 Xcode。选择上传到 App Store按钮后,应用程序加载器将立即启动。
当你在应用程序加载器中时,一旦你将二进制信息加载到 iTunes Connect,你的应用程序名称就会显示在下拉菜单中。当你交付你的应用程序时,从你保存文件的位置选择已压缩的二进制文件。
文件上传后,将出现一个确认窗口,并且会向分配给你的 Apple 账户的 Apple ID 发送一封电子邮件。你的二进制文件将在 iTunes Connect 中显示为等待审核状态。
经过所有这些步骤,你现在正式知道如何将 iOS 应用程序提交到 App Store。太棒了!
成为英雄——制作通用的 iOS 构建
如果你只为 iPhone 开发了应用程序,尝试将其实现为 iPad 版本,这样它就可以成为通用构建。使用你从上一章学到的知识,利用build.settings
和config.lua
调整你的应用程序大小。同时,别忘了你的应用程序图标所需的内容。这就像一石二鸟!
Google Play 商店
Google Play 商店是一个发布平台,帮助你向全球用户宣传、销售和分发你的 Android 应用程序。
要注册为 Google Play 开发者并开始发布,请访问 Google Play Android 开发者控制台网站发布者网站。你可以在以下 URL 注册账户:play.google.com/apps/publish/
。
创建启动器图标
启动器图标是一种代表您应用的图形。启动器图标由应用使用,并出现在用户的首页上。启动器图标也可以用来表示进入您应用的快捷方式。这些图标与为 iOS 应用创建的图标类似。以下是需要满足的启动器图标要求,它们也需要是 32 位的.png
格式:
-
Icon-ldpi.png
—36x36 像素图像。需要达到 120 dpi。用于低密度屏幕。 -
Icon-mdpi.png
—48x48 像素图像。需要达到 160 dpi。用于中等密度屏幕。 -
Icon-hdpi.png
—72x72 像素图像。需要达到 240 dpi。用于高密度屏幕。 -
Icon-xhdpi.png
—96x96 像素图像。需要达到 320 dpi。用于超高密度屏幕。
启动器图标需要在您构建应用时放置在您的项目文件夹中。Google Play Store 还要求您有一个 512 x 512 像素的图标版本,该版本将在构建上传时在开发者控制台中上传。有关启动器图标的更多信息,请访问:developer.android.com/guide/practices/ui_guidelines/icon_design_launcher.html
。
行动时间——为 Google Play Store 签名您的应用
Android 系统要求所有已安装的应用都必须使用由应用开发者持有的私钥签名的证书进行数字签名。Android 系统使用证书作为识别应用作者和建立应用之间信任关系的手段。证书不用于控制用户可以安装哪些应用。证书不需要由证书颁发机构签名;它可以自签名。证书可以在 Mac 或 Windows 系统上签名。
-
在 Mac 上,转到应用程序 | 工具 | 终端。在 Windows 上,转到开始菜单 | 所有程序 | 附件 | 命令提示符。使用 Keytool 命令,输入以下行并按Enter:
keytool -genkey -v -keystore my-release-key.keystore -alias aliasname -keyalg RSA -validity 999999
小贴士
将
my-release-key
替换为您的应用名称。此外,如果您添加超过 999999(即额外的 9)的任何额外数字,应用将显示为损坏。 -
您将被要求输入密钥库密码。从这里开始,您将创建一个唯一的密码,这是作为开发者您必须想出来的。您将需要重新输入它。随后将询问的其他问题涉及您的开发者/公司信息、位置等。填写所有信息。一旦您完成了所需信息,您就生成了一个用于签名 Android 构建的密钥。有关应用签名的更多信息,请访问:
developer.android.com/guide/publishing/app-signing.html
。 -
启动 Corona 模拟器,导航到应用程序项目文件夹,并运行它。转到 Corona 模拟器的菜单栏,选择 文件 | 构建 | Android。填写与您的应用程序相关的 应用程序名称 和 版本 信息。使用 Java 方案指定一个 包 名称。在 密钥库 下,选择 浏览 按钮以定位您的已签名私钥,然后从下拉菜单中选择您为发布构建生成的密钥。您将被提示输入用于在 Keytool 命令中签名应用程序的密钥库密码。在 密钥别名 下,从下拉菜单中选择 aliasname,并在提示时输入您的密码。选择 浏览 按钮以选择应用程序构建的位置。完成后,选择 构建 按钮。
刚才发生了什么?
Keytool 会生成一个名为 my-release-key.keystore
的文件作为密钥库。密钥库和密钥由您输入的密码保护。密钥库包含一个密钥,有效期为 999999 天。别名是一个您稍后将要使用的名称,用于在签名应用程序时引用此密钥库。
您的密钥库密码是在您在 Corona 中构建应用程序时创建并必须记住的。如果您想为别名名称使用不同的密码,将会有一个选项。当您在终端或命令提示符中时,您可以按 Enter 使用相同的密码。
当您在 Corona 中创建构建时,请确保您的版本号是一个没有特殊字符的整数。此外,您还必须确保您的 build.settings
包含 versionCode
。这将与您的版本号相同。有关更多信息,请参阅 第九章,处理多个设备和网络应用程序。
您构建中的 Java 方案是您域名的反向,后面附加了您的产品/公司名称以及您的应用程序名称。例如,com.mycompany.games.mygame
。
当您使用私有密钥构建应用程序并选择了一个别名名称后,.apk
文件将被创建并准备好在 Google Play 商店上发布。
行动时间——将应用提交到 Google Play 商店
我们将使用开发者控制台。这是您创建开发者配置文件以发布到 Google Play 商店的地方。
-
一旦您登录到开发者控制台,点击显示 上传应用程序 的按钮。您将看到一个弹出窗口,允许您上传您的构建。点击 选择文件 按钮,找到您的应用程序的
.apk
文件。选择 上传 按钮。当您的.apk
文件上传后,点击 保存 按钮。最后,点击 关闭 按钮。 -
选择APK 文件选项卡,然后按屏幕右侧的激活链接激活
.apk
文件。 -
现在,选择产品详情选项卡。在上传资源部分,您需要从您的应用程序中提供一些截图和 512 x 512 像素的您的应用程序图标版本。如果您愿意,还可以包括可选的图形和视频。滚动到页面底部的列表详情部分。填写有关您的应用程序的详细信息,包括应用程序标题、描述、应用程序类型和类别。下一部分是发布选项。勾选与您的应用程序相关的信息。定价默认为免费。如果您想制作付费版本,您必须设置 Google Checkout 的商家账户。下一部分是联系信息。填写必要的信息。最后一部分是同意。仔细了解发布到 Google Play 商店的必要条件,并勾选复选框。
-
一旦您在产品详情选项卡中填写了详细信息,请点击保存按钮,这样您所有的 APK 信息就会保存在开发者控制台中。
-
最后,点击发布按钮。您将被带到开发者控制台的主屏幕,您的应用程序状态将显示为已发布。恭喜您,您已将应用程序发布到 Google Play 商店!!操作时间——提交应用至 Google Play 商店
刚才发生了什么?
开发者控制台页面显示了一个简单的分步过程,说明如何发布您的 .apk
文件。当您在APK 文件选项卡中时,选择激活链接将允许控制台检查构建与哪些设备兼容。
发布应用程序所需的资源显示了每个部分旁边可接受的分辨率和图像类型。包括促销图形、特色图形和促销视频是可选的,但最好为您的应用程序页面添加足够的内容。这将使其对潜在客户更具吸引力。
完成与您的应用程序相关的所有信息后,请确保保存您的进度。一旦您点击发布按钮,您就完成了!您应该能够在发布后的一小时内看到您的应用程序在 Google Play 商店中。
英雄尝试——添加更多促销内容
Google Play 商店为您提供了许多推广应用程序的选项。您可以从开发者控制台添加额外的资源。尝试以下操作:
-
添加一个促销图形。
-
添加一个特色图形。
-
创建您应用程序的促销视频。像 YouTube 这样的网站是分享游戏预告片的好方法。
快速问答——发布应用程序
-
在创建 iOS 分发配置文件时,您需要使用哪种分发方式?
-
a. 开发
-
b. App Store
-
c. 临时性的
-
d. 以上都不是
-
-
你从哪里查询已提交的 iOS 应用程序的状态?
-
a. iTunes Connect
-
b. iOS 配置文件门户
-
c. 应用程序加载器
-
d. 以上都不是
-
-
构建适用于 Google Play 商店的应用程序需要什么?
-
a. 在 Keytool 命令下创建私钥
-
b. 使用调试密钥签名你的应用程序
-
c. 使用你的私钥签名你的应用程序
-
d. a 和 c
-
摘要
通过本章,我们取得了巨大的里程碑。我们不仅知道了如何提交到一个主要的应用市场,而且两个!将你的应用程序发布到 App Store 和 Google Play Store 最终并不是一个可怕的地方。
我们已经涵盖了以下主题:
-
内存效率的重要性
-
为分发到 App Store 创建配置文件
-
管理 iTunes Connect
-
将二进制文件提交到应用程序加载器
-
为 Android 应用程序签名发布版本
-
将
.apk
文件提交到 Google Play 商店
在下一章和最后一章中,我们将讨论将应用内购买应用到你的应用程序的所有内容。我们将深入研究 Corona 的商店模块,学习如何进行购买、交易等!你不想错过这个激动人心的章节。
第十一章。实现应用内购买
应用内购买是一个可选功能,开发者可以使用它将商店直接嵌入到应用中。有时你可能想扩展你当前游戏中的更多功能,以保持消费者对游戏的兴趣。这是你的机会,也许还能让你的口袋里多些收入!
本章仅涉及 iOS 平台的 Apple iTunes Store 中的应用内购买。如果你是付费的 Corona SDK 订阅者,从构建 2012.760 开始,Daily Builds 页面提供了通过 Google Play Store(Kindle Fire 和 Nook 上不可用)的 Android 应用内购买支持。Daily Builds 可以在以下网址找到:
developer.anscamobile.com/downloads/daily-builds
。希望在自己的应用中实现应用内购买的 Android 开发者可以使用这个作为替代方案。
本章我们将涵盖以下内容:
-
可消耗、不可消耗和订阅购买
-
进行交易
-
恢复已购买的项目
-
初始化 Corona 的商店模块
-
在设备上创建和测试应用内购买
准备,设置,出发!
应用内购买的奇妙之处
实现应用内购买的目的是在应用内添加支付功能,以收集增强功能或可用于游戏的额外内容的费用。以下是将此功能整合的选项:
-
提供新关卡包以供在默认内容之外游玩的游戏
-
一款允许你在游戏过程中购买虚拟货币以创建/构建新资产的免费增值游戏
-
添加额外的角色或特殊能力提升游戏元素
这些是一些可以使用应用内购买完成的事例。
应用内购买允许用户在应用程序内购买额外内容。App Store 仅管理交易信息。开发者不能使用 App Store 来交付内容。因此,你可以在发布应用时将内容捆绑到应用中,等待购买解锁,或者如果你希望交付内容,你必须制定自己的系统来下载数据。
应用内购买类型
你可以在你的应用中应用几种不同的应用内购买类型。具体如下:
-
可消耗的: 这些是每次用户需要该物品时都必须购买的产品。它们通常是单次服务,例如在应用中需要付费购买用于建造结构的材料。
-
不可消耗的: 这些是用户只需购买一次的产品。这可能是游戏中额外的关卡包。
-
自动续订订阅: 这些是允许用户购买设定时间段的内嵌内容的商品。自动续订订阅的一个例子是利用 iOS 中内置的自动续订功能订阅的杂志或报纸。
-
免费订阅:这些用于将免费订阅内容放入新闻亭。一旦用户注册免费订阅,它将在与用户的 Apple ID 关联的所有设备上可用。请注意,免费订阅不会过期,并且只能在启用了新闻亭的应用中提供。
-
非续订订阅:类似于自动续订订阅,这是一种非续订订阅,每次订阅到期时都需要用户续订。您的应用程序必须包含代码以识别何时发生到期。它还必须提示用户购买新的订阅。自动续订订阅消除了这些步骤。
Corona 的 store 模块
在您的应用程序中应用内购可能是一个有点令人困惑且繁琐的过程。与 Corona 集成需要调用store
模块:
store = require("store")
store
模块已经集成到 Corona API 中,类似于 Facebook 和游戏网络。您可以在以下 URL 中找到有关 Corona 的store
模块的更多信息:developer.anscamobile.com/reference/in-app-purchases
。
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
store module, Corona APIstore module, Corona APIstore.init()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 )
event.transaction
包含交易的对象。
交易对象支持以下只读属性:
-
state
—包含交易状态的字符串。有效值是"purchased"
、"restored"
、"cancelled"
和"failed"
。 -
productIdentifier
—与交易关联的产品标识符。 -
receipt
—从商店返回的唯一收据。它以十六进制字符串的形式返回。 -
identifier
—从商店返回的唯一交易标识符。它是一个字符串。 -
date
—交易发生的日期。 -
originalReceipt
—从原始购买尝试中从商店返回的唯一收据。这在恢复情况下尤为重要。它以十六进制字符串的形式返回。 -
originalIdentifier
—从原始购买尝试中从商店返回的唯一交易标识符。这在恢复情况下尤为重要。它是一个字符串。 -
originalDate
—原始交易发生的日期。这在恢复情况下尤为重要。 -
errorType
—在状态为"failed"
时发生的错误类型(一个字符串)。 -
errorString
—在"failed"
情况下描述错误的错误消息。
store.loadProducts()
此方法检索有关可供销售的商品的信息。这包括每个商品的价格、名称和描述。
语法:
store.loadProducts( arrayOfProductIdentifiers, listener )
参数:
-
arrayOfProductIdentifiers
—包含您想要了解的 In-App 产品 Product 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
如果允许购买,则返回 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()
此方法通知 App Store 交易已完成。
在处理完交易后,您必须在交易对象上调用store.finishTransaction()
。如果您不这样做,App Store 会认为您的交易被中断,并在下一次应用启动时尝试恢复它。
语法:
store.finishTransaction( transaction )
参数:
transaction
- 您想要标记为完成的交易所属的交易对象。
示例:
store.finishTransaction( transaction )
store.restore()
任何已从设备中清除或升级到新设备的先前购买的项目可以在用户的账户中恢复,而无需再次为产品付费。store.restore()
API 启动此过程。可以通过transactionCallback
监听器恢复交易,该监听器与store.init()
注册。交易状态将为"restored"
,然后您的应用可以使用交易对象的"originalReceipt"
、"originalIdentifier"
和"originalDate"
字段。
语法:
store.restore()
示例:
块将运行 transactionCallback()
函数,并确定应用程序是否之前已购买过产品。如果结果是 true,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)
store.restore()store.restore()exampleprint("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()
创建 In-App Purchase
在继续阅读之前,请确保您知道如何从 iOS Provisioning Portal 创建 App ID 和分发配置文件。同时,请确保您已经知道如何在 iTunes Connect 中管理新应用程序。如果您不确定,请参阅第十章,优化、测试和发布您的游戏,以获取更多信息。在创建 In-App Purchase 之前,您的应用程序需要准备以下事项:
-
为您的应用程序已制作的分发证书。
-
为您的应用程序显式指定的 App ID,即
com.companyname.appname
。不要使用通配符(星号 *)。包标识符需要完全唯一才能使用内购功能。 -
用于测试 In-App Purchases 的临时分发配置文件(ad-hoc Distribution Provisioning Profile)。当您准备好提交带有 In-App Purchase 的应用程序时,需要一个 App Store 分发配置文件。
-
在 iTunes Connect 中设置您的应用程序信息。您不需要上传二进制文件来创建或测试 In-App Purchases。
-
确保您已经与苹果公司签订了有效的 iOS 付费应用程序合同。如果没有,您需要在 iTunes Connect 主页上的合同、税务和银行部分请求一个。为了在您的应用程序中提供 In-App Purchases,您需要提供您的银行和税务信息。
创建 In-App Purchase 的操作时间——在 iTunes Connect 中创建
我们将通过 iTunes Connect 实现内购,并在示例应用程序中创建一个将调用交易的场景。让我们创建将在我们的 In-App Purchase 中使用的 Product ID。
-
登录到 iTunes Connect。在主页上,选择管理您的应用程序。选择您计划添加 In-App Purchase 的应用程序。
-
一旦您在应用程序摘要页面上,点击管理内购按钮,然后点击左上角的创建新按钮。
-
您将被带到一页,显示您可以创建的 In-App Purchases 类型摘要。在这个例子中,不可消耗被选中。我们将创建一个只需购买一次的产品。
-
在下一页是您填写产品信息的区域。这些信息适用于消耗性、非消耗性和非续订订阅 In-App Purchase。填写您产品的 参考名称 和 产品 ID。产品 ID 需要是一个唯一的标识符,可以是任何字母数字序列(即,
com.companyname.appname.productid
)。注意
自动续订订阅需要您生成一个共享密钥。如果您要在您的应用中使用自动续订订阅,那么在 管理 In-App Purchase 页面上,点击 查看或生成共享密钥 链接。您将被带到生成共享密钥的页面。点击 生成 按钮。共享密钥将显示一串 32 个随机生成的字母数字字符。当您选择自动续订订阅时,与其他 In-App Purchase 类型不同的是,您必须选择您产品的自动续订间隔。有关自动续订订阅的更多信息,请访问:
itunesconnect.apple.com/docs/iTunesConnect_DeveloperGuide.pdf
。 -
点击 添加语言 按钮。选择用于 In-App Purchase 的语言。为您的产品添加一个 显示名称 和简短描述。完成后,点击 保存 按钮。
-
在 定价和可用性 中,确保 已批准销售 处选择 是。在 价格层级 下拉菜单中,选择您计划销售的 In-App Purchase 的价格。在本例中,层级 1 被选中。在 审核截图 中,您需要上传您的 In-App Purchase 的截图。如果您正在对 ad-hoc 构建进行测试,则不需要截图。一旦您准备好分发,截图是必需的,以便在提交时对 In-App Purchase 进行审核。完成后,点击 保存 按钮。
-
您将在下一页看到您创建的 In-App Purchase 的摘要。如果所有信息看起来都正确,请点击 完成 按钮。
刚才发生了什么?
添加新的 In-App Purchase 是一个相对简单的过程。产品 ID 中包含的信息将在交易期间被调用。管理 In-App Purchase 的类型完全取决于您想在游戏中销售哪种类型的产品。本例演示了选择代表购买/解锁游戏新级别的非消耗性产品的目的。这是用户想要销售关卡包的常见场景。
您的应用程序不必完成即可测试 In-App Purchases。所需的所有内容是将您的应用程序信息设置在 iTunes Connect 中,以便您可以管理 In-App Purchase 功能。
行动时间——使用 Corona 商店模块创建 In-App Purchase
现在我们已经在 iTunes Connect 中设置了我们的 In-App Purchase 产品 ID,我们可以将其实现到我们的应用程序中,以购买我们打算出售的产品。一个名为 Breakout 的示例菜单应用程序被创建来演示如何在应用程序内购买级别。该应用程序在级别选择屏幕中有两个级别。第一个默认可用,第二个被锁定,并且只能通过购买它来解锁,价格为 $0.99。我们将创建一个级别选择屏幕,使其以这种方式运行。
-
在
Chapter 11
文件夹中,将Breakout In-App Purchase Demo
项目文件夹复制到您的桌面上。您可以从 Packt 网站下载本书附带的项目文件。您会注意到所需的配置、库、资源和.lua
文件都已包含在内。 -
创建一个新的
levelselect.lua
文件并将其保存到项目文件夹中。 -
使用以下变量和保存/加载函数设置场景。最重要的变量是
local store = require("store")
,它调用 In-App Purchases 的store
模块。local storyboard = require( "storyboard" ) local scene = storyboard.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 )
-
创建
createScene()
事件并删除"mainmenu"
、"level1"
和"level2"
场景。-- Called when the scene's view does not exist: function scene:createScene( event ) local screenGroup = self.view -- completely remove maingame and options storyboard.removeScene( "mainmenu" ) storyboard.removeScene( "level1" ) storyboard.removeScene( "level2" ) print( "\nlevelselect: createScene event" ) end
-
接下来,创建
enterScene()
事件和一个包含在 iTunes Connect 中设置为 In-App Purchase 的 产品 ID 字符串的数组。function scene:enterScene( event ) local screenGroup = self.view print( "levelselect: enterScene 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 }
-
添加一个名为
validProducts
的本地空表和invalidProducts
。创建一个名为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
-
创建一个名为
loadProductsCallback()
的本地函数,带有event
参数。设置处理程序以使用print
语句接收产品信息。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
-
创建一个名为
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
-
设置背景和第 1 级别按钮的显示对象。
local backgroundImage = display.newImageRect( "levelSelectScreen.png", 480, 320 ) backgroundImage.x = 240; backgroundImage.y = 160 screenGroup:insert( backgroundImage ) local level1Btn = movieclip.newAnim({"level1btn.png"}, 200, 60) level1Btn.x = 240; level1Btn.y = 100 screenGroup:insert( level1Btn ) local function level1touch( event ) if event.phase == "ended" then audio.play( tapSound ) storyboard.gotoScene( "loadlevel1", "fade", 300 ) end end level1Btn:addEventListener( "touch", level1touch ) level1Btn:stopAtFrame(1)
-
设置第 2 级别按钮的位置。
-- LEVEL 2 local level2Btn = movieclip.newAnim({"levelLocked.png","level2btn.png"}, 200, 60) level2Btn.x = 240; level2Btn.y = 180 screenGroup:insert( level2Btn )
-
使用本地的
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 ) storyboard.gotoScene( "mainmenu", "fade", 300 )
-
在相同的
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")
-
在交易完成后,添加一个
elseif
语句来检查是否已购买并解锁了第 2 级别。elseif event.phase == "ended" and level2Unlocked == tonumber(loadedLevel2Unlocked) then audio.play( tapSound ) storyboard.gotoScene( "loadlevel2", "fade", 300 ) end end level2Btn:addEventListener( "touch", onBuyLevel2Touch ) if level2Unlocked == tonumber(loadedLevel2Unlocked) then level2Btn:stopAtFrame(2) end
-
使用
store.init()
激活内购,并将transactionCallback()
作为参数调用。同时,使用 500 毫秒的计时器调用setupMyStore()
。store.init(transactionCallback) timer.performWithDelay (500, setupMyStore)
-
创建关闭 UI 按钮,并创建一个带有事件参数的本地函数
onCloseTouch()
。在释放关闭按钮时,将函数过渡场景设置为loadmainmenu.lua
。使用end
关闭enterScene()
事件。local closeBtn local onCloseTouch = function( event ) if event.phase == "release" then audio.play( tapSound ) storyboard.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 screenGroup:insert( closeBtn ) menuTimer = timer.performWithDelay( 200, function() closeBtn.isVisible = true; end, 1 ) end
-
创建
exitScene()
和destroyScene()
事件。在exitScene()
事件中,取消menuTimer
计时器。将所有事件监听器添加到场景事件中,并return scene
。-- Called when scene is about to move offscreen: function scene:exitScene() if menuTimer then timer.cancel( menuTimer ); end print( "levelselect: exitScene event" ) end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) print( "((destroying levelselect's view))" ) end -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be scene:addEventListener( "destroyScene", scene ) return scene
-
保存文件并在 Corona 模拟器中运行项目。当您选择 播放 按钮时,您将在关卡选择屏幕上注意到一个 1 按钮和一个 锁定 按钮。通过按下 锁定 按钮来调用商店进行交易。您将在终端中注意到一个
print
语句,显示正在购买的产品 Product ID。在模拟器中无法测试完整的内购功能。您必须创建一个分发构建并将其上传到 iOS 设备上,以在商店中引发购买。
刚才发生了什么?
在此示例中,我们使用了 BeebeGames
类中的 saveValue()
和 loadValue()
函数来实现如何使用电影剪辑作为按钮,将我们的锁定级别从锁定状态转换为解锁状态。local listOfProducts
中的数组以字符串格式显示产品 ID。在此示例中,Product 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。一旦 store.init(transactionCallback)
初始化并调用 setupMyStore()
,事件就会被处理。
onBuyLevel2Touch(event)
函数允许我们检查已为锁定级别进行了内购操作。当用户能够购买并接受内购时,交易将被处理,level2Unlocked
的值将与 tonumber(loadedLevel2Unlocked)
相匹配。一旦产品 ID 返回有效,buyLevel2(product)
函数将使用 store.purchase()
验证购买的商品。
在 In-App Purchase 之后,屏幕切换到主菜单,以便将 锁定 按钮更改为 2 级按钮。一旦按钮切换到帧 2,就可以访问第二级。
尝试处理多个产品 ID 的英雄——处理多个产品 ID
现在您已经知道如何为单个产品创建 In-App Purchase,尝试在同一个应用程序中添加多个。场景是开放的。
您可以添加以下内容:
-
购买更多级别
-
用户可以扮演的新角色,如果您的游戏有用户角色功能。
-
为您的应用程序添加新的背景场景
您如何处理商店中的新产品完全取决于您。
测试 In-App Purchase
您想确保购买操作正确无误。Apple 提供了一个沙盒环境,允许您测试您的应用程序的 In-App Purchase。沙盒环境使用与 App Store 相同的模型,但不处理实际支付。交易返回时,就像支付已成功处理一样。在将 In-App Purchase 提交给 Apple 审查之前,必须在沙盒环境中测试 In-App Purchase 是一项要求。
在沙盒环境中进行测试时,您需要创建一个与您当前的 iTunes Connect 账户不同的单独用户测试账户。不允许使用当前账户在沙盒中测试您的商店。
用户测试账户
登录您的 iTunes Connect 账户后,您需要从主页选择 管理用户 链接。在 选择用户类型 页面上选择 测试用户。添加新用户,并确保测试账户使用的是与任何其他 Apple 账户不相关的电子邮件地址。所有测试账户应在测试 In-App Purchase 时仅在测试环境中使用。当所有信息填写完毕后,点击 保存 按钮。
一旦创建了用户测试账户,您需要确保在设备的 商店 设置中注销您的 Apple 账户。这将防止在测试 In-App Purchase 时使用非测试账户。您只能在 In-App Purchase 沙盒测试您的应用程序时被提示时登录用户测试账户。在应用程序启动之前不要登录测试账户。这将防止测试账户无效。
行动时间——使用 Breakout In-App Purchase 示例测试 In-App Purchase
在您可以在 iOS 设备上测试 In-App Purchase 之前,请确保您已经在 iTunes Connect 中创建了一个测试用户账户。此外,请确保您使用针对测试 In-App Purchase 功能的应用程序创建了使用 ad-hoc 分发配置文件的发行版构建。如果您遵循了本章中此前的所有步骤,通过商店进行的购买测试将相应地工作。
-
在 Corona 模拟器中,创建 Breakout In-App Purchase 示例的发行版构建。一旦构建编译完成,请将构建上传到您的 iOS 设备。
-
保持您的设备连接到您的计算机,并启动 Xcode。在工具栏中,选择窗口 | 组织者。一旦进入组织者,在设备部分选择连接的设备,然后选择控制台。这将允许您检查设备的控制台输出,以捕获代码中的调试消息(即
print
语句)和任何应用程序崩溃。 -
在启动应用程序之前,您需要在您的设备上选择设置图标。向上滚动,直到您看到商店图标,然后选择它。
-
如果您已登录到 iTunes Store 账户,请先退出,以便您可以在沙盒环境中测试内购功能。
-
从您的设备启动 Breakout 内购演示。选择播放按钮,然后选择锁定按钮。屏幕将切换回主菜单,并弹出一个窗口以确认您的内购。点击确定继续购买。
-
接下来,您将看到一个窗口,要求您使用 Apple ID 登录。这是您将使用在 iTunes Connect 中创建的测试用户账户登录的地方。不要使用您用于登录 iTunes Connect 的实际 Apple 账户 ID。
-
登录后,再次选择播放按钮。您会注意到2按钮已被解锁。选择它后,您将可以访问该场景。
-
退出应用程序并查看控制台。您会注意到设备输出的输出以及我们代码中的一些熟悉的
print
语句。控制台日志显示用于内购的产品 ID,并告知您它是否有效以及交易是否成功。 -
如果您想确保内购确实已成功,请从您的设备中删除应用程序并退出您的用户测试账户。将相同的构建上传到您的设备。无需创建新的。启动应用程序并再次运行内购。使用相同的用户测试账户登录。您应该会收到一个弹出窗口,提示您已购买该产品,并询问您是否想要免费重新下载。收到通知意味着您的内购已成功!!
刚才发生了什么?
严格按照应用内购买测试步骤进行非常重要。为了确保你在沙盒环境中得到准确的结果,从商店设置中注销您的 Apple 账户是整个过程中的关键。
一旦您启动应用程序并通过点击锁定按钮调用 store 函数,您将注意到应用内购买的显示名称和价格。如果您正确实现,它应该与您在 iTunes Connect 中创建的相匹配。
当您使用在 iTunes Connect 中创建的测试用户账户登录时,如果没有苹果服务器端的问题或设备上的连接问题,交易应该没有错误地完成。关卡选择屏幕上的Level 2将被解锁并可供访问。恭喜!你已经创建了一个应用内购买!
尝试一下英雄——使用其他应用内购买类型
在 Breakout 应用内购买演示中,我们更关注不可消耗的应用内购买。尝试将可消耗、自动续订的订阅或非续订订阅集成到您自己的应用中。
具有可消耗产品的应用是那些在免费游玩环境中需要货币来购买或建造东西的游戏。订阅产品可以针对那些永不结束且不断更新新关卡或可能需要在线服务器以在多人环境中交互的游戏。看看你能想出什么!
快速问答——关于应用内购买
-
什么是不可消耗的购买?
-
a. 用户只需购买一次的产品。
-
b. 每次用户需要物品时都需要购买的产品。
-
c. 允许用户购买一定时长内容的商品。
-
d. 每次到期时都需要用户续订的订阅。
-
-
关于测试应用内购买,以下哪个说法是正确的?
-
a. 您需要始终登录您的账户。
-
b. 您的 Apple 账户用于测试应用内购买。
-
c. 在应用内购买沙盒中提示时登录您的用户测试账户。
-
d. 以上都不是。
-
-
测试应用内购买需要使用哪种类型的配置文件?
-
a. 开发配置文件。
-
b. 临时分配配置文件。
-
c. 应用商店分配配置文件。
-
d. 以上都不是。
-
摘要
我们终于看到了隧道尽头的曙光。到现在为止,你应该已经对如何在游戏中实现应用内购买有了想法。在组织、设置代码以及在沙盒环境中测试准确的购买是一个非常漫长的过程。
本章讨论了以下内容:
-
在 iTunes Connect 中设置应用内购买的产品 ID
-
使用 Corona 的
store
模块实现购买项 -
在 iTunes Connect 中添加测试用户账户
-
在设备上测试应用内购买
理解应用内购买的概念可能需要一些时间。最好研究示例代码并回顾与 Corona 的store
模块相关的功能。
请查看 Apple 的 In-App Purchase Programming Guide,网址为:developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StoreKitGuide/StoreKitGuide.pdf
,以及 Anscamobile 网站 API 参考部分的 In-App Purchases,网址为:developer.anscamobile.com/reference/index/app-purchases
,以获取更多与此主题相关的参考资料。
经过 11 章的学习,我们已到达这本书的结尾。你现在已经拥有了足够的知识来创建自己的应用并在 App Store 或 Google Play Store 上销售。希望你所获得的所有信息都对你有所帮助。我期待着听到你使用 Corona SDK 开发的游戏!
附录 A. 快速问答答案
第一章,开始使用 Corona SDK
快速问答——理解 Corona
1 | a |
---|---|
2 | d |
3 | b |
第二章,Lua 入门课程和 Corona 框架
快速问答——Lua 基础
1 | d |
---|---|
2 | c |
3 | d |
第三章,构建我们的第一个游戏:Breakout
快速问答——构建游戏
1 | d |
---|---|
2 | d |
3 | a |
第四章,游戏控制
快速问答——与游戏控制一起工作
1 | c |
---|---|
2 | c |
3 | d |
第五章,动画化我们的游戏
快速问答——动画化图形
1 | a |
---|---|
2 | c |
3 | c |
第六章,播放声音和音乐
快速问答——关于音频的一切
1 | c |
---|---|
2 | d |
3 | a |
第七章,物理:下落物体
快速问答——动画化图形
1 | a |
---|---|
2 | c |
3 | a |
第八章,操作故事板
快速问答——游戏过渡和场景
1 | b |
---|---|
2 | a |
3 | c |
4 | b |
第九章,处理多个设备和网络应用
快速问答——处理社交网络
1 | b |
---|---|
2 | d |
3 | d |
第十章,优化、测试和发布你的游戏
快速问答——发布应用程序
1 | b |
---|---|
2 | a |
3 | d |
第十一章,实现应用内购买
快速问答——关于应用内购买的一切
1 | a |
---|---|
2 | c |
3 | b |