精通-Cocos2d-游戏开发-全-
精通 Cocos2d 游戏开发(全)
原文:
zh.annas-archive.org/md5/4366caba07a87589dcf8a606e611cf4c译者:飞龙
前言
如果您曾想了解一款优秀游戏从开始到结束的全过程,这本书将引导您通过这个过程。您会发现,您的游戏只需几步就能成为可能的最优秀游戏。
您将跟随一个项目,并了解当数百甚至数千款类似游戏未能留下持久影响时,是什么让一款游戏脱颖而出。您将体验 Cocos2d、Chipmunk、OALSimpleAudio 的简单性以及更多。您将获得一些独特游戏机制的代码,使您的游戏与众不同。本书确保无论您正在开发的游戏规模如何——无论是独立游戏还是大型发行商——您都将获得创建高度精致和愉悦体验所需的技能。
本书涵盖内容
第一章, 刷新您的 Cocos2d 知识,介绍了您在开发 Cocos2d 游戏时可以使用的一些工具,如何创建 Apple 开发者账户,以及在设计项目时设定灵活目标的重要性。
第二章, 通过原型快速失败,讨论了快速迭代和快速启动的需求。本章为书的项目设定了初始大纲或线框,并详细介绍了使用第一章中介绍的一些工具。
第三章, 关注物理,介绍了 Chipmunk 物理引擎及其背后的力量,只需几行代码即可。本章向您展示如何轻松地将触摸控制、物理和加速度计集成到单个项目中。
第四章, 声音和音乐,涵盖了添加声音和音乐,这对于游戏体验至关重要。本章解释了如何最佳地使用 OALSimpleAudio 以及您在游戏中使用声音的各种方式。
第五章, 创建酷炫内容,展示了粒子效果、贝塞尔曲线和视差滚动的使用。本章包含三个迷你项目以及对该书主要项目的某些补充。
第六章, 整理和润色,解释了润色游戏的重要过程。本章涉及许多主题,从按钮动画到图形变化。它深入解释了您可以做什么来使您的游戏从概念发展到成品。
第七章, 达到目的地,完成了本书的项目,并为发布做好了准备,包括在 iTunes Connect 上创建应用、设置加载屏幕和应用程序图标,以及添加分析。本章还涵盖了如何实际提交游戏到 App Store 以及提交后需要做什么。
第八章, 探索 Swift,通过创建一个非常简单的游戏介绍了 Swift 编程语言(以及 Swift 与 Cocos2d 的使用)。本章还解释了 Swift 的工作原理的基本知识以及 Swift 与 Objective-C 之间的主要区别。
第九章,简单的 Swift 应用程序,涵盖了使用 Swift 作为主要语言创建非游戏应用程序的基础。尽管这一章在书中不可用,但它可以在 www.packtpub.com/sites/default/files/downloads/6718OS_Chapter9.pdf 下载。
您需要为本书准备的内容
在阅读本书之前,您应该对 Cocos2d 和 Objective-C 的工作原理有了解。虽然本书一开始对 Cocos2d 概念有简要介绍,但很快就会结束。
此外,本书中使用的任何内容都附有如何获取它的适当解释——无论是下载和安装工具、在 iTunes Connect 上设置账户,还是使用本书提供的资产。
这本书面向的对象
本书面向的是对 Cocos2d 和 Objective-C 有良好掌握的开发者,他们希望将他们的游戏开发技能提升到下一个层次。也许您过去制作过下载量不多的游戏,并希望创建一个更高品质的游戏。这本书将帮助您润色您的游戏,使其达到应有的水平。
惯例
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:"如果您看不到 .xcodepoj 文件扩展名,请不要担心。它应在类型过滤器中标记为 Xcode 项目,并在其旁边有一个蓝图图标。"
代码块设置如下:
@interface MainScene : CCNode
{
CGSize winSize;
}
+(CCScene*)scene;
@end
新术语和重要词汇以粗体显示。屏幕上出现的单词,例如在菜单或对话框中,在文本中如下所示:"在 SpriteBuilder 中,转到文件 | 发布。这将显示一个进度条,如果您的项目是全新的,它将运行得相当快。"
注意
警告或重要注意事项以如下框中的形式出现。
提示
小技巧和技巧以如下形式出现。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要发送一般反馈,请简单地发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在 www.packtpub.com 的账户下载示例代码文件,适用于您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从 www.packtpub.com/sites/default/files/downloads/6718OS_ColorImages.pdf 下载此文件。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata 来报告它们,选择您的书,点击错误提交表单链接,并输入您的错误详情。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误部分下的现有错误清单中。
要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书的名称。所需信息将出现在错误清单部分。
盗版
互联网上对版权材料的盗版是一个持续存在的问题,涵盖了所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您的帮助,以保护我们的作者和为您提供有价值的内容的能力。
问题和建议
如果您对本书的任何方面有问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章。刷新你的 Cocos2d 知识
在本章中,我们将通过刷新高级用户的记忆来让你跟上进度。本章还将涵盖在项目创建之前的项目规划。你将看到为什么早期确定你想要的功能可以帮助加快游戏创建的过程。项目示例、下载参考和设置信息将在这里找到。然后我们将深入研究 Cocos2d 生态系统中的各种第三方库和工具,你可能想要考虑。
在本章中,我们将涵盖以下主题:
-
规划成功游戏
-
你可能还想考虑的其他工具
-
设定灵活、专注的目标
准备心态
首先,我们将讨论如何从开始到结束构建你的项目,以及为什么你应该这样做而不是直接跳进去。我们将讨论一些理论,并问你一些问题,帮助你确定在写代码之前你的项目到底需要什么。
在开始规划之前,你需要对你的游戏所需的关键功能提出问题,因为它们将需要额外的支持和开发。例如,是否涉及到物理引擎?你打算实现 Game Center 或 In-App Purchases,或者可能连接到数据库来存储用户信息?机制有多复杂?开始思考这些答案以及类似的问题,因为知道答案将帮助你更快地移动,同时仍然保持高效。
物理引擎
虽然这并不是大多数人在刚开始规划项目时会立刻想到的事情,但了解这一点很重要。幸运的是,使用 Cocos2d,代码中已经内置了物理引擎,这很好,因为你不需要额外努力去实现一个。
如果你发现你的项目确实需要物理引擎(或者有了它会显得更加“真实”),你可以参考本书后面的第五章,了解如何实现它,以及示例代码的使用。
如果你不确定是否应该使用物理引擎,这里有一些使用 Box2D(Cocos2d 内置的物理引擎之一)的游戏示例。看看 Rovio 的Angry Birds中的物理效果:

在前面的截图中,每只鸟和每头猪都是一个圆形的物理对象,地面是一个矩形,右手边(除了猪)的所有障碍物都是矩形。当鸟从左侧被弹到右侧时,重力开始作用于鸟,并开始将其向下拉。一旦发生碰撞,它推动物体并基于 incoming 和 colliding 物体的速度进行一些复杂的“伤害”计算。
以下截图展示了 KeitGames 开发的Crescent Ridge Mini Golf中的物理效果:

在前面的截图中,球是一个圆形的物理对象,每个墙壁都有一组顶点,这些顶点充当球不能通过的边界。这里没有重力作用,否则球会不断落到屏幕底部。如果球在墙上弹跳,物理引擎会确切地知道球将转向的角度以及它将向后移动的速度。能够确定高尔夫球的反射角度是使用物理引擎创建简单迷你高尔夫游戏的主要原因。
如果你仍然不确定,那么对以下任何问题的回答是“是”,意味着你应该考虑使用物理引擎。如果你对以下任何问题的回答是“是”,请参考第三章,关注物理,了解如何实现物理引擎。如果你对所有这些问题的回答都是“否”,那么我建议你现在暂时不要使用,因为你的游戏可能不需要引擎,而可以通过编码的方式给人一种物理的感觉,但实际上并不使用物理引擎:
-
你是否需要非圆形或非矩形的物体来碰撞?
-
物体是否需要相互弹跳或推动其他物体?
-
你的游戏是否使用了实时重力?
如果你感兴趣,想在游戏中使用任何液体物理或使用软体,请随意查看 LiquidFun。然而,这本书不会涵盖如何实现或使用该引擎。
应用内购买
应用内购买(IAPs)是设计决策的结果,可能是为了使游戏更便宜,或者包括游戏中的扩展或额外内容。你可以利用 IAPs 的很多方式,但如果你将它们强加到游戏中而不是将其设计为体验的有机部分,可能会让你的玩家要么不使用你花费了大量时间实现的 IAPs,要么完全停止玩游戏。
关于 IAPs 的问题在于,苹果要求你的项目配置文件中添加 IAPs 权限——这是容易的部分。困难的部分是让它们包含在你的代码中。以下是一些各种游戏中应用内购买的例子。以下截图是 Supercell 的Clash of Clans中的商店:

在前面的截图中,你可以看到Clash of Clans中可用的各种 IAPs。你可以看到它们有一个分层结构,即玩家一次性花更多的钱,每美元可以获得的宝石就越多。以下截图是 KeitGames 的Bina Blocks中的银行:

你刚才看到的是Bina Blocks中银行的截图。你可以看到与Clash of Clans中相似的定价层级。
尽管这本书没有涵盖 IAP 的实现,但它仍然可以是你的游戏的一个重要组成部分。如果你希望继续学习如何实现自己的 IAP,你可以从以下这些来源了解它们:
-
www.raywenderlich.com/21081/introduction-to-in-app-purchases-in-ios-6-tutorial -
www.playngive.com/blog/2010/3/6/adding-in-app-store-to-cocos2d.html -
troybrant.net/blog/2010/01/in-app-purchases-a-full-walkthrough/
利用数据分析
分析工具包是任何开发者都可以利用的伟大工具。如果你想了解用户在哪个关卡上遇到的最大困难,你可以跟踪每个关卡被击败的次数,以查看掉落发生的地方。或者,你可能想跟踪人们如何使用你刚刚添加的新角色。在第七章“到达我们的目的地”中,你将学习如何实现 Flurry Analytics,它使用一个简单的事件驱动系统来跟踪某些方法何时被调用。基本上,你可以跟踪从按钮点击和屏幕加载到用户在问题上的花费时间。所有这些数据都发送到 Flurry 的服务器,并编译成易于阅读的图表和图形。
虽然你的用户不会注意到任何区别,但长远来看,这肯定会让你的游戏变得更好,因为你可以看到玩家与你的游戏互动时发生了什么。因此,随着时间的推移,你可以提交更好的更新。
复杂机制和特效
术语“复杂机制”有时可能有些模糊或过于笼统。然而,它可能从游戏场景之间非常平滑的过渡到玩家游戏方式的独特控制系统。如果你觉得你的游戏需要这些复杂机制来获得成功,那么请确保你做得正确。如果你只是计划添加额外的功能,因为你觉得你必须这样做,而不是因为它能让你游戏变得更好,那么提前考虑这一点可以帮助你以某种方式构建项目计划,以便在开发后期添加额外的(或奖励)功能。这样做比试图在中途添加东西并延迟必要功能要好,如果你不能立即找到正确编码它们的方法。
有些游戏因其独特的游戏机制而非常特别,结合使用触摸屏(我们将在第四章“声音和音乐”中详细讨论),例如,如果你看看《Smash Hit》、《Blek》、《Tiny Wings》或《Threes!》,你会发现它们并不完全符合任何传统类型。
注意,如果你相对较新手游戏设计(尤其是移动设计),尝试完全新颖或独特的东西可能并不完全符合你的最佳利益。相反,至少尝试复制已经存在的机制,并可能对其进行轻微调整,以更好地适应你试图制作的游戏。以下截图是 Smash Hit:

下一个截图是 Blek:

以下截图展示了 Tiny Wings:

这个截图是 Threes!:

这些游戏依靠其复杂的力学和特效而繁荣。它们规划得非常周密,设计得也很恰当。如果你只想要特效(例如,粒子爆炸或 Threes! 卡牌上的面部表情),那么你应该查看第七章,达到我们的目的地,这是你应该查找的地方。那一章详细介绍了如何润色你的游戏并提高其受欢迎程度。但就目前而言,只需知道,如果你计划将独特、创新或前所未有的东西作为你设计的一个组成部分,你应该相应地计划。
如果你只是因为觉得有它会很酷而添加机制,而这些机制对游戏进程并不必要,那么你应该在最后计划。没有什么比因为一个一开始就并不重要的功能而推迟游戏发布日期更糟糕的了。
选择工具和开始
在开始工作之前就知道你的项目需要哪些工具是一个好的开始。这样,当需要实现某个特定功能的时候,你可以轻松打开程序并使用你事先获取的工具。在项目中途获取工具也不是太糟糕,但根据经验,最好在开始工作之前就考虑好。
有一些第三方程序可以使使用 Cocos2d 开发游戏的速度大大加快。以下列出了这样的产品,有关如何下载和安装它们的信息,以及它们的成本(如果有)以及其他你需要了解的事情。
小贴士
注意,这里列出的产品并不意味着在用 Cocos2d 创建游戏时是必需的。本节仅作为“以防你有所疑问”的指南,用于在尝试开发更好、更精致的游戏时考虑的事项。
本节还假设您要么有专门的人为您的游戏制作艺术作品,要么您将在任何图像编辑软件中自行完成,例如 Photoshop。这是假设的,因为这里没有列出图像编辑器,因为编辑器的数量很大。在制作游戏时,任何一种都可以使用。然而,这里列出的程序在它们所追求的方面都是最好的,尽管可能还有其他类似的程序。
-
纹理打包器(Texture Packer): 这是一个图像压缩工具,用于将所有图像放置在一个精灵图上。这有助于优化绘图(提高游戏每秒帧数)以及减少游戏整体大小的空间(这可能是用户是否需要通过 Wi-Fi 下载的区别)。
-
粒子设计师(Particle Designer): 这是一个可视化粒子编辑器,允许快速创建粒子效果,从激光和烟雾效果到爆炸和蜡烛效果。
-
符号设计师(Glyph Designer): 这是一个自定义字体创建器,允许您创建看起来漂亮的字体,而不是使用带有文本的预渲染图像或仅具有填充颜色的普通字体。
-
顶点助手专业版(VertexHelper Pro): 这是一个用于与物理引擎一起使用的可视化顶点计算器。它可以用于其他目的,但主要用于与物理引擎一起使用,以确定碰撞的确切坐标,而不是使用标准的长方体或圆形来计算对象的碰撞边界。
-
GAF: 这是一种将 Flash 动画存储在开放跨平台格式中的方法,以便在多种游戏开发框架和设备中进一步播放。GAF 使艺术家和动画师能够使用 Flash CS 创建复杂的动画,并能够无缝地与各种游戏开发框架(如 Cocos2d、Cocos2d-x、Unity3D 和 Starling 等)一起使用。
-
Git 和 GitHub/Bitbucket: 在项目开发中,有许多使用源控制的方法。目前最好的解决方案,以及许多开发者使用的解决方案,是 Git。您可以管理代码随时间的变化,如果出现错误,您总是可以回滚到代码的最新版本,而无需在试图找出更改了什么时浪费宝贵的发展时间。这也是在计算机故障时备份项目的好方法。
TexturePacker
TexturePacker 允许您轻松地为游戏创建精灵图。它将单个图像压缩成一个大图像,这样您可以在不牺牲质量的情况下有效地提高游戏的优化。
最终的图集可以在各种游戏引擎中使用,如 Cocos2d、Unity、Corona SDK、SpriteKit 等。以下截图显示了 TexturePacker 的用户界面:

专业版的价格为 39.95 美元(提供免费试用)。
要下载 TexturePacker,请访问www.codeandweb.com/texturepacker/download。在那里,您可以点击您正在使用的操作系统名称,然后您就可以开始了!
TexturePacker 支持 Mac 10.7/8/9、Linux(Ubuntu 64 位)、以及 Windows 32 位和 64 位(XP/Vista/7/8)。
Particle Designer
如果您想要实时生成的美丽爆炸、令人惊叹的烟雾效果或闪亮的激光束,那么 Particle Designer 就是您需要的。导入您自己的自定义“粒子”,调整大量参数以获得您想要的效果,然后将文件导出到您的游戏项目中。以下截图显示了 Particle Designer 的用户界面:

价格为 74.99 美元(提供免费试用)。
要下载 Particle Designer,请访问71squared.com/particledesigner。在那里,您将看到一个购买链接。然而,您可以通过点击他们网站上的试用按钮并输入您的电子邮件地址来获取试用版。
Particle Designer 支持 Mac 10.8、9 和 10。
从技术上讲,您可以在代码中手动创建每个粒子,但这非常低效。此外,在您设计粒子时能够实时看到粒子可以使整个过程更加顺畅,因为将粒子效果导入 Cocos2d 非常简单。
如果您拥有 Particle Designer 但之前从未使用过,建议您只需打开它,并开始尝试调整一些设置。只需拖动一些滑块,您就可以获得一些非常酷的效果。
Glyph Designer
如果您想要花哨的字体而不是可用的正常 TrueType 字体,或者例如在 Photoshop 中预渲染的文本图像,您可以使用 Glyph Designer。它基本上允许您获得一个样式优美的字符集,可以与 BMFont 标签(位图字体)一起使用。这实际上意味着您可以在保持使用 Glyph Designer 创建的彩色、样式化字体的情况下生成自定义文本标签。

其价格为 39.99 美元(提供免费试用)。
要下载 Glyph Designer,请访问71squared.com/en/glyphdesigner。在那里,您将看到一个购买链接。然而,您可以通过点击他们网站上的试用按钮并输入您的电子邮件地址来获取试用版。
Glyph Designer 支持 Mac 10.8、9 和 10。
如果您想要像您之前看到的某些游戏(如Clash of Clans、Angry Birds、Crescent Ridge Mini Golf和Bina Blocks)中使用的字体,而不是其他游戏中显示的纯色字体,您需要一个好的位图字体创建工具。就像粒子一样,如果您想要漂亮的字体,市面上只有少数几个程序。其中之一就是由创建 Particle Designer 的同一群人制作的 Glyph Designer。
如果你已经安装了 Glyph Designer 但还没有开始使用它,请打开它,并在各种字体上测试一些不同的样式,以了解程序的工作方式以及你可以使用 Glyph Designer 创建的一些东西。
在设计你的美学(尤其是在选择自定义字体时)时,需要考虑的是,这种字体/风格是否适合整个项目的整体美学?如果它感觉不协调,或者感觉像两种不同的风格冲突,那么选择一个更适合你游戏的字体可能对你更有利。不用担心;它可能不会立刻出现在你的脑海中,但如果你找到了合适的字体,它将为你的游戏增添额外的磨光层,这对于使你的游戏成功至关重要(我们将在第六章[part0049.xhtml "第六章. 整理和磨光"]中更多关于磨光的内容)。
VertexHelper
你在游戏中使用物理(或碰撞)吗?你需要一个比盒子或圆形更好的形状来表示复杂物体吗?VertexHelper 可以通过导入图片并允许你点击想要顶点所在的位置来帮助你找到组成复杂物体形状的坐标。如果你真的想的话,你可以手动猜测并程序化地测试坐标,但 VertexHelper 使这个过程变得更快,并保持坐标的准确性(假设代码中物理实现得正确)。
如果你打算使用物理(或碰撞),你应该知道顶点碰撞可能非常昂贵。如果可能的话,最好坚持使用原语(例如,圆形或正方形),如果那样的话,你可能甚至不需要物理引擎。因此,仔细考虑你的选项。

它的价格是 $7.99。
下载请访问 itunes.apple.com/us/app/vertexhelper-pro/id411684411,或搜索 VertexHelper Pro,你应该能在搜索结果的第一页找到前往 Mac App Store 的链接。
就像任何其他 Mac 应用程序一样,一旦完全安装,你就可以运行它。
为任何物理对象生成边界有两种方法:
-
使用
b2Vec2数组手动编写值,并希望它是正确的。 -
使用 VertexHelper 等程序快速为每个对象创建边界。
在你的游戏中使用物理引擎时了解这个程序并准备好它是有好处的,因为几乎每个物体都需要与另一个物体发生碰撞(除了少数背景图像)。话虽如此,每个物体可能不是正方形或圆形,因此能够快速创建自定义形状的物理边界是很好的。
如果你已经在你的电脑上安装了 VertexHelper,请继续;打开它并导入一张图片。然后开始点击操作,感受一下程序的工作方式。这在你开始实现每个物体的物理特性时会有很大帮助。
GAF
如果你想要将任何 Flash 动画转换为用于 Cocos2d 游戏的格式,你可以使用 GAF,它本质上是一种单一格式,可以转换为各个框架和设备的要求。

其费用如下:免费版为 $0;工作室版为 $995;企业版为 $2,995。免费使用每年限制为 $50,000,并且只能开发你自己的游戏,不能进行合同工作。你可以在gafmedia.com/pricing了解更多关于他们的定价选项。
要下载 GAF,请访问gafmedia.com/downloads,选择操作系统。然后按照说明进行安装。
GAF 可在 Windows 和 OSX 上使用。
如果你有一个熟悉 SWF 文件动画的艺术开发者,并且可以将它们转换为适用于多个游戏引擎的平台无关格式,GAF 会很有帮助。如果你正在移植已经使用 SWF 文件的游戏(例如你希望在移动设备上实现的在线 Flash 游戏),它也会很有帮助。
Git 和 GitHub/Bitbucket
如果你曾经处理过代码,那么很可能会出现一些问题。幸运的是,存在源代码管理软件来管理你的代码版本,并确保在出现问题时,你可以回滚到之前的版本。
这是一张 Cocos2d-Swift GitHub 仓库(简称 repo)的截图github.com/cocos2d/cocos2d-swift:

这是免费的。
如果你已经在你的 Mac 上安装了 Xcode,要下载这个,只需转到 Xcode | Preferences | Downloads。然后安装命令行工具。有了这个,你可以使用 Xcode 内置的 Source Control 菜单,或者通过终端使用你的 Git 命令。
如果你希望手动安装 Git,只需访问他们的网站,按照链接下载和安装,详情请见git-scm.com/。
注意,如果你以前从未使用过任何源代码管理,你可以自由地阅读它是如何工作的help.github.com/。
实际上,你只需要在 Mac 上安装 Git,因为代码就在那里。然而,你可以在任何桌面操作系统上安装它。
GitHub 和 Bitbucket 都是免费的。然而,关键的区别在于 GitHub 提供无限量的公共仓库,而 Bitbucket 提供无限量的私有仓库。所以如果你想隐藏你的代码不让别人看到,同时仍然使用列出的服务,建议选择 Bitbucket。
这两项服务都使用 Git,并使用相同的命令和工具。这只是一个选择,即你想要使用公共仓库还是私有仓库(或者你合作的人有偏好的情况)。
设定灵活、专注的目标
当你最初想制作游戏时,你可能认为这是有史以来最好的游戏想法。诚然,可能真的是这样;不要让我说的话阻碍了你。然而,随着时间的推移,你对这个游戏的看法可能会改变,你的方向可能会在项目生命周期的整个过程中有所改变或很大。最好是提出一个既专注又有点灵活的目标,这样可以在保持方向的同时留出改进的空间。
扩展
如果你正苦于想出一个原创的想法,我建议你拿一块白板(越大越好;相信我),在电脑上打开图像编辑软件,或者甚至拿出一些笔和纸,开始写下你想要制作的游戏内容。选择与游戏类型相关的主题,或者与主要机制相关的同义词,并开始制作一个想法图(类似于蜘蛛网),这些想法可以在你游戏创建的初始阶段使用。
然而,仅仅产生最初的想法本身可能就很有挑战性;例如,何时一个项目真正准备好开始编码或艺术创作过程呢?是在第一次实现力学之后吗?还是当 27 个级别都被系统地从开始到结束规划好之后?重要的是要意识到那一刻,当你停止扩展和改进游戏时,随着你产生更多想法,游戏也在变得更好,而你只是因为可以而提出想法。
专注
一旦你对想要制作的类型有了良好的想法,就需要将这个最终目标集中起来,以便在以后保持灵活性。坦白说,制作游戏就是关于流程和随着时间的推移产生想法。所以,如果你正在制作游戏,并且认为添加另一种敌人类型或几个支线任务可能会使游戏更有趣,那么请不要犹豫,去做吧。
话虽如此,你不能从一个想法跳到另一个想法,否则你的游戏在玩家体验过程中会缺乏方向感。他们可能会觉得剧情从未解决最初提出的冲突。
此外,如果你的游戏缺乏一个明确的目标,并且随着时间的推移不断变化,那么开发时间将会更长。所以,从经验的角度来看,如果你想创造两个不同的游戏想法,第二个想法是在第一个游戏开发过程中的一半时产生的,那么你可以在第一个游戏发布后将其作为更新或扩展添加进去,或者将其变成一个独立的项目,但不要让开发时间比原本更长。
那么,在专注的目标中,你应该有哪些东西呢?如果你的游戏有叙事或以某种方式是情节驱动的,尽量确保这一点在整个开发过程中保持不变。或者,如果你的游戏依赖于某个机制的单个特性,你希望在游戏中途添加另一个特性,确保新添加的特性不会影响最初构思游戏时考虑到的初始用户体验。然而,如果你想要添加的功能对游戏最初的部分并不是非常重要,那么现在不必担心,只需在更新中包含它即可。相信我;除非添加新功能需要几分钟时间,否则它可能不值得你花费时间在那些对游戏按最初设计意图工作并不非常必要的事情上。或者,如果游戏的艺术风格并不非常吸引人或者可以用另一种方式表示,考虑一下艺术对你游戏的重要性。如果它是低质量的艺术,例如像 MS Paint 那样,那么考虑获取一些高质量的艺术。然而,如果它已经非常完善,那么在发布后不必麻烦去改变它。
弹性
我在上一节中提到过:在游戏机制、叙事、艺术风格等方面,不要犹豫,要有点宽容。随着游戏的进展,你几乎不可避免地会想,“哦,天哪,如果我们只是在游戏结束时添加一个额外的 Boss,那岂不是世界上最好的游戏?”也许吧。这就是你需要灵活的地方。
但请记住,要尽量保持我们在上一节中提到的那个专注目标。这关乎在每一步都保持激光般的专注目标和自由流动的创造力之间的平衡。
下载 IDE 和源代码
如果你的电脑上还没有安装 Xcode 和 Cocos2d 库,现在可能是一个安装的好时机。本节将指导你完成这个过程。
步骤 1 – 通过苹果的 iOS 计划成为开发者
现在,这本书并不完全需要支付每年 99 美元的费用来成为官方苹果 iOS 开发者。但如果你希望在设备上测试应用程序和游戏或发布应用程序到 App Store,这是必需的。
如果你现在想跳过这一步,请随意。当你准备好时,可以回到这里。
首先,前往developer.apple.com/programs/start/standard/开始你的 iOS 应用开发者计划的注册流程。如果你已经有了苹果账户,你可以使用它。否则,创建一个账户。
接下来,选择个人或公司。苹果在其网站上提供了很好的描述,所以我觉得如果你需要帮助确定选择哪一个,他们的网站可以帮助你做出决定。

按照步骤输入一些联系信息,然后选择iOS 开发者计划,并点击继续。
同意许可协议,输入您的购买信息,然后您就可以开始了!
第 2 步 – 下载并安装 Xcode
前往developer.apple.com/xcode/downloads/并点击下载 Xcode 的链接。在撰写本书时,它说在 Mac App Store 中查看,本书中引用的 Xcode 版本是 Xcode 6。它需要 OS X 10.9.3。
就像 Mac App Store 中的普通应用程序一样,它应该安装到您的Applications文件夹中,并且您应该在安装后能够运行它。
添加设备
注意,如果您想在设备上运行您的应用程序,需要一个开发者账户(这在步骤 1 – 通过 Apple 的 iOS 程序成为开发者部分中提到过)。如果您已经设置好了,您应该能够通过在 Xcode 中转到窗口 | 组织者,然后点击设备来将您的设备添加到您的开发者账户中。
在下面,您应该能看到您的设备名称,以及主视图中一个写着用于开发的按钮。
如果它没有显示,而是显示类似“Brian 的 iPhone”上的 iOS 版本不支持此安装...的消息,这意味着您必须安装 Xcode 的最新版本以获取最新版本的 SDK,以便您的设备能够得到适当的支持。
第 3 步 – 通过 SpriteBuilder 下载 Cocos2d
前往www.cocos2d-swift.org/download并点击最新发布下的下载链接,标记为 SpriteBuilder(这应该会打开 Mac App Store)。在撰写本书时,最新版本是Cocos2D 3.2.1,所以除非另有说明(例如,版本 2.1),本书中的所有内容都将遵循该版本。
截至 3.2 版本,Cocos2d 只能通过 SpriteBuilder 进行安装。对于那些不熟悉 SpriteBuilder 的人来说,让我告诉您,它用于通过拖放界面创建项目。您不需要完全通过 SpriteBuilder 创建您的游戏。然而,截至 Cocos2D 3.2.1 版本,项目创建只能通过 SpriteBuilder 进行。
就像任何 Mac 应用程序一样,它将被下载到您的Applications文件夹中,并在完全安装后运行。
通过 SpriteBuilder 创建新项目
一旦安装了 Xcode 和 SpriteBuilder,我们就可以设置一个初始项目,以便看到所有操作。Cocos2d 足够好,在项目创建时给我们一些初始临时文件,这样我们可以有一个更好的开始。它很棒,因为一旦我们需要,我们基本上可以用自己的文件替换它们。
打开SpriteBuilder。它可能会要求您加入他们的邮件列表,但在这本书中,无论您是否注册都没有关系。
之后,它可能看起来像已经启动了一个新项目(这可能是真的,但让我们确保我们从零开始)。转到文件 | 新建 | 项目,并选择一个您会记住的文件夹位置(例如,桌面或文档)。然后,它应该打开一个带有蓝色背景和 SpriteBuilder 文本的预览,如下面的截图所示:

如果您没有看到这条确切的消息,请不要感到惊讶。这仅仅是本书编写时 SpriteBuilder 1.2.1 版本中发生的情况。
恭喜!您的项目现在已经设置好了。
如果您想使用 SpriteBuilder 视觉编辑器为 Cocos2d 创建项目,您现在可以这样做。然而,本书不包括如何使用该程序的教程,所以如果您想了解这个程序的工作原理以及视觉编辑器的全部潜力,请查看www.makegameswith.us/tutorials/getting-started-with-spritebuilder/。他们有一套很好的 SpriteBuilder 教程。
如果您想开始编写代码而不是拖放,请按照下一节的步骤操作。
将 SpriteBuilder 项目导出至 Xcode
在 SpriteBuilder 中,转到文件 | 发布。这将显示一个进度条,如果您的项目是全新的,它将非常快地完成。
默认情况下,当按下发布按钮时生成的 Xcode 文件将保存在您首次创建项目时选择的与项目位置相同的文件夹中。记得我告诉您要保存到一个您会记住的地方吗?回到那个位置,无论是在查找器中还是在Xcode | 打开查看器中。
找到与您的项目同名文件。它看起来可能像这样:
ProjectName.xcodeproj
如果您没有看到.xcodeproj文件扩展名,请不要担心。它应该被标记为 Xcode 项目,并在旁边有一个蓝图图标。
点击它打开。它应该会打开 Xcode。
您可以自由地选择在您选择的模拟器中运行应用程序,或者连接设备并尝试一下。到目前为止看起来不错!如果您想在设备上运行应用程序,请转到本章标题为添加设备的部分。
摘要
我们探讨了如何根据您希望在游戏中包含的具体元素来规划您的游戏,如何在创建游戏的过程中选择所需的工具,以及在实际创建想法并在项目整个生命周期中前进时的一些最佳实践。
最后,我们介绍了如何下载和安装 Xcode,下载和安装 SpriteBuilder,以及下载各种使您生活更轻松的第三方应用程序。
下一章将深入探讨原型对于优秀游戏设计的重要性,为什么快速迭代原型以更快地失败是关键,以及为什么创建一个人们可以实际玩的游戏最小可行产品(即使它比您见过的任何东西都更易出 bug)对于您游戏长期成功非常关键。
第二章. 使用原型快速失败
本章全部关于让项目的线框开始运行,这样你就可以在稍后填充游戏的“内容”。你将获得基线图形、菜单和游戏的流程结构,以便快速测试。测试越早,失败越快——这是你作为游戏开发者的新座右铭。尽管编写代码可能看起来非常简单,但我们的意图是尽快创建最小可行产品——一个有形且可工作的产品,以便对整个项目有一个感觉。在本章中,我们将涵盖以下内容:
-
为什么使用原型?
-
让场景运行起来
-
创建文本(标签)
-
开始使用 TexturePacker 中的精灵表
-
创建按钮
-
创建菜单、场景和场景转换
-
创建节点和单位(精灵)
在本书中,从头到尾创建一个完整游戏,从本章开始。创建一个持续的项目有两个目的,如下:
-
展示每个部分如何适合整个项目,而不仅仅是作为单独的代码片段
-
从头到尾可视化整个项目的流程,不跳过任何步骤
在本书附带文件中,你可以找到完成的项目,看看它将看起来和感觉如何。此外,在每一章的开头,都会有一个关于到那时为止书中项目版本的参考,这样你就可以跟随书中的完整解释/示例。
小贴士
到目前为止,项目是通过遵循第一章中的教程创建为一个全新项目的,即刷新我们的 Cocos2d 知识。如果你更愿意使用默认的起点,你可以使用本书附带文件中的空白Project文件夹中的项目。
你也可以从github.com/keitzer/MasteringCocos2d下载代码。
文件后缀与目录
当 Cocos2d 和 SpriteBuilder 在 Cocos2d 3.0 中集成时,它们改变了 Cocos2d 读取纹理的方式。在之前的版本中,如果你想为 iPhone 和 iPad 制作游戏,你必须给你的文件添加各种后缀。例如,如果你的图像命名为btnPlay.png,你必须在你项目中创建不同尺寸的文件,其命名如下:
-
btnPlay.png -
btnPlay-hd.png -
btnPlay-ipad.png -
btnPlay-ipadhd.png
将文件保存的方法通常被称为使用文件后缀。
在带有 SpriteBuilder 的 Cocos2d 新版本中,你可以通过将最大尺寸的文件(例如,Retina iPad)拖入 SpriteBuilder 来管理你的纹理。当你点击发布时,SpriteBuilder 会为你处理文件大小的变化。这种处理文件的方式被称为使用目录。
注意
这里有一个警告:如果你决定手动将文件添加到Published-iOS文件夹中,请不要在 SpriteBuilder 中点击Clean Cache,否则你将丢失所有这些文件。
然而,使用目录有其缺点。例如,你可能想使用 TexturePacker(我们将在本章后面以及整本书中用到它),但是没有简单的方法来使用它和新的文件保存目录风格。技术上你可以做到,但这样做太麻烦了,以至于 TexturePacker 的任何优势都被你额外要做的工作所抵消。因此,TexturePacker 可以与文件读取的目录风格一起工作。所以,如果你打算使用目录风格,直接将文件拖入 SpriteBuilder 会更容易。
那么,如果我们想利用 TexturePacker,我们将如何解决这个问题?嗯,直到存在一个集成 TexturePacker 使用的 Cocos2d 和 SpriteBuilder 版本,我们不得不回到文件后缀方法。
小贴士
注意,如果你不想购买 TexturePacker,不用担心;它不是必需的。后面的代码将保持不变,因为 Cocos2d 搜索图像,无论它是通过精灵图集还是作为单个文件加载的。如果你不想使用 TexturePacker,请随意避免更改你的搜索模式(如后文所述),并继续使用目录方法。每当这本书提到将文件添加到精灵图集时,那就是你将其添加到 SpriteBuilder 的信号。
由于 Cocos2d 3.0 及以上版本默认使用目录方法,并且我们将切换到使用文件扩展名,我们必须查找CCFileUtilsSearchModeDirectory的 Xcode 项目。其中一个结果应该是一个名为CCBReader.m的文件。点击结果,它应该带你到大约第 109 行,在那里你会看到以下代码行:
sharedFileUtils.searchMode = CCFileUtilsSearchModeDirectory;
你将要更改这一行以使用后缀搜索模式:
sharedFileUtils.searchMode = CCFileUtilsSearchModeSuffix;//CCFileUtilsSearchModeDirectory;
在此基础上,我们准备开始项目的原型阶段。但首先,为什么制作原型很重要?为什么快速制作原型很重要?为什么不慢慢编写代码,最终在适当的时候实现游戏的核心机制呢?
为什么需要原型?
除了在游戏完全完成之前询问朋友“好玩吗?!”的明显原因之外,在开发初期快速进行游戏原型,尤其是对于几个不同的原因非常有用:
-
你可以从最终用户的视角来询问你游戏的原创性/创新性,而不仅仅是你的个人观点
-
你可以在游戏还来得及做出改变之前,就生成如何改进游戏的点子
-
你可以感受到游戏从一阶段到下一阶段是如何实际流转的,并构想出一个具体的产品,而不仅仅是想法
-
如果向公众展示,这可能是开始游戏营销和开始成功在 iOS 上取得成功所需曝光的雪球效应的绝佳方式。
此外,这是开始一个项目,尤其是包含可能难以完全编码并按预期工作的新概念或想法的项目,最好的方式。你可能听说过“概念验证”这个术语;这一章正是这个意思。这是对你整个游戏的一个非常快速的概述,你可以向其他人展示并询问:“这是我能够将这个概念制作成完整游戏的一个证明。你有什么看法?”
现在你已经理解为什么为你的游戏制作原型是一个好主意,让我们快速了解一下这本书将要介绍的项目。
游戏测试和反馈分析
当你的原型准备好供其他人体验时,最好是走出去,实际上找一些人来玩你的游戏。理想情况下,你应该找到对该游戏类型以及你的游戏目标受众年龄范围都有所了解的游戏测试者,这样他们才能提供高质量的反馈。
你收到的反馈将各不相同,从“哇!这太棒了!”到“我不懂怎么玩这个游戏。”你必须准备好听到各种各样的回应。此外,即使有人说你的游戏不好,也不一定意味着它真的不好。然而,如果他们说的是客观真实的,并且实施他们的建议将改善玩家的体验,那么你应该听取他们的意见,因为这不仅会使你的当前游戏变得更好,也会使未来的游戏变得更好。
总之,让我们开始制作一个其他人可以玩的项目。
书籍的项目
为了了解这本书将要介绍的项目,想象一个 9 x 9 的网格,你的“基地”位于中心,敌人围绕着边缘的方块生成。这是一个回合制游戏。在每一回合,每个单位移动一个方块。每个单位在每个回合的数字增加 1。如果你的单位与敌人的单位相撞,两个数字相减,结果为正的单位存活。你的目标是保护你的主要基地免受敌人单位的攻击,尽可能多地在主要基地上存活回合。
以下是完成的游戏(到第七章,抵达目的地)的一些截图:
当游戏首次启动时,它将看起来像以下截图所示:

游戏进行到一半时的样子如下:

当中央广场被占领时,游戏结束屏幕将看起来像以下截图所示:

快速原型
不论是 Photoshop、Illustrator、MS Paint、一张纸和一支铅笔、白板、蜡笔,还是任何其他创造性方式来绘制你的想法,最好是将项目的视觉形象化,这样当你开始编码时,至少有一个基础来解释你选择这些颜色和文本位置的原因。
此外,尽管我们有了我们刚才看到的最终产品的截图,但我们仍需要想象从哪里开始。例如,在下面的截图中,你会看到一个为本书项目快速制作的草图,如之前所述。尺寸为 2048 x 1536(iPad 横屏)。这是为了展示游戏作为一个概念,而不是作为用于营销目的的成品。不用担心;如果你的艺术技能不足,你的作品不必像这样好。

这是一个很好的起点示例,因为它没有任何菜单或花哨的过渡;只有游戏。我知道有一个写着 菜单 的按钮,但那是为了稍后使用,当我们真正实现菜单时。记住,原型应该是快速的。图形、颜色、字体,甚至是菜单位置或文字选择的变化都不重要。重点是尽可能快地完成。尽早测试,快速失败。
Cocos2d 引擎工作概述
在我们深入代码之前,让我们快速了解一下 Cocos2d 引擎的工作原理。如果你是第一次使用 Cocos2d,这将很有帮助。如果你之前使用过 Cocos2d,请随意阅读,因为这可能对你来说是一个复习。
Cocos2d 实质上是一系列父节点和子节点的集合。基础父节点是当前正在运行的场景。在任何给定时间,你只能显示一个场景。在场景内部,将会有子节点,所有这些子节点都必须是 CCNode 类型。一个 CCNode 对象是一个具有位置、旋转、缩放、颜色和其他各种属性的实体。一个 CCNode 对象可以添加其他 CCNode 对象到它上面。
每个 CCNode 的子类都继承自它,并在 CCNode 类之上添加功能。例如,如果我们想在屏幕上绘制图像,我们会使用 CCSprite,它本质上是一个带有图像附加的 CCNode 对象。甚至场景(类型为 CCScene)也是 CCNode 的子类(这就是每个场景可以拥有子节点的原因)。
这里有一张图片,用来描述 Cocos2d 中父节点和子节点之间的关系。首先,我们有我们想在屏幕上显示的单独的图像,在一个非常简单的纹理图集中。

接下来,我们有一个 CCScene 的示例图。场景中添加了五个 CCSprite 对象:天空、两棵树、道路和玩家。

天空位于 z-index 等于 0 的位置,树木位于 z-index 等于 1(这意味着它们将显示在天空前面),道路位于 z-index 等于 2(这意味着它将显示在天空和树木前面),玩家位于 z-index 等于 3(这意味着它将显示在所有内容前面)。默认的 z-index 是 0。
Cocos2d 中的其他一切都很简单——只是一个带有其他CCNode对象作为子对象的CCNode对象。
关于 Cocos2d 使用的父子关系,有一件事需要记住:例如,如果你移动一个父节点 20 个单位,那么子节点也会以相同的量移动。
现在我们简要地介绍了 Cocos2d 的工作原理,让我们开始制作我们的原型。
启动场景并运行
在我们开始向屏幕添加任何内容之前,我们需要确保我们有一个可以在我们的设备或模拟器上查看的游戏。一旦你在 SpriteBuilder 中创建了项目(或获取了前面列出的空白项目)并在 Xcode 中打开了项目,就进行下一步。
创建打开场景的初始代码
你应该看到一个名为MainScene.h的文件和另一个名为MainScene.m的文件。打开头文件(它具有.h扩展名)。
在头文件中,在@interface行和@end行之间添加几行代码。头文件应该看起来像这样:
@interface MainScene : CCNode
{
CGSize winSize;
}
+(CCScene*)scene;
@end
然后,在主文件(它具有.m扩展名)中,应在@implementation和@end行之间添加一些代码。它应该看起来如下:
#import "MainScene.h"
@implementation MainScene
+(CCScene *)scene
{
return [[self alloc] init];
}
-(id)init
{
if ((self=[super init]))
{
//used for positioning items on screen
winSize = [[CCDirector sharedDirector] viewSize];
float grey = 70 / 255.f;
//these values range 0 to 1.0, so use float to get ratio
CCNode *background = [CCNodeColor nodeWithColor:[CCColor colorWithRed:grey green:grey blue:grey]];
[self addChild:background];
}
return self;
}
@end
最后,打开AppDelegate.m文件,滚动到文件底部,你应该在startScene方法中看到一行看起来像这样的代码:
return [CCBReader loadAsScene:@"MainScene"];
我们将把它改为以下内容:
return [MainScene scene];
代码可能会在这一行给出错误。这可以通过将MainScene头文件导入到 AppDelegate 的主文件中来解决。只需将以下内容添加到AppDelegate.m文件的顶部:
#import "MainScene.h"
一旦所有这些设置都到位,你就可以自由地在你的设备或 Xcode 内置的模拟器上运行你的项目了。你可以在接下来的章节中了解更多关于每个选项的信息。
在模拟器上运行它 – 不需要 iOS 开发者许可证
在模拟器上运行适用于测试你拥有的设备。例如,如果你拥有一部 iPhone 5s,并想测试你的游戏在 iPhone 6 或 6 Plus 上的外观,只需加载该模拟器并测试游戏的外观。
小贴士
注意,最好只在设备上测试性能。不要在模拟器上测试性能。当在模拟器上运行时,你永远不会得到设备能力的完美表示。此外,你应该只测试游戏的外观。
对于在模拟器上的测试,只需从 Xcode 中可用的模拟器中选择你想要模拟的设备,如以下截图所示:

选择你想要的任何模拟器,最好是最终支持游戏的模拟器,然后按左边的播放按钮,或者按command + R来运行。打开模拟器可能需要几分钟,所以请耐心等待。但是一旦打开,它应该会自动在模拟器上打开。如果它没有,只需尝试在模拟器已经打开的情况下重新运行它。
恭喜!如果您使用了模拟器,现在您有一个可以运行的项目了!接下来,我们将介绍如何在设备上运行它。
在设备上运行 – 需要 iOS 开发者许可证
如果您不确定是想在设备上还是模拟器上运行您的游戏,让我来解释一下为什么在测试目的上设备是王。您不仅可以像其他用户一样看到和感受项目,还可以体验设备的实际性能,而不是它的模拟版本。此外,如果您的项目在触摸屏使用上很重(这确实应该如此,否则可能不应该是一个 iOS 游戏),那么您可以有效地测试游戏的触感。
对于设备测试,只需将您的设备连接即可。其名称应列在 Xcode 中,如下所示:

如果您看不到您的设备名称,请确保您选择了 iOS 设备目标,而不是任何模拟器。如果您的设备已连接,但仍然显示iOS Device,请确保您在 Xcode 中有一个开发者账户订阅。有关此问题的更多详细信息,请参阅第一章,刷新我们的 Cocos2d 知识。
一旦您看到您的设备名称,您可以按下左侧的播放按钮,或者按command + R来运行它。构建可能需要一分钟左右,但一旦完成,项目将自动在您的设备上打开。
恭喜!您现在有一个可以运行的项目了。现在我们可以开始添加一些内容,例如文本和按钮,然后继续创建另一个场景并过渡到该场景,再返回到原始场景。
创建按钮和文本(标签)
如果您想在屏幕上放置一行文本,您需要创建一个标签。Cocos2d 中有两种标签类型:CCLabelBMFont和CCLabelTTF。位图字体标签是使用前面在这本书中提到的 Glyph Designer 创建的精美标签。TrueType 字体标签是常规的、未格式化的文本标签,它使用手机上已有的字体文件或您添加到项目中的文件。
小贴士
注意,如果您有一个经常需要更新的标签,例如分数计数器或健康值,在这种情况下使用 BM 字体会更有效率,即使字体是纯白色且在 TTF 格式下看起来完全相同。
让我们显示一些文本 – CCLabelTTF
如前所述,TTF 标签是简单、未格式化的标签。这些有什么用呢?答案是,您可以快速启动游戏的原型,这样您可以更好地理解游戏的流程。然后,一旦准备好,您就可以切换到使用 BM 字体来使其看起来更美观。有关改进游戏美学的更多方法,请参阅第六章,整理和抛光。
小贴士
下面是一些关于 BMFonts 的简要说明:如果您想在游戏中使用它们,出于性能考虑您应该这样做,请注意 BMFonts 的各种限制,而 TTF 字体则没有。首先是标签放大时的质量较差。然后,BMFonts 只能使用字体图集中的字符,任何外语支持可能意味着需要很多额外的 BMFonts,这可能会在空间上迅速增加。
我们将要做的第一件事是让左侧的标签动起来。以下代码应添加到MainScene.h中(变量前面的lbl前缀将指示我们这是一个标签;同样,btn用于按钮,num用于数字等):
@interface MainScene : CCNode
{
CGSize winSize;
//Add the following:
//the labels used for displaying the game info
CCLabelTTF *lblTurnsSurvived, *lblUnitsKilled, *lblTotalScore;
}
以下代码将放入MainScene.m的init方法中,并将创建标签。然后我们想要为每个标签设置位置,因为默认位置在左下角。最后,我们将每个标签添加到场景中。记住,场景只是一个可以有任意多个子节点的节点:
CCLabelTTF *lblTurnsSurvivedDesc = [CCLabelTTF labelWithString:@"Turns Survived:" fontName:@"Arial" fontSize:12];
lblTurnsSurvivedDesc.position = ccp(winSize.width * 0.1, winSize.height * 0.8);
[self addChild:lblTurnsSurvivedDesc];
lblTurnsSurvived = [CCLabelTTF labelWithString:@"0" fontName:@"Arial" fontSize:22];
lblTurnsSurvived.position = ccp(winSize.width * 0.1, winSize.height * 0.75);
[self addChild:lblTurnsSurvived];
CCLabelTTF *lblUnitsKilledDesc = [CCLabelTTF labelWithString:@"Units Killed:" fontName:@"Arial" fontSize:12];
lblUnitsKilledDesc.position = ccp(winSize.width * 0.1, winSize.height * 0.6);
[self addChild:lblUnitsKilledDesc];
lblUnitsKilled = [CCLabelTTF labelWithString:@"0" fontName:@"Arial" fontSize:22];
lblUnitsKilled.position = ccp(winSize.width * 0.1, winSize.height * 0.55);
[self addChild:lblUnitsKilled];
CCLabelTTF *lblTotalScoreDesc = [CCLabelTTF labelWithString:@"Total Score:" fontName:@"Arial" fontSize:12];
lblTotalScoreDesc.position = ccp(winSize.width * 0.1, winSize.height * 0.4);
[self addChild:lblTotalScoreDesc];
lblTotalScore = [CCLabelTTF labelWithString:@"1" fontName:@"Arial" fontSize:22];
lblTotalScore.position = ccp(winSize.width * 0.1, winSize.height * 0.35);
[self addChild:lblTotalScore];
注意,我们使用了winSize变量进行定位。这很有用,因为它不仅保持了屏幕上的相对位置,而且在为不同屏幕尺寸的多个设备编写代码时也很有帮助(例如,iPhone 4、iPhone 5、iPad 等具有不同的尺寸)。
另一种处理方式是将我们标签的positionType设置为CCPositionTypeNormalized。然后我们可以将位置值设置在0到1之间,其中0代表屏幕的左侧(或底部),而1代表屏幕的右侧(或顶部)。
显示一些文本 - CCLabelBMFont
如果您还不熟悉,让我们回顾一下:BMFonts 是那些美观、风格化的字体,它们可以为您的游戏增添额外的打磨效果,而无需您付出太多努力。请参考第六章,整理与打磨,以提升您游戏的美观度。要创建 BMFont,您必须使用 BMFont 创建器。我们将使用 Glyph Designer,如第一章中提到的,刷新我们的 Cocos2d 知识。
小贴士
如果您已经遵循了前面的 TTF 部分,那么您只需注释掉或删除那些代码行即可,因为我们将在本节中重新制作这些字体,并将它们制作成 BMFont 标签。
我们将要做的第一件事是在场景的左侧创建字体。在 Glyph Designer 打开的情况下,从左侧面板中选择一个字体(我选择了Britannic Bold,这是原型中的字体)。您可以在右侧的设置中调整以获得适合您项目的字体,但请记住,这只是一个原型,您不应该在上面花费太多时间。看看下面的截图:

小贴士
确保字体大小不要太大或太小。如前一个截图所示,它设置为 60。这对于项目来说是一个合适的大小。如果它变得太大或太小,调整它相对简单。
当你对所选择的设置满意时,点击顶部的 另存为,并选择你想要保存 Glyph Designer 文件的位置(而不是实际的字体文件)。现在,如果决定稍后编辑字体,我们已经保存了它,接下来让我们继续导出字体,以便我们可以在 Cocos2d 中使用它。
根据你决定采用的文件读取方式,你需要以两种不同的方式导出字体。确保你遵循相同的风格,因为你可以在同一个项目中只使用一种风格(但不能同时使用两种)。无论你选择哪条路线,都要从你需要的最大字体大小开始;例如,前一个截图显示字体大小为 60,因为它将在 Retina iPad 上显示。如果它只用于 iPhone,60 就太大了。
使用文件后缀保存你的 BMFont
在 Glyph Designer 中,点击顶部的 导出 并导航到项目目录中的 Resources/Published-iOS 文件夹(见以下截图)。这就是你将导出字体以用于 Cocos2d 的位置。请注意,因为这个字体是最大的,用于 Retina 尺寸的 iPad,所以文件名后的后缀是 -ipadhd。如果你不是为 iPad 设计,你最大的文件名后缀将是 -hd。
小贴士
注意,你应该保留 .fnt/.png 扩展名(见以下截图)不变。Glyph Designer 将自动为你添加它。

一旦导出了最大的手机字体,接下来调整每个层级所需的设置。例如,由于我们正在以 60 点字体导出 -ipadhd,我们还想为较小的设备制作 30 点和 15 点大小的字体。除了减小字体大小外,我们还可以修改笔触和阴影设置,以使所有大小看起来相对一致。
总体来说,如果你将你的字体命名为 bmFont,你应该有以下文件(每个 .fnt 文件都将附带一个 .png 文件),其中最大的字体大小为 60:
-
bmFont-ipadhd.fnt - 60-pt -
bmFont-ipad.fnt - 30-pt -
bmFont-hd.fnt - 30-pt -
bmFont.fnt - 15-pt

当使用文件扩展名方法时,只要将你的文件导出到 Published-iOS 文件夹,Xcode 项目就会以包含你的字体在项目中的方式设置。通过这种方式,你不需要担心复制任何内容。话虽如此,让我们开始使用我们刚刚创建的新字体显示标签。
使用目录保存你的 BMFont
如果你选择使用目录,那么将字体文件拖入 SpriteBuilder 并不是那么简单(在撰写本书时)。相反,你必须在项目目录的 Published-iOS 文件夹内创建四个文件夹:
-
resources-phone -
resources-phonehd -
resources-tablet -
resources-tablethd

在 Glyph Designer 中,点击顶部的 导出 并导航到项目目录中的 Published-iOS/resources-tablethd 文件夹(见以下截图)。这就是你将导出用于 Cocos2d 的字体的地方。这里的文件名将是字体的名称。
小贴士
保持 .fnt/.png 扩展名不变(见以下截图)。Glyph Designer 将自动为你添加它。

一旦导出了 tablethd 版本,执行相同的导出操作,但修改字体大小和任何其他你希望修改的设置。例如,由于我们正在导出 tablethd 大小为 60 点的字体,我们还想创建 30 点和 15 点的大小以适应较小的设备。除了减小字体大小外,我们还可以修改描边和阴影设置,以使所有大小看起来相对一致。
因此,总的来说,如果你的字体名为 bmFont,你应该有以下文件(每个 .fnt 文件都将附带一个 .png 文件),其中最大的字体大小为 60:
-
resources-tablethd/bmFont.fnt - 60-pt -
resources-tablet/bmFont.fnt - 30-pt -
resources-phonehd/bmFont.fnt - 30-pt -
resources-phone/bmFont.fnt - 15-pt

当使用目录方法时,如果你已经将字体文件导出到之前提到的文件夹中,Xcode 项目将自动设置以包含这些文件,因此你不需要担心复制任何内容。话虽如此,让我们开始使用我们刚刚创建的新字体来显示标签。
导出 BMFont 并导入到 Xcode
打开 Xcode 并打开 MainScene.h 文件。你将在 CGSize winSize 这一行下面添加以下变量。再次提醒,如果你已经按照前面的 TTF 教程操作,你可以删除或注释掉在那个教程中创建的变量,因为我们在这里将使用相同的变量名。我们再次使用 lbl 来声明变量,这样我们就可以很容易地将其识别为标签:
@interface MainScene : CCNode
{
CGSize winSize;
//the labels used for displaying the game info
//this line now uses CCLabelBMFont instead of CCLabelTTFFont
CCLabelBMFont *lblTurnsSurvived, *lblUnitsKilled, *lblTotalScore;
}
然后打开 MainScene.m 文件,在背景层的代码下面添加以下代码行以显示标签。如果你选择以不同的名称导出你的字体,你必须将 fntFile 参数更改为你选择的名称:
CCLabelBMFont *lblTurnsSurvivedDesc = [CCLabelBMFont labelWithString:@"Turns Survived:" fntFile:@"bmFont.fnt"];
lblTurnsSurvivedDesc.position = ccp(winSize.width * 0.125, winSize.height * 0.8);
[self addChild:lblTurnsSurvivedDesc];
lblTurnsSurvived = [CCLabelBMFont labelWithString:@"0" fntFile:@"bmFont.fnt"];
lblTurnsSurvived.position = ccp(winSize.width * 0.125, winSize.height * 0.75);
[self addChild:lblTurnsSurvived];
CCLabelBMFont *lblUnitsKilledDesc = [CCLabelBMFont labelWithString:@"Units Killed:" fntFile:@"bmFont.fnt"];
lblUnitsKilledDesc.position = ccp(winSize.width * 0.125, winSize.height * 0.6);
[self addChild:lblUnitsKilledDesc];
lblUnitsKilled = [CCLabelBMFont labelWithString:@"0" fntFile:@"bmFont.fnt"];
lblUnitsKilled.position = ccp(winSize.width * 0.125, winSize.height * 0.55);
[self addChild:lblUnitsKilled];
CCLabelBMFont *lblTotalScoreDesc = [CCLabelBMFont labelWithString:@"Total Score:" fntFile:@"bmFont.fnt"];
lblTotalScoreDesc.position = ccp(winSize.width * 0.125, winSize.height * 0.4);
[self addChild:lblTotalScoreDesc];
lblTotalScore = [CCLabelBMFont labelWithString:@"1" fntFile:@"bmFont.fnt"];
lblTotalScore.position = ccp(winSize.width * 0.125, winSize.height * 0.35);
[self addChild:lblTotalScore];
添加这些行后,你应该能够运行游戏并在屏幕左侧看到一些看起来很酷的标签,如下所示(这是在 iPhone 5 上运行的):

如果只是显示文本,那就不算是一个游戏,所以让我们添加一些按钮。但首先,我们必须了解如何使用 TexturePacker 创建精灵表。
如果你决定不使用 TexturePacker,请阅读不使用 TexturePacker – 简要说明部分,并可以自由跳过关于使用 TexturePacker 的精灵表的部分。如果是这样,你也应该使用文件读取的目录方法,因为除了使用像 TexturePacker 这样的自动维护它们的程序之外,文件扩展名几乎没有任何好处。
不使用 TexturePacker – 简要说明
如前所述,如果你选择不使用 TexturePacker,每次提到将其图像添加到精灵表时,那就是你将其添加到 SpriteBuilder 并重新发布的信号,因为它假定你将使用文件读取的目录模式。
要这样做,将 iPad 视网膜分辨率的图像拖入 SpriteBuilder,然后点击发布。SpriteBuilder 将自动缩放。
小贴士
然而,请注意,本书的后续章节中,将提供精灵表(以及如果你想自己完成,也可以提供单个图像)。
开始使用 TexturePacker 的精灵表
Sprite sheets 用于提高游戏性能,不仅减少了游戏加载所需的时间,而且在游戏运行时也能提高性能。
小贴士
不幸的是,截至本书编写时,TexturePacker 与 Cocos2d 兼容,但 SpriteBuilder 并不直接支持其使用。然而,当需要有效地构建精灵表时,TexturePacker 是一个很好的解决方案。如果你希望使用 TexturePacker,但目前正在使用目录方法(截至本书编写时的 SpriteBuilder 默认设置),请返回并更改你的样式为文件扩展名。
如第一章中提到的,刷新我们的 Cocos2d 知识,我们将使用 TexturePacker 作为我们的精灵表创建者的首选。TexturePacker 有几个优点:
-
它允许一键导出到 Cocos2d
-
它具有自动缩放(向上或向下)以支持所有分辨率类型
-
它使得以后更新图像时更容易导入图像
首先,打开 TexturePacker。然后转到Images Pre-Chapter 6文件夹,在那里你会看到btnMenu.png图像(我们的菜单按钮图像)。将其拖入 TexturePacker 的右侧列。它应该看起来像这样:

在更改任何文件位置之前,请确保你做了以下操作:
-
在纹理格式下拉框中,确保已选择PNG。这种格式应该适用于您将要制作的多数游戏。然而,如果您发现自己想要在不牺牲质量的情况下使游戏最终项目的大小更小,建议切换到zlib pvr.ccz 压缩。这是 Cocos2d 最优化格式,不仅适用于每像素的压缩,而且在屏幕上绘制图像时的性能也是最佳的。
-
选中表示预乘 Alpha的复选框。现在不需要完全了解它是如何工作的细节。目前只需知道,在 Cocos2d 中,勾选此选项可以使纹理渲染更快。
保存到项目位置
现在我们已经将图像放入 TexturePacker 中,让我们修改一些设置以确保我们可以有效地管理此精灵表的任何未来版本。点击数据文件文本框旁边的文件夹图标,进入项目的Resources/Published-iOS目录。您可以随意命名文件,但尽量保持相关性。在这个例子中,我们将它命名为buttonSheet,因为它将包含游戏中所有按钮的精灵表。准备好后,点击保存。
小贴士
注意,尽管文件被命名为buttonSheet.plist,但文件名末尾有{v}。这很重要,也是 TexturePacker 为我们进行自动缩放的原因。

至于图像格式,通常保持为RGBA8888是合适的。但是,如果您的游戏在屏幕上有大量艺术资产并且性能不佳,将其更改为较低的设置可能会有所帮助。
调整图像并发布精灵表
现在我们需要确保 TexturePacker 将正确缩放我们需要的内容。点击AutoSD旁边的齿轮图标,然后打开顶部标有预设的顶部下拉框。选择最适合您需求的选项,然后点击应用。
小贴士
如果您只制作 iPhone 游戏(而不是 iPad 版本),选择cocos2d hd/sd。
否则(如果您正在制作 iPad 版本,本书的项目就是这样),选择cocos2d ipad/hd/sd。
最后,在屏幕的左上角,我们点击保存默认设置按钮,因为它允许我们在需要再次创建 TexturePacker 精灵表时保存这些设置。然后点击保存(或按command +S)。这将询问您希望将 TexturePacker 文件(而不是精灵表)保存的位置。通常,您会将与所有其他艺术资产相同的文件夹保存此文件。例如,我们将为项目以及稍后要复制的单个艺术资产创建一个单独的目录。

一旦你在选择的位置保存了 TPS 文件,请继续点击 发布。发布将根据我们之前输入的各种设置生成项目所需的精灵表。
导入精灵表并将其加载到内存中
最后,一旦你在项目目录中(或者实际上任何地方,但最好位于项目目录中以便于稍后更新),Xcode 项目的设置应该会导致它们自动添加到你的项目中。
一旦发布了精灵表,打开 Xcode 并转到 AppDelegate.m 文件。在 startScene 方法中的返回语句上方添加一行代码,使其看起来像这样:
- (CCScene*) startScene
{
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"buttonSheet.plist"];
return [MainScene scene];//[CCBReader loadAsScene:@"MainScene"];
}
现在你已经准备好开始使用精灵表了。设置它花了一些时间,但到目前为止,你所需要做的就是将图像添加到 TexturePacker 文件中,点击 保存,然后点击 发布。你的更改将在你下次运行项目时自动反映出来。话虽如此,让我们通过将菜单按钮图像转换为实际按钮来利用精灵表。
通过 CCButton 和 CCLayout 创建按钮
Cocos2d 3.0 改变了按钮的显示方式。如果你之前使用过 Cocos2d 的早期版本,你可能对 CCMenu 很熟悉。在 Cocos2d 中,这不再是创建和显示可点击按钮的方式。相反,我们将使用 CCButton 并将它们放置在 CCLayout 类型的节点中。如果你跳过了精灵表部分,我强烈建议你回去阅读它。这将帮助你避免在项目进展过程中遇到许多令人沮丧的时刻。
对于本书的项目,我们将在左下角添加菜单按钮。就像我说的,一旦你将图像包含到项目中,添加按钮就极其简单。
打开 MainScene.m 文件,并在 init 方法中的标签代码下方添加这些代码行:
CCButton *btnMenu = [CCButton buttonWithTitle:@""
spriteFrame:[CCSpriteFrame frameWithImageNamed:@"btnMenu.png"]];
btnMenu.position = ccp(winSize.width * 0.125, winSize.height * 0.1);
[self addChild:btnMenu];
当你运行它时,你应该在左下角看到菜单按钮出现。如果你使用了 pvr.ccz 格式,并且按钮水平翻转,不要担心。只需回到 TexturePacker,勾选 Flip PVR 的复选框,保存文件,然后发布。回到 Xcode 并重新运行项目。它应该看起来像这样:

添加重启按钮
根据你使用的方法,将 重启 按钮(在这种情况下,btnRestart.png)添加到 TexturePacker 或 SpriteBuilder 中,点击 保存,然后发布以更新文件,以便 重启 按钮可以使用。添加了 重启 按钮的图像后,你可以修改 MainScene.m 文件的代码,使其看起来像这样:
CCButton *btnMenu = [CCButton buttonWithTitle:@"" spriteFrame:[CCSpriteFrame frameWithImageNamed:@"btnMenu.png"]];
CCButton *btnRestart = [CCButton buttonWithTitle:@"" spriteFrame:[CCSpriteFrame frameWithImageNamed:@"btnRestart.png"]];
CCLayoutBox *layoutButtons = [[CCLayoutBox alloc] init];
[layoutButtons addChild:btnRestart];
[layoutButtons addChild:btnMenu];
layoutButtons.spacing = 10.f;
layoutButtons.anchorPoint = ccp(0.5f, 0.5f);
layoutButtons.direction = CCLayoutBoxDirectionVertical;
[layoutButtons layout];
layoutButtons.position = ccp(winSize.width * 0.125, winSize.height * 0.15);
[self addChild:layoutButtons];
这将使重启按钮和菜单按钮完美对齐。此外,如果你决定移动两个按钮,但希望它们相对于彼此的距离相同,只需重新定位布局框。然后,Voilà!
你可以作为一个快速的学习体验,尝试调整间距值;或者改变方向,甚至锚点。当你尝试测试不同的值时,你会更好地理解为什么每一行代码对于创建这个效果是绝对必要的。
好吧,修改一些值。你总是可以恢复到前面的代码。
到目前为止,使用你刚才看到的初始代码,如果你运行项目,它看起来会是这样:

创建节点和单位(精灵)
记住,Cocos2d 中的所有内容,在其基础层面,都是一个CCNode对象。节点可以有其他节点作为子节点。例如,如果你想创建一个带有喷气背包的角色,角色可以是一个CCSprite对象(一个带有图像的节点对象),喷气背包可以是一个作为角色子节点的CCSprite对象。
无论如何,这是一章关于原型的内容,我们还没有创建任何真正的游戏玩法。让我们通过一些图像、一些触摸控制和更多内容来开始。
设置背景
将背景图像添加到精灵图中(或 SpriteBuilder),保存、发布,然后在MainScene.m文件的init方法中,将图像作为CCSprite对象添加到屏幕上,位于CCLayoutBox代码下方:
CCSprite *board = [CCSprite spriteWithImageNamed:@"imgBoard.png"];
board.position = ccp(winSize.width * 0.625, winSize.height/2);
[self addChild:board];
让我们运行游戏,哎呀!我们似乎遇到了原型中的第一个问题。虽然在这个阶段完全不需要找出所有的错误和问题,但这个问题对于游戏玩法来说很重要。此外,这是一个了解设备特定缩放的好机会。如果你查看以下截图,其中一个是 iPhone 5 拍摄的,另一个是 iPad Retina 拍摄的,你会注意到游戏板在手机上有点太大。以下是 iPhone 5 上的游戏截图:

游戏的 iPad Retina 截图如下:

幸运的是,它并不太离谱,因为从 TexturePacker 或 SpriteBuilder 的自动缩放已经为我们提供了游戏板的相对准确的缩放。我们唯一需要做的是仅对手机上的板进行非常轻微的缩放修改,而不是平板电脑。这可以通过在声明板变量后添加以下代码来完成:
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
board.scale = 0.8;
相反,如果你想检测 iPad,只需使用UIUserInterfaceIdiomPad即可。
现在,如果你在手机上运行它(无论是你的手机还是模拟器),你会看到网格很好地位于屏幕边界内。
定义并添加屏幕上的单位
由于每个单位基本上是相同的,只是颜色和数字不同,我们应该为自己定义一个类。为此,我们遵循有关新场景的相同说明,但这次,我们将调用Unit类并将子类设置为CCSprite类型。

打开 Unit.h 文件,使其看起来如下:
#import "CCSprite.h"
NS_ENUM(NSInteger, UnitDirection)
{
DirUp,
DirDown,
DirLeft,
DirRight,
DirStanding //for when a new one spawns at the center
};
@interface Unit : CCSprite
@property (nonatomic, assign) NSInteger unitValue;
@property (nonatomic, assign) BOOL isFriendly;
@property (nonatomic, assign) enum UnitDirection direction;
//9x9 grid, 1,1 is top left, 9,9 is bottom right
@property (nonatomic, assign) CGPoint gridPos;
@property (nonatomic, strong) CCColor *color;
@property (nonatomic, strong) CCLabelBMFont *lblValue;
+(Unit*)friendlyUnit;
+(Unit*)enemyUnitWithNumber:(NSInteger)value atGridPosition:(CGPoint)pos;
@end
这基本上允许我们给我们的单位一个移动方向。我们还得到了与它们相关的一个值,一个布尔值用来确定它是否是友军单位(这对于移动和碰撞都是必需的),以及各种其他内容。
现在,打开 Unit.m 文件,并在 @implementation 和 @end 之间添加以下代码:
+(Unit*)friendlyUnit
{
return [[self alloc] initWithFriendlyUnit];
}
+(Unit*)enemyUnitWithNumber:(NSInteger)num atGridPosition:(CGPoint)pos
{
return [[self alloc] initWithEnemyWithNumber:num atPos:pos];
}
-(id)initCommon
{
if ((self=[super initWithImageNamed:@"imgUnit.png"]))
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
self.scale = 0.8;
self.lblValue = [CCLabelBMFont labelWithString:@"1" fntFile:@"bmFont.fnt"];
self.lblValue.scale = 1.5;
self.lblValue.position = ccp(self.contentSize.width/2, self.contentSize.height/1.75);
[self addChild:self.lblValue];
}
return self;
}
-(id)initWithFriendlyUnit
{
if ((self=[self initCommon]))
{
self.isFriendly = YES;
self.unitValue = 1;
self.direction = DirStanding;
self.color = [CCColor colorWithRed:0 green:0.8f blue:0]; //green for friendly
self.gridPos = ccp(5,5);
}
return self;
}
-(id)initWithEnemyWithNumber:(NSInteger)num atPos:(CGPoint)p
{
if ((self=[self initCommon]))
{
self.isFriendly = NO;
self.unitValue = num;
self.lblValue.string = [NSString stringWithFormat:@"%ld", (long)num];
self.direction = DirLeft;
self.color = [CCColor colorWithRed:0.8f green:0 blue:0]; //red for enemy
self.gridPos = p;
}
return self;
}
init 方法设置了一些重要内容:网格上的位置、颜色、是否为友军单位、单位首次生成时的值、显示值的标签以及它打算在下个回合移动的方向。
让我们打开 MainScene.m 文件,并在屏幕上生成一个友军单位和敌军单位。因为我们定义了类非常出色,所以只用几行代码就可以生成两个单位。确保你也在顶部包含了 Unit.h 文件:
Unit *friendly = [Unit friendlyUnit];
friendly.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:friendly];
Unit *enemy = [Unit enemyUnitWithNumber:1 atGridPosition:ccp(1,1)];
enemy.position = ccp(winSize.width - 50, winSize.height/2);
[self addChild:enemy];

然而,位置仍然需要计算,而我们分配的网格坐标对游戏来说没有任何意义。我们需要确定屏幕上的实际位置。也就是说,如果我们说位置是 (5, 5),它最好知道这意味着正正好在网格的正中央。然而,唯一知道屏幕坐标的地方是主场景,所以打开 MainScene.m 文件,并添加以下方法以根据网格坐标获取屏幕位置:
-(CGPoint)getPositionForGridCoord:(CGPoint)pos
{
CGPoint screenPos;
Unit *u = [Unit friendlyUnit];
CGFloat borderValue = 1.f;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
borderValue = 0.6f;
screenPos.x = winSize.width * 0.625 + (u.boundingBox.size.width + borderValue) * (pos.x-5);
screenPos.y = winSize.height/2 - (u.boundingBox.size.width + borderValue) * (pos.y-5);
return screenPos;
}
Now change the positioning of the units to reflect this change:
Unit *friendly = [Unit friendlyUnit];
friendly.position = [self getPositionForGridCoord:friendly.gridPos];
[self addChild:friendly];
Unit *enemy = [Unit enemyUnitWithNumber:2 atGridPosition:ccp(4,7)];
enemy.position = [self getPositionForGridCoord:enemy.gridPos];
[self addChild:enemy];
看看下面的截图;这就是你的游戏现在的样子:

运行游戏应该会得到前面截图所示的内容。是的,尽管这个截图来自 iPad,但代码在任意分辨率的 iPhone 上同样有效,因为确定位置的公式是基于 (5, 5) 是网格中心的假设。
现在,让我们通过一些触摸机制让这些单位在屏幕上移动。首先,将红色单位放在网格最右侧的绿色单位的右侧——(9, 5),对于那些懒惰且不想计算的人来说。
使用触摸控制移动单位
在 Cocos2d 中进行触摸检测非常简单。从版本 3.3 开始,你只需要添加一行代码和一些方法,就可以完成了。
话虽如此,向 Unit 类的 initWithFriendlyUnit 方法添加以下代码行(我们希望在友军单位上启用触摸,而不是在敌人单位上):
[self setUserInteractionEnabled:YES];
Then add the following methods in Unit.m that will intercept all touches made on each unit:
-(void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
}
-(void)touchMoved:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
}
-(void)touchEnded:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
}
这些方法在描述它们的功能方面相当直观。touchBegan 方法在手指触摸屏幕时注册一个触摸,touchMoved 方法在手指沿屏幕拖动时注册一个触摸,而 touchEnded 方法在手指从屏幕上抬起时注册。
为了确定被拖动的单位及其方向,将以下三个变量添加到 Unit.h 文件中:
@property (nonatomic, assign) BOOL isBeingDragged;
@property (nonatomic, assign) CGPoint touchDownPos;
@property (nonatomic, assign) enum UnitDirection dragDirection;
还需要添加以下方法声明:
-(void)updateLabel;
一旦完成,打开 Unit.m 并向以下方法添加代码。
首先,你必须创建此方法,它将设置单位标签显示的字符串为单位的实际值:
-(void)updateLabel
{
self.lblValue.string = [NSString stringWithFormat:@"%ld", (long)self.unitValue];
}
然后,我们需要处理我们的触摸以更新标签,因此需要在 touchBegan 方法中添加以下代码,这将获取 CCTouch 方法相对于给定节点的位置的坐标。目前,我们想知道触摸相对于 Unit 本身的位置,并将其设置在我们的 touchDownPos 变量中:
self.touchDownPos = [touch locationInNode:self];
self.dragDirection = DirStanding;
然后,你必须向 touchMoved 方法添加以下代码。这将根据 touchDownPos 和当前 touchPos 变量的 x 和 y 差异来确定手指被拖动的方向:
CGPoint touchPos = [touch locationInNode:self];
//if it's not already being dragged and the touch is dragged far enough away...
if (!self.isBeingDragged && ccpDistance(touchPos, self.touchDownPos) > 6)
{
self.isBeingDragged = YES;
CGPoint difference = ccp(touchPos.x - self.touchDownPos.x, touchPos.y - self.touchDownPos.y);
//determine direction
if (difference.x > 0)
{
if (difference.x > fabsf(difference.y))
self.dragDirection = DirRight;
else if (difference.y > 0)
self.dragDirection = DirUp;
else
self.dragDirection = DirDown;
}
else
{
if (difference.x < -1* fabsf(difference.y))
self.dragDirection = DirLeft;
else if (difference.y > 0)
self.dragDirection = DirUp;
else
self.dragDirection = DirDown;
}
}
最后,将此代码段添加到 touchEnded 方法中。这实际上将根据单位被拖动的方向更新单位的网格位置:
//if it was being dragged in the first place
if (self.isBeingDragged)
{
CGPoint touchPos = [touch locationInNode:self];
//stop the dragging
self.isBeingDragged = NO;
if (ccpDistance(touchPos, self.touchDownPos) > self.boundingBox.size.width/2)
{
NSInteger gridX, gridY;
gridX = self.gridPos.x;
gridY = self.gridPos.y;
//move unit that direction
if (self.dragDirection == DirUp)
--gridY;
else if (self.dragDirection == DirDown)
++gridY;
else if (self.dragDirection == DirLeft)
--gridX;
else if (self.dragDirection == DirRight)
++gridX;
//keep within the grid bounds
if (gridX < 1) gridX = 1;
if (gridY > 9) gridX = 9;
if (gridY < 1) gridY = 1;
if (gridY > 9) gridY = 9;
//if it's not in the same place... aka, a valid move taken
if (!(gridX == self.gridPos.x && gridY == self.gridPos.y))
{
self.gridPos = ccp(gridX, gridY);
self.unitValue++;
self.direction = self.dragDirection;
[self updateLabel];
}
}
}
现在,如果你运行游戏,你会看到当你轻触(或在模拟器上运行时点击)并拖动该单位……哦,我的天!为什么单位没有移动?我们设置了网格坐标,并且一切正常!甚至单位的值也在正确增加。
但是,我们没有告诉主场景单位需要移动,因为单位定位是在那里发生的。话虽如此,我们想要一种方式让我们的主场景知道单位已经被移动,这样我们就可以更新其位置。
场景间的通信
做这件事的一个非常常见的方法是利用 NSNotificationCenter。它分为两部分:发送者和接收者。发送者被称为通知,接收者被称为观察者。我们需要通过 NSNotificationCenter 发送一个通知,以便任何设置的观察者都能接收到该通知。
首先,我们需要声明一个常量以减少编码时的人为错误。我们这样做是因为通知需要精确,否则它们将不起作用。
所以,打开 Unit.h 并在 #import 语句下方但 NS_ENUM 语句上方添加此行代码:
FOUNDATION_EXPORT NSString *const kTurnCompletedNotification;
然后,在 Unit.m 的顶部,在 #import 语句下方但 @implementation 语句上方,插入以下代码行:
NSString *const kTurnCompletedNotification = @"unitDragComplete";
字符串是什么并不完全重要;只是它必须与其他你后来创建的通知中的任何内容都不同。
然后,在 Unit.m 的 touchEnded 方法中 [self updateLabel] 行下面添加以下代码行:
//pass the unit through to the MainScene
[[NSNotificationCenter defaultCenter] postNotificationName:kTurnCompletedNotification object:nil userInfo:@{@"unit" : self}];
这将向观察者发送一个通知,表示发生了某些事情。在这种情况下,我们想要通知主场景当前单位已被拖动并且需要更新其位置。这就是为什么我们传递 self(当前 Unit)——这样我们就可以更新被移动的具体单位的位置。
最后,让我们跳转到 MainScene.m 并在 init 方法的底部(或顶部;由你选择)添加以下代码:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moveUnit:) name:kTurnCompletedNotification object:nil];
然后添加以下方法:moveUnit和dealloc。moveUnit方法是我们希望在通知被推送时调用的方法。我们有一个NSNotification参数,它获取从Unit类传递过来的NSDictionary参数。我们还需要dealloc来移除观察者,否则它可能会意外地捕获未来的通知,这可能导致游戏崩溃:
-(void)moveUnit:(NSNotification*)notif
{
NSDictionary *userInfo = [notif userInfo];
Unit *u = (Unit*)userInfo[@"unit"];
u.position = [self getPositionForGridCoord:u.gridPos];
}
-(void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
哈喽!现在当你运行代码时,你应该可以看到单位在每个滑动中从网格位置移动到网格位置,任何方向都可以。现在它开始感觉像是一款游戏了。然而,它仍然缺少与敌人的互动,并且没有任何分数被增加。让我们在下一部分添加这些功能。
与敌人互动和得分
首先,我们需要一些变量来跟踪数字。所以,在MainScene.h文件中,在 BMFont 标签变量下添加以下内容:
NSInteger numTurnSurvived, numUnitsKilled, numTotalScore;
在MainScene.m文件中,添加以下方法来更新每个计数器的标签:
-(void)updateLabels
{
lblTotalScore.string = [NSString stringWithFormat:@"%ld", (long)numTotalScore];
lblTurnsSurvived.string = [NSString stringWithFormat:@"%ld", (long)numTurnSurvived];
lblUnitsKilled.string = [NSString stringWithFormat:@"%ld", (long)numUnitsKilled];
}
将以下行添加到moveUnit方法中以增加相应的数字:
++numTurnSurvived;
++numTotalScore;
[self updateLabels];
然后在init方法中的某个地方初始化变量:
numTotalScore = 1;
numTurnSurvived = 0;
numUnitsKilled = 0;
现在,你会注意到,每次你的单位移动时,分数和turns survived标签都会增加 1。但是单位仍然可以直穿敌人单位。让我们修复这个问题。
首先,我们需要比较移动后的网格位置与屏幕上所有可能的敌人,以查看是否发生了碰撞。然而,如果我们打算遍历所有敌人,这意味着我们需要一个数组来保存它们,因此我们在MainScene.h文件中声明了一个NSMutableArray参数:
NSMutableArray *arrEnemies;
在MainScene.m文件中init方法的底部,在你生成敌人之后,添加以下代码行:
arrEnemies = [[NSMutableArray alloc] init];
[arrEnemies addObject:enemy];
在moveUnit方法的末尾,我们需要遍历所有敌人并检查网格位置是否相同(即,我们是否即将遇到敌人):
//for each Unit in the arrEnemies array...
for (Unit *enemy in arrEnemies)
{
if (enemy.gridPos.x == u.gridPos.x &&
enemy.gridPos.y == u.gridPos.y)
{
//collision!
}
}
在碰撞注释下,我们现在想要比较单位值。值较高的单位将获胜,值较低的单位将从板上(和数组中)移除。在平局的情况下,双方都将被移除。在这里更新unitsKilled计数器也很重要:
{
//collision!
NSInteger enemyVal = enemy.unitValue;
NSInteger friendVal = u.unitValue;
//tie, both dead
if (enemyVal == friendVal)
{
[self removeChild:u];
[arrEnemies removeObject:enemy];
[self removeChild:enemy];
++numUnitsKilled;
}
//enemy higher
else if (enemy.unitValue > u.unitValue)
{
enemy.unitValue -= friendVal;
[enemy updateLabel];
[self removeChild:u];
}
//friendly higher
else
{
u.unitValue -= enemyVal;
[u updateLabel];
[arrEnemies removeObject:enemy];
[self removeChild:enemy];
++numUnitsKilled;
}
//exit the for loop so no "bad things" happen
break;
}
最后,将[self updateLabels]方法调用在moveUnit方法中移动到方法的末尾(即,在循环结束后),否则unitsKilled标签不会在下一轮更新,这可能会让玩家感到困惑。
就这样!你可以四处移动,遇到敌人,合并你的分数,更新标签,甚至如果你输了(通过转到菜单并点击播放按钮)可以重新开始游戏。到目前为止,游戏大致看起来是这样的。
在 iPad 上,游戏场景如下所示:

在 iPhone 5 上,游戏场景如下所示:

创建菜单、场景和场景转换
场景是 Cocos2d 的核心。当你从主菜单转到关卡选择屏幕时,那些(当以最佳实践的方式进行编码时)是两个不同的场景。你可以以任何你想要的方式从一个场景切换到另一个场景。然而,通常是通过点击按钮来完成的。例如,播放、设置和商店都是用户可能会按下来触发场景切换的菜单按钮的例子。
为场景创建新文件
很可能你的游戏不会只有一个屏幕。如果是这样,你可以快速浏览这部分内容,因为它可能与你无关。然而,绝大多数游戏至少都有一个主菜单、设置菜单、暂停屏幕以及除了主游戏屏幕之外的一些屏幕。
接下来的几幅截图展示了如何以CCNode对象作为父类创建文件。如果你已经知道如何操作,可以直接跳到下一步。
在 Xcode 中,在项目导航器顶部的源文件夹上右键单击(或按Ctrl并单击),然后点击新建文件,如图所示:

在打开的对话框中,在 iOS 源部分下,选择Cocoa Touch 类并点击下一步,如图所示:

给类起一个相关的名字,因为你可能需要稍后返回它。我们将使用MenuScene作为我们类的名字。一旦命名了类,点击下一步。
小贴士
确保将子类为改为CCNode,否则你将无法从它创建新的场景。

谈到保存文件的位置,建议将所有项目类文件保存在同一目录下。如图所示,我们将要创建的MenuScene文件保存在Source文件夹中,其中也存放着MainScene文件。
一旦选择了位置,点击创建,如图所示:

将类转换为官方 CCScene 子类
它还不是官方的场景,但这是我们接下来要添加的。在你刚刚创建的类的头文件中,在@interface和@end行之间添加一个类似的代码块——就像我们在MainScene.m文件中有的那样。例如,你的头文件可能看起来像这样:
@interface MenuScene : CCNode
{
CGSize winSize;
}
+(CCScene*)scene;
@end
然后,在类的主体文件中,在@implementation和@end行之间添加以下内容(是的,每个创建的场景都需要复制粘贴):
+(CCScene *)scene
{
return [[self alloc] init];
}
-(id)init
{
if ((self=[super init]))
{
}
return self;
}
在此设置完成后,你现在可以开始向主菜单场景添加代码。让我们快速给init方法添加一个背景颜色,以便我们知道当最终链接按钮并切换到它时场景是否工作。我们将给它一个随机的绿色颜色,因为默认颜色是黑色:
-(id)init
{
if ((self=[super init]))
{
//these values range 0 to 1.0, so use float to get ratio
CCNode *background = [CCNodeColor nodeWithColor:[CCColor colorWithRed:58/255.f green:138/255.f blue:88/255.f]];
[self addChild:background];
}
return self;
}
在游戏中链接按钮以跳转到菜单
打开MainScene.m,并将你创建的场景包含在文件顶部:
#import "MainScene.h"
#import "MenuScene.h" //the line to add. Note: it says MENU scene, not MAIN scene. They're similar, but different. We want both here.
@implementation MainScene
然后转到你声明菜单按钮的代码部分。在你声明之后,添加以下代码行。这将连接菜单按钮到名为goToMenu的方法。setTarget方法是CCButton知道在点击时应该做什么的方式:
[btnMenu setTarget:self selector:@selector(goToMenu)];
然后,在init方法下面添加goToMenu方法,如下所示:
-(void)goToMenu
{
[[CCDirector sharedDirector] replaceScene:[MenuScene scene]];
}
添加这三样东西后,你应该能够运行游戏并点击菜单按钮。哇!我们有了到Menu场景的过渡,尽管目前看起来相当丑陋。但是没有办法回到游戏屏幕,所以让我们添加一个播放按钮来实现这一点。
在菜单中创建和链接按钮以跳转到游戏
现在我们能够到达菜单场景了,让我们快速添加一个播放按钮,这样我们就可以开始创建游戏的核心玩法并完善这个原型。
首先,将播放按钮添加到 TexturePacker 中,保存并发布。然后打开MenuScene.m,并在init方法中添加以下内容:
winSize = [CCDirector sharedDirector].viewSize;
CCButton *btnPlay = [CCButton buttonWithTitle:@"" spriteFrame:[CCSpriteFrame frameWithImageNamed:@"btnPlay.png"]];
btnPlay.position = ccp(winSize.width/2, winSize.height/2);
[btnPlay setTarget:self selector:@selector(goToGame)];
[self addChild:btnPlay];
还在init方法下面添加goToGame方法,以便按钮实际上有一个可以调用的方法:
-(void)goToGame
{
[[CCDirector sharedDirector] replaceScene:[MainScene scene]];
}
运行项目并点击菜单按钮。你应该看到一个可点击的播放按钮,它会带你回到游戏。耶,场景过渡!现在播放按钮已经就位,并且我们有了场景之间的基本布局,我们可以开始着手游戏的核心。
你还可以做的一件事是创建一个restartGame方法,将重启按钮的目标设置为self,并将重启按钮的选择器设置为restartGame方法。在你创建的restartGame方法内部,只需调用replaceScene方法(就像你刚才做的那样),但这次使用MainScene而不是MenuScene,以便场景过渡到一个全新的版本。这是一个好主意,因为这是我们在这里试图实现的最小代码。
接下来要去哪里?
很明显,在这个阶段,项目远未完成。然而,有很多事情是游戏实现的核心:拖动、得分和网格形成。从现在开始,最好继续迭代项目,并逐渐添加内容,直到它成为一个功能齐全的原型,具有非常基本的基线机制。例如,我们可以添加一些敌人生成、人工智能(AI)、用户单位自动移动等等。
但就这本书而言,我们将继续前进,因为本章的目的是直接进入原型。这基本上就是我们在这里所拥有的东西——我们可以向我们的朋友和家人展示的东西,并说:“嘿,这就是概念,这是我目前所拥有的。”通过原型,你可以评估以下方面:
-
游戏板对玩家的手指来说太小了吗?
-
这个概念是否过于复杂?
-
与角色交互是否困难?
随着时间的推移,所有这些都会被整理好,但最好是尽早了解最大的问题,而不是在游戏发布到 App Store 并注意到没有人下载它之后才发现。
一些建议
如果你正在跟随本书的教程/示例项目(我希望你在这样做),请尝试自己添加以下内容,所有这些都将添加到本书之外,用于巩固:
-
每回合自动移动红色单位并增加其分数
-
在边界周围每三或四个回合生成一个红色单位,并将其添加到数组中(并且可能像处理上一个红色单位那样实现移动)
-
当你从(5,5)位置移开时,生成一个值为
1的另一个友好单位 -
创建和维护一个友好的单位数组
-
让所有友好的单位移动到它们最后被指示的方向
如果你不想花时间自己实现这些,不要担心。后面的章节将预先实现它们,你可以在开始那一章之前下载源代码以获取最新版本。
然而,强烈建议你自己尝试编码,因为这就是这本书的全部目的——推动你作为一个程序员更进一步。教程在这里是为了提供支持,但主要目的是展示一些酷炫的东西,并让你利用手头的工具自由发挥。
摘要
如本章所示,游戏的原型可以相对快速地完成(与放入的内容相比,页面并不多)。如果你有一个大规模的游戏(这几乎肯定是这样),现在就是创建其他场景、添加链接场景的按钮、创建游戏中的角色以及甚至为游戏的核心添加一些基本代码的最佳时机。
通常,快速原型和频繁迭代的办法是先获取最简单的部分,先绘制出项目的线框图,这样持有原型的人就可以用他们的想象力来填补空白(或者如果你给他们一个几乎完成的项目,他们就不需要填补任何空白)。这就像在纸上画人一样。首先,你画出他们身体位置的草图,然后填充一点肌肉和脂肪,最后绘制细节,如手指、衣服、面部表情等。
在下一章中,我们将深入探讨如何创建一些真正酷炫的机制,并使用 Cocos2d 做大多数开发者不做的事情。
第三章。专注于物理
本章是为那些想要将物理融入游戏中的您而准备的。无论您是在构建一个使用逼真墙壁弹跳的迷你高尔夫游戏,还是一个具有无尽重力的平台游戏,本章都是为您准备的。它将提供有关游戏物理方面的教程,并展示如何在没有重力的情况下使用物理引擎。
在本章中,我们将涵盖以下主题:
-
Chipmunk 的工作原理
-
设置项目和创建基本对象
-
通过倾斜设备设置重力
-
在 Chipmunk 中处理碰撞
注意
您必须仅使用 Chipmunk 进行碰撞检测(而不是物理)。并非所有游戏都需要(或甚至应该考虑使用)物理引擎。有时,最好将其排除在外。然而,如果您觉得您的游戏将更加精致或生产速度更快,那么请务必使用它。话虽如此,本书中的项目将不需要物理引擎。因此,我们不会在这里遵循项目,而是将创建一个小项目,其中包含许多模块化示例,这些示例可以适应您的其他项目。本书的主要项目将在下一章继续。
您可能已经习惯了使用 Box2D 物理引擎,但自从 Cocos2d 的 3.0 版本以来,开发者不再提供任何支持,使得 Box2D 无法像之前版本那样直接工作。话虽如此,本章将专注于 Chipmunk。如果本章没有涵盖您在物理方面的所有需求,请随时查看chipmunk-physics.net/documentation.php的文档。它还提供了各种在线教程。
学习 Chipmunk 的工作原理
如前所述,Chipmunk 是与 Cocos2d 集成的物理引擎,并且是从 3.0 版本开始的主要物理引擎。对于新人和 Box2D 的粉丝来说,好消息是 Chipmunk 非常易于使用。让我们来看看 Chipmunk 基本上是如何工作的。
Chipmunk 的整体结构
Chipmunk 是 Cocos2d 中的一个物理引擎,它模拟现实世界的物理,即利用重力、碰撞、物体相互弹跳等。
Chipmunk 使用“世界中的物体”的方式来处理事情。这意味着,如图所示,有一个物理模拟正在进行(称为世界),并且任何适用于物理的物体都是一个物体。您只需创建一个世界,它将在其中的物体上模拟物理,然后就可以开始了。您创建的每个世界都将有自己的重力附加到它。
这是对世界中物体的简单表示。请注意,整个绿色矩形是世界,而单个正方形是其中的物体。

每个物体都有一个类型(下一节将解释)以及密度、质量、摩擦、弹性、速度等属性。在 Cocos2d 中,你可以用一行代码将物理体附加到精灵上,精灵将移动到身体所在的位置。
当这些物体的边界相互接触/相交时,就发生了碰撞。当发生碰撞时,你可以按自己的意愿处理它。
物体的类型
Chipmunk 有三种类型的物理体可以添加到世界中。它们是静态的、动态的和运动的:
-
静态体:这些是墙壁、地面、不可移动的岩石和游戏中的其他物体。它们不会受到任何重力或其他试图与之交互的力的作用。
-
动态体:创建
CCPhysicsBody对象时的默认设置。这些是会四处飞溅、与其他物体碰撞并受到力作用的物体。 -
运动学体:这是一种混合体类型,不能受到力或重力的作用,但仍然可以通过
CCActions和其他方法移动。
通常,你只会使用静态和动态体(本章也是如此)。如果你觉得需要更多关于运动学体类型的帮助,请查看 Cocos2d 关于物理体的文档,网址为 www.cocos2d-swift.org/docs/api/Constants/CCPhysicsBodyType.html。
现在你已经从技术角度了解了 Chipmunk 的工作原理,让我们实际开始编码,以便我们可以亲自看到这些物理体。
设置项目和创建基本对象
Chipmunk 物理引擎在 Cocos2d 库中集成得相当好。它可以在 SpriteBuilder 中工作,也可以在 Cocos2d 中以编程方式工作。与本书的其余部分一样,我们将专注于使用代码创建项目,并且只使用 SpriteBuilder 作为项目创建的工具。因此,在 SpriteBuilder 中创建一个新的项目并发布它。如果你忘记了如何做,请随时回到第一章中参考项目创建。
为使用物理设置 Cocos2d
首先,我们需要打下基础,以便我们可以开始使用它进行编码,因为发布的 SpriteBuilder 项目使用的是 SpriteBuilder 文件,而不是实际的编码场景。类似于上一章,打开 AppDelegate.m 并在文件顶部添加以下代码行:
#import "MainScene.h"
然后,在 AppDelegate.m 文件的 startScene 方法中,用以下代码替换已经存在的那一行代码:
return [MainScene scene];
在此基础上,打开 MainScene.h 并添加代码,使你的文件看起来像这样(确保将 CCNode 继承改为 CCScene,否则一些后续的方法将无法工作):
@interface MainScene : CCScene {
CGSize winSize;
}
+(CCScene*)scene;
@end
最后,打开 MainScene.m 并在 @implementation 和 @end 行之间添加以下代码块:
+(CCScene*)scene
{
return [[self alloc] init];
}
-(id)init
{
if ((self=[super init]))
{
winSize = [[CCDirector sharedDirector] viewSize];
//these values range 0 to 1.0, so use float to get ratio
CCNode *background = [CCNodeColor nodeWithColor:[CCColor colorWithRed:58/255.f green:138/255.f blue:88/255.f]];
[self addChild:background];
}
return self;
}
如果你在此时运行项目(在任何模拟器或设备上),你会看到一个全屏的绿色屏幕。如果没有,请返回并确保你已按照指示复制了所有代码。如果你看到了绿色,那么你已经准备好进入下一部分。
为物理存在构建一个世界
Cocos2d 只是一个图形引擎,我们需要创建一个 Chipmunk 物理模拟环境,以便我们可以使用这个库。这听起来比实际情况要复杂得多。基本上,我们创建一个 CCPhysicsNode 对象,然后把我们的小精灵和节点添加到这个对象中,而不是 self。
小贴士
记住,self 是对当前对象的引用。在过去,我们使用 [self addChild:] 将对象添加到屏幕上,但使用 CCPhysicsNode 对象,我们将使用 [world addChild:]来添加对象,因为world将是我们CCPhysicsNode` 对象的名称。
因此,打开 MainScene.h 文件,并在 winSize 声明下方添加对 world 变量的声明:
CCPhysicsNode *world;
然后打开 MainScene.m 文件,并在 init 方法中背景创建之后添加以下代码块:
//create the physics simulation world
world = [CCPhysicsNode node];
world.debugDraw = YES;
world.gravity = ccp(0, -300);
[self addChild:world];
就这样!前面的代码将创建一个允许物理模拟的物理世界。我们接下来需要做的是创建一些 CCNode 对象,将这些物理体添加到 CCNode 对象中,然后将 CCNode 对象添加到物理世界中(而不是 self)。
小贴士
下载示例代码
你可以从你在 www.packtpub.com 的账户下载示例代码文件,以获取你购买的所有 Packt Publishing 书籍。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给你。
将 debugDraw 设置为 YES 确保每次我们将任何物理体附加到 CCNode 对象并添加到世界中时,我们都会在我们的对象上绘制该体的轮廓。因此,我们将确切知道每个体的位置以及它是如何碰撞的。通常,当 debugDraw 未开启时,我们不会看到这些形状。
无论你是否开启了 debugDraw,碰撞效果都会相同。主要目的是调试项目,确保物理体被添加并按照预期进行碰撞。
启用触摸创建我们的对象
现在我们需要在屏幕上放置一些想要相互碰撞的对象。我们不是通过编程生成对象,而是让对象在用户触摸屏幕的任何位置生成。
因此,打开 MainScene.m 文件,并在 init 方法中的任何位置添加以下代码行。这将允许你从用户那里获取任何触摸数据:
[self setUserInteractionEnabled:YES];
然后,在init方法下方,我们添加这个方法,这样我们就可以在我们的场景中开始接收触摸。这个方法(如前一章所示)将捕获任何触摸事件,我们想要捕获触摸的位置,以便我们可以相应地定位对象。然后我们将在触摸位置生成一个黑色方块:
-(void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
CGPoint touchPos = [touch locationInNode:self];
//create a black square
CGFloat width = winSize.width * 0.1f;
CCNode *square = [CCNodeColor nodeWithColor:[CCColor colorWithRed:0
green:0 blue:0] width:width height:width];
square.position = touchPos;
square.anchorPoint = ccp(0.5f,0.5f);
[world addChild:square];
}
这将创建一个占屏幕宽度的 10%的节点并将其添加到world对象中。我们将它添加到world而不是self,因为我们添加物理体到方块时,希望模拟处理对象的运动。我们需要设置节点的anchorPoint,因为默认情况下,CCNode的锚点在(0,0)。
如果你此时运行游戏,你会看到相同的绿色屏幕。当你触摸屏幕时,黑色方块会在触摸位置出现。
然而,你会注意到,尽管我们已经将它们添加到世界对象中,但它们并没有按照我们设置的引力下落,它们也没有相互碰撞和推动。我们接下来要做的就是创建一个CCPhysicsBody对象,并将其分配给创建的方块,以便物理模拟可以正确处理运动。
小贴士
如果你还不确定CCPhysicsBody究竟是什么,它本质上是一个具有形状或周界的对象,这定义了对象可以与哪些其他对象发生碰撞。CCPhysicsBody还具有其他属性,如弹性、恢复系数、密度等,并且最好将其附加到CCNode对象上,以便节点可以由物理体处理其运动。
使物体下落——添加 CCPhysicsBody
在MainScene.m文件中,将以下代码添加到touchBegan方法的底部。这将添加我们想要的物理体到方块中:
//add a physics body to the black square
CCPhysicsBody *squareBody = [CCPhysicsBody bodyWithRect:CGRectMake(0, 0, width, width) cornerRadius:0];
squareBody.elasticity = 0.5f;
square.physicsBody = squareBody;
在这里放置好代码后,当你运行游戏时,你会注意到物体不仅开始下落,而且还有之前提到的调试方块。注意,我们不需要声明为动态的物理体类型,因为这是默认设置。还要注意弹性(你可能还记得,这是弹跳性),设置为 0.5。这是为了确保方块不会到处弹跳。如果我们想要这样,我们会将弹性设置为更高的数字,比如 1.0。
但是哦不!方块就像无底洞一样从屏幕上掉落。让我们来修复这个问题。
添加地面和墙壁
创建地面对象很容易,但由于墙壁和天花板基本上是同一件事,让我们定义一个方法,它将接受CGRect并为我们创建一个具有这些尺寸的静态、不可见节点。
因此,在MainScene.m的任何地方,添加以下方法:
-(void)addWallWithRect:(CGRect)rect
{
CCPhysicsBody *wallBody = [CCPhysicsBody bodyWithRect:rect cornerRadius:0];
wallBody.type = CCPhysicsBodyTypeStatic;
wallBody.elasticity = .5f;
CCNode *wall = [CCNode node];
wall.physicsBody = wallBody;
[world addChild:wall];
}
这段代码与方块的代码非常相似,除了一个明显的区别:物理体的类型。我们将其设置为static,因为我们不希望重力影响它,也不希望方块以任何方向推动地面。
仅因为我们声明了一个方法,并不意味着它会自动为我们生成墙壁。所以,去你的init方法中,添加以下代码块来在设备的边缘添加墙壁:
/***** Add Ground, Walls, and Ceiling *****/
//ground
[self addWallWithRect:CGRectMake(0, 0, winSize.width, 1)];
//left wall
[self addWallWithRect:CGRectMake(0, 0, 1, winSize.height)];
//right wall
[self addWallWithRect:CGRectMake(winSize.width, 0, 1, winSize.height)];
//ceiling
[self addWallWithRect:CGRectMake(0, winSize.height, winSize.width, 1)];
这段代码在定位方面相当直观。每个 Cocos2d 场景的左下角是(0,0),CGRectMake函数接受x和y坐标,然后是矩形的宽度和高度。
这就是创建一个具有一些物理作用的简单对象的所有内容!请随意调整一些变量,如弹性、重力等。
小贴士
到目前为止,如果你注意到你的物体在屏幕上移动时似乎有些滞后——即使 Xcode 显示游戏正在以 60 FPS 运行——请在AppDelegate.m中的startScene方法顶部添加以下行代码:
[CCDirector sharedDirector].fixedUpdateInterval = 1.0f/120.f;
通过倾斜设备设置重力
当你的用户倾斜设备时,设备的加速度计会在最轻微、最微小的值上捕捉到这些信息。这对希望在游戏中使用加速度计(或倾斜)的人来说是有益的,尤其是在移动角色或操纵重力方面,这是我们将在本节中讨论的内容。
自从 iOS 5.0 以来,UIAccelerometer 已被 Core Motion 框架数据所取代。幸运的是,它并不复杂,让我们开始吧。
小贴士
如果你不是注册的开发者,无法使用实际设备进行测试(并且一直在模拟器上这样做),请注意,除非数据可以发送到模拟器,否则无法使用模拟器测试加速度计。这里有一个在这种情况下可能有用的链接:www.vimov.com/isimulate/。
然而,即使默认情况下你无法在模拟器上测试加速度计,它仍然会在任何设备上按预期工作,所以如果你计划在你的游戏中包含加速度计,请随意继续操作。
设置加速度计
打开MainScene.h并导入 Core Motion 框架:
#import <CoreMotion/CoreMotion.h>
然后为名为CMMotionManager的东西添加一个变量。此对象将计算加速度计数据,我们可以在需要时收集这些数据。在world变量下添加以下行代码:
CMMotionManager *motionManager;
现在打开MainScene.m,在任何init方法的位置,添加以下代码块,以便我们刚刚声明的运动管理器开始获取加速度计的数据:
//60 times per second, in theory once per frame
CGFloat interval = 1/60.f;
motionManager = [[CMMotionManager alloc] init];
motionManager.accelerometerUpdateInterval = interval;
[motionManager startAccelerometerUpdates];
目前,如果你运行游戏,你不会注意到它在玩法或任何调试信息上的任何变化。尽管运动管理器已初始化并且正在获取数据,但我们必须设置一个收集数据的方法,以便我们可以对它进行操作。
读取数据
在你的MainScene.m的init方法中,初始化运动管理器后,添加以下行代码。它将在给定的间隔(每秒 60 次)运行指定的方法:
[self schedule:@selector(getAccelerometerData:) interval:interval];
然后,在MainScene.m中的任何地方,我们添加我们想要在指定间隔被调用的方法,这样我们就可以读取加速度计数据:
-(void)getAccelerometerData:(CCTime)delta
{
NSLog(@"%f\t%f\t%f",
motionManager.accelerometerData.acceleration.x, motionManager.accelerometerData.acceleration.y, motionManager.accelerometerData.acceleration.z);
}
现在,如果你运行游戏,你将在 Xcode 的控制台输出中看到大量信息。如果数字在你倾斜设备时发生变化,这意味着一切正常。太好了!如果不这样,请返回并确保你正确添加了所有内容。
接下来,我们将根据设备在任何给定时刻的旋转设置游戏的重力。
随心所欲地操纵重力
而不是每次都输入那么长的文本行,让我们创建一个具有相关名称的方法并传递加速度计数据。换句话说,将你的getAccelerometerData方法修改如下,并添加这个新方法,它将根据加速度计的数据设置物理世界的重力:
-(void)getAccelerometerData:(CCTime)delta
{
//NSLog(@"%f\t%f\t%f", motionManager.accelerometerData.acceleration.x, motionManager.accelerometerData.acceleration.y, motionManager.accelerometerData.acceleration.z);
[self setGravityFromAcceleration: motionManager.accelerometerData.acceleration];
}
-(void)setGravityFromAcceleration:(CMAcceleration)accel
{
CGFloat xGravity = 500 * accel.y;
CGFloat yGravity = -500 * accel.x;
world.gravity = ccp(xGravity, yGravity);
}
现在,如果你运行游戏并生成几个方块,你会看到它们漂浮、下落、侧滑,或者以 iOS 设备感知的“下”的任何方向移动。你可能看到的截图如下:

从现在开始,你可以随意关闭调试绘制,也许可以玩一下设置的重力大小,或者甚至改变重力影响的轴。
你可能会注意到一个方块在墙上停止移动,然后当你旋转设备时也不移动;这是因为物体的身体处于休眠状态。这样做是为了帮助节省没有活跃碰撞和作用力的物体在 CPU 处理时间和能量。
然而,如果你需要它们持续移动,请将以下代码行添加到你的init方法中:
world.sleepTimeThreshold = 100000; //100,000 seconds, or about 27 hours
默认情况下为 0.5 秒,并且当方块停止与墙壁相对移动时会发生休眠。将其设置为相对较大的值,例如 100,000,将确保它们永远不会停止移动(即,除非方块在这段时间内保持静止,但阈值越高,这种情况发生的可能性就越小)。
在 Chipmunk 中处理碰撞
在使用 Chipmunk(在 Cocos2d 中几乎做任何事情)时处理碰撞相对容易。这就是为什么许多开发者使用 Cocos2d。为了做到这一点,我们需要执行几个不同的步骤,以便 Cocos2d 能够正确检测和处理我们的碰撞。
设置碰撞代理
代理模式是处理由一个类发送给其任何潜在父类的消息的常见方式。例如,如果你想使用UITableView(这是UIKit中的标准表格),你必须将表格视图的代理设置为你要添加到其中的类,这样当表格视图尝试刷新表格中的数据时,它就知道要调用哪个类的哪些方法。
话虽如此,我们需要告诉MainScene它将成为我们的碰撞处理程序的代理,所以打开MainScene.h并将CCPhysicsCollisionDelegate添加到@interface行。这将允许CCPhysicsNode对象在我们的MainScene类上设置碰撞检测事件:
@interface MainScene : CCScene <CCPhysicsCollisionDelegate>
然后打开MainScene.m,在init方法中,将world对象的碰撞代理设置为self,如下所示:
world.collisionDelegate = self;
回想一下,world是我们的CCPhysicsNode对象,所以在这个世界(或模拟)中发生的任何碰撞检测事件都需要发送到某个地方进行处理。我们将它设置为self,因为self指的是当前的MainScene实例(我们正在运行的场景)。最后,这有助于我们确定哪些对象与哪些对象相撞。如果没有这一行代码,我们就无法在代码中看到两个对象何时相撞,更不用说知道它们实际上是哪些对象了。
虽然如果你现在运行游戏这不会直接做任何事情,但它正确地设置了你的物理模拟,以便能够检测和处理碰撞。
设置游戏对象的碰撞标签
在我们可以创建检测碰撞的方法之前,我们需要指定哪些对象将相互碰撞。现在,我们只考虑正方形与其他正方形之间的碰撞。
因此,在MainScene.m文件的touchBegan方法中添加以下代码行,以便碰撞检测代理知道哪个对象正在相撞:
squareBody.collisionType = @"square";
你可以用任何物理体做这件事,但就目前而言,这是我们唯一的对象。有了这个设置,让我们添加代码来检测两个正方形之间的实际碰撞。
检测碰撞
Chipmunk 在 Cocos2d 中处理碰撞的方式是通过检测所有碰撞并向每个碰撞类型相应的函数发送方法调用。因此,既然我们要检测两个正方形的碰撞,两个参数都必须命名为 square。否则,方法将无法正确调用。参数变量的实际名称(firstSquare和secondSquare)对于检测来说并不重要。
因此,在MainScene.m中的任何地方,添加以下方法:
-(BOOL)ccPhysicsCollisionBegin:(CCPhysicsCollisionPair *)pair square:(CCNode *)firstSquare square:(CCNode *)secondSquare
{
NSLog(@"squares collided!");
return YES;
}
如果你现在运行游戏,你应该会看到每次一个正方形与另一个正方形碰撞时,squares collided!文本被打印到控制台输出。如果看不到,请返回并确保你正确添加了所有内容。从现在开始,当两个对象碰撞时,你可以做任何你想做的事情,因为你已经有了两个对象的指针以及它们的类型。
作为另一个例子,假设我们想要检测正方形与墙壁之间的碰撞;这非常简单。首先,在addWallWithRect方法中将wall作为collisionType添加到身体中:
wallBody.collisionType = @"wall";
然后,添加检测正方形-墙壁碰撞的方法(注意在之前添加的collisionBegan方法中参数名称的变化):
-(BOOL)ccPhysicsCollisionBegin:(CCPhysicsCollisionPair *)pair square:(CCNode *)nodeA wall:(CCNode *)nodeB
{
NSLog(@"square-wall collision!");
return YES;
}
如果你现在运行游戏并生成一个方块,一旦它碰到墙壁,你应该会看到输出被打印出来。这就是检测碰撞的全部!只需设置 collisionDelegate 属性,设置 collisionType 属性,并添加碰撞方法。
如果你想检测对象上的碰撞,但不想它们与其他对象弹跳?在 Chipmunk 中,你可以做到这一点。
仅使用 Chipmunk 进行碰撞检测
小贴士
这里有一个重要的注意事项:如果你只是要检测以下之一,你不需要使用物理引擎进行碰撞检测,而是使用列出的方法:
-
对于相交的矩形,请使用
CGRectIntersectsRect -
对于矩形内部的点,请使用
CGRectContainsPoint -
对于半径/距离交集,请使用
ccpDistance
如果你将检测非矩形和非圆形对象的碰撞,请继续阅读。
有时候,你所做的只是将对象发送到屏幕上,或者用你自定义的动作旋转它们,你只想知道两个对象何时碰撞,但不想有整个物理引擎带来的弹跳、推动和碰撞。幸运的是,我们可以在 Chipmunk 中做到这一点。
将物理体转换为传感器
在 Chipmunk 中,传感器基本上是可以检测碰撞但穿过其他物体的物体。当你在屏幕上有区域或部分触发某些事件,但实际上不造成任何基于物理的交互时,这些很有用。
例如,如果你正在使用物理引擎制作一个俯视迷你高尔夫游戏,并且想要包含斜坡,最好的方法是将斜坡设置为传感器。当球和斜坡碰撞时,设置重力在某个方向。
要将一个物体设置为传感器,只需将 sensor 变量设置为 true,如下所示:
[squareBody setSensor:YES];
如果你现在运行游戏,你会注意到方块直接穿过彼此,以及穿过地板。
就这样!你所需要的就是碰撞处理(在上一节中),以及将传感器变量设置为 true。任何作为传感器的物理体都会触发碰撞检测事件,但不会引起任何移动或改变其他物体。
摘要
在本章中,你学习了如何创建物理模拟,向模拟中添加物体,使用加速度计设置世界的重力,处理碰撞,甚至仅使用模拟进行碰撞检测。
当涉及到真正复杂的物理引擎机制,如绳索、关节和枢轴时,www.cocos2d-swift.org/docs/api/index.html 上的 Cocos2d 文档解释了很多。在撰写本书时,关于此类复杂机制的可用的教程非常少(如果有的话)。
在下一章中,你将学习关于 Cocos2d 中的声音以及你可以在引擎中为声音效果做的酷炫事情。
第四章:音频与音乐
本章主要介绍使用 Cocos2d 向游戏玩家呈现音乐和音效的不同方式,以及为什么高质量的音乐和音效在优秀游戏中很重要。相信我,音效不仅仅是当事件发生时播放一个声音文件。否则,为什么会有整整一章来介绍它?尽管有很多用户在移动设备上关闭声音玩游戏,但它仍然是某些玩家体验的一部分,因此我们必须密切关注我们选择的声音以及它们的实现方式。
在本章中,我们将涵盖以下主题:
-
加载和卸载效果
-
以创意方式播放音效和循环背景音乐
-
在飞行中修改播放的声音
-
其他好的声音示例
小贴士
对于到目前为止的代码,请打开第四章项目以及书中包含文件中的“声音”目录中的音效。
建议您遵循提供的代码,因为本章以及未来的章节将引用书中提供或提到的方法和类。
如果原型中存在任何游戏玩法错误或不平衡,那没关系;我们将在后面的章节中介绍润色。记住,原型是快速完成的,目的是向他人展示游戏的核心概念,而不是提供一个成品。
先决条件
确保你已经将声音文件复制到你的项目中。如果它们不在那里,无论你尝试多少次预加载、卸载、播放或循环,声音文件都不会播放。与用于 TexturePacker 的精灵表和用 Glyph Designer 创建的 BMFonts 不同,最好将声音文件拖到你的项目中,并确保勾选了“如果需要则复制项目”复选框,如图所示。这将确保文件在你决定删除它们之前都存在于你的项目中。

观察不同音频类型之间的差异
如果你想知道 MP3、CAF 和其他音频文件和数据格式之间的区别,请查看www.raywenderlich.com/69365/audio-tutorial-ios-file-data-formats-2014-edition,这是一篇关于所有不同类型音频的详细解释。对于这本书来说,这不是必需的,但如果您试图节省空间或想知道是否可以使用某些音频文件,这个链接将很有帮助。
在本章(以及本书的其余内容)中,我们将使用 MP3 格式(以及随后的内容),因为它是一个非常常见的格式,同时也是 OALSimpleAudio 和 iPhone 本地支持的格式。
了解 OALSimpleAudio
如果您曾经想要一种简单的方法来播放音效文件,OALSimpleAudio 就是您需要的工具。它可以非常容易地加载各种音效文件,播放音效,循环背景音乐,以及更多。它的存在和与 Cocos2d 的集成使得使用声音和音乐的沉浸式功能使您的游戏栩栩如生变得容易得多。
小贴士
如果您之前使用过 Cocos2d 进行编程并且想知道 SimpleAudioEngine 在哪里,从 Cocos2d v3.0 开始,OALSimpleAudio 是播放音效的新方法。它基本上是 SimpleAudioEngine 的所有功能。
预加载音效
如果您尝试使用 OALSimpleAudio 播放音效,设备会尝试快速将音效加载到内存中并立即播放,这会导致轻微的冻结或延迟。幸运的是,有一种方法可以加载音效和音乐,这样在您尝试播放音效时,不会在用户面前冻结。
OALSimpleAudio 允许预加载音效,这实际上是在您需要音效之前很久就将音效读入内存。您可以选择在游戏开始时(当用户从主屏幕首次启动它时)或在关卡之间通过卸载和重新加载即将到来的关卡的效果来进行。使用 OALSimpleAudio 将音文件加载到内存中的方法是添加以下代码行:
ALBuffer *buffer = [[OALSimpleAudio sharedInstance] preloadEffect:@"soundEffect.mp3"];
buffer 变量赋值是可选的,如果您需要打印有关音文件的各种信息,如频率或缓冲区的位数,则使用它。
小贴士
尽管前面的代码示例显示了 .mp3 作为文件扩展名,但 OALSimpleAudio 可以加载 iOS 支持的任何音效文件。
然而,如果您想减少加载所有游戏内音效所需的时间,您可以在后台进行,这被称为异步加载。
异步加载文件
以异步方式加载您的文件是减少加载时间的同时加载所有文件的最佳方式。然而,请注意,由于以这种方式加载发生在后台,无法保证当用户开始与您的游戏交互时文件已经准备好。
如果您希望在游戏开始时(在加载屏幕,主菜单开始之前,主菜单开始时,或您认为游戏开始的地方)让某些音效可用,如果您仍然希望大多数音效异步加载,建议只加载所需的最小音效量。
要这样做,可以使用以下代码行。它将加载推入后台,并在完成后通知您:
__block ALBuffer *soundBuffer;
[[OALSimpleAudio sharedInstance] preloadEffect:@"soundEffect.mp3" reduceToMono:NO completionBlock:^(ALBuffer* buffer)
{
soundBuffer = buffer;
}];
卸载音效
如果你知道你不会在一段时间内使用音效,或者你经常遇到内存警告,卸载你的音效可能会有用。例如,如果你的游戏在教程中只使用某个声音文件进行旁白,一旦用户通过教程,你就可以卸载这个音效以释放一些内存。OALSimpleAudio 不会卸载正在播放或暂停的音效。
要卸载特定的音效,你可以使用以下代码行:
[[OALSimpleAudio sharedInstance] unloadEffect:@"soundEffect.mp3"];
要一次性卸载所有音效,你可以使用以下代码行:
[[OALSimpleAudio sharedInstance] unloadAllEffects];
小贴士
将这些卸载调用放在 AppDelegate 类的 applicationDidReceiveMemoryWarning 方法中是一个推荐的位置。
播放音效和循环背景音乐
显然,你不会整天只加载和卸载音效,所以让我们来看看这些声音的实际播放。
播放一些背景音乐
总是通过播放背景音乐来设定声音的基调非常重要。无论是意味着一种阴郁、枯萎的基调,还是一种快乐、轻松、向上的基调,音乐都可以帮助吸引玩家,使他们更加投入到游戏中。
由于背景音乐可能会在整个游戏的大部分时间里播放,因此预加载它并不是完全必要的。然而,仍然建议这样做,因为它可以防止音乐开始播放时游戏开始时的轻微延迟。你可以使用以下代码预加载背景音乐:
[[OALSimpleAudio sharedInstance] preloadBg:@"backgroundMusic.mp3"];
在添加了前面的代码行之后,你可以通过一个调用简单地循环播放背景音乐。要播放预加载的背景音乐,请添加以下代码行:
[[OALSimpleAudio sharedInstance] playBgWithLoop:YES];
因此,对于这本书的项目,我们将在游戏进行时播放背景音乐。因此,我们希望用户能够尽早沉浸其中,所以我们将预加载背景音乐,甚至在第一个场景加载之前就开始播放。打开 AppDelegate.m 文件,进入 startScene 方法。在返回语句上方添加以下代码行:
[[OALSimpleAudio sharedInstance] preloadBg:@"backgroundMusic.mp3"];
现在我们已经让 OALSimpleAudio 知道了我们的背景音乐是什么,我们可以立即循环播放文件,因此当第一个场景显示时,已经有音乐在播放。所以,在预加载行下方添加以下内容:
[[OALSimpleAudio sharedInstance] playBgWithLoop:YES];
难道这不很可爱吗?但仅有背景音乐是不够的。让我们在用户在游戏中进行不同活动时添加一些音效。
按钮点击时的声音
人类心理特质之一是在采取行动时渴望反馈。因此,当在数字空间中按下按钮时,我们需要给用户反馈,告诉他们他们的操作已被接收。这就是为什么按钮会稍微变暗,以表示它被按下。在释放按钮时,我们还想播放一个音效,告诉用户他们的操作正在处理中。
要在 Cocos2d 中播放按钮按下时的声音效果,只需将播放声音文件的代码行添加到按钮调用的任何方法中。所以,对于这个项目,打开MainScene.m,转到goToMenu方法,并在调用replaceScene方法之前的行添加此代码行:
[[OALSimpleAudio sharedInstance] playEffect:@"buttonClick.mp3"];
这将在开始加载下一个场景之前播放声音效果一次。同样,对于重启按钮,转到MenuScene.m中的restartGame方法和goToGame方法:
-(void)restartGame
{
[[OALSimpleAudio sharedInstance] playEffect:@"buttonClick.mp3"];
[[CCDirector sharedDirector] replaceScene:[MainScene scene]];
}
-(void)goToGame
{
[[OALSimpleAudio sharedInstance] playEffect:@"buttonClick.mp3"];
[[CCDirector sharedDirector] replaceScene:[MainScene scene]];
}
小贴士
如果你在第一次点击按钮时注意到轻微的延迟,这表明你应该在用户按下指定的按钮之前预加载声音效果。
现在,如果我们只有背景音乐和按钮点击,我们当然需要更多地吸引用户。因此,当用户在游戏板上移动单位一段距离时,我们将添加一些声音。
单位移动声音
与按钮点击效果类似,我们希望在调用moveUnit方法时播放效果。为什么在这里而不是在Unit类中?因为如果我们在这个类中调用它,我们可能会一次调用 81 次(9 x 9 网格)。是的,这是很难实现的,但从技术上讲是可能的。一次调用 81 次会导致效果堆叠在一起,比我们想要的要响得多。
因此,打开MainScene.m并转到moveUnit方法。在这里,在我们更新用户想要移动的单位的位置之后,我们将播放声音效果:
-(void)moveUnit:(NSNotification*)notif
{
NSDictionary *userInfo = [notif userInfo];
Unit *u = (Unit*)userInfo[@"unit"];
u.position = [self getPositionForGridCoord:u.gridPos];
// Add this line:
[[OALSimpleAudio sharedInstance] playEffect:@"moveUnit.mp3"];
++numTurnSurvived;
// ..etc..
}
当你运行游戏并移动一个单位时,你应该听到一个非常微妙的响声。之所以如此微妙,是因为用户将在整个游戏中这样做。我们不想让他们被移动声音效果淹没,因为这可能会激怒一些玩家,导致他们关闭声音或简单地退出游戏,我们不想看到这种情况发生。
单位组合声音
虽然移动动作实际上每回合都会发生(否则回合就不会发生),但两个单位每回合结合的情况可能并不总是如此。因此,当玩家将两个弱单位结合成一个强单位时,我们希望给他们一种奖励感。
由于我们的单位组合代码有点分散,我们必须小心地放置代码,以确保单位组合声音效果在每个组合中只播放一次。例如,如果所有三个单位同时进入一个方块,我们应该只播放一次效果,而不是意外地播放两次。这使得事情有点棘手,但到目前为止,我们不必担心三或四单位的组合,只需处理两个单位的组合。对于三和四单位的组合,将分别播放两次和三次,但对于游戏的早期版本来说这没问题。
首先,打开你的MainScene.m文件,并在代码中的任何位置添加此方法:
-(void)playUnitCombineSoundWithValue:(NSInteger)total
{
}
然后转到checkForAnyDirectionCombineWithUnit方法,并在NSInteger fv和NSInteger ov行(fv和ov分别代表第一个值和其他值,它们将保存传递给此方法的第一个单元和其他单元的值)下面添加以下代码行:
if (first.isFriendly)
[self playUnitCombineSoundWithValue:fv+ov];
此外,转到checkForCombineWithUnit方法,并在相同的位置(在两个NSInteger声明下面)添加以下代码行。
我们有if语句的原因是我们需要确保只有在友好单位与另一个友好单位结合时才播放音效。我们不需要检查other,因为我们只调用此方法时使用的是相同类型的两个单元。至于fv+ov和total参数,它们将在本章后面使用,所以现在只需等待。
最后,在playUnitCombineSound方法中,你需要添加以下代码行,以便实际播放效果:
[[OALSimpleAudio sharedInstance] playEffect:@"unitCombine.mp3"];
如果你现在运行游戏,当一方单位与另一方单位结合时,你会听到声音。我们还想在这个游戏的早期版本中添加一种更多类型的音效。
用户失败时的声音
最后但同样重要的是,我们想在用户输掉游戏时播放一些声音。一遍又一遍地听到失败音效并不是很有激励性,但它有助于保持用户留存率,因为它强调了“让我再试一次,我差一点就做到了”的感觉。挑选合适的音效可能有点困难,但一旦你找到了一个你觉得足够好的音效,你就可以继续将其添加到游戏结束屏幕上。
所以首先,我们需要一个游戏结束屏幕。类似于Unit类,创建一个GameOverScene类,其子类为CCScene。你的GameOverScene.h文件应该看起来像这样:
#import "CCScene.h"
@interface GameOverScene : CCScene
{
CGSize winSize;
}
@property (nonatomic, assign) NSInteger numUnitsKilled;
@property (nonatomic, assign) NSInteger numTotalScore;
@property (nonatomic, assign) NSInteger numTurnsSurvived;
+(CCScene*)scene;
@end
然后打开你的GameOverScene.m文件。它看起来像这样:
#import "GameOverScene.h"
@implementation GameOverScene
+(CCScene*)scene
{
return [[self alloc] init];
}
-(id)init
{
if ((self=[super init]))
{
winSize = [[CCDirector sharedDirector] viewSize];
//these values range 0 to 1.0, so use float to get ratio
CCNode *background = [CCNodeColor nodeWithColor:[CCColor colorWithRed:128/255.f green:0/255.f blue:88/255.f]];
[self addChild:background];
}
return self;
}
-(void)goToMenu
{
//to be filled in later
}
-(void)restartGame
{
//to be filled in later
}
@end
在GameOverScene的init方法中,我们希望播放游戏结束音效。我们在这里添加它,以便游戏结束场景加载时立即播放音效。所以,在背景颜色代码行下面,添加以下代码以播放游戏结束音效:
[[OALSimpleAudio sharedInstance] playEffect:@"gameOver.mp3"];
要播放声音,我们需要在用户失败时将用户发送到GameOverScene,所以转到MainScene.m文件中的endGame方法,并将该代码行更改为以下内容(不要忘记在文件顶部添加#include "GameOverScene.h"代码行):
[[CCDirector sharedDirector] replaceScene:[GameOverScene scene]];
由此,你就有了一个游戏结束的音效!所以,到目前为止,我们已经放置了所有的音效:单位移动、组合、游戏结束、背景音乐等等。但是过了一段时间后,声音有点重复,所以让我们稍微修改一下声音,以减少重复声音的烦恼。
在线修改音效
使用 OALSimpleAudio(以及 SimpleAudioEngine 也是如此)的一个非常酷的特性是,你可以修改音频文件在播放给用户时的声音。例如,如果你希望有一系列硬币被收集,并且每个连续收集的硬币播放的音调比前一个略高,你可以简单地根据收集到的硬币数量来修改音调。
音量(或增益)、音调和声像
通过一个简单的调用,向默认的playEffect方法添加几个参数,你可以修改音效的响度、音效的音调和音效在扬声器中的播放位置。你可以用以下代码做到这一点:
//volume range: 0.0 to 1.0
//pitch range: 0.0 to inf (1.0 is normal)
//pan range: -1.0 to 1.0 (far left to far right)
//loop: If YES, will play until stop is called on the sound
[[OALSimpleAudio sharedInstance] playEffect:@"soundEffect.mp3" volume:1 pitch:1 pan:1 loop:NO];
停止循环音效
如果你之前回答“是”并希望在某个时刻停止循环,你必须获取前面函数的返回值,如下所示:
id<ALSoundSource> effect;
effect = [[OALSimpleAudio sharedInstance] playEffect:@"soundEffect.mp3" volume:1 pitch:1 pan:1 loop:NO];
然后调用变量上的相应停止函数:
[effect stop];
修改组合音效
理想情况下,我们不希望用户每次组合单位时都听到相同的音效。这不仅会让用户感到厌烦和烦恼,而且会让游戏感觉更加无聊,缺乏刺激。因此,我们希望对组合音效进行轻微的修改,这样随着用户组合单位数量的增加,他们会因为成功而感到自信,从而愿意玩得更久。
其中一种方法是通过修改音效的音调。这将在某个点上有效,直到音效被推向一个方向,此时提供另一个音效来处理真正大量的单位组合会更好。
打开MainScene.m文件并滚动到playUnitCombineSoundWithValue方法。在这里,你需要修改代码,使其看起来像这样:
CGFloat pitchValue = 1.0 - (total / 100.f);
//eg: fv+ov = 20 ... 1.0 - 0.2 = 0.8
if (total < 50)
{
[[OALSimpleAudio sharedInstance] playEffect:@"unitCombine.mp3"
volume:1 pitch:pitchValue pan:0 loop:NO];
}
else
{
[[OALSimpleAudio sharedInstance]
playEffect:@"largeUnitCombine.mp3"];
}
当你运行游戏并组合一个单位时,你会听到音效越来越深沉,直到达到某个值(临界点——在这种情况下,新的单位值为 50 或更高)。在这个时候,我们希望播放不同的音效,这正是内部if语句所处理的。
其他优秀的音效位置
以下是一些充分利用音效的游戏示例。这些游戏不仅更能吸引用户,而且也是如何将音效用于除基本事件(如简单的用户移动或按钮点击)之外的好例子:
-
三合一:这款游戏卡片上有脸谱,如果你什么都不做,过一会儿你会听到卡片发出随机噪音。此外,如果你尝试将卡片滑动到不会移动任何东西的位置(无效操作),你会听到其中一张卡片说“不!”这真的很可爱,也是游戏氛围通过声音得到维持的另一种方式。看看游戏的 UI 界面,如下面的截图所示:
![其他优秀的音效位置]()
-
《穿越公路》:在这款游戏中,时不时地,你会遇到一辆播放音乐的车辆,比如警车或垃圾车。这些都比较罕见,但当玩家遇到并听到额外的音效层次时,游戏体验会更加愉快,因为它不仅仅是一辆路过的车。此外,游戏中所有的汽车和火车,如果你戴着耳机,你会听到车辆的音乐从一只耳朵传到另一只耳朵。看看这款游戏的用户界面:
![其他出色的声音地点]()
摘要
本章教你如何使用OALSimpleAudio和 Cocos2d 预加载、卸载、播放和修改声音文件。你还看到了一些我们在这本书的项目中整合声音的酷方法。由于游戏仍然处于类似原型的阶段,声音可能会改变或被修改。然而,绝大多数都已经实现。此外,如果你想了解如何在菜单或设置中通过选项来打开/关闭游戏中的声音或音乐,请阅读第六章,整理和抛光,因为那一章将涵盖更多此类细节。
在OALSimpleAudio中还有很多情境方法,这一章没有涉及。如果你想了解更多关于它们的信息,你可以查看www.learn-cocos2d.com/api-ref/1.0/ObjectAL/html/interface_o_a_l_simple_audio.html#aaf877e4f0526408d569fd12f37e8e1f7的文档。
在下一章中,我们将介绍一些非常酷的概念和机制,大多数游戏开发者都没有花时间在他们的游戏中实现——这不仅仅是 Game Center 或 iCloud 支持。
第五章:创建酷炫内容
在本章中,你将学习如何实现真正复杂、微妙的游戏机制,这是许多开发者不会做的。这就是好游戏与伟大游戏之间的区别。本章将包含许多示例、教程和代码片段,旨在适应你自己的项目,所以随时欢迎回来查看你可能第一次错过或只是好奇的内容。
在本章中,我们将涵盖以下主题:
-
添加分数表格
-
为单位添加细微的滑动
-
在贝塞尔曲线上创建运动而不是直线路径
-
通过设备倾斜实现深度感知(以及视差滚动)
-
创建单位流线或幽灵的三种方法
-
触屏控制与 D-pad 适配(以及为什么了解这种区别如此重要)
-
本章的一个常见主题将向您展示如何将看似复杂的事物转化为易于编码和修改的段,您可以将它们实现到自己的项目中(或项目中)。
此外,本章介绍了一些与本书项目不兼容的内容,并且前述列表中的前两点与游戏项目相关,该项目一直缓慢地进行。其余的都是独立的示例项目,代码以模块化方式设计,以便你可以更快地将它们提取到自己的项目中。
小贴士
强烈建议在处理前两个部分之前打开第五章的代码。自上一章以来,已经添加和/或修改了大量代码,并且本书没有提及。因此,如果你不使用第五章的项目代码而跟随本书,可能会遇到编译错误。感谢您的理解!
添加分数表格
因为我们要提供一个方式让用户看到他们的过去高分,在GameOver场景中,我们将添加一个表格来显示保存的最新高分。为此,我们将使用CCTableView。它仍然相对较新,但对我们将要使用它是适用的。
CCTableView 与 UITableView 的比较
虽然对于之前制作过非 Cocos2d 应用的你们来说,UITableView可能已经熟知,但在 Cocos2d 中使用时,你应该意识到它的缺点。例如,如果你想在表格中使用 BMFont,你不能添加LabelBMFont(你可以尝试将 BMFont 转换为 TTF 字体并在表格中使用,但这超出了本书的范围)。
如果你仍然希望使用UITableView对象(或任何UIKit元素),你可以像平常一样创建对象,并将其添加到场景中,如下所示(tblScores是UITableView对象的名称):
[[[CCDirector sharedDirector] view] addSubview:tblScores];
保存高分(NSUserDefaults)
在我们显示任何高分之前,我们必须确保保存它们。最简单的方法是利用苹果内置的数据保存工具—NSUserDefaults。如果你以前从未使用过它,让我告诉你,它基本上是一个具有“保存”机制的字典,它将值存储在设备上,以便下次用户加载设备时,这些值可用于应用程序。
另外,因为我们正在跟踪每个游戏玩法中的三个不同值,所以我们只说一个游戏的总分高于另一个游戏时,总分更高。
因此,让我们创建一个saveHighScore方法,该方法将遍历我们保存列表中的所有总分,并查看当前的总分是否高于任何已保存的分数。如果是这样,它将插入自己并将其余的向下移动。在MainScene.m中添加以下方法:
-(NSInteger)saveHighScore
{
//save top 20 scores
//an array of Dictionaries...
//keys in each dictionary:
// [DictTotalScore]
// [DictTurnsSurvived]
// [DictUnitsKilled]
//read the array of high scores saved on the user's device
NSMutableArray *arrScores = [[[NSUserDefaults standardUserDefaults] arrayForKey:DataHighScores] mutableCopy];
//sentinel value of -1 (in other words, if a high score was not found on this play through)
NSInteger index = -1;
//loop through the scores in the array
for (NSDictionary *dictHighScore in arrScores)
{
//if the current game's total score is greater than the score stored in the current index of the array...
if (numTotalScore > [dictHighScore[DictTotalScore] integerValue])
{
//then store that index and break out of the loop
index = [arrScores indexOfObject:dictHighScore];
break;
}
}
//if a new high score was found
if (index > -1)
{
//create a dictionary to store the score, turns survived, and units killed
NSDictionary *newHighScore = @{ DictTotalScore : @(numTotalScore),
DictTurnsSurvived : @(numTurnSurvived),
DictUnitsKilled : @(numUnitsKilled) };
//then insert that dictionary into the array of high scores
[arrScores insertObject:newHighScore atIndex:index];
//remove the very last object in the high score list (in other words, limit the number of scores)
[arrScores removeLastObject];
//then save the array
[[NSUserDefaults standardUserDefaults] setObject:arrScores forKey:DataHighScores];
[[NSUserDefaults standardUserDefaults] synchronize];
}
//finally return the index of the high score (whether it's -1 or an actual value within the array)
return index;
}
最后,在过渡到下一个场景之前,在endGame方法中调用此方法:
-(void)endGame
{
//call the method here to save the high score, then grab the index of the high score within the array
NSInteger hsIndex = [self saveHighScore];
NSDictionary *scoreData = @{ DictTotalScore : @(numTotalScore),
DictTurnsSurvived : @(numTurnSurvived),
DictUnitsKilled : @(numUnitsKilled),
DictHighScoreIndex : @(hsIndex)};
[[CCDirector sharedDirector] replaceScene:[GameOverScene sceneWithScoreData:scoreData]];
}
现在我们已经保存了高分,让我们创建一个表格来显示它们。
创建表格
设置CCTableView对象非常简单。我们只需要修改contentSize对象,然后添加一些处理每个单元格大小和内容的方法。
因此,首先打开GameOverScene.h文件,并将场景设置为CCTableView的数据源:
@interface GameOverScene : CCScene <CCTableViewDataSource>
然后,在initWithScoreData方法中,创建标题标签以及初始化CCTableView:
//get the high score array from the user's device
arrScores = [[NSUserDefaults standardUserDefaults] arrayForKey:DataHighScores];
//create labels
CCLabelBMFont *lblTableTotalScore = [CCLabelBMFont labelWithString:@"Total Score:" fntFile:@"bmFont.fnt"];
CCLabelBMFont *lblTableUnitsKilled = [CCLabelBMFont labelWithString:@"Units Killed:" fntFile:@"bmFont.fnt"];
CCLabelBMFont *lblTableTurnsSurvived = [CCLabelBMFont labelWithString:@"Turns Survived:" fntFile:@"bmFont.fnt"];
//position the labels
lblTableTotalScore.position = ccp(winSize.width * 0.5, winSize.height * 0.85);
lblTableUnitsKilled.position = ccp(winSize.width * 0.675, winSize.height * 0.85);
lblTableTurnsSurvived.position = ccp(winSize.width * 0.875, winSize.height * 0.85);
//add the labels to the scene
[self addChild:lblTableTurnsSurvived];
[self addChild:lblTableTotalScore];
[self addChild:lblTableUnitsKilled];
//create the tableview and add it to the scene
CCTableView * tblScores = [CCTableView node];
tblScores.contentSize = CGSizeMake(0.6, 0.4);
CGFloat ratioX = (1.0 - tblScores.contentSize.width) * 0.75;
CGFloat ratioY = (1.0 - tblScores.contentSize.height) / 2;
tblScores.position = ccp(winSize.width * ratioX, winSize.height * ratioY);
tblScores.dataSource = self;
tblScores.block = ^(CCTableView *table){
//if the press a cell, do something here.
//NSLog(@"Cell %ld", (long)table.selectedRow);
};
[self addChild: tblScores];
当CCTableView对象的数据源设置为self时,我们可以添加三个方法,这些方法将确定我们的表格外观以及每个单元格(即行)中的数据。
注意
注意,如果我们没有设置数据源,表格视图的方法将不会被调用;如果我们将其设置为除self之外的其他任何内容,方法将调用该对象/类。
话虽如此,添加以下三个方法:
-(CCTableViewCell*)tableView:(CCTableView *)tableView nodeForRowAtIndex:(NSUInteger)index
{
CCTableViewCell* cell = [CCTableViewCell node];
cell.contentSizeType = CCSizeTypeMake(CCSizeUnitNormalized, CCSizeUnitPoints);
cell.contentSize = CGSizeMake(1, 40);
// Color every other row differently
CCNodeColor* bg;
if (index % 2 != 0) bg = [CCNodeColor nodeWithColor:[CCColor colorWithRed:0 green:0 blue:0 alpha:0.3]];
else bg = [CCNodeColor nodeWithColor: [CCColor colorWithRed:0 green:0 blue:0 alpha:0.2]];
bg.userInteractionEnabled = NO;
bg.contentSizeType = CCSizeTypeNormalized;
bg.contentSize = CGSizeMake(1, 1);
[cell addChild:bg];
return cell;
}
-(NSUInteger)tableViewNumberOfRows:(CCTableView *)tableView
{
return [arrScores count];
}
-(float)tableView:(CCTableView *)tableView heightForRowAtIndex:(NSUInteger)index
{
return 40.f;
}
第一个方法,tableView:nodeForRowAtIndex:,将根据索引格式化每个单元格。目前,我们将用两种不同的颜色之一来着色每个单元格。
第二个方法,tableViewNumberOfRows:,返回表格视图中将有的行数或单元格数。既然我们知道将有 20 个,我们可以技术上输入 20,但如果我们决定稍后更改这个数字怎么办?所以,让我们继续使用数组的计数。
第三种方法,tableView:heightForRowAtIndex:,是用来返回给定索引处的行或单元格的高度。由于我们对于任何特定的单元格都没有做任何不同的事情,我们可以将这个值硬编码为一个相当合理的 40 像素高度。
到目前为止,你应该能够运行游戏,当你失败时,你会被带到游戏结束屏幕,顶部有标签,屏幕右侧有一个可以滚动的表格。
小贴士
学习 Cocos2d 时,一个好的实践就是随意尝试一些东西,看看你能创造出什么样的效果。例如,你可以尝试使用一些ScaleTo动作将文本从 0 缩放,或者使用MoveTo动作从底部或侧面滑动它。
随意看看你现在是否能想出一个酷炫的方式来显示文本。
既然我们已经有了表格,让我们来看看如何显示数据,好吗?
显示得分
现在我们已经创建了表格,只需简单地将数据添加到我们的代码中,就可以正确地显示数字。
在nodeForRowAtIndex方法中,在给单元格添加背景色之后,添加以下代码块:
//Create the 4 labels that will be used within the cell (row).
CCLabelBMFont *lblScoreNumber = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d)", index+1] fntFile:@"bmFont.fnt"];
//Set the anchor point to the middle-right (default middle-middle)
lblScoreNumber.anchorPoint = ccp(1,0.5);
CCLabelBMFont *lblTotalScore = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d", [arrScores[index][DictTotalScore] integerValue]] fntFile:@"bmFont.fnt"];
CCLabelBMFont *lblUnitsKilled = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d", [arrScores[index][DictUnitsKilled] integerValue]] fntFile:@"bmFont.fnt"];
CCLabelBMFont *lblTurnsSurvived = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d", [arrScores[index][DictTurnsSurvived] integerValue]] fntFile:@"bmFont.fnt"];
//set the position type of each label to normalized (where (0,0) is the bottom left of its parent and (1,1) is the top right of its parent)
lblScoreNumber.positionType = lblTotalScore.positionType = lblUnitsKilled.positionType = lblTurnsSurvived.positionType = CCPositionTypeNormalized;
//position all of the labels within the cell
lblScoreNumber.position = ccp(0.15,0.5);
lblTotalScore.position = ccp(0.35,0.5);
lblUnitsKilled.position = ccp(0.6,0.5);
lblTurnsSurvived.position = ccp(0.9,0.5);
//if the index we're iterating through is the same index as our High Score index...
if (index == highScoreIndex)
{
//then set the color of all the labels to a golden color
lblScoreNumber.color =
lblTotalScore.color =
lblUnitsKilled.color =
lblTurnsSurvived.color = [CCColor colorWithRed:1 green:183/255.f blue:0];
}
//add all of the labels to the individual cell
[cell addChild:lblScoreNumber];
[cell addChild:lblTurnsSurvived];
[cell addChild:lblTotalScore];
[cell addChild:lblUnitsKilled];
就这样!当你玩游戏并最终到达游戏结束屏幕时,你会看到高分正在显示(甚至包括之前的尝试得分,因为它们已经被保存了,记得吗?)。注意那个黄色的最高分。这是一个指示,表明你刚刚玩的游戏得分已经出现在排行榜上,并显示了它的位置。
虽然当滚动时CCTableView中东西消失和重新出现可能会感觉有点奇怪,但我们将在下一章中介绍如何使它变得更好。现在,让我们来一些Threes!——像滑动进入我们的游戏。
小贴士
如果你正在考虑将CCTableView添加到自己的项目中,这里的关键要点是确保你正确地修改contentSize和位置。默认情况下,contentSize是一个归一化的CGSize,范围从 0 到 1,锚点是(0,0)。
此外,确保你执行以下两个步骤:
-
设置表格视图的数据源
-
添加三个表格视图方法
考虑到所有这些,实现CCTableView应该相对简单。
为单元添加微妙的滑动效果
如果你曾经玩过Threes!(或者即使你没有,也可以看看asherv.com/threes/上的预告片,甚至可以在你的手机上下载游戏),你就会知道当用户开始移动但尚未完成移动时,会有滑动功能。随着拖动手指的速度,单元会向它们将要移动的方向滑动,向用户显示每个单元将去哪里以及每个单元将如何与其他单元结合。
这很有用,因为它不仅增加了额外的“酷炫因素”,还如果用户想在做出决定之前撤销并做出不同的、更谨慎的移动,它还提供了对未来的预览。
小贴士
这里有一个小贴士:如果你想让你的游戏真正流行起来,你必须让用户相信他们输是因为自己的错误,而不是你的“愚蠢的游戏机制”(正如一些玩家可能会说的)。
想想愤怒的小鸟,Smash Hit,Crossy Road,Threes!,Tiny Wings…这个列表可以一直继续,更多流行的游戏都有一个共同的主题:当用户失败时,完全取决于他们自己决定赢或输,并且他们做出了错误的移动。
这种看不见的机制促使玩家带着更好的策略再次玩游戏。这正是我们希望用户在做出动作之前看到他们的动作的原因。这对开发者和玩家来说都是双赢的局面。
滑动一个单位
如果我们可以让一个单位滑动,那么我们当然可以通过简单地遍历它们、模块化代码或某种其他形式的泛化来让其他单位也滑动。
话虽如此,我们需要设置Unit类,使其能够检测手指拖动的距离。这样,我们可以确定单位移动的距离。所以,打开Unit.h并添加以下变量。它将跟踪从上一个触摸位置的距离:
@property (nonatomic, assign) CGPoint previousTouchPos;
然后,在Unit.m的touchMoved方法中,向previousTouchPos添加以下赋值。它将前一个触摸位置设置为触摸下落位置,但前提是距离大于 20 个单位:
if (!self.isBeingDragged && ccpDistance(touchPos, self.touchDownPos) > 20)
{
self.isBeingDragged = YES;
//add it here:
self.previousTouchPos = self.touchDownPos;
一旦设置好,我们就可以在手指拖动时开始计算距离。为此,我们将进行一个简单的检查。在touchMoved的末尾,在初始if块之后添加以下代码块:
//only if the unit is currently being dragged
if (self.isBeingDragged)
{
CGFloat dist = 0;
//if the direction the unit is being dragged is either UP or DOWN
if (self.dragDirection == DirUp || self.dragDirection == DirDown)
//then subtract the current touch position's Y-value from the previously-recorded Y-value to determine the distance to move
dist = touchPos.y - self.previousTouchPos.y;
//else if the direction the unit is being dragged is either LEFT or RIGHT
else if (self.dragDirection == DirLeft ||
self.dragDirection == DirRight)
//then subtract the current touch position's Y-value from the previously-recorded Y-value to determine the distance to move
dist = touchPos.x - self.previousTouchPos.x;
//then assign the touch position for the next iteration of touchMoved to work properly
self.previousTouchPos = touchPos;
}
在末尾对previousTouchPos的赋值将确保,当单位被拖动时,我们继续更新触摸位置,以便我们可以确定距离。此外,距离仅在单位被拖动的方向上计算(上下用 Y 表示,左右用 X 表示)。
现在我们已经计算了手指拖动的距离,让我们将其推入一个函数,该函数将根据单位被拖动的方向移动我们的单位。所以,在你计算出上一段代码块中的dist之后,调用以下方法来根据拖动的量移动我们的单位:
dist /= 2; //optional
[self slideUnitWithDistance:dist withDragDirection:self.dragDirection];
小贴士
将距离除以2是可选的。你可能认为方块太小,希望用户能看到他们的方块。所以请注意,除以2或更大的数字意味着,每当手指移动 1 点,单位就会移动 1/2(或更少)点。
那个方法调用准备好了,我们需要实现它,所以现在添加以下方法体。由于这个方法相当复杂,我们将分部分添加:
-(void)slideUnitWithDistance:(CGFloat)dist withDragDirection:(enum UnitDirection)dir
{
}
我们需要做的第一件事是设置一个变量来计算单位的新的x和y位置。我们将称这些为newX和newY,并将它们设置为单位的当前位置:
CGFloat newX = self.position.x, newY = self.position.y;
接下来,我们想要获取单位开始的位置,即单位如果位于其当前网格坐标的位置。为此,我们将从MainScene调用getPositionForGridCoordinate方法(因为位置计算已经在那里进行,我们不妨使用那个函数):
CGPoint originalPos = [MainScene getPositionForGridCoord:self.gridPos];
接下来,我们将根据单位被拖动的方向移动newX或newY。现在,让我们先添加向上方向:
if (self.dragDirection == DirUp)
{
newY += dist;
if (newY > originalPos.y + self.gridWidth)
newY = originalPos.y + self.gridWidth;
else if (newY < originalPos.y)
newY = originalPos.y;
}
在这个 if 块中,我们首先将距离添加到newY变量中(因为我们向上移动,所以我们添加到 Y 而不是 X)。然后,我们想确保位置最多向上移动 1 个方块。我们将使用gridWidth(这实际上是方块的宽度,在initCommon方法中分配)。此外,我们还需要确保如果他们把方块移回原始位置,它不会进入下面的方块。
因此,让我们添加其余的方向作为 else if 语句:
else if (self.dragDirection == DirDown)
{
newY += dist;
if (newY < originalPos.y - self.gridWidth)
newY = originalPos.y - self.gridWidth;
else if (newY > originalPos.y)
newY = originalPos.y;
}
else if (self.dragDirection == DirLeft)
{
newX += dist;
if (newX < originalPos.x - self.gridWidth)
newX = originalPos.x - self.gridWidth;
else if (newX > originalPos.x)
newX = originalPos.x;
}
else if (self.dragDirection == DirRight)
{
newX += dist;
if (newX > originalPos.x + self.gridWidth)
newX = originalPos.x + self.gridWidth;
else if (newX < originalPos.x)
newX = originalPos.x;
}
最后,我们将根据新计算出的x和y位置设置单元的位置:
self.position = ccp(newX, newY);
在这个阶段运行游戏应该会导致你拖动的单元随着你的手指滑动。不错,对吧?因为我们有一个移动一个单元的函数,我们可以很容易地修改它,以便每个单元都可以这样移动。
但首先,你可能已经注意到了一段时间前(或者可能只是最近),那就是单元移动只有在将手指移回原始触摸位置时才会取消。因为我们正在拖动单元本身,所以我们可以通过将单元拖回起始位置来“取消”移动。然而,手指可能处于完全不同的位置,所以我们需要修改确定取消移动的方式。
要做到这一点,请在Unit.m的touchEnded方法中找到这个 if 语句:
if (ccpDistance(touchPos, self.touchDownPos) > self.boundingBox.size.width/2)
将其更改为以下内容,这将确定单元的距离,而不是手指的距离:
CGPoint oldSelfPos = [MainScene getPositionForGridCoord:self.gridPos];
CGFloat dist = ccpDistance(oldSelfPos, self.position);
if (dist > self.gridWidth/2)
是的,这意味着如果你收到那个警告并且希望消除它,你不再需要在touchEnded中需要touchPos变量。但对于滑动 1 个单元来说,这就足够了。现在我们准备好滑动所有单元,让我们开始吧!
滑动所有单元
现在我们有了正在滑动的拖动单元,让我们继续并使所有单元滑动(甚至包括敌方单元,这样我们可以更好地预测我军部队的移动)。
首先,我们需要一种方法来移动屏幕上的所有单元。然而,由于单元类只包含有关单个单元的信息(这是好事),我们需要在MainScene中调用一个方法,因为那里有单元数组。
此外,我们不能简单地调用[MainScene method],因为数组是实例变量,而实例变量必须通过对象本身的实例来访问。
话虽如此,因为我们知道我们的单元将作为子单元添加到场景中,所以我们可以利用 Cocos2d 的优势,并通过父参数在MainScene类上调用一个实例方法。因此,在Unit.m的touchMoved中,进行以下更改:
[(MainScene*)self.parent slideAllUnitsWithDistance:dist withDragDirection:self.dragDirection];
//[self slideUnitWithDistance:dist withDragDirection:self.dragDirection];
基本上,我们已经注释掉了(或者删除了)这里的老方法调用,而是调用我们的父对象(我们将它转换为MainScene,这样我们知道它有哪些函数)。
但我们还没有创建那个方法,所以请在MainScene.h中添加以下方法声明:
-(void)slideAllUnitsWithDistance:(CGFloat)dist withDragDirection:(enum UnitDirection)dir;
提示
顺便说一下,你可能没有注意到,枚举 UnitDirection 在 Unit.h 中声明,这就是为什么 MainScene.h 导入 Unit.h 的原因——这样我们就可以在这个类中使用该枚举,并具体化该函数。
然后在 MainScene.m 中,我们将遍历友方和敌方数组,并对每个单独的单位调用 slideUnitWithDistance 函数:
-(void)slideAllUnitsWithDistance:(CGFloat)dist withDragDirection:(enum UnitDirection)dir
{
for (Unit *u in arrFriendlies)
[u slideUnitWithDistance:dist withDragDirection:dir];
for (Unit *u in arrEnemies)
[u slideUnitWithDistance:dist withDragDirection:dir];
}
然而,这仍然不起作用,因为我们还没有在 Unit 类的头部文件中声明该函数。所以现在就去声明它。在 Unit.h 中声明函数头:
-(void)slideUnitWithDistance:(CGFloat)dist withDragDirection:(enum UnitDirection)dir;
我们几乎完成了。
我们最初设置 slideUnitWithDistance 方法时考虑了拖动方向。然而,只有当前被拖动的单位会有拖动方向。其他所有单位都需要使用它们当前面对的方向(即它们已经移动的方向)。
要做到这一点,我们只需要修改 slideUnitWithDistance 方法进行检查以确定如何修改距离的方向。
但首先,我们需要处理负值。这意味着什么?好吧,如果你正在将单位向左拖动,而要移动的单位应该向左移动,它将正常工作,因为 x-10(例如)仍然小于网格的宽度。然而,如果你向左拖动,而要移动的单位应该向右移动,它将根本不会移动,因为它试图添加一个负值 x -10,但由于它需要向右移动,它将立即遇到左边界(小于原始位置),并保持静止。
以下图表应该有助于解释“处理负值”的含义。如图所示,在上部区域,当非拖动单位应该向左移动 10(换句话说,x 方向的负 10)时,它起作用。但当非拖动单位移动的方向相反(换句话说,x 方向的正 10)时,它不起作用。

为了处理这个问题,我们设置了一个相当复杂的 if 语句。它检查拖动方向和单位自己的方向是否相反(正与负),并将距离乘以 -1(翻转)。
将以下内容添加到 slideUnitWithDistance 方法的顶部,在你获取 newX 和原始位置之后:
-(void)slideUnitWithDistance:(CGFloat)dist withDragDirection:(enum UnitDirection)dir
{
CGFloat newX = self.position.x, newY = self.position.y;
CGPoint originalPos = [MainScene getPositionForGridCoord:self.gridPos];
if (!self.isBeingDragged &&
(((self.direction == DirUp || self.direction == DirRight) &&
(dir == DirDown || dir == DirLeft)) ||
((self.direction == DirDown || self.direction == DirLeft) &&
(dir == DirUp || dir == DirRight))))
{
dist *= -1;
}
}
这个 if 语句的逻辑如下:
假设单位没有被拖动。此外,假设方向是正的而阻力方向是负的,或者方向是负的而阻力方向是正的。然后乘以 -1。
最后,如前所述,我们只需要处理非拖动单位。因此,在每一个 if 语句中,添加一个“或”部分,以检查相同方向,但只有当单位当前没有被拖动时。换句话说,在 slideUnitWithDistance 方法中,修改你的 if 语句,使其看起来像这样:
if (self.dragDirection == DirUp || (!self.isBeingDragged && self.direction == DirUp))
{}
else if (self.dragDirection == DirDown || (!self.isBeingDragged && self.direction == DirDown))
{}
else if (self.dragDirection == DirLeft || (!self.isBeingDragged && self.direction == DirLeft))
{}
else if (self.dragDirection == DirRight || (!self.isBeingDragged && self.direction == DirRight))
{}
最后,我们可以运行游戏。砰!所有单位都随着我们的拖动在屏幕上滑行。这不是很棒吗?现在玩家可以更好地选择他们的移动。
这就是滑动部分(以及这个项目章节的部分)的全部内容。本章的其余部分充满了真正令人惊叹的东西,鼓励你查看它们,因为它们可能对你在这本书之外的项目或你自己的未来项目有所帮助。
小贴士
单位滑动的关键是循环遍历数组,以确保所有单位移动的距离相等,因此传递给 move 函数的距离。
在贝塞尔曲线上创建运动
如果你不知道贝塞尔曲线是什么,它基本上是一条从点 A 到点 B 经过曲线的线。它不是一条由两个点组成的直线,而是使用第二组点,即控制点,以平滑的方式弯曲线条。当你想在 Cocos2d 中应用带有动画的运动时,按顺序排队一大堆 MoveTo 动作是非常诱人的。然而,如果你使用更平滑的贝塞尔曲线动画,游戏和代码看起来会更好。
这里有一个贝塞尔曲线的例子看起来是什么样子的:

如你所见,红线从点 P0 到 P3。然而,这条线受到控制点 P1 和 P2 方向的影响。
使用贝塞尔曲线的例子
让我们列出一些使用贝塞尔曲线而不是常规的 MoveTo 或 MoveBy 动作的好例子:
-
一个将执行跳跃动画的角色,例如,在 超级马里奥兄弟 中
-
玩家投掷的回旋镖作为武器
-
发射导弹或火箭并给它一个抛物线曲线
-
一个指示用户必须用手指绘制曲线路径的教程手
-
在半管道坡道上的滑板运动员(如果不是用 Chipmunk 完成)
显然,还有很多其他例子可以使用贝塞尔曲线来模拟它们的运动。但让我们实际编写一个,怎么样?
样本项目 – 贝塞尔地图路线
首先,为了使事情变得更快——因为这个不是本书项目的部分——只需从代码仓库或网站下载项目。
如果你打开项目并在你的设备或模拟器上运行它,你会注意到一个蓝色屏幕和位于左下角的正方形。如果你在屏幕上的任何地方点击,你会看到蓝色的正方形形成一个 M 形状,结束在右下角。如果你保持手指,它会重复。再次点击,动画将重置。
想象这个正方形走过的路径是在地图上,并指示玩家将如何使用他们的角色进行路线。这是一条非常粗糙、非常尖锐的路径。通常,路径是弯曲的,所以让我们做一个弯曲的路径吧!
这是最终结果(使用本章“三种制作单位流或‘幽灵’”部分中描述的 CCMotionStreak 方法跟踪)。
这里是一张显示蓝色正方形非常直的路径的截图:

以下截图显示了黄色正方形的贝塞尔路径:

曲线 M 形状
打开 MainScene.h 并添加另一个 CCNodeColor 变量,命名为 unitBezier:
CCNodeColor *unitBezier;
然后打开 MainScene.m 并在初始化方法中添加以下代码,以便你的黄色方块出现在屏幕上:
unitBezier = [[CCNodeColor alloc] initWithColor:[CCColor colorWithRed:1 green:1 blue:0] width:50 height:50];
[self addChild:unitBezier];
CCNodeColor *shadow2 = [[CCNodeColor alloc] initWithColor:[CCColor blackColor] width:50 height:50];
shadow2.anchorPoint = ccp(0.5,0.5);
shadow2.position = ccp(26,24);
shadow2.opacity = 0.5;
[unitBezier addChild:shadow2 z:-1];
然后,在 sendFirstUnit 方法中,添加重置黄色方块位置的代码行,并将该方法排队以移动黄色方块:
-(void)sendFirstUnit
{
unitRegular.position = ccp(0,0);
//Add these 2 lines
unitBezier.position = ccp(0,0);
[self scheduleOnce:@selector(sendSecondUnit) delay:2];
CCActionMoveTo *move1 = [CCActionMoveTo actionWithDuration:0.5 position:ccp(winSize.width/4, winSize.height * 0.75)];
CCActionMoveTo *move2 = [CCActionMoveTo actionWithDuration:0.5 position:ccp(winSize.width/2, winSize.height/4)];
CCActionMoveTo *move3 = [CCActionMoveTo actionWithDuration:0.5 position:ccp(winSize.width*3/4, winSize.height * 0.75)];
CCActionMoveTo *move4 = [CCActionMoveTo actionWithDuration:0.5 position:ccp(winSize.width - 50, 0)];
[unitRegular runAction:[CCActionSequence actions:move1, move2, move3, move4, nil]];
}
在此之后,你需要实际创建 sendSecondUnit 方法,如下所示:
-(void)sendSecondUnit
{
ccBezierConfig bezConfig1;
bezConfig1.controlPoint_1 = ccp(0, winSize.height);
bezConfig1.controlPoint_2 = ccp(winSize.width*3/8, winSize.height);
bezConfig1.endPosition = ccp(winSize.width*3/8, winSize.height/2);
CCActionBezierTo *bez1 = [CCActionBezierTo actionWithDuration:1.0 bezier:bezConfig1];
ccBezierConfig bezConfig2;
bezConfig2.controlPoint_1 = ccp(winSize.width*3/8, 0);
bezConfig2.controlPoint_2 = ccp(winSize.width*5/8, 0);
bezConfig2.endPosition = ccp(winSize.width*5/8, winSize.height/2);
CCActionBezierBy *bez2 = [CCActionBezierTo actionWithDuration:1.0 bezier:bezConfig2];
ccBezierConfig bezConfig3;
bezConfig3.controlPoint_1 = ccp(winSize.width*5/8, winSize.height);
bezConfig3.controlPoint_2 = ccp(winSize.width, winSize.height);
bezConfig3.endPosition = ccp(winSize.width - 50, 0);
CCActionBezierTo *bez3 = [CCActionBezierTo actionWithDuration:1.0 bezier:bezConfig3];
[unitBezier runAction:[CCActionSequence actions:bez1, bez2,
bez3, nil]];
}
前面的方法创建了三个贝塞尔配置,并将它们附加到一个带有贝塞尔配置的 MoveTo 命令。这样做的原因是每个贝塞尔配置只能接受两个控制点。正如你在标记的截图中所见,每个白色和红色方块代表一个控制点,你只能使用单个贝塞尔配置制作 U 形抛物线。
因此,要制作三个 U 形,你需要三个贝塞尔配置。

最后,确保在 touchBegan 方法中,使 unitBezier 停止所有动作(即,在重置时停止):
[unitBezier stopAllActions];
就这样!当你运行项目并点击屏幕(或点击并保持)时,你会看到蓝色正方形的 M 形状穿过,随后是黄色正方形的波浪形 M 形状。
小贴士
如果你想要将贝塞尔 MoveTo 或 MoveBy 动作适应到你的项目中,你应该知道你可以使用每个贝塞尔配置创建一个 U 形。它们相当容易实现,并且可以快速复制粘贴,如 sendSecondUnit 函数所示。
此外,由于控制点和终点位置只是 CGPoint 值,它们可以是相对的(即,相对于单元的当前位置、世界位置或敌人的位置),并且作为一个常规的 CCAction,它们可以很容易地与任何 CCNode 对象一起运行。
通过设备倾斜实现深度感知
一种可以在不真正改变游戏的情况下提高游戏酷炫程度的方法是,当用户倾斜设备时产生一种深度感知的感觉。这在 Shadowmatic 和 Jump! Chump! 游戏中、Wunderlist 清单中,或者将 Perspective Zoom 设置为 开启 的锁屏背景图片中都可以看到。虽然这很微妙,但它可以使你的游戏感觉更加精致,并增加用户参与度,因为它只是他们发现游戏酷或有趣的一个额外因素。
小贴士
作为提醒,你无法在模拟器中使用设备的倾斜/加速度计。这必须在物理设备上完成。
这里是菜单上看到的 Shadowmatic 游戏倾斜效果。正如你所见,它是一个三维物体,相机围绕物体旋转,以及阴影。
第一个如下所示:

第二个如下所示:

以下是在 Jump! Chump! 游戏中看到的倾斜效果,你可以看到主要角色和敌人的影子被移动。
第一个如下所示:

第二个如下所示:

这不是视差滚动吗?
这与之前类似,但略有不同。视差滚动——如果你不熟悉的话——是指有多个背景层,每个层以不同的速度在屏幕上移动,从而产生更真实运动的感觉。Cocos2d 还有一个叫做 CCParallaxNode 的功能,它基本上允许节点子代之间有不同的相对移动速度。例如,假设你添加了一个背景图像、一个中间图像和一个前景图像,它们有不同的比例。当你移动 CCParallaxNode 对象时,它将自动根据每个子代设置的比率移动子代。
视差滚动类似,因为仍然会有多个层在移动,并且它们将根据设备的倾斜在某个方向上稍微移动。因此,用户会感觉到有物体(例如按钮)实际上在其他物体(例如背景中的草地)之前。
让我们开始实现一些简单的深度感知效果。
示例项目 – 深度
从代码仓库下载 Depth 示例项目并运行项目。你会注意到有一些背景山脉、草地和几个按钮,它们并没有指向任何地方。我们将改变这一点,让按钮在屏幕周围移动,以增加一些深度感。
创建视差节点并添加对象
在 MainScene.h 文件中,添加此处列出的变量,并导入 CoreMotion 框架:
#import <CoreMotion/CoreMotion.h>
@interface MainScene : CCNode
{
CGSize winSize;
//add these
CCParallaxNode *parallax;
CMMotionManager *motionManager;
CGFloat xFiltered, yFiltered ;yFiltered
}
CCParallaxNode 很明显。motionManager 用于跟踪加速度计数据,而 xFiltered 和 yFiltered 将用作加速度计的过滤器,以防止它变得过于抖动。
现在,在 MainScene.m 的 init 方法中,注释掉添加 layoutbox 到场景的行。添加初始化视差节点、将 layoutbox 添加到其中以及将视差节点添加到场景中的代码:
//[self addChild:layout];
parallax = [CCParallaxNode node];
//Ratio: For every 1 pixel moved, move the child that amount
parallax.position = ccp(winSize.width/2, winSize.height/2);
[parallax addChild:layout z:0 parallaxRatio:ccp(1,1) positionOffset:ccp(0,0)];
[self addChild:parallax];
如果你想知道比例参数是什么,它就像注释中说的那样:对于每个移动的视差对象像素,它将移动子代相同数量的像素。例如,如果视差节点向左移动了 100 像素,并且如果子代的比例为 0.5,子代将向左移动 50 像素。这说得通吗?所以,对于我们的按钮,我们希望比例是 1:1。这意味着,对于每个视差节点移动的像素,按钮将以相同的数量移动。
现在运行项目并不会做任何事情,但是,让我们开始获取加速度计数据,这样我们就可以通过倾斜设备来切换菜单按钮。
可视化深度
在MainScene.m的init方法中,添加以下代码块,这将设置运动管理器以便开始收集加速度计数据:
//60 times per second. In theory once per frame
CGFloat interval = 1/60.f;
motionManager = [[CMMotionManager alloc] init];
motionManager.accelerometerUpdateInterval = interval;
[motionManager startAccelerometerUpdates];
[self schedule:@selector(getAccelerometerData:) interval:interval];
为了让加速度计影响视差节点的位置,我们必须创建getAccelerometerData方法,并在那里修改位置:
-(void)getAccelerometerData:(CCTime)delta
{
CMAcceleration accel = motionManager.accelerometerData.acceleration;
CGFloat filterValue = 0.8f;
xFiltered = filterValue * xFiltered + (1.0 - filterValue) * accel.x;
yFiltered = filterValue * yFiltered + (1.0 - filterValue) * accel.y;
parallax.position = ccp(winSize.width/2 + 50 * yFiltered, winSize.height/2 - 50 * xFiltered);
}
此方法基本上每秒读取 60 次加速度计数据,将其通过一个过滤器(如果你想更平稳的运动,增加 K 值,我们在前面的代码中将其称为filterValue,最大值为 1),并根据过滤后的x和y值分配视差的位置。
小贴士
如果这是你自己在项目中想要实现的所有效果,那么请在这里停止。如果你在自己的项目中使用这个功能,需要注意的关键点是视差节点根据给定的比例影响每个子节点的位置。此外,如果你使用倾斜机制,确保对加速度计数据进行过滤,否则它将会非常抖动,实际上会损害你的游戏而不是帮助它。
接下来,我们将平衡视差节点,这样按钮就不会总是被推上去或偏到一边。
恢复平衡(校准到新的旋转)
在用户调整他们的手机到新的位置后,你希望慢慢地将他们的平衡调整到他们持握设备的方式。例如,如果他们开始时将设备平放在桌子上,然后倾斜它大约 45 度朝向自己,你需要相应地移动项目。然后你需要慢慢地使 45 度的位置看起来与它平放时一样。
这很容易做到,只要我们有一个变量来保存平衡值。要做到这一点,打开MainScene.h并添加以下代码:
CGFloat avgXValue, avgYValue;
这两个值将存储最后记录的 100 个xFiltered和yFiltered值的平均值。
然后在MainScene.m中,修改getAccelerometerData方法中的三个相关行,使其看起来像这里所示:
avgXValue = (avgXValue * 99 + xFiltered)/100.f;
avgYValue = (avgYValue * 99 + yFiltered)/100.f;
parallax.position = ccp(winSize.width/2 + 50 * (yFiltered - avgYValue), winSize.height/2 - 50 * (xFiltered - avgXValue));
起初,数学可能没有意义,让我们来了解一下。
首先,avgXValue和avgYValue通过逐个点缓慢地添加到平均值中,以估计的方式计算平均值。这并不是在真正的“平均值”意义上的 100%准确,但它足够接近。这样做也稍微好一些,因为它意味着更少的代码、更少的内存和更快的执行速度。由于我们每秒做 60 次这样的操作,因此获得一个准确的平均值并不完全重要。几秒钟内,你将获得足够多的点,足以接近真实值。
第二,xFiltered/xFiltered和avgYValue/avgXValue的减法是为了慢慢将其带回中心。例如,如果你的yFiltered值是-1,而avgYValue是0,它将迅速跳到新的位置。但如果设备在-1处保持足够长的时间,avgYValue将非常接近-1,减去这两个变量将得到一个归零的位置,这正是我们想要的。
小贴士
如果你正在自己的项目中实现校准效果,这里需要注意的关键是减去两个值。无论你使用单值校准还是伪平均值(就像这里使用的),如果你不减去过滤后的x和y值,你将看不到任何变化。
对于慢速校准,前面提到的方法是可行的。如果你想实现即时校准(例如,当用户说的时候通过按钮重新对齐),只需在按钮按下时存储单个加速度计值,而不是存储平均值。
一个快速滚动的示例
由于我们正在讨论视差滚动,让我们快速回顾一下视差滚动的简单示例,正如它被设计的那样。在MainScene.m(或头文件;实际上并不重要),导入GameScene.h文件:
#import "GameScene.h"
然后,在buttonPressed方法中,添加以下代码行。它将进入游戏场景:
[[CCDirector sharedDirector] replaceScene:[GameScene scene]];
现在让我们将CCParallaxNode添加到GameScene.h中:
@interface GameScene : CCScene
{
CGSize winSize;
//add this:
CCParallaxNode *parallax;
}
然后,在GameScene.m文件的init方法中,设置视差节点并添加一些精灵到节点中:
CCSprite *bg1 = [CCSprite spriteWithImageNamed:@"mountains.png"];
CCSprite *bg2 = [CCSprite spriteWithImageNamed:@"mountains.png"];
CCSprite *mg1 = [CCSprite spriteWithImageNamed:@"midground.png"];
CCSprite *mg2 = [CCSprite spriteWithImageNamed:@"midground.png"];
CCSprite *fg1 = [CCSprite spriteWithImageNamed:@"foreground.png"];
CCSprite *fg2 = [CCSprite spriteWithImageNamed:@"foreground.png"];
fg1.anchorPoint = fg2.anchorPoint = ccp(0.5,0);
CCSprite *sun = [CCSprite spriteWithImageNamed:@"sun.png"];
parallax = [CCParallaxNode node];
parallax.anchorPoint = ccp(0,0);
parallax.position = ccp(0,0);
[parallax addChild:bg1 z:0 parallaxRatio:ccp(0.35,0) positionOffset:ccp(winSize.width/2, winSize.height/2)];
[parallax addChild:bg2 z:0 parallaxRatio:ccp(0.35,0) positionOffset:ccp(winSize.width/2 + winSize.width - 2,winSize.height/2)];
[parallax addChild:sun z:1 parallaxRatio:ccp(0.5,0) positionOffset:ccp(winSize.width, winSize.height * 0.8)];
[parallax addChild:mg1 z:1 parallaxRatio:ccp(0.5,0) positionOffset:ccp(winSize.width/2, winSize.height/2)];
[parallax addChild:mg2 z:1 parallaxRatio:ccp(0.5,0) positionOffset:ccp(winSize.width/2 + winSize.width, winSize.height/2)];
[parallax addChild:fg1 z:2 parallaxRatio:ccp(1,0) positionOffset:ccp(winSize.width/2, 0)];
[parallax addChild:fg2 z:2 parallaxRatio:ccp(1,0) positionOffset:ccp(winSize.width/2 + winSize.width, 0)];
[self addChild:parallax];
如果你正在想positionOffset的作用,它会在应用任何视差比例之前将节点移动到作为参数给出的位置。这在首次设置场景时很有用(就像我们现在正在做的那样)。
然后,我们想要启用触摸,所以我们向init方法中添加这一行:
[self setUserInteractionEnabled:YES];
但首先,我们需要创建一个变量来保存手指之前的位置,这样我们才知道要移动视差节点多远。因此,在GameScene.h中添加以下内容:
CGPoint previousPosition;
最后,添加touchBegan和touchMoved方法,以便视差节点根据touchMoved的距离移动,同时确保滚动永远不会超出边界:
-(void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
previousPosition = [touch locationInNode:self];
}
-(void)touchMoved:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
CGPoint newPosition = [touch locationInNode:self];
parallax.position = ccp(parallax.position.x + (newPosition.x - previousPosition.x), 0);
if (parallax.position.x < -winSize.width)
parallax.position = ccp(-winSize.width,0);
if (parallax.position.x > 0)
parallax.position = ccp(0,0);
previousPosition = newPosition;
}
从这里开始,你可以运行项目,当你点击播放时,你将被带到游戏场景,现在它有一个(相当粗糙的)背景、中景和前景元素集合。当你用手指在屏幕上滑动时,你会注意到草地以相同的速度滚动,而山脉、树木和太阳则以不同的速度滚动。
小贴士
如果你正在自己的项目中实现视差滚动背景,需要注意添加对象之间的比例差异。一旦这个问题解决,你只需要根据手指拖动、角色移动或其他操作移动视差节点。
制作单元流线或“幽灵”的三种方法
如果你曾经玩过像 Fruit Ninja、Blek、Jetpack Joyride 或 Tiny Wings 这样的游戏,你肯定见过这种效果。屏幕上有一个位置会生成类似星星、划痕效果、烟雾云或跟随手指的线条。
样本项目 – 鬼魂
前往代码仓库,在 Sample Projects 文件夹下打开 Ghosts 项目。如果你运行它,你会看到只有三个作为按钮的标签,它们引导到主要是空白的屏幕,实际上并没有做什么。这就是你发挥作用的地方。
在这里,你将学习如何制作你刚刚阅读的效果。制作类似效果通常有三种方法。让我们逐一介绍每种方法,从最简单的开始,到最复杂的结束。
方法 1 – 粒子系统
粒子系统基本上是大量创建的精灵,例如烟花效果、喷泉或闪烁的蜡烛。使用粒子系统与你自己创建精灵之间的区别在于,粒子系统通常只允许移动、缩放、旋转和颜色变化。但对于像烟雾云这样的简单效果,粒子系统就足够了。
小贴士
如果你希望为这个样例项目或你自己的任何项目创建自己的粒子,如第一章中提到的,刷新你的 Cocos2d 知识,粒子设计师是这个目的的绝佳工具。你可以在代码中手动创建它们,但使用像粒子设计师这样的编辑器可视化创建它们要容易得多,也更高效。
在 Xcode 中打开 Ghosts 项目,打开 ParticleExampleScene.h 文件,并将 CCParticleSystem 变量添加到列表中:
CCParticleSystem *smokeParticle;
然后打开 ParticleExampleScene.m 文件,并在 init 方法中将粒子系统添加到屏幕上:
smokeParticle = [CCParticleSystem
particleWithFile:@"SmokeParticle.plist"];
[self addChild:smokeParticle];
之前代码中使用的粒子系统是在本书开头提到的粒子设计师中创建的。它使用一个简单的云形状图像,并设置了某些属性以创建淡出效果。
最后,在 updateParticleSource 方法中,设置粒子系统的源位置,这样当你用手指在屏幕上拖动时,烟雾会从不同的位置开始:
smokeParticle.sourcePosition = startPosition;
如果你现在运行项目,你会看到烟雾会不断生成,即使你移开了手指。虽然这对这个样例项目来说意义不大,但想象一下,你可能只想在用户处于空中或执行连击动作时显示粒子。换句话说,我们需要一种方法来根据需要停止和启动粒子流(在这种情况下,当用户放置或移除手指时)。
虽然 Cocos2d 没有开始粒子系统的方法,但添加这样的方法非常简单。所以,查找 stopSystem 的项目。在 CCParticleSystemBase.h 文件(应该是搜索结果中的第一个)中,在 stopSystem 声明之上添加以下代码:
-(void)startSystem;
然后,在 CCParticleSystemBase.m 文件(搜索结果中的下一个)中添加此方法,以便你可以按需启动粒子系统:
-(void)startSystem
{
_active = YES;
_elapsed = _duration;
_emitCounter = 0;
}
现在回到 ParticleExampleScene.m 文件,在 init 方法中,在你将烟雾粒子添加到场景之后,调用 stopSystem 方法,这样当场景首次启动时,它就不会出现在屏幕上了:
[smokeParticle stopSystem];
将 stopSystem 方法添加到 touchEnded 方法中,以便粒子系统停止产生新的粒子,并让旧的粒子消亡:
-(void)touchEnded:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
isStreaming = NO;
[smokeParticle stopSystem];
}
最后,在 touchBegan 方法中调用新创建的 startSystem 方法,以便当手指在屏幕上时,粒子开始流动:
[smokeParticle startSystem];
在这个阶段运行项目将允许你看到已经创建的粒子系统的启动和停止。
小贴士
你可能想知道如何将此代码适应到自己的项目中。一旦粒子系统被添加到屏幕上,基本上就是一个更新粒子系统源位置的问题,使用“起始位置”。例如,你可以有一个火箭在屏幕上飞行,火箭的位置就是起始位置。
现在,让我们继续到一个外观非常相似但需要更多代码的风格,这允许对游戏的整体外观进行更大的操作。
方法 2 – 精灵或节点
虽然创建粒子系统需要的代码较少,但当你自己创建精灵并处理其所有事件时,你可以做更多的事情。例如,你可以让精灵改变颜色几次,设置疯狂的运动模式,完全更换图像,或者应用粒子系统无法做到的其他 CCAction 动作。
小贴士
粒子系统无法做到的一件事是震动机制。你可以通过排队一系列 CCMove 动作来实现这一点,这些动作将按照你想要的方向进行(通常,任何方向不超过几个点),并使用一个变量来设置持续时间,这样你就可以根据情况增加或减少速度。
在 Xcode 中打开 Ghosts 项目后,打开 SpriteExampleScene.m 文件,并将以下代码块添加到 spawnStreamer 方法中:
CCSprite *heart = [CCSprite spriteWithImageNamed:@"heart.png"];
heart.color = [CCColor redColor];
heart.position = startPosition;
[self addChild:heart];
CCActionScaleTo *shrink = [CCActionScaleTo actionWithDuration:0.5 scale:0];
CCActionCallBlock *block = [CCActionCallBlock actionWithBlock:^{
[self removeChild:heart];
}];
[heart runAction:[CCActionSequence actions:shrink, block, nil]];
此代码将产生一个红色的心形(图像是白色的,但我们将其染成了红色),在半秒内将其缩小到 0 的比例,然后将其从场景中移除,这样在运行一段时间后就不会产生延迟。
如果你运行项目并在屏幕上拖动手指,你会看到一些美丽的心形以流畅的方式被创建出来。就这样!之所以提到你可能需要使用更多的代码,是因为你想要在流线(鬼魂、幻影或 whatever)上做更多的事情,那么你需要更多的代码来实现你想要的效果。
例如,如果你想使精灵沿着贝塞尔曲线移动,旋转 360 度,然后缩小并消失,同时变成绿色心形。代码看起来可能像这样:
CCSprite *heart = [CCSprite spriteWithImageNamed:@"heart.png"];
heart.color = [CCColor redColor];
heart.position = startPosition;
[self addChild:heart];
CCActionTintTo *tint = [CCActionTintTo actionWithDuration:0.5 color:[CCColor greenColor]];
ccBézierConfig bezConfig;
bezConfig.controlPoint_1 = ccp(0,100);
bezConfig.controlPoint_2 = ccp(100,100);
bezConfig.endPosition = ccp(100,0);
CCActionBézierTo *move = [CCActionBézierBy actionWithDuration:0.5 Bézier:bezConfig];
CCActionRotateBy *rotate = [CCActionRotateBy actionWithDuration:0.5 angle:360];
CCActionScaleTo *shrink = [CCActionScaleTo actionWithDuration:0.5 scale:0];
CCActionCallBlock *remove = [CCActionCallBlock actionWithBlock:^{
[self removeChild:heart];
}];
[heart runAction:tint];
[heart runAction:[CCActionSequence actions:move, rotate, shrink, remove, nil]];
小贴士
如果你希望在项目中实现类似的功能,关键要求是 isStreaming 布尔值和在更新方法中的检查。其他一切都是直截了当且非常类似于改变 startPosition 的粒子方法。
最后,让我们来处理所有方法中最独特的方法——即 Fruit Ninja 和 Blek 都实现过的恒定线条。
方法 3 – 恒定线条
恒定线条是流线最先进的版本。它实际上不是一个粒子,因此不能作为精灵创建。相反,我们将使用 CCMotionStreak 来创建划痕效果。CCMotionStreak 的实现非常简单,因为它只需要一个位置、一个图像和一个颜色。
在 Xcode 中打开 Ghosts 项目,打开 ConstantLineExampleScene.h 并将一个 CCMotionStreak 对象添加到列表中:
{
CGSize winSize;
CGPoint startPosition;
CCMotionStreak *streak;
}
然后,在 ConstantLineExampleScene.m 中,创建轨迹并将其添加到场景的 init 方法中:
streak = [CCMotionStreak streakWithFade:0.35 minSeg:1 width:15 color:[CCColor yellowColor] textureFilename:@"blade.png"];
[self addChild:streak];
这里我们使用黄色颜色和名为 blade.png 的文件。你可以使用任何你想要的图像,但因为我们想要一个刀片划痕效果,我们将使用形状像钻石的图像,以给开始和结束部分一个尖锐的边缘。
接下来,在 touchBegan 方法中,设置 CCMotionStreak 对象的位置,并重置它,以便当用户再次触摸时,它不会连接两条线(除非你想要这样做;在这种情况下,你不应该重置它):
streak.position = startPosition;
[streak reset];
最后,在 touchMoved 方法中,设置位置,以便每次手指拖动时,动作轨迹滑动到下一个位置:
streak.position = startPosition;
就这样!如果你运行项目,转到示例行,并拖动手指,你会看到漂亮的划痕效果。如果你想使线条更大(或更小),只需调整 CCMotionStreak 初始化中的宽度。同样适用于颜色、图像,甚至是持续时间。然而,不建议将持续时间设置得太高,因为动作轨迹可能会有些延迟。
小贴士
如果你想在你的项目中实现这种风格的流线/鬼魂,关键点是 CCMotionStreak 依赖于某个位置。因此,如果你想在一个太空船上有一个轨迹,你必须在更新函数中更新动作轨迹的位置,如下所示:
-(void)update:(CCTime)delta
{
streak.position = ship.position;
}
触摸屏控制与 D-pad 适配(以及为什么了解这种区别如此重要)
由于我们是为 iOS 编码,因此需要注意我们为游戏创建的设备的独特性,即触摸屏本身与用户手中拥有的带有摇杆和扳机的塑料控制器之间的区别。当为 iOS(或任何带有触摸屏的智能手机设备)创建游戏时,你必须创建的控制方式要使它们在设备上感觉自然流畅,就像游戏从未在其他游戏机上存在过一样。
当然,可以理解有些游戏更适合 D-pad 风格的移动。然而,始终有加速度计、滑动控制,或者像《无尽之刃》那样(使用屏幕上的小按钮来阻挡或躲避,并使用屏幕中央的滑动来挥剑攻击)。
不幸的是,没有“如何编程”部分专门介绍最佳控制方式,因为一切都基于你自己的游戏。例如,在这本书的游戏中,我们不是点击一个单位然后使用 D-pad 告诉游戏将单位发送到哪个方向。相反,我们利用 iOS 的触摸和拖动功能,并将其集成到我们的控制方式中。
本章后面还有更多关于触摸集成控制的优秀例子,但首先,让我们来看看哪些是不应该做的。
iOS 游戏控制的不良示例
以下是一些游戏示例,它们只是简单地将它们在游戏机或 PC 版本上的控制方式直接应用到触摸屏上,希望这样能奏效。
《游戏开发故事》:虽然这款游戏本身并不差,但控制方式完全是菜单驱动的,感觉就像它们不是为移动设备设计的。

《古墓丽影 I》:仅从截图来看,就能轻易看出控制方式的复杂性以及开发者投入在为移动平台创新控制方案上的时间之少。

《杜克·努克 3D》:同样,仅从下面的截图来看,你就能理解这里所指出的糟糕控制方式:

《中土世界街机》:在循环迷你游戏甚至迷你游戏本身方面,它们本可以做得更好。有些游戏感觉适合移动设备,但其余的则没有发挥出应有的作用。

触摸屏控制的好例子
在本节中,你会发现一些游戏,无论它们是否在游戏机上开始,在移动设备上都有很好的控制方式。它们展示了为带有触摸屏和加速度计的手持设备编写游戏的所有美好之处。
首先是 Shadowmatic;你拖动屏幕来旋转物体,倾斜设备会让摄像头轻微移动,让你在这款游戏中真正体验到三维的感觉。

接下来是Smash Hit;它不仅会在你的手指触摸的地方产生金属球,而且易于按下的按钮也不会打断游戏进程。

我不能忘记Angry Birds,它具有缩放手势、拉回机制和易于理解的游戏玩法,所有这些都源于出色的触摸屏控制。

还有Temple Run,它将滑动机制整合到每个方向,使用户感觉就像实际上在推动他们的角色向特定方向前进。

最后但同样重要的是(因为肯定还有更多拥有出色触摸屏控制功能的设备),是Blek。我甚至不确定这款游戏是否能在非触摸屏环境中存在,因为它真是太流畅了!

摘要
在这一章中,你学习了如何做各种事情,从制作得分表和预览下一步操作,到利用贝塞尔曲线和创建单位流线。
记住,如果你需要回到这一章节(或者任何其他章节),请不要犹豫。代码是按照复制粘贴的心态编写的,因此它可以适应任何项目,无需太多修改(如果真的需要的话)。
在下一章中,我们将增强这款游戏,它至今只使用了程序员艺术,使其看起来像是一个 20 人团队花费数月时间开发出来的作品。
第六章. 整理与润色
润色是你在整本书中学到最重要的东西。如果你能掌握制作游戏润色的艺术,那么它无论简单还是复杂,你都会有一个很棒的游戏。而且,即使你可能是一个多年没有程序错误的优秀程序员,或者是一个当人们看到你的角色设计时会晕倒的艺术家,你仍然需要润色你的游戏。这对于任何现代游戏来说,让玩家有一个坚实的体验是绝对必要的。
在本章中,你将了解清理游戏、平滑粗糙边缘,并将具有酷炫机制的游戏变成有趣、可玩且难忘体验所需的一切。但这不仅仅是让它看起来好。它需要感觉好,整个用户体验都需要是愉快的。不分先后,我们将涵盖以下内容:
-
按钮按下视觉效果
-
单位组合脉冲
-
一个教程
-
在 Facebook 和 Twitter(以及更多)上分享
-
声音开/关
-
游戏中心排行榜
-
滑动过渡
显然,你不必做所有这些,但如果你想让更多人玩你的游戏,他们更有可能玩它,如果它经过了打磨。现在,润色时最重要的事情是图形和声音风格。它们需要保持一致,同时也要视觉上吸引人,这样用户在玩游戏时就不会感到困惑或被吓到。
话虽如此,如果你打开 第六章 项目,你会看到游戏的图形和字体已经更新。看起来和感觉都很扎实。唯一剩下要做的就是本章所涵盖的内容(正如你刚才在列表中看到的)。那么,让我们开始吧。
按钮按下视觉效果
嗯,按钮按下视觉效果很棒!Cocos2d 足够好,当触摸按钮时,它会提供按钮的暗化效果,但让我们给我们的按钮添加更多动画,让它们有更多的“点击”感觉。
修改 CCButton 类
如果我们只想让单个按钮具有特定的效果,我们只需修改它存在的位置上的那个按钮。然而,我们希望游戏中的所有按钮都有相同的效果,因此我们需要打开 CCButton.m 文件。最简单的方法是在项目中搜索 CCButton 并点击任何指向 CCButton.m 的链接。
在这里,你将在文件顶部附近添加两个函数:scaleButtonUp 和 scaleButtonDown。这两个函数都在按钮上运行动作,这些动作给按钮带来了许多游戏中常见的弹跳按下效果:
小贴士
或者,你也可以扩展/继承 CCButton 并重写创建所需效果所需的方法。这种方法在 Cocos2d 版本更新更改 CCButton 默认代码的情况下也稍微更稳定。但就目前而言,我们只需修改现有的 CCButton 代码。
@implementation CCButton
//add this one...
-(void)scaleButtonDown
{
[self stopAllActions];
id scaleDown = [CCActionEaseInOut actionWithAction:[CCActionScaleTo actionWithDuration:0.11f scale:0.8f] rate:2];
id scaleBackUp = [CCActionEaseInOut actionWithAction:[CCActionScaleTo actionWithDuration:0.13f scale:0.9f] rate:2];
id actionSequence = [CCActionSequence actions:scaleDown, scaleBackUp, nil];
[self runAction:actionSequence];
}
//and this one...
-(void)scaleButtonUp
{
[self stopAllActions];
id scaleDown = [CCActionEaseInOut actionWithAction: [CCActionScaleTo actionWithDuration:0.11f scale:1.15f] rate:1.5f];
id scaleBackUp = [CCActionEaseInOut actionWithAction: [CCActionScaleTo actionWithDuration:0.13f scale:1.0f] rate:2];
id actionSequence = [CCActionSequence actions:scaleDown, scaleBackUp, nil];
[self runAction:actionSequence];
}
然后在 touchEntered 方法的底部添加对 scaleButtonDown 方法的调用,如下所示:
- (void) touchEntered:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
...
//add this:
[self scaleButtonDown];
}
最后,在touchExited和touchUpInside中,你必须添加对scaleButtonUp方法的调用。这是针对以下情况:要么当玩家的手指离开按钮(表示如果他们抬起手指,它就不会被激活),要么当他们实际上“按下”按钮时:
- (void) touchExited:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
self.highlighted = NO;
//add this:
[self scaleButtonUp];
}
- (void) touchUpInside:(CCTouch *)touch withEvent:(CCTouchEvent *)event
{
...
//add this:
[self scaleButtonUp];
}
就这样!给我们的按钮添加那一点额外的抛光只花了整整 2 分钟。这并不是什么能单独卖出游戏的东西,但它足够微妙,能让玩家感觉到:“哇,开发者们真的花了时间来制作这个游戏!”
单位组合时的脉冲
当我们的单位组合时,实际上并没有发生什么特别的事情。所以,我们要做的是添加一个轻微的脉冲效果。基本上,当两个单位组合时,我们希望单位以类似于我们刚刚创建的按钮视觉(它扩张然后再次缩小)的方式增长。
小贴士
当单位生成时,你还可以做一些花哨的事情,比如粒子效果(例如,微妙的爆炸/爆发效果)、精灵动画和其他各种事情。
精炼的关键是跳出思维定式,同时观察游戏中已有的东西,以保持美学的一致性和稳固性。
为了执行我们想要的脉冲效果,因为我们很可能会在多个地方放置相同的代码,让我们创建一个接受CCNode对象并应用我们心中所想效果的函数。所以,在MainScene.m中,在代码的某个地方(最好是组合代码附近)添加以下方法:
-(void)pulseUnit:(CCNode*)unit
{
CGFloat baseScale = 1.0f;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
baseScale = 0.8f;
id scaleUp = [CCActionEaseInOut actionWithAction:[CCActionScaleTo actionWithDuration:0.15f scale:baseScale * 1.2f] rate:2];
id scaleDown = [CCActionEaseInOut actionWithAction:[CCActionScaleTo actionWithDuration:0.15f scale:baseScale * 0.9f] rate:2];
id scaleToFinal = [CCActionEaseInOut actionWithAction:[CCActionScaleTo actionWithDuration:0.25f scale:baseScale] rate:2];
id seq = [CCActionSequence actions:scaleUp, scaleDown, scaleToFinal, nil];
[unit stopAllActions];
[unit runAction:seq];
}
这个函数考虑了单位的“原始缩放”,然后运行一个向上和向下缩放的二步序列。然而,我们想确保如果有超过两个单位组合(我们将在同一单位上多次调用此函数),我们停止单位上所有当前的动作。
现在我们已经有一个函数准备好了,让我们在四个单位组合的地方添加对这个函数的调用。
前两个地方在checkForCombineWithUnit方法中(如果你在这里没有使用“第六章”的代码,这个方法就没有实现):
-(void)checkForCombineWithUnit:(Unit*)first andUnit:(Unit*)other usingDeletionArray:(NSMutableArray*)array
{
...
if (ov > fv)
{
...
[self pulseUnit:other];
}
else
{
...
[self pulseUnit:first];
}
...
}
在checkForAnyDirectionCombine方法的最后两个地方:
–(void)checkForAnyDirectionCombineWithUnit:(Unit*)first andUnit:(Unit*)other usingDeletionArray:(NSMutableArray*)array
{
...
if (ov > fv)
{
...
[self pulseUnit:other];
}
else
{
...
[self pulseUnit:first];
}
}
}
就这样!运行游戏并组合两个单位,你就会看到脉冲效果。同样,这也是微妙的,这正是我们所需要的——许多微妙的变化随着时间的积累会变得很多。
指南
我们已经到了需要教玩家如何玩我们的游戏的时候了。虽然你可能能够在你站在测试者的肩膀上时向他们解释游戏,但你无法为从 App Store 下载应用的人做同样的事情。因此,我们需要一个指南,而且要快,因为我们希望玩家玩游戏,而不是玩指南。
对于这个项目,我们将有一个简单的指南,基本上通过几个词和一些图片来解释游戏的主要概念:
-
玩家用手指滑动单位
-
组合他们自己的单位
-
击败敌人单位
-
保护中心广场
显然,我们可以通过解释更多细微的概念来深入探讨,但相反,我们给玩家留出空间去学习、实验和亲自测试。我们只想让教程为他们做好准备,这样他们在游戏开始时不知道该做什么,或者输了却不知道为什么输时不会感到沮丧。
教程阶段变量和 NSUserDefaults 键
我们想知道用户在教程中的哪个阶段。因此,我们需要知道要显示哪些文本和选项。例如,如果我们创建了一个使用多个场景的教程,我们就不需要变量,因为场景会指示我们正在进行哪个教程。然而,因为我们是在 MainScene 中完成所有这些(并且因为我们希望在教程结束后能够平滑地过渡到常规游戏),最好使用一个变量来跟踪我们已经走了多远。
因此(因为我们将在后面的部分中需要访问这个变量),让我们在 MainScene.h 中创建一个 @property 变量,如下所示:
}
+(CCScene*)scene;
...
//here:
@property (nonatomic, assign) NSInteger tutorialPhase;
@end
如果这是一个好的教程,用户在第一次通过时就能学会,所以我们可以假设在完成所有步骤后,将“他们是否完成?”变量设置为 true。这意味着我们想在变量中记录他们之前是否完成了教程,所以我们将再次使用 NSUserDefaults。让我们定义另一个键,这样我们就可以消除人为错误并提高代码的可读性。在 MainScene.h 文件中,在文件顶部声明以下键:
FOUNDATION_EXPORT NSString *const KeyFinishedTutorial;
在 MainScene.m 文件中,定义关键变量在顶部,可以像以下这样:
NSString *const KeyFinishedTutorial = @"keyFinishedTutorial";
最后,我们想确定是否显示教程。因为我们有一个存储决定因素的键,我们可以简单地从 NSUserDefaults 中读取它,然后以正常方式运行游戏或从阶段 1 开始教程。
因此,在 MainScene.m 文件的 init 方法底部,修改 spawnNewEnemy 调用为以下内容:
if ([[NSUserDefaults standardUserDefaults] boolForKey:KeyFinishedTutorial])
{
[self spawnNewEnemy:[self getRandomEnemy]];
self.tutorialPhase = 0;
}
else
{
//spawn enemy on far right with value of 1
Unit *newEnemy = [Unit enemyUnitWithNumber:1 atGridPosition:ccp(9, 5)];
newEnemy.position = [MainScene getPositionForGridCoord: newEnemy.gridPos];
[newEnemy setDirection:DirLeft];
[self spawnNewEnemy:newEnemy];
self.tutorialPhase = 1;
[self showTutorialInstructions];
}
此外,为了消除错误并为我们之后的编码工作打下基础,我们定义了 showTutorialInstructions 对象(现在空体是完全可以接受的;我们将在下一节中介绍它):
-(void)showTutorialInstructions
{
}
在前面的 if-else 语句中,你可以看到 tutorialPhase 被设置为 0(这次不进行教程)或 1(从阶段 1 开始教程),这取决于他们是否完成了教程。如果没有完成,它还会在最右边生成一个值为 1 的新敌人。
这是我们教程的开始——设置必要的结构。接下来,我们将处理根据教程所处的阶段实际显示文本。
显示每个阶段的文本(以及 CCSprite9Slice)
每个教程阶段都需要有自己的文本。为此,我们将仅参考教程阶段变量并将文本分配给标签,根据我们所在的阶段。也就是说,在刚刚创建的 showTutorialInstructions 方法中,我们添加以下行来显示初始阶段 1 的文本:
NSString *tutString = @"";
if (self.tutorialPhase == 1)
{
tutString = @"Drag Friendly Units";
}
CCLabelBMFont *lblTutorialText = [CCLabelBMFont labelWithString:tutString fntFile:@"bmScoreFont.fnt"];
lblTutorialText.color = [CCColor colorWithRed:52/255.f green:73/255.f blue:94/255.f];
lblTutorialText.position = [MainScene getPositionForGridCoord:ccp(5,2)];
lblTutorialText.name = @"tutorialText";
[self addChild:lblTutorialText z:2];
CCSprite9Slice *background = [CCSprite9Slice spriteWithSpriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"imgUnit.png"]];
background.margin = 0.2;
background.position = ccp(0.5,0.4);
background.positionType = CCPositionTypeNormalized;
background.contentSize = CGSizeMake(1.05f, 1.2f);
background.contentSizeType = CCSizeTypeNormalized;
[lblTutorialText addChild:background z:-1];
运行项目。你会看到文本横跨网格的顶部中央。但从代码角度来看,我们刚才添加的块中有很多内容,所以让我们快速浏览一下新内容。
首先,我们给标签命名(在之前的版本中是一个标签属性,但现在是一个字符串),这样我们就可以通过 getChildByName 函数在以后搜索它来访问 CCNode。接下来,我们将标签定位在 z:2,这样我们就确保它位于其他所有内容之上(默认是 z:0,最多我们有我们的单位在 z:1,所以 z:2 应该是合适的)。
此外,还有一个名为 CCSprite9Slice 的对象,这对你来说可能很陌生。如果你之前从未听说过 9 切片(或 9 拼图)精灵,请参考以下图表来了解它:

简而言之,中心部分可以在任何方向上缩放,角落不缩放,顶部和底部边距水平缩放,左侧和右侧边距垂直缩放。
当你需要边距缩放时,才需要 9 切片精灵。在其他任何情况下,最好使用常规的 CCSprite。
由于我们希望保持我们的艺术风格一致,我们可以使用 Unit.png 作为我们的 9 切片精灵,并附带 20%的边距(其余部分是空白,所以这是一个很好的数字)。然后我们将它定位在标签后面(使用 z:-1)并将内容大小设置为略大于标签的宽度和高度。
小贴士
当使用 CCSprite9Slice 时,如果你想改变按钮的 scale,你必须改变它的 contentSize 值,而不是缩放属性。
此外,边距值(或值)只能达到最大值 0.5(这意味着在任何方向上的图像的 50%)。
现在,我们将教程推进到下一个阶段。
推进教程
仅显示文本并不意味着我们有令人印象深刻的内容,因为到目前为止这并不是真正的教程。我们需要实现高级部分。因此,创建一个名为 advanceTutorial 的函数以及 removePreviousTutorialPhase(将用于删除前一个阶段的文本)并按照以下方式编辑它们:
-(void)advanceTutorial
{
++self.tutorialPhase;
[self removePreviousTutorialPhase];
if (self.tutorialPhase<7)
{
[self showTutorialInstructions];
}
else
{
//the tutorial should be marked as "visible"
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:KeyFinishedTutorial];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
-(void)removePreviousTutorialPhase
{
}
实际上,我们是在说,如果我们推进到下一个教程阶段,并且阶段小于 7,我们就显示下一个教程的说明。否则,我们只需将 didFinishTutorial 布尔值设置为 true。
最后,我们应该为每个阶段包含适当的文本,这样当我们开始推进教程阶段时,我们实际上可以看到进度。所以,在showTutorialInstructions函数中,修改if语句,使其看起来像以下(它还为第一阶段创建并显示了一个如何玩标签):
if (self.tutorialPhase == 1)
{
tutString = @"Drag Friendly Units";
CCLabelBMFont *lblHowToPlay = [CCLabelBMFont labelWithString:@"How to Play:" fntFile:@"bmScoreFont.fnt"];
lblHowToPlay.color = [CCColor colorWithRed:52/255.f green:73/255.f blue:94/255.f];
lblHowToPlay.position = [MainScene getPositionForGridCoord:ccp(5,1)];
lblHowToPlay.name = @"lblHowToPlay";
lblHowToPlay.scale = 0.8;
[self addChild:lblHowToPlay z:2];
CCSprite9Slice *bgHowTo = [CCSprite9Slice spriteWithSpriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"imgUnit.png"]];
bgHowTo.margin = 0.2;
bgHowTo.position = ccp(0.5,0.4);
bgHowTo.positionType = CCPositionTypeNormalized;
bgHowTo.contentSize = CGSizeMake(1.05f, 1.2f);
bgHowTo.contentSizeType = CCSizeTypeNormalized;
[lblHowToPlay addChild:bgHowTo z:-1];
}
else if (self.tutorialPhase == 2)
{
tutString = @"Combine Friendly Units";
id fadeRemoveHowToPlay = [CCActionSequence actions:[CCActionEaseInOut actionWithAction:[CCActionFadeOut actionWithDuration:0.5f] rate:2], [CCActionCallBlock actionWithBlock:^{
[self removeChildByName:@"lblHowToPlay"];
}], nil];
[[self getChildByName:@"lblHowToPlay" recursively:NO] runAction:fadeRemoveHowToPlay];
}
else if (self.tutorialPhase == 3)
{
tutString = @"Defeat Enemies";
}
else if (self.tutorialPhase == 4)
{
tutString = @"Protect Center";
}
else if (self.tutorialPhase == 5)
{
tutString = @"Survive";
}
else if (self.tutorialPhase == 6)
{
tutString = @"Enjoy! :)";
}
小贴士
注意,前面的代码也可以写成 switch-case 语句的形式。
那就是全部了!让我们实际上推进我们的教程,这样我们可以在走过每个阶段时看到我们的进展。
在所有正确的地方推进
由于我们已经安排好了所有函数,我们只需要在我们想要开始下一阶段时调用advanceTutorial函数。
第一阶段将在我们第一次移动单位后推进,所以在moveUnit函数中,请在底部添加以下内容:
if (self.tutorialPhase == 1 || self.tutorialPhase == 2)
[self advanceTutorial];
嘿!既然我们已经在做了,为什么不包括第二阶段呢?毕竟,我们只是在两个阶段都滑动了一次。
第三阶段将在从右边进来的敌人被摧毁时结束,所以在handleCollisionWithFriendly函数中,你需要在下面的 if 语句中添加以下方法调用。第四阶段也将随着一个单位的摧毁而结束,所以我们将包括它:
if (enemy.unitValue<= 0)
{
[arrEnemies removeObject:enemy];
[selfremoveChild:enemy];
++numUnitsKilled;
if (self.tutorialPhase == 3 || self.tutorialPhase == 4)
[selfadvanceTutorial];
}
接下来是当教程阶段 5 结束时,这发生在用户想要移动但任何单位移动尚未计算之前。对阶段 6 也是如此,所以请在moveUnit函数的顶部添加以下调用:这是因为我们不希望意外地推进教程两次(如果我们将其添加到底部就会发生这种情况):
if (self.tutorialPhase == 5 || self.tutorialPhase == 6)
[self advanceTutorial];
但等等!我们想要确保教程中每个人的体验都是一样的。所以,就像我们在init方法中在场景开始时创建自定义单位一样,我们将在moveUnit函数中创建一个自定义单位。在你的moveUnit函数中,修改这个if语句,在相应的教程阶段创建一个自定义单位:
if (numTurnSurvived % 3 == 0 || [arrEnemiescount] == 0)
{
if (self.tutorialPhase == 4)
{
Unit *newEnemy = [Unit enemyUnitWithNumber:4 atGridPosition:ccp(5,9)];
[newEnemy setDirection:DirUp];
newEnemy.position = [MainScene getPositionForGridCoord:ccp(5,9)];
[self spawnNewEnemy:newEnemy];
}
else
{
[self spawnNewEnemy:[self getRandomEnemy]];
}
}
好吧!有了这个,我们应该有一个相当扎实的教程,但它仍然有点笨拙,我们肯定可以用一些润色(巧合的是,我们就在这一章)。所以,让我们继续让它成为最好的教程。
移除之前阶段的文本
现在,旧文本只是堆积起来,让我们清理一下。在removePreviousTutorialPhase函数中,添加以下代码块。它将获取文本,重命名它(这样就不会意外地发生命名冲突),快速淡出文本,并移除它:
-(void)removePreviousTutorialPhase
{
CCLabelBMFont *lblInstructions = (CCLabelBMFont*)[self getChildByName:@"tutorialText" recursively:NO];
lblInstructions.name = @"old_instructions";
id fadeRemoveInstructions = [CCActionSequence actions:[CCActionEaseInOut actionWithAction:[CCActionFadeTo actionWithDuration:0.5f opacity:0] rate:2], [CCActionCallBlock actionWithBlock:^{
[self removeChild:lblInstructions];
}], nil];
[lblInstructions runAction:fadeRemoveInstructions];
}
好的!但仍然需要更多润色。让我们添加一些图形元素到我们的教程中,以便更好地解释我们希望用户做什么。
指示方向的指针
文字很好,但对于那些不能阅读英语的人来说呢?或者对于那些不理解我们所说的拖动友好单位的人来说呢?最好有一个图像来展示我们的意思。在这种情况下,我们将使用一个小手,用食指指向,以展示预期的拖动动作。
这是我们将要添加的内容。注意手指(正在向右移动并同时淡出),以及在上一个部分中添加的文本,如截图所示:

在我们的 showTutorialInstructions 方法中,我们想要创建一个手指,引导用户走向正确的方向。因此,在你的 showTutorialInstructions 方法的底部,添加以下代码块来创建一个手指并将其定位到中间正方形的中心:
CCSprite *finger = [CCSprite spriteWithSpriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"imgFinger.png"]];
finger.anchorPoint = ccp(0.4,1);
finger.position = [MainScene getPositionForGridCoord:ccp(5,5)];
finger.name = @"finger";
finger.opacity = 0;
[self addChild:finger z:2];
注意我们是如何命名手指并将其定位在 z:2(为了与我们的教程保持一致)。
下一步是让我们的手指以我们希望用户滑动单位的方向进行动画处理。因此,在你已经将手指添加到场景之后,调用以下函数:
...
[self addChild:finger z:2];
[self runFingerArrowActionsWithFinger:finger];
}
-(void)runFingerArrowActionsWithFinger:(CCSprite*)finger
{
}
在这里,我们只是直接将手指变量传递给函数(因为搜索具有匹配名称的子项会占用更多处理时间)。现在,我们只需要对我们手指的图像做以下操作:
-
淡入手指
-
向右滑动
-
在它向右滑动时逐渐淡出
-
稍等片刻
-
重新定位手指
-
重复
这看起来相当简单,对吧?是的,除了当我们想要按顺序排列所有这些事件时。在这种情况下,代码看起来相当复杂。这是我们希望函数看起来像这样的:
-(void)runFingerArrowActionsWithFinger:(CCSprite*)finger
{
Unit *u = [Unit friendlyUnit];
if (self.tutorialPhase == 1 || self.tutorialPhase == 3)
{
id slideRight = [CCActionSequence actions:[CCActionEaseIn actionWithAction:[CCActionFadeIn actionWithDuration:0.25f] rate:2], [CCActionEaseInOut actionWithAction:[CCActionMoveBy actionWithDuration:1.0f position:ccp(u.gridWidth*2, 0)] rate:2],[CCActionDelay actionWithDuration:0.5f], nil];
id fadeOutAndReposition = [CCActionSequence actions:[CCActionDelay actionWithDuration:0.25f], [CCActionEaseInOut actionWithAction:[CCActionFadeOut actionWithDuration:1.0f] rate:2], [CCActionDelay actionWithDuration:0.5f], [CCActionCallBlock actionWithBlock:^{
finger.position = [MainScene getPositionForGridCoord:ccp(5,5)];
}], nil];
[finger runAction:[CCActionRepeatForever actionWithAction:slideRight]];
[finger runAction:[CCActionRepeatForever actionWithAction:fadeOutAndReposition]];
}
else if (self.tutorialPhase == 2)
{
finger.position = [MainScene getPositionForGridCoord:ccp(6,5)];
id slideLeft = [CCActionSequence actions:[CCActionEaseIn actionWithAction:[CCActionFadeIn actionWithDuration:0.25f] rate:2], [CCActionEaseInOut actionWithAction:[CCActionMoveBy actionWithDuration:1.0f position:ccp(-u.gridWidth*2, 0)] rate:2],[CCActionDelay actionWithDuration:0.5f], nil];
id fadeOutAndReposition = [CCActionSequence actions:[CCActionDelay actionWithDuration:0.25f], [CCActionEaseInOut actionWithAction:[CCActionFadeOut actionWithDuration:1.0f] rate:2], [CCActionDelay actionWithDuration:0.5f], [CCActionCallBlock actionWithBlock:^{
finger.position = [MainScene getPositionForGridCoord:ccp(6,5)];
}], nil];
[finger runAction:[CCActionRepeatForever actionWithAction:slideLeft]];
[finger runAction:[CCActionRepeatForever actionWithAction:fadeOutAndReposition]];
}
}
实际上,手指将淡入,向右滑动(同时淡出),然后重新定位,并在教程的第一阶段和第三阶段(第二阶段相反方向)无限期地重复这些动作。
很遗憾,我们还没有完成手指的编码。当我们想要进入下一阶段时,我们必须移除它,记得吗?
因此,在 removePreviousTutorialPhase 方法中,我们只是给标签添加一个非常相似的移除样式,唯一的区别是,我们将将其应用于手指(这次,我们需要使用 getChildByName 的搜索功能,因为这个函数在不确定的时间被调用):
CCSprite *finger = (CCSprite*)[self getChildByName:@"finger" recursively:NO];
finger.name = @"old_finger";
id fadeRemoveFinger = [CCActionSequence actions:[CCActionEaseInOut actionWithAction:[CCActionFadeTo actionWithDuration:0.5f opacity:0] rate:2], [CCActionCallBlock actionWithBlock:^{
[self removeChild:finger];
}], nil];
[finger runAction:fadeRemoveFinger];
就这些关于手指的内容!我们已经得到了一个按照我们想要的方向滑动的手指,包括漂亮的淡入淡出效果。我们还有文本显示和移除,文本前进等等。唯一剩下要做的就是确保我们的用户只能按照我们想要的方向移动他们的单位。
拒绝非教程移动
只有当他们在特定的顺序中移动时,我们的教程才能按预期工作。因此,我们需要在通过教程时限制他们的初始移动。
在Unit.m中,在touchMoved函数中,我们想要确保单位只有在第一阶段三个阶段中朝正确方向移动时才能开始被拖动。因此,向touchMoved函数中添加以下 if 语句(当距离小于20时):
if (!self.isBeingDragged && ccpDistance(touchPos, self.touchDownPos) >20)
{
...
if (
(((MainScene*)self.parent).tutorialPhase == 1 && self.dragDirection != DirRight) ||
(((MainScene*)self.parent).tutorialPhase == 2 && (self.dragDirection != DirLeft || self.unitValue == 1)) ||
(((MainScene*)self.parent).tutorialPhase == 3 && self.dragDirection != DirRight))
{
self.isBeingDragged = NO;
self.dragDirection = DirStanding;
}
}
这就是为什么我们创建了一个tutorialPhase对象作为属性——这样我们就可以从另一个类中访问阶段。但这里发生的事情本质上是对教程阶段的检查,如果它是第一阶段、第二阶段或第三阶段中的任何一个,它将进行另一个检查以查看dragDirection是否指示正确的方向。对于第二阶段还有一个额外的检查,因为它不允许单位值为 1。
如果这些情况中的任何一个是真的,我们将isBeingDragged设置为NO,并将拖动方向设置为站立(这样在第二阶段就不会发生意外的行为)。
教程到此结束!虽然花了一些时间,但它不仅简单快捷,而且相当全面,同时不影响游戏体验。
然后,一旦我们的教程结束,它将从那个点无缝地过渡到常规游戏。另一个优点是:假设玩家输了,没有完成,或者点击了菜单或重新开始;或者手机在教程期间某个时刻没电了。当他们回来时,教程将简单地从开始处重新开始,这是好的,也是故意的。
小贴士
从教程中得到的要点是要保持简短,在他们完成时保存,并测试所有可能的“愚蠢”方式,用户可能会尝试搞砸教程(因此有关于拒绝错误动作的最后一部分)。
在 Facebook 和 Twitter(以及更多)上分享
在社交媒体上分享游戏现在非常普遍。我们的游戏不会是那个例外。这不仅是你游戏的营销工具,因为用户会在他们的社交媒体页面上为你推广游戏,而且也是一个提升参与度的好方法,因为人类喜欢竞争。能够分享和比较他们的分数(以及间接竞争)让用户更想玩游戏,这对开发者和玩家来说都是双赢的局面。
使用内置的分享功能
要整合 Facebook、Twitter、消息、电子邮件和其他分享选项,最简单的方法是通过UIActivityView对象。它就是你在照片应用左下角按钮按下时看到的相同分享方式。
基本上,我们只需要告诉ActivityView对象我们想要显示什么,以及我们想要排除哪些活动类型,然后通过CCDelegate呈现视图控制器。
首先,我们需要创建一个分享按钮。
创建分享按钮
在GameOverScene.m中,将以下代码块添加到initWithScoreData方法中。这将在我们游戏结束屏幕的底部中央创建一个分享按钮:
//add share buttons
CCButton *btnShare = [CCButton buttonWithTitle:@"" spriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"btnShare.png"]];
[btnShare setTarget:self selector:@selector(openShareView)];
btnShare.position = ccp(winSize.width/2, winSize.height * 0.1);
[self addChild:btnShare];
然后,创建当分享按钮被按下时将调用的方法(现在必须添加,否则当达到GameOverScene时游戏会崩溃):
-(void)openShareView
{
}
运行游戏,当你到达游戏结束屏幕时,你会在底部中央看到分享按钮。目前,它没有任何作用,所以让我们显示活动视图。
创建一个用于当前分数的变量
我们需要一种方法来跟踪玩家的当前分数和最近一次游戏的分数。尽管我们可以将此信息传递给游戏结束场景,除非我们将该值存储在实例变量中,否则我们无法在我们的分享中使用它。
因此,在GameOverScene.h中,添加一个用于当前分数的变量,如下所示:
@interface GameOverScene : CCScene <CCTableViewDataSource>
{
CGSize winSize;
NSArray *arrScores;
NSInteger highScoreIndex;
//add this:
NSInteger numCurrentScore;
}
然后,在initWithScoreData方法中,我们添加以下行,以便我们可以获取传递给场景的总分:
numCurrentScore = [dict[DictTotalScore] integerValue];
现在我们已经准备好在分享的文本中使用分数了。
创建 UIActivityView 对象
在你刚刚创建的openShareView方法中,添加这几行代码(之后会有解释):
NSString *textToShare = [NSString stringWithFormat:@"I scored %d in MathGame! See if you can beat me!",numCurrentScore];
NSString *appID = @"123456789"; //change to YOUR app's ID
NSURL *appStoreURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://itunes.apple.com/app/id%@", appID]];
NSArray *objectsToShare = @[textToShare, appStoreURL];
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil];
首先是我们要显示的文本。我们需要保持它简短,有几个原因。最重要的原因是 Twitter 只允许 140 个字符,所以我们需要确保我们不会超过这个限制。第二个原因是,我们的潜在未来玩家可能不会阅读超过一两个句子的内容。最后,我们想要发送的通用消息至少要让人感觉是个人化的。它必须像阅读两个最好的朋友之间的对话一样。
接下来是 App Store 的链接,它也接受应用的 ID。注意appID变量是1到9。目前这还不是确切的 App ID(甚至不是书籍项目的 App ID),所以当我们创建 iTunes Connect 中的应用时(或者如果你已经创建了一个应用,你现在可以使用那个 App ID)我们将修改这一行代码。
之后是包含在分享中的对象数组。只需将它们添加到数组中即可。
最后,我们使用objectsToShare数组创建UIActivityViewController对象。
但目前还没有显示任何内容,所以让我们来处理这个问题。
显示 UIActivityViewController
在你初始化activityVC变量在openShareView方法后,添加以下代码块。它将确保活动视图不会显示某些活动,然后通过共享的CCDelegate显示视图控制器:
NSArray *excludeActivities = @[UIActivityTypeAirDrop,
UIActivityTypePrint,
UIActivityTypeAssignToContact,
UIActivityTypeSaveToCameraRoll,
UIActivityTypeAddToReadingList,
UIActivityTypePostToFlickr,
UIActivityTypePostToVimeo];
activityVC.excludedActivityTypes = excludeActivities;
[[CCDirector sharedDirector] presentViewController:activityVC animated:YES completion:nil];
由于我们不希望用户打印任何内容,将任何内容分配给联系人,或将它们添加到他们的阅读列表(Flickr,Vimeo 等),我们需要排除这些活动,这意味着它们不会出现在用户点击分享按钮时出现的视图中。
这是一个所有可能的UIAcitivityTypes列表:
-
UIActivityTypeAddToReadingList; -
UIActivityTypeAirDrop; -
UIActivityTypeAssignToContact; -
UIActivityTypeCopyToPasteboard; -
UIActivityTypeMail; -
UIActivityTypeMessage; -
UIActivityTypePostToFacebook; -
UIActivityTypePostToFlickr; -
UIActivityTypePostToTencentWeibo; -
UIActivityTypePostToTwitter; -
UIActivityTypePostToVimeo; -
UIActivityTypePostToWeibo; -
UIActivityTypePrint; -
UIActivityTypeSaveToCameraRoll;
因此,对于您自己的项目,您可以随意包含或排除这些内容中的任意多少。例如,如果您正在分享一个视频,您可以非常容易地允许 Vimeo 或保存照片。
就这样!如果您现在运行游戏并点击分享按钮,您将看到活动视图弹出,以及消息、电子邮件、Facebook 和 Twitter 的各种按钮。点击任何一个都会加载相应的视图,以及添加的消息和 URL。
小贴士
当您将此添加到自己的项目中时,需要注意的关键点是排除列表。
这就是最终版本的外观:

添加截图到分享
尽管我们添加了一些文本并包括了一个指向 App Store 上游戏的链接,但我们可能还应该包括一个截图,因为如果附有截图,人们至少会检查一下游戏。
话虽如此,我们并不真的想要从游戏结束屏幕截取截图,所以我们必须在过渡到GameOverScene之前截取游戏的截图。
因此,打开MainScene.m并添加以下方法。它将截取游戏屏幕:
-(UIImage*)screenshot
{
[CCDirector sharedDirector].nextDeltaTimeZero = YES;
CCRenderTexture* rtx =
[CCRenderTexture renderTextureWithWidth:winSize.width
height:winSize.height];
[rtx begin];
[[[CCDirector sharedDirector] runningScene] visit];
[rtx end];
return [rtx getUIImage];
}
然后,在endGame方法中,让我们调用前面的方法并将其存储在局部变量中,以便我们可以将其传递给GameOverScene数据:
UIImage *image = [self takeScreenshot];
NSDictionary *scoreData = @{DictTotalScore : @(numTotalScore),
DictTurnsSurvived :@(numTurnSurvived),
DictUnitsKilled :@(numUnitsKilled),
DictHighScoreIndex :@(hsIndex),
@"screenshot" : image};
注意到在scoreData字典中添加了@"screenshot"键。这将传递我们的UIImage,这样我们就可以在GameOverScene中获取它。
接下来,在GameOverScene.h中,添加一个用于截图的变量,如下所示:
@interface GameOverScene : CCScene <CCTableViewDataSource>
{
CGSize winSize;
NSArray *arrScores;
NSInteger highScoreIndex;
NSInteger numCurrentScore;
//add this:
UIImage *screenshot;
}
然后在GameOverScene.m中的initWithScoreData方法中,我们想要使用@"screenshot"键将截图存储在字典中的变量:
screenshot = dict[@"screenshot"];
最后,在游戏结束场景的openShareView方法中,我们只需要将截图变量添加到objectsToShare数组中,它将自动包含:
NSArray *objectsToShare = @[textToShare, myWebsite, screenshot];
就这样!通过现在运行游戏并到达分享按钮,无论您是通过 Facebook、Twitter、消息还是电子邮件分享,您都会看到图像。
小贴士
如果您真的想要做得更精致,您可以允许用户保存图像,以防他们想要保留自己的记录。为此,只需从openShareView方法中的排除数组中删除SaveToCameraRoll选项。
这就是添加截图后的游戏外观(通过 Facebook 分享):

开关声音
目前,无论发生什么情况,我们都在播放音效和音乐。即使你可能是一个喜欢听声音或音乐的人,当你设计游戏时(这确实是一个增加用户参与度的元素),你可能会开放地接受这样一个事实:有时人们根本不喜欢听到任何声音。因此,我们必须给他们提供开关声音的选项。
没有选项或设置?那就去主菜单吧!
由于我们没有暂停屏幕、选项、设置或类似的东西,我们将把开关声音的按钮添加到主菜单中。这意味着添加按钮所需的代码要少得多,而不是为它们创建一个全新的场景。这种安排也与游戏的简洁感保持一致。
提示
如果你想要创建一个暂停屏幕并将这两个按钮添加到其中,那么请尽你所能去做。然而,代码将略有不同,因为如果你推入一个 CCScene 而不是替换(这本质上允许你在临时进入新场景的同时暂停游戏),然后弹出你推入的场景(换句话说,恢复暂停的游戏),你需要确保正确的变量被设置为 false。
创建按钮
首先,我们将在主菜单中创建声音和音乐开/关按钮。这些按钮将与普通按钮略有不同,因为它们需要在不同状态之间切换,而不仅仅是普通的按钮点击。
在 MenuScene.m 文件中,让我们在 init 方法中创建初始按钮:
if ((self=[super init]))
{
//these values range 0 to 1.0, so use float to get ratio
CCNode *background = [CCNodeColor nodeWithColor:[CCColor whiteColor]];
[self addChild:background];
winSize = [CCDirector sharedDirector].viewSize;
CCButton *btnPlay = [CCButton buttonWithTitle:@"" spriteFrame:[CCSpriteFrame frameWithImageNamed: @"btnPlay.png"]];
btnPlay.position = ccp(winSize.width/2, winSize.height * 0.5);
[btnPlay setTarget:self selector:@selector(goToGame)];
[self addChild:btnPlay];
//add the sound and music buttons:
CCButton *btnSound = [CCButton buttonWithTitle:@"" spriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"btnSoundOn.png"]];
[btnSound setBackgroundSpriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName: @"btnSoundOff.png"] forState:CCControlStateSelected];
btnSound.position = ccp(winSize.width * 0.35, winSize.height * 0.2);
[btnSound setTogglesSelectedState:YES];
[btnSound setTarget:self selector:@selector(soundToggle)];
[self addChild:btnSound];
CCButton *btnMusic = [CCButton buttonWithTitle:@"" spriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"btnMusicOn.png"]];
[btnMusic setBackgroundSpriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"btnMusicOff.png"] forState:CCControlStateSelected];
btnMusic.position = ccp(winSize.width * 0.65, winSize.height * 0.2);
[btnMusic setTogglesSelectedState:YES];
[btnMusic setTarget:self selector:@selector(musicToggle)];
[self addChild:btnMusic];
...
}
然后,我们需要确保创建按钮按下时将调用的方法:
-(void)soundToggle
{
}
-(void)musicToggle
{
}
在此点运行游戏将导致以下截图所示的结果。如果你按下任一按钮,它们将在彼此之间切换,如果你退出并返回,按钮将重置到其原始状态。

创建键
为了获取(并存储)声音和音乐变量,我们需要像过去一样使用 NSUserDefaults。为了在编码时减少用户错误,我们希望为我们的字典键定义常量。
由于 MainScene 已经被导入到我们所有的类中,我们可以在那里安全地定义常量。因此,打开 MainScene.h 文件,并将以下代码添加到文件顶部,与其他常量一起:
FOUNDATION_EXPORT NSString *const KeySound;
FOUNDATION_EXPORT NSString *const KeyMusic;
然后,在 MainScene.m 文件中,将以下代码添加到文件顶部,与所有其他常量一起,以便它们被定义:
NSString *const KeySound = @"keySound";
NSString *const KeyMusic = @"keyMusic";
现在我们能够获取存储的数据,以及如果我们需要的话,高效地保存任何值。
从 NSUserDefaults 中获取声音和音乐布尔值
我们希望将数据存储在变量中,这样我们就不必反复从 NSUserDefaults 中读取和写入,而只需在我们真正需要时进行操作。
因此,在 MainScene.h 文件中,添加两个布尔变量用于控制声音开/关和音乐开/关,如下所示:
@interface MenuScene :CCNode
{
CGSize winSize;
BOOL isSoundOn, isMusicOn;
}
然后,在你将按钮添加到场景中 MainScene.m 的 init 方法之后,使用你刚刚定义的键从 NSUserDefaults 中读取声音和音乐值:
isSoundOn = [[NSUserDefaults standardUserDefaults] boolForKey:KeySound];
isMusicOn = [[NSUserDefaults standardUserDefaults] boolForKey:KeyMusic];
现在我们想实际告诉声音和音乐按钮是否应该显示它们的X标记或勾选标记。为此,我们将只设置选中的值为其变量的相反。这是因为如果声音没有开启,我们希望显示选中版本:
btnSound.selected= !isSoundOn;
btnMusic.selected= !isMusicOn;
这可行,但没有办法测试它,所以当按下相应的按钮时,我们实际上修改一下值。
设置和保存值
在soundToggle方法中,我们将把isSoundOn变量设置为它的相反(切换开和关)。紧接着,我们将设置(并保存)它的值到我们之前定义的键:
-(void)soundToggle
{
isSoundOn = !isSoundOn;
[[NSUserDefaults standardUserDefaults] setBool:isSoundOn forKey:KeySound];
[[NSUserDefaults standardUserDefaults] synchronize];
}
然后,我们也将对musicToggle方法中的isMusicOn变量做同样的处理:
-(void)musicToggle
{
isMusicOn = !isMusicOn;
[[NSUserDefaults standardUserDefaults] setBool:isMusicOn forKey:KeyMusic];
[[NSUserDefaults standardUserDefaults] synchronize];
}
如果你现在运行游戏,你将能够切换声音和音乐的真/假变量,并且当你要么去另一个场景然后回来,要么退出游戏然后回来时,值将保留为你最后设置的值。但音乐仍然没有暂停,声音也没有关闭,所以让我们修复这个问题。
暂停/恢复背景音乐和声音
如果在按下音乐按钮时背景音乐正在播放,我们需要暂停它,反之亦然。
首先,让我们转到musicToggle方法并添加对isMusicOn变量的检查。如果它被启用,我们可以播放背景音乐。否则,我们将暂停音乐,直到用户再次将其打开:
isMusicOn ? [[OALSimpleAudio sharedInstance] playBg] : [[OALSimpleAudio sharedInstance] bgPaused];
之后,我们还将添加一个检查来查看isSoundOn是否被启用。如果是,我们将播放buttonClick音效:
if (isSoundOn)
[[OALSimpleAudio sharedInstance] playEffect:@"buttonClick.mp3"];
我们也将对toggleSound方法和goToGame方法做同样的处理,这两个方法都是在按钮被按下时被调用的。因此,我们将播放一个按钮点击音效(只有当声音被启用时):
-(void)goToGame
{
if (isSoundOn)
[[OALSimpleAudio sharedInstance] playEffect:@"buttonClick.mp3"];
[[CCDirector sharedDirector] replaceScene:[MainScene scene]];
}
-(void)soundToggle
{
...
if (isSoundOn)
[[OALSimpleAudio sharedInstance] playEffect:@"buttonClick.mp3"];
}
对于MenuScene,所有工作都已完成!如果你按下音乐按钮和/或声音按钮,你会注意到效果的开和关,正如预期的那样。现在我们已经处理了MenuScene,让我们继续处理其他所有我们将播放音效(以及开始音乐)的位置。
小贴士
或者,你可以创建一个类,它有启动和停止背景音乐、播放特定音效和播放按钮点击的方法,所有这些方法都使用OALSimpleAudio。
然后,你可以找到并替换所有OALSimpleAudio的实例,用你自己的自定义类替换。
处理主场景声音
由于我们在游戏的几乎每个类中都有音效和音乐播放,我们需要确保它们只在相应的值是 true 时播放。
所以首先,让我们打开MainScene.h并添加一个类似的变量用于声音:
@interface MainScene :CCScene
{
BOOL isSoundOn;
}
接下来,在init方法中,确保你从NSUserDefaults获取值:
小贴士
还确保你在尝试播放任何音效之前在init方法中设置isSoundOn变量。如果你之后分配值,你可能会遇到意外的结果。
-(id)init
{
if ((self=[super init]))
{
//used for positioning items on screen
winSize = [[CCDirector sharedDirector] viewSize];
isSoundOn = [[NSUserDefaults standardUserDefaults] boolForKey:KeySound];
...
}
return self;
return self;
}
在 MainScene.m 中搜索 OALSimpleAudio,并访问每一个实例,在其上方添加以下 if 语句,以便仅在特定的声音被启用时播放声音效果:
if (isSoundOn)
...
在 playUnitCombineSound 中应该有两个,一个在 goToMenu 中,一个在 restartGame 中,一个在 moveUnit 中。显然,如果你有更多的声音效果在播放,那么你也应该在那里添加它们,但到目前为止只有这五个。
对 GameOverScene(以及任何其他场景)重复操作
这基本上和 MainScene 一样,所以不会有太多的解释。但你真正需要做的只是以下这些:
-
在
GameOverScene.h中创建isSoundOn变量。 -
在
init方法中从NSUserDefaults赋值 -
在每个要播放的声音效果之前添加 if 语句
-
由于这是你唯一的另一个场景,继续到
AppDelegate。
处理 AppDelegate 音乐
我们需要确保如果用户在之前的版本中决定关闭音乐,音乐不会在用户首次加载游戏时随机开始播放。
所以在 AppDelegate.m 中,在调用 playBgWithLoop 方法之前添加以下 if 语句。注意,我们不需要将其存储在变量中,因为我们只会使用它一次:
if ([[NSUserDefaults standardUserDefaults] boolForKey:KeyMusic])
[[OALSimpleAudiosharedInstance] playBgWithLoop:YES];
确保声音/音乐开始时启用
我们想要确保的一件事是,当用户开始游戏时,声音和音乐应该被启用。默认情况下,NSUserDefaults 中的任何布尔值都是 false。因此,我们需要确保在游戏开始之前,它们都被设置为 true,但只在他们第一次运行游戏时。
所以在 AppDelegate.m 中,在 startScene 的开头,让我们添加代码来检查他们是否之前玩过:
- (CCScene*) startScene
{
//if they have not played before (in other words, first time playing)
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"hasPlayedBefore"])
{
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:KeySound];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:KeyMusic];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"hasPlayedBefore"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
...
return [MainScene scene];//[CCBReader loadAsScene:@"MainScene"];
}
就这样!这需要一点仔细的计划来确保我们处理了用户可能遇到的所有情况,但这正是打磨你的游戏的全部要点——确保无论用户做什么或能做什么,游戏都能适当地、如预期地响应。
小贴士
为声音和音乐添加开关设置的关键要点如下:将值存储在 NSUserDefaults 中,从每个场景的局部变量中获取该值,并使用该变量来确定是否应该播放声音效果。如果你想确保你已获取了所有实例,查找项目中的 OALSimpleAudio 并遍历你创建的所有类。
确保检查你添加的任何未来声音效果的变量。
游戏中心排行榜
我们已经有一组存储在用户设备上的前 20 名高分,为什么不创建一个全球排行榜呢?实际上,即使许多玩家在玩游戏时没有使用 Game Center,它也是另一个推动参与度的元素。而且(这是真正酷的部分)——如果你还不知道的话——你实际上可以在 Game Center 中让玩家对你的游戏进行评分。这甚至不是一个额外的功能,因为他们只需点击排行榜或成就面板顶部的星级数量,就会将评分发送到 App Store。
在我们的游戏中创建一个用于显示的排行榜不仅仅是关于代码,因此我们首先需要在 iTunes Connect 中创建排行榜。在我们可以创建 iTunes Connect 中的应用程序之前,我们需要设置一个 App ID。
创建 App ID
到目前为止,如果你想要添加排行榜,你必须注册一个 Apple 开发者账户,如果你还没有的话。在第一章,刷新我们的 Cocos2d 知识中,注册过程被解释了。否则,如果你只是为了练习而跟随,请随意这样做,但没有开发者账户,你不能创建排行榜或成就。
首先,你需要从开发者网站创建 App ID。前往 developer.apple.com/devcenter/ios/index.action 并登录到拥有 iOS 开发者许可证的 Apple 账户。
登录后,在iOS 开发中心下,前往证书、标识符和配置文件部分,如下面的截图所示:

从这里,前往标识符部分,以便我们可以创建一个 App ID,如下面的截图所示:

然后,通过点击右上角的+按钮开始创建 App ID。在这里,我们需要为 App ID 输入一个名称,包标识符的名称(这通常是反向 DNS 表示法;例如,www.keitgames.com可能有 com.keitgames.mygame 作为包 ID),以及我们想要的任何服务(我们目前将其保留为默认设置)。以下截图供您参考:

确保你使用自己的包标识符(前面的截图仅显示了一个示例以及所有内容的位置)。
完成这些后,点击继续然后点击提交。然后 App ID 应该就创建完成了。
接下来,在 Xcode 项目的设置中,选择 iOS 目标并更新包标识符为你刚刚创建的那个。

现在,包 ID 和 App ID 已经设置好了,我们可以在 iTunes Connect 中创建应用程序,这将允许我们设置和测试我们的排行榜。
在 iTunes Connect 中创建应用程序
在 iTunes Connect 中创建应用相对简单,并且仅用作线框,以便我们可以创建排行榜。在下一章中,我们将涵盖所有细节;现在您只需要知道我们现在在 iTunes Connect 中设置它(而不是稍后)的唯一原因是用于设置排行榜。
首先,前往itunesconnect.apple.com/WebObjects/iTunesConnect.woa并登录您的开发者账户。然后点击我的应用,通过点击左上角的+按钮创建一个新应用,并点击新 iOS 应用。

然后填写它要求的信息,包括您之前创建的应用 ID。SKU 实际上并不重要,因为它仅用于您自己的内部使用。

一旦在 iTunes connect 中创建了应用,点击游戏中心。

当询问它是用于单个游戏还是多个游戏时,这取决于您在创建项目时的决定。但为了本书项目的目的,我们将为单个游戏创建它。

之后,您将被带到游戏中心设置屏幕,然后就可以继续下一步了。
创建排行榜
在 iTunes Connect 中的游戏中心设置部分,点击添加排行榜按钮,然后点击单个排行榜。

排行榜参考仅用于 iTunes Connect 内部的内部使用,以防您需要搜索它(或一眼就能知道它是哪个排行榜)。排行榜 ID将在代码中使用,因此它应该是独特且与其他排行榜(如果您创建了任何)不同的。得分格式仅使用整数,因此我们将使用整数格式。我们希望只提交最佳得分(因为排行榜中每个人只能有一个得分)并按从高到低排序。最后,我们希望范围从 0 到 999,999。技术上我们不必设置这个,但我们还是设置了。
接下来,我们添加一种语言。以下是一个显示英语的示例:

名称是用户将看到的排行榜顶部的标题,因此我们希望它很明显,表明这是哪个排行榜。得分格式与您刚才看到的一样。得分格式后缀的作用如下:由于我们有分数,得分为 625 的排行榜将显示为625 分数。如果您认为在每个得分中都包含单词分数看起来会很奇怪,您可以随意省略它,但到目前为止,我们将保留它。
在输入所有这些信息后,点击底部的保存。哇!它已经创建好了!接下来是编写 Game Center 登录、验证、排行榜展示、提交分数以及处理 Game Center 的所有其他内容。
添加 GameKit 框架
在我们开始编写任何代码之前,我们需要在我们的项目中包含 GameKit 框架。所以,在你的项目设置中,转到iOS 目标,然后转到构建阶段,在链接二进制与库部分,点击+按钮将框架添加到项目中,如图所示:

然后,搜索gamekit(不区分大小写),点击GameKit.framework的结果,然后点击添加。

现在我们完成了!现在我们准备好编写代码了。
GameKit 辅助文件
为了简化,你只需将GKHelper文件复制到你的项目中(确保选中了复制复选框)。实际上,编写 Game Center 代码并不难——对于有 Game Center 的每个项目都是如此。所以,为什么要在手动输入代码上浪费时间,当你可以直接准备好文件时?
实际上,GKHelper单例类所做的是管理你的GKLocalPlayer(设备上 Game Center 当前登录的用户),以及任何来自服务器或发往服务器的调用,以及任何排行榜分数提交和成就跟踪。
小贴士
GKHelper类是使用在线教程www.raywenderlich.com/23189/whats-new-with-game-center-in-ios-6创建的,如果你需要更多解释。还有一个更深入的指南在www.appcoda.com/ios-game-kit-framework/,它涵盖了 Game Center 的各个方面。如果你遇到困难,或者仍然需要额外帮助(对于刚开始接触 Game Center 编程的人来说可能会很困惑),请参考它。
重要提示:假设你遇到了以下错误信息:
GameKitHelper ERROR: {
NSLocalizedDescription = "The requested operation could not be completed because this application is not recognized by Game Center.";
}
然后,你必须前往设备设置 | 游戏中心并启用沙盒,如图所示:

验证用户
现在一切就绪,创建完成,准备使用,我们可以开始编写实际的排行榜代码,并在用户登录 Game Center 时显示它。
首先,我们需要验证本地玩家;也就是说,如果他们还没有登录,就让他们登录,或者只需发送一个请求来获取GKLocalPlayer。
我们将在应用开始时进行这一操作,所以请在AppDelegate.m中导入GKHelper.h文件,并将以下方法调用添加到startScene方法的顶部:
[[GKHelper sharedGameKitHelper] authenticateLocalPlayer];
如果一切操作都正确,当你现在运行游戏时,你应该会在屏幕顶部看到一个横幅欢迎当前登录的玩家。如果没有,它会要求他们登录。
如果你收到一条错误消息,内容类似于请求的操作被取消或禁用,请前往设置应用,然后进入游戏中心。尝试重新登录和登出,或者启用底部的沙盒模式(参见前面的截图)。这应该可以解决问题。
创建游戏中心按钮
我们想要一个按钮来访问排行榜和其他游戏中心内容,所以请在GameOverScene.m文件的init方法中添加以下代码。它将在屏幕的右下角创建一个按钮:
//add Game Center buttons
CCButton *btnGameCenter = [CCButton buttonWithTitle:@"" spriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"btnGameCenter.png"]];
[btnGameCenter setTarget:self selector:@selector(viewGameCenter1)];
btnGameCenter.position = ccp(0.75, 0.1);
btnGameCenter.positionType = CCPositionTypeNormalized;
[self addChild:btnGameCenter];
我们还想要显示排行榜,这需要GKHelper,所以请在GameOverScene.m的顶部导入GKHelper.h文件:
#import "GKHelper.h"
然后创建viewGameCenter方法,这只是一个调用以显示排行榜:
-(void)viewGameCenter
{
[[GKHelper sharedGameKitHelper] presentLeaderboards];
}
如果你现在运行游戏并进入游戏结束场景,你会在屏幕右下角看到一个游戏中心按钮,点击它(如果你已经登录到游戏中心),它将打开排行榜。现在唯一剩下的事情是将得分提交到游戏中心,以便在排行榜中显示。
提交得分
由于我们的GKHelper类可以为我们完成所有的得分报告,我们只需要调用相应的函数。既然我们知道GameOverScene将包含最近一局的得分总和,我们可以使用传入的字典作为提交给排行榜的得分值。
因此,在GameOverScene.m的init方法中,添加对submitScore函数的调用:
[[GKHelper sharedGameKitHelper] submitScore:[dict[DictTotalScore] integerValue]];
就这样!设置花费了一些时间,大约 10 行代码,但我们已经有一个全局排行榜。
这里有两个需要注意的事项:
-
你可能想实现成就,但
GKHelper类中还没有实现这些代码,所以很遗憾,你需要自己完成。尽管这并不太难,但在这个游戏中不是必需的,所以它被跳过了。如前所述,www.appcoda.com/ios-game-kit-framework/有一个很好的资源,解释了如何实现成就。它是在 2014 年 3 月写的,所以当你读到这本书的时候,代码应该仍然有效。 -
在实现排行榜时,如果你遇到任何问题,请确保从 iTunes Connect 设置到游戏中心、沙盒模式和代码本身,所有这些都在线。游戏中心可能有点棘手和令人烦恼。尽管这些年来它有所改进,但仍然可能有点挑剔。如果你仍然有问题,你可能不是唯一一个。一个很好的开始搜索的地方是 Stack Overflow。如果你不熟悉该网站,不要担心。他们有一个问答格式,有成千上万的人提出了关于代码和其他问题的疑问,还有更多的人给出了正确的答案。
那就是游戏中心的全部内容!为了完善我们的项目,最后要做的就是在场景之间创建一个自定义过渡,使游戏感觉更健壮,而不是只是瞬间在场景之间切换。
滑动过渡
虽然对于测试目的来说这很好,但场景之间的即时过渡(以及游戏首次加载时突然出现)是糟糕的。我们将添加一个快速的滑动过渡,在每个场景之间。换句话说,当用户点击按钮或发生应该用新场景替换场景的事件时,我们将让它看起来好像所有场景都在一个巨大的白色纸张上,只是用户看不到。
创建一个通用的滑动函数
由于我们将在各个地方做这件事,我们需要一个足够通用的函数,以便我们可以将任何场景从任何位置传递给它,并且它将执行我们想要它执行的确切操作。
因此,由于 MainScene.h 在所有地方都被导入,我们将在其中创建一个通用函数。打开 MainScene.h 文件,并在 @interface 行上方添加此枚举:
NS_ENUM(NSInteger, kMoveDirection)
{
kMoveDirectionUp,
kMoveDirectionDown,
kMoveDirectionLeft,
kMoveDirectionRight
};
@interface MainScene :CCScene
{ ... }
这将允许我们告诉通用函数将场景滑动到哪个方向。
然后,在 MainScene.h 文件中添加以下方法声明:
+(void)rubberBandToScene:(CCNode*)scene fromParent:(CCNode*)parent withDuration:(CGFloat)duration withDirection: (enumkMoveDirection)direction;
此方法接受的场景参数是你想要过渡到的场景、父级(你当前所在的场景)、你想要滑动持续多长时间,以及你想要滑动到哪个方向。
接下来,在 MainScene.m 文件中添加实际的功能。它将使我们的场景滑动到视图中,然后替换为 CCDirector(详细解释见注释):
+(void)rubberBandToScene:(CCScene*)scene fromParent:(CCNode*)parent withDuration:(CGFloat)duration withDirection:(enumkMoveDirection)direction
{
//grab the view size, so we know the width/height of the screen
CGSize winSize = [[CCDirector sharedDirector] viewSize];
//add the new scene to the current scene
[parent addChild:scene z:-1];
//set a distance to "over move" by
NSInteger distance = 25;
//variables for how much to move in each direction
CGPoint posBack = ccp(0,0);
CGPoint posForward = ccp(0,0);
//determine the specifics based on which direction the slide is going to go
if (direction == kMoveDirectionUp)
{
posBack.y = -distance;
posForward.y = winSize.height + distance*2;
scene.position = ccp(0,-winSize.height);
}
elseif (direction == kMoveDirectionDown)
{
posBack.y = distance;
posForward.y = -(winSize.height + distance*2);
scene.position = ccp(0,winSize.height);
}
elseif (direction == kMoveDirectionLeft)
{
posBack.x = distance;
posForward.x = -(winSize.width + distance*2);
scene.position = ccp(winSize.width, 0);
}
elseif (direction == kMoveDirectionRight)
{
posBack.x = -distance;
posForward.x = winSize.width + distance*2;
scene.position = ccp(-winSize.width,0);
}
//declare the slide actions
id slideBack = [CCActionEaseInOut actionWithAction:[CCActionMoveBy actionWithDuration:duration/4 position:posBack] rate:2];
id slideForward = [CCActionEaseInOut actionWithAction:[CCActionMoveBy actionWithDuration:duration/2 position:posForward] rate:2];
id slideBackAgain = [CCActionEaseInOut actionWithAction:[CCActionMoveBy actionWithDuration:duration/4 position:posBack] rate:2];
id replaceScene = [CCActionCallBlock actionWithBlock:^{
//remove the new scene from the current scene (so we can use it in the replace)
[parent removeChild:scene cleanup:NO];
//reset its position to (0,0)
scene.position = ccp(0,0);
//actually replace our scene with the passed-in one
[[CCDirector sharedDirector] replaceScene:scene];
}];
//arrange the actions into a sequence (which also includes the replacing)
id slideSeq = [CCActionSequence actions:slideBack, slideForward, slideBackAgain, replaceScene, nil];
//execute the sequence of actions
[parent runAction:slideSeq];
}
然而,由于我们在一个方向或另一个方向上稍微将场景移动到屏幕外,我们需要确保我们有足够的“背景”来覆盖额外的部分。
扩展背景
在 MainScene.m 文件 init 方法的顶部,我们将背景的声明更改为以下内容:
CCNode *background = [CCNodeColor nodeWithColor:[CCColor whiteColor] width:winSize.width*5 height:winSize.height*5];
background.anchorPoint = ccp(0.5,0.5);
background.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:background z:-2];
将 z 值设置为 -2,以便我们可以将新场景放置在 -1(如前述代码所示)。因此,即使背景是屏幕宽度和高度的五倍,当它滑动到视图中时,新场景仍然可见。
现在只剩下调用函数这一步了。
使用橡皮筋过渡替换场景
由于我们创建了一个如此方便的通用函数,我们不需要做任何事情,只需调用一次。因此,在 MainScene.m 文件中,修改你的 goToMenu 函数以调用你刚刚创建的 rubberBandToScene 方法:
-(void)goToMenu
{
if (isSoundOn)
[[OALSimpleAudio sharedInstance] playEffect:@"buttonClick.mp3"];
[MainScene rubberBandToScene:[MenuScene scene] fromParent:self withDuration:0.5f withDirection:kMoveDirectionDown];
}
还要修改 MainScene.m 文件中 endGame 方法的相同行:
-(void)endGame
{
//right here:
NSInteger hsIndex = [self saveHighScore];
UIImage *image = [self takeScreenshot];
NSDictionary *scoreData = @{DictTotalScore : @(numTotalScore),
DictTurnsSurvived :@(numTurnSurvived),
DictUnitsKilled :@(numUnitsKilled),
DictHighScoreIndex :@(hsIndex),
@"screenshot" : image};
[MainScene rubberBandToScene:[GameOverScene sceneWithScoreData: scoreData] fromParent:self withDuration:0.5f withDirection:kMoveDirectionUp];
}
注意,去菜单的方向是 DirectionDown,而 endGame 对象是 DirectionUp。现在运行游戏并按下那个菜单按钮。美吧?但这只是我们拥有的许多场景过渡之一。所以,让我们处理剩下的部分,好吗?
MenuScene 中的过渡
当我们点击播放按钮时,我们理想上希望有相同的效果,所以首先我们需要创建一个超大的背景。在MenuScene.m中,修改背景代码以看起来像MainScene:
-(id)init
{
if ((self=[super init]))
{
winSize = [CCDirector sharedDirector].viewSize;
//these values range 0 to 1.0, so use float to get ratio
CCNode *background = [CCNodeColor nodeWithColor: [CCColorwhiteColor] width:winSize.width*5 height:winSize.height*5];
background.anchorPoint = ccp(0.5,0.5);
background.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:background z:-2];
...
}
然后,在goToGame函数中,我们只需调用我们之前创建的通用函数:
-(void)goToGame
{
if (isSoundOn)
[[OALSimpleAudio sharedInstance] playEffect: @"buttonClick.mp3"];
[MainScene rubberBandToScene:[MainScene scene] fromParent:self withDuration:0.5f withDirection:kMoveDirectionUp];
}
游戏结束时的过渡
到目前为止,我们已经将进入菜单和游戏结束过渡添加到了主游戏场景中。我们还在主菜单场景中实现了进入游戏过渡。唯一剩下要做的就是将进入菜单和进入游戏的过渡放到GameOverScene中。
要做到这一点,你可以在GameOverScene.m中做你迄今为止所做的一切,并修改goToMenu和restartGame方法中的replaceScene代码行:
-(void)goToMenu
{
//to be filled in later
[MainScene rubberBandToScene:[MenuScene scene] fromParent:self withDuration:0.5f withDirection:kMoveDirectionDown];
}
-(void)restartGame
{
//to be filled in later
[MainScene rubberBandToScene:[MainScene scene] fromParent:self withDuration:0.5f withDirection:kMoveDirectionDown];
}
就这样!我们不仅成功创建了一个自定义过渡(与 Cocos2d 3.0+版本附带的那种相当无聊的过渡相比),而且还轻松地实现了它。它增加了那么一点“哇!这很有趣!”的感觉,这是好事,因为这就是玩家会感受到的。快乐的玩家意味着更高的参与度,更高的参与度意味着更高的评分和更多的朋友推荐(这意味着你口袋里的钱更多)。
其他润色想法
本章未涵盖的一些其他润色想法如下:
-
为角色或单位提供更平滑的动画
-
流畅的动作(例如,如前一章所述的贝塞尔效果)
-
没有加载屏幕(结束当前场景看起来正好像下一个场景开始的样子,并且瞬间过渡)
-
没有崩溃(是的,修复所有这些问题)
-
一些细微的细节,如背景移动或简短的 NPC 旁白
-
在中断(如电话、电池耗尽等)的情况下保存用户的进度
但这些只是几个例子。你可以在游戏中做无数的小改动来让它变得更好一点,但遗憾的是!在某个时候,我们需要发布游戏,所以下一章将专注于添加最后的修饰和将游戏提交给苹果。
摘要
在本章中,你学习了各种润色游戏的方法,并真正关注了一些微妙但重要的元素,例如声音和音乐的开关按钮、场景之间的滑动以及社交分享功能。
总是有些东西可以被调整和优化,使其更加精致。例如,如果你想了解更多关于更传统的精灵动画,你可以使用CCAnimation来实现。关于这方面的优秀参考指南可以在www.cocos2d-swift.org/docs/api/Classes/CCAnimation.html找到。
注意,到目前为止,我们还没有为游戏想出一个名字。尽管名字是所有用户都会看到并熟悉的东西,但它对游戏开发并不重要,这就是为什么它排在最后。
第七章. 达成目标
与前几章相比,本章内容将相对较少,因为这款游戏现在已经准备好发布。我们将讨论项目中的最终化步骤,以及在你将游戏提交给苹果审核之前在 iTunes Connect 中需要做的事情。具体来说,我们将在本章相对简短的篇幅中涵盖以下主题:
-
添加默认图像
-
图标
-
分析
-
在 iTunes Connect 上准备应用
-
发布游戏及其后续步骤
对于本章,我们将使用Chapter 7项目,因为它包含一些错误修复以及一些其他润色元素。建议你在继续之前打开此项目。
小贴士
在撰写本书时,Cocos2d 版本 3.4 不支持原生 iPhone 6 或 6 Plus。因此,不幸的是,当苹果将 iPhone 5 屏幕放大以匹配 iPhone 6 或 6 Plus 的分辨率时,艺术作品将看起来有些模糊。如果你决心让原生分辨率工作,你可以参考forum.cocos2d-swift.org/t/iphone-6-ios-resolutions-and-assets/15062/68上的一个帖子。它描述了在运行 6 Plus 版本时使用 iPad Retina 版本 6 的资产,在运行时使用 iPhone Retina 资产。
添加默认图像
现在,我们只需要为每一款 iPad 和每一款预 6 代 iPhone 创建启动图像。此外,我们还将进行一种类似黑客式的解决方案,这将使 iPhone 6 或 6 Plus 的图形略好一些(尽管仍然不是原生),同时使 iPhone 5 的外观在某种程度上同时变得更好和更差(取决于你如何看待它)。
首先,在项目的Icon文件夹中,删除现有的Default图像,将它们移动到回收站而不是仅仅删除引用。
项目文件中应包含一个名为Default Images的文件夹,其中包含项目所需的每个文件。将这些文件拖入 Xcode 项目(确保勾选了复制复选框)。
由于我们没有许多需要原生默认图像的设备,我们现在将跳过资产目录。如果你想使用它们,当然可以,但当你有正确命名的默认图像,就像我们这里一样,这不是必需的(也没有任何真正的益处)。
添加加载屏幕
虽然默认图像是用户首先看到的,但我们仍然希望从初始图像到我们的游戏有一个平滑的过渡,而不是它突然改变。因此,我们将创建一个仅用于将游戏视图滑入位置的过渡场景。
首先,创建一个新的类——一个CCScene的子类——并将其命名为LoadingScene。
然后,将scene方法添加到LoadingScene.h文件中:
+(CCScene*)scene;
接下来,用以下代码替换你的LoadingScene.m文件:
#import "LoadingScene.h"
#import "MainScene.h"
@implementation LoadingScene
+(CCScene*)scene
{
return [[self alloc] init];
}
-(id)init
{
if ((self=[super init]))
{
//sets the window size and adds a white background
CGSize winSize = [[CCDirector sharedDirector] viewSize];
CCNodeColor *background = [CCNodeColor nodeWithColor:[CCColor whiteColor] width:winSize.width height:winSize.height*5];
background.position = ccp(0.5,0.5);
background.positionType = CCPositionTypeNormalized;
background.anchorPoint = ccp(0.5,0.5);
[self addChild:background z:-2];
//creates the Cubic! Title in the middle of the screen (where it's located in the Default.png image)
CCLabelBMFont *lblTitle = [CCLabelBMFont labelWithString:@"Cubic!" fntFile:@"bmTitleFont.fnt"];
lblTitle.position = ccp(0.5,0.5);
lblTitle.color = [CCColor colorWithRed:52/255.f green:73/255.f blue:94/255.f];
lblTitle.positionType = CCPositionTypeNormalized;
[self addChild:lblTitle];
//creates a progress circle that shows the user that stuff is happening (even though everything is technically already loaded at this point)
CCSprite *circle = [CCSprite spriteWithSpriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"imgLoadingCircle.png"]];
CCProgressNode *loadingTime = [CCProgressNode progressWithSprite:circle];
loadingTime.type = CCProgressNodeTypeRadial;
loadingTime.midpoint = ccp(0.5,0.5);
[loadingTime runAction:[CCActionProgressFromTo actionWithDuration:1 from:0.0f to:100.0f]];
loadingTime.position = ccp(winSize.width/2, winSize.height * 0.2);
[self addChild:loadingTime];
//schedules the transition to the main menu after 2 seconds (1 second for progress circle, and another second of waiting for a smoother transition)
[self scheduleOnce:@selector(transitionToMainScene) delay:2];
}
return self;
}
//method to transition using our rubber band effect created previously
-(void)transitionToMainScene
{
[MainScene rubberBandToScene:[MainScene scene] fromParent:self withDuration:0.6 withDirection:kMoveDirectionUp];
}
@end
理解我们想要过渡到MainScene,因此我们必须导入它。
还要注意CCProgressNode对象。这将是一个模拟加载的环形。由于我们的游戏加载相对较快,无需真正的加载条或其他类型的进度测量,我们不希望用户在连续几秒钟看到相同的图像时感到困惑。因此,我们添加了一个CCProgressNode对象,让他们知道“嘿,有事情发生了!”
当您查看代码时,这相当直观。中点本质上就像其他CCNode对象上的锚点。如果您想要一个条形而不是圆形,只需更改类型。其他所有内容都应该保持不变。
从主场景切换到加载场景
最后,为了让这一切发生,我们只需要将LoadingScene.h的导入语句添加到AppDelegate.m文件中:
#import "LoadingScene.h"
然后,在您的startScene方法底部,查找以下行:
return [MainScene scene];
用以下新代码替换它,这将进入我们刚刚创建的LoadingScene对象:
return [LoadingScene scene];
如果您此时运行游戏,您将看到初始默认图像加载,然后在一两秒后进度环出现,最后,加载屏幕将消失并拖动MainScene一起消失。
现在我们已经处理了初始启动,让我们设置图标。
图标
SpriteBuilder 为我们提供的默认图标方便了测试,但现在是时候让我们与众不同,创建我们自己的图标。
小贴士
要使用下一节中将要讨论的模板,您需要 Photoshop。如果您有设计团队,您可能已经拥有它。如果您没有 Photoshop,您可以从其网站获取免费试用版。
模板
根据您计划支持的设备和 iOS 版本,大约有 10 到 15 种不同的应用程序图标大小。如果您更改图标,手动更新所有这些大小将是一件痛苦的事情。幸运的是,使用由 Michael Flarup 创建的模板创建所需的图标大小比以往任何时候都要容易。
您可以从www.appicontemplate.com下载模板。下载完成后,您可以从 Photoshop 中打开它并开始使用。准备好后,只需运行下载中包含的动作,它就会为您生成文件。
还有关于如何使用模板的非常好的视频,所以本着不重复的原则,这里不会展示如何使用。
将图标添加到项目中
首先,我们希望移除旧图标,因为我们不希望有任何干扰或意外出现在某人的设备上。所以只需删除旧图标。
一旦您创建了新的图标(或者如果您希望使用本项目包含的图标),只需将文件拖入 Xcode 中,确保勾选复制复选框。这也在下面的屏幕截图中有展示:

目前,项目不会使用新的图标,因此我们需要为每个尺寸分配用于资产目录的图标文件。
资产目录
我们将使用资产目录来存储我们的图标。理想情况下,我们也应该将它们用于启动图像,但由于我们目前不支持任何疯狂的设备尺寸或方向,所以目前并不必要。
然而,对于图标来说,创建资产目录然后根据你希望支持的 iOS 版本将图标拖放到适当的位置要容易得多。
因此,首先,你需要通过访问项目的常规设置并点击使用资产目录按钮在 Xcode 中创建资产目录。
如果资产目录已经存在,只需点击下拉框右侧的箭头导航到该资产目录。以下截图也展示了这一过程供您参考:

然后,在创建完目录后,只需根据它们所需的尺寸将图片拖放到适当的位置。以下截图是它们应该是什么样子的粗略表示,但请确保拖动正确的图片,否则 Xcode 会发出警告,指出你提供了错误尺寸的图片。幸运的是,我们使用的模板为我们提供了良好的图片名称,因此我们可以快速查看哪个图标是什么尺寸的。

注意,我们不支持iOS 6.1 及以下的尺寸。虽然你可能认为这只是几个图标尺寸的问题,但如果你真正愿意支持 iOS 6 和更早的版本,你必须在一个运行旧设备的设备上运行你的应用,以确保没有任何崩溃。
总之,一旦你将图片添加到资产目录中,清理项目并重新运行。你会看到预期的更新图标。就这样了!
分析和用户数据
在我们提交应用之前,还有一件最后的事情要做,那就是设置一些快速分析工具来确定用户如何玩游戏。我们将使用一个名为Flurry的 API。如果你之前没有听说过它,或者听说过但从未使用过,没关系,接下来的部分将带你完成设置过程。
Flurry 有三大优点:
-
设置和使用简单
-
可用的详细分析
-
它是免费的!谁会反对呢?
那我们就开始吧,从注册过程开始。
注册 Flurry
前往www.flurry.com/,然后在右上角点击注册。填写所需信息,然后点击注册。
之后,它会询问你希望跟踪哪个平台的分析。在这里,因为我们正在为 iPhone 和 iPad(iOS 的通用应用)制作应用,我们将选择 iPhone。如果应用仅在 iPad 上可用,你将选择 iPad 版本。
我们接着添加应用的名称,并选择应用所属的类别。
准备就绪后,单击底部的 创建应用,Flurry 将要求您验证您的电子邮件。一旦您点击电子邮件中的链接(或复制他们发送的代码并将其粘贴到他们提供的验证框中),您将被引导到一个屏幕,告知您过程已成功。然后您应该会得到一个 API 密钥。
小贴士
请记住将此 API 密钥复制到某个地方或保持网页打开,因为我们稍后开始会话跟踪时需要它。
在成功设置后,您应该会看到一个下载 SDK 的链接。下载 SDK(ZIP 文件),并等待其完成。这可能需要一两分钟。

一旦 SDK 下载并解压,我们就可以将 Flurry 添加到我们的项目中。
将 Flurry 添加到您的项目中
由于 Flurry API 应该与所有其他库一起包含,在下载的 SDK 中,你应该会看到一个 Flurry 文件夹。将整个文件夹拖到 Xcode 的 libs 文件夹中(确保勾选了复制复选框)。

然后,我们需要确保我们的项目中包含适当的框架,以便 Flurry 正确地执行其功能。因此,在我们的项目 构建阶段 中,我们将添加一些框架。只需在 链接二进制与库 部分的 + 按钮下单击即可。

然后,搜索并添加以下框架:
-
Security.framework -
SystemConfiguration.framework -
CFNetwork.framework
最后,库已经被包含到我们的项目中,我们可以在 AppDelegate.m 文件中导入 Flurry 头文件,并在 application:didFinishLaunchingWithOptions 方法中开始跟踪会话:
#import "Flurry.h"
@implementation AppController
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[Flurry startSession:@"API_KEY_HERE"];
[Flurry setLogLevel:FlurryLogLevelAll];
...
}
注意,API_KEY_HERE 字符串应该是您在首次创建应用时获取的实际 API 密钥,因此现在您可以随意复制它。
当您刚开始(或实际上任何时候)进行调试时,您也会有一些调试信息。理想情况下,您应该在发布时禁用这些信息,但现在,在测试时查看正在发生的事情是有帮助的。
记录事件
事件是 Flurry 的核心。它们基本上是一种无限的方式来跟踪您自己的自定义事件。您不需要保留预定义的函数或事件,只需简单地传递您希望跟踪的事件名称(只是一个字符串),Flurry 会为您处理其余部分。
例如,假设我们想跟踪用户启动游戏多少次。由于我们经常访问 MainScene,我们不希望跟踪到错误的信息。因此,只有在他们移动一个单位时(但仅当 numTurnsSurvived 等于 1 时)才记录事件。
打开 MainScene.m 文件,在 moveUnit 函数中,将以下代码添加到方法顶部,该方法将记录当前会话的 started_game 事件:
if (numTurnSurvived == 1)
[Flurry logEvent:@"started_game"];
另一个例子可能是当用户完成教程时。所以,在MainScene.m中,在我们的advanceTutorial方法中,让我们为用户完成教程的情况添加一个事件:
-(void)advanceTutorial
{
...
else
{
...
[Flurry logEvent:@"tutorial_finished"];
}
}
添加事件跟踪竟然这么简单!你可以拥有无限数量的不同名称的事件,每个会话最多可以跟踪 300 个独特的事件(所以请尽可能多地使用与你的应用/游戏相关的),并且对特定事件跟踪的次数没有限制。
小贴士
此外,还有带有参数的事件记录,例如用户是否注册,他们使用的是哪种设备,他们是否使用了特定的设置,等等。任何字符串都可以作为参数。你可以在 Flurry API 中了解更多关于参数的信息,请参阅developer.yahoo.com/flurry/docs/analytics/gettingstarted/events/ios/。
当你完成使用应用时,Flurry 会自动提交任何会话数据。虽然这听起来有些含糊不清,但本质上是在按下主页按钮时发送数据。
话虽如此,如果你正在测试,请确保 Flurry 使用 Xcode 正确记录了你的事件,并且在按下设备上的主页按钮(或模拟器)之前不要点击停止按钮。数据可能永远不会发送,你可能会花费数小时甚至数天 wondering why nothing seems to be working, when you actually never gave Flurry a chance to send the information.
现在我们的事件已经被记录并发送到 Flurry 服务器,让我们实际跟踪和分析我们的数据。
跟踪和可视化数据
幸运的是,Flurry 不仅抓取我们的原始数据,而且还为我们创建了有用的分析,所以我们不需要自己进行数据分析。例如,如果你想查看每天有多少人开始玩游戏,或者有多少人通过存活超过 20 回合来完成游戏,你可以在他们的网站上轻松查看此类数据。以下是使用 Flurry 几周后的截图:

注意,事件出现在你的 Flurry 事件仪表板(不是数据,而是事件名称)上大约需要 15 到 45 分钟。如果在几小时后你仍然没有在仪表板中看到事件列表,请再次尝试,但这次请确保你正确地退出了应用。这意味着在 Xcode 中停止应用之前先按下主页按钮,以便 Flurry 可以检测到该事件并发送数据。
此外,Flurry 不会实时显示,所以发送给他们的任何实际数据都会被处理,并且至少需要 24 小时才能显示。在测试时,如果你发现自己想知道数据在哪里,只需等待一两天。然后它应该开始显示。如果它没有显示,请确保你在 Xcode 中看到了正确的调试日志消息。
小贴士
如果你在使用 Flurry 时迷路了,需要进一步解释,或者只是想了解更多关于 Flurry 高级功能的信息,你可以在他们的网站上查看入门指南,网址为developer.yahoo.com/flurry/#get-started。它相当全面。
如果你感兴趣的是 Flurry 的替代品,你可以通过快速搜索 iOS Analytics,以及查看www.apptamin.com/blog/app-analytics-tools/来了解,它展示了(并详细介绍了)各种其他分析工具。
在 iTunes Connect 上准备应用
现在我们已经准备好了所有东西,让我们继续准备 iTunes Connect (itunesconnect.apple.com),以便我们可以提交这个游戏供苹果审核。本节假设你已经有一个开发者账号并且在 iTunes Connect 中有一个应用。
小贴士
如果你还没有开发者账号,现在是获取账号的好时机。如果你不知道如何获取,请阅读第一章,刷新我们的 Cocos2d 知识。如果你已经有了开发者账号,但还没有在 iTunes Connect 中创建应用,请回到第六章的游戏中心部分,整理和润色。在那里,你会看到在 iTunes Connect 中创建应用的逐步方法。一旦完成,你就可以从这里继续了。
对于大部分内容,iTunes Connect 是相当直观的。然而,我们将快速了解每个部分的内容以及你需要输入的信息。

当你在 iTunes Connect 中打开你的应用时,你会在顶部看到许多部分,以及截图和视频预览的部分。必须为每个你支持的平台包含截图。由于我们将支持所有设备,因此最好为每个设备添加截图(也许还可以添加一个简短的 30 秒视频)。
理想情况下,你应该为每个设备拍摄自己的截图,这就是为什么项目文件中没有包含截图(每个设备最多可以添加五个截图)。

在截图下方,你会看到 App Store 中的应用显示名称,一个描述(4000 字符限制)的应用描述,用户可以在 App Store 中输入以找到你的应用/游戏,以及一个链接到你的网站,供想要了解更多关于你的用户访问。
关键词以逗号分隔,且最多 100 个字符。例如,以下是一些可以用来描述这个游戏的术语。这个字符串长度为 28 个字符:
cube,square,grid,number,math
如果你需要帮助确定关键词的长度,你只需搜索字符串长度计数器,通常任何一个都可以完成这项工作。

接下来是应用的图标(这必须是 1024 x 1024 像素,并且是 PNG 或 JPG 格式),它将在整个 App Store 中显示——版本号、它将出现在哪些类别下等等。版权信息的例子可以是 KeitGames 2015(想象一下在填写这部分时旁边有一个版权符号)。
需要注意的一点是评分部分。在这里,尽管它很微妙,但你必须点击编辑按钮,通过自我评分的过程来确定应用适合的最小年龄组。在我们的案例中,它将是 4+。
此外,在你提交构建到 iTunes 后,它将进入一个持续几分钟的处理阶段。之后,在构建部分,你可以选择你想要提交给应用特定版本的构建。

接下来是游戏中心部分。这部分与屏幕顶部的游戏中心标签页是分开的,因为尽管你可能已经设置了一个正常工作的排行榜并在测试构建中工作,除非你在这里启用游戏中心并选择你想要添加的排行榜,否则它不会出现在发布版本中。
如果你的应用或游戏需要登录才能工作,你也可以提供演示账户,以及提交你可能有的任何关于审查你应用的笔记。
最后,如果我们选择自动发布此版本,那么每当应用被苹果公司批准时,它将立即开始为 App Store 处理。它将在当天或第二天(或你设置在顶部定价部分中的任何未来日期)显示。假设你选择手动发布此版本。那么即使应用被苹果公司批准,它也不会开始处理(因此即使在设定的发布日期之后,也无法下载),直到你点击发布此版本。
在填写了所有相关信息后,我们实际上可以开始创建构建并将其提交给苹果公司。
发布游戏及其后续步骤
这就是真正的兴奋时刻,你会对自己说“是时候了。”
但说真的,当你的游戏终于准备好发布,你将其发送给苹果公司进行审查,并计划让可能数百万的人接触到它时,这是一个激动人心的时刻。
发布游戏的第一件事是创建一个可以上传到苹果服务器的应用归档文件。为此,只需转到产品 | 归档(如果变灰,请将设备类型从模拟器更改为设备,即使你没有连接设备)。
归档完成后,将打开一个类似于以下截图所示的屏幕(如果未打开,你可以转到窗口 | 组织者):

从这里,点击 导出。选择 保存为 iOS 应用部署 并点击 下一步。
如果您尚未登录,请登录,然后它会带您到一个类似这样的屏幕:
小贴士
如果它说找不到与捆绑 ID 匹配的应用,只需点击 重试。第二次应该能找到它。

点击 导出,然后将其保存到您可以稍后找到的地方(例如桌面)。
现在通过前往 Xcode | 打开开发者工具 | 应用加载器 来打开 应用加载器。登录并选择 交付您的应用。

现在,导航到您导出的 IPA 文件所在的任何位置,并打开它。它将读取数据,屏幕看起来类似于这样:

当您点击 下一步 时,它将验证所有图标是否已正确添加,默认图像是否存在,以及确保应用与您提供的信息相匹配的其他任何内容。
如果出现任何错误、警告或问题,您可能不是唯一遇到该问题的人。快速进行谷歌搜索从未伤害过任何人。
一旦完成验证过程,它将把应用上传到苹果的服务器。
小贴士
注意,这并不意味着应用已经“提交”以供审查。它只是上传了,以便您在准备好时可以从 iTunes Connect 中选择它。即使它说在审查时会被电子邮件通知,这也是不正确的(这很奇怪)。
等待几分钟之后,现在返回到 iTunes Connect,您应该在 构建 部分旁边看到一个 + 按钮。当您点击它时,您应该能够选择最近上传的构建。

在顶部点击 保存。然后点击 提交以供审查。然后完成关于您的应用包含内容的必填问卷(在我们的案例中,没有任何内容)。然后点击 提交。
我们正在路上!
小贴士
一个更快的方法是无需使用应用加载器来验证和提交您的应用到 App Store。您不需要点击 导出,而是可以点击 验证 或 提交,然后执行相同的流程。
提交后
现在您的应用状态为 等待审查,最好利用您的时间,要么准备更新(因为理论上,您可以不断推送更新,一旦一个被接受,就推送另一个)要么向公众推广您的游戏。
理想情况下,您一直在早期就做这些事情:记录您的进度,在社交媒体上发布,告诉您的朋友和家人您正在创建的游戏,等等。但如果没有,没关系!接下来的几个部分将引导您完成这个过程。
测试人员和目标市场
如果你还没有人玩你的游戏,也没有进行 alpha 或 beta 测试,现在可能是做这件事的最佳时机。你应该尝试吸引不同技能水平的玩家,看看不同用户如何与你游戏互动,这样你就可以尽可能多地消除混乱。
此外,如果你想知道,“我应该寻找什么样的人来测试我的游戏?”你需要考虑你的理想玩家是谁。如果你在制作儿童游戏,试着让儿童(或者甚至他们的父母)玩你的游戏。如果游戏类型是塔防,试着找到那些非常喜欢塔防游戏的人,这样他们就可以给你提供很好的反馈。
如果你感兴趣的是与不在你附近的人进行 beta 测试,请考虑苹果的 TestFlight 集成。最初,TestFlight 是一个独立的产品,但现在苹果已经将其包含在 iTunes Connect 中(如果你点击了预发布标签,你可能已经看到了它)。基本上,你可以通过获取他们的电子邮件来邀请人们使用你的应用程序/游戏。你以提交的方式上传构建版本,除了你必须将构建版本添加到预发布中,经过审查过程后,苹果允许测试者下载并玩游戏。
但拥有 beta 测试者和确定你的目标受众只是第一步。第二步涉及将你的游戏交给很多人。
应用程序评论网站
使你的游戏成功的关键之一是将游戏交给其他人,特别是那些写博客或文章的人,这些文章是其他人制作的游戏的评论。如果一个游戏很棒,你会在各个地方看到它,也许苹果也会注意到,并在他们 App Store 的特色部分推荐它。
如果你给任何开发者(他们不在知名公司)发电子邮件并询问苹果是如何介绍他们的,通常得到的回答都是一样的:“我们根本不知道会发生这样的事情。我们只是让一些网站评论我们的游戏,然后突然之间,我们的游戏就在 App Store 上被推荐了一周!”
话虽如此,有一个巨大的应用程序评论网站列表在maniacdev.com/2012/05/ios-app-review-sites,这样你就不必自己寻找它们。
尽管 URL 显示“2012”,但在撰写本书时,它已经被更新到 2014 年 8 月,网站按 Alexa 排名排序,将浏览量最大的网站放在列表顶部(浏览量最小的放在底部)。
苹果为每个提交到 App Store 的版本提供 100 个促销代码。在向这些网站推广/提交你的游戏时使用这些代码。
小贴士
注意,如果你的游戏还没有成为像Mashable或TechCrunch这样的网站的热门,那么在没有成为大热门的情况下,直接进入这些网站的可能性不大,所以为了更好地利用促销代码,从列表的较低位置开始,逐步上升可能是一个更好的选择,而不是下降。
其他信息来源
有一个名为PixelProspector的网站。在游戏开发方面(尤其是独立游戏,如果你在读这本书,这很可能是你喜欢的),他们拥有大量的资源、链接、指南等。即使你是知名工作室的一员,这也是一个获取信息的好来源(特别是他们相对较新的营销部分)。你可以在这里查看www.pixelprospector.com/indie-resources/。你可能会找到一些有用的东西。
摘要
在本章中,你学习了如何为你的应用创建默认图片,正确设置图标(包括使用资源库),通过 Flurry 向应用添加分析,在 iTunes Connect 上设置一切,发布你的应用,并规划发布后的行动。
在下一章中,我们将介绍苹果公司最新的编程语言,称为Swift。我们将看到一个非游戏示例,以及一个使用 Cocos2d Swift 版本的简单游戏。
第八章:探索 Swift
在这本书的最后一章,如果你愿意,你可以开始学习 Swift,苹果的新编程语言。在这里,你将简要了解 Swift 是什么,语言是如何工作的,以及 Objective-C 和 Swift 之间的一些各种语法差异。我们还将介绍使用 Swift 创建的一些简单应用程序。
在本章中,我们将涵盖以下主题:
-
Swift 是如何工作的
-
通过 Playgrounds 学习 Swift
-
使用 Cocos2d-Swift 在 Swift 中而不是 Objective-C 中创建游戏
小贴士
本章不会涵盖 Swift 的大部分内容。相反,它旨在通过 Cocos2d 介绍在 Swift 中编码,使用这个引擎创建游戏。
还要注意,作为一门语言,Swift 仅与 iOS 7 及以上版本兼容。如果你打算支持 iOS 6 或更早版本,你无法在你的项目中使用 Swift。
Swift 是如何工作的
Objective-C 和 Swift 之间的主要区别在于外观。它仍然感觉像 Objective-C(在这个意义上,你可以调用所有相同的方法),但语法不同。
例如,假设你想要在 Objective-C 中调用以下内容:
[object someMethodWithParam:param1 andOtherParam:param2];
相反,你最终会在 Swift 中调用以下内容:
object.someMethodWithParam(param1, andOtherParam:param2)
此外,与 Objective-C 中使用头文件和主文件不同,只有一个.swift文件被用于所有内容。
显然,在 Swift 中编码时,你将不得不学习一些语法差异,但熟悉 Objective-C 的人会很容易地掌握 Swift。不用担心,即使你不熟悉 Objective-C。Swift 是一门易于学习的语言,这使得学习它变得更加容易。
话虽如此,让我们通过苹果新创建的工具来介绍一些语法差异,这个工具可以帮助学习和调试 Swift——Playgrounds。
通过 Playgrounds 学习 Swift
Playgrounds 提供了一种快速轻松地测试 Swift 代码的方法,而无需像在其他语言中创建测试项目时那样携带很多负担。它们旨在作为用于原型设计和快速调整代码的文件,如果你在某个小部分有问题的话。它们非常容易使用和理解,所以让我们开始吧。
注意
你首先会注意到 Swift 的一个特点是,不需要分号(除非在一行上写多个语句,这些语句必须由分号分隔)。没错——一个都不需要!它们是可选的,但编写 Swift 代码的首选风格是不使用分号。
此外,在编写 if 语句、循环、switch 语句等时,你必须使用花括号包围将要运行的代码块(即使只有一行代码将要执行)。
现在,请打开这本书附带在“示例项目”文件夹中的SwiftSyntax.playground文件。
小贴士
如果你无法在 Xcode 中打开文件,请更新你的 Xcode 版本到最新版本,因为你的版本可能已经过时。
当你在 Xcode 中打开此文件时,你会看到很多事情发生。它是通过注释行(// ------)分隔的。每个部分代表 Objective-C 和 Swift 之间不同的一组语法差异。
虽然没有明确说明 Objective-C 的等效语法是什么,但你可以看到如何声明变量,如何运行循环,创建和调用函数等等。此外,这里并没有包括 Swift 的所有语法差异。这里只列出了语言中最常见的用法。
在打开 playground 文件后,让我们看看 Playgrounds 是如何组织的,这样你可以更好地理解正在发生的事情(以及为什么 Playgrounds 除了作为一个存放代码的地方之外还有其他用途)。
Playgrounds 是如何组织的
左侧显然是代码。右侧是代码状态的描述;例如,如果是一个变量,它会显示变量的值。如果是一个println语句(Swift 中打印控制台的标准方式),它会显示输出。
如果你想在 Playground 中输入任何新代码,你会看到 Playground 的右侧窗格随着你的输入而更新。如果你复制并粘贴任何代码,你会看到所有行都更新为每行的最终结果。
因为这是一个 Playground,我们不需要担心在这里使用println或NSLog语句。它将自动显示在 Playground 的右侧。
查看随时间推移的结果
使用 Playgrounds 的一个酷特点是你可以跟踪随时间推移的循环进度。例如,如果你想看到在 names 数组中打印的每个项目,或者每个迭代中变量的值,你可以通过 Playground 的值历史按钮直观地看到它。
此外,如果你想在一个项目中调试自己的 Swift 代码(而不仅仅是 Playground),你可以将其带入 Playground 中,并观察随时间推移的结果。所以,如果你有一个敌人用循环来回巡逻,你可以使用 Playground 的值历史记录来查看循环每次迭代的每个值(在这种情况下,位置)。
因此,首先,转到for循环部分(大约在第 90 行),找到循环中说的 total += x的行,如图所示:

要查看,将鼠标箭头移至右侧窗格中的(100 倍)行上,你应该会看到在其右侧出现两个按钮。第一个是一个眼球,它会显示给定对象包含的每个值。例如,如果你有一个字典,你可以查看该字典中所有的键/值对,如图所示:

第二个按钮是值历史按钮。当你的鼠标箭头放在它上面时,它会变成一个+按钮,如图所示:

当你点击这个按钮时,你会看到一个图表,显示了循环迭代时该变量随时间变化的值(你也会看到输出控制台,它显示了从上一个循环中按预期打印的名称)如图所示:

如果你想要获取更多关于变量随时间变化详情,你可以通过拖动红色刮擦器到值历史部分的底部,或者点击图表上的任何数据点来查看其相应的值,如图所示:

了解更多关于 Swift
在 Swift 方面,这里没有涵盖很多内容,例如懒变量、身份操作符、空合并操作符、类初始化器、继承等等。
了解更多关于 Swift 的绝佳地方包括www.lynda.com(有一个关于 Swift 的精彩课程在www.lynda.com/Swift-tutorials/Swift-Essential-Training/180105-2.html)和www.raywenderlich.com/tutorials,这些网站提供了许多关于 Swift 基本和高级功能的在线课程和教程。你还可以查看苹果的开发者视频;只需在developer.apple.com/videos/wwdc/2014/搜索 Swift。然后还有示例项目、Swift 开发者博客developer.apple.com/swift/blog/,以及其他如 Stack Overflow 等网站。
如前所述,Playgrounds 对于刚开始学习 Swift 的开发者以及那些经验更丰富并希望进行代码调试的开发者来说非常有用。
但是,Playground 本身并不是一个完整的 App,所以让我们用 Swift 作为 Cocos2d 的语言来制作一个简单的游戏。再次提醒,你会发现代码本身与 Objective-C 非常相似;只是语法略有不同,所以你应该能很快掌握。
用 Swift 而不是 Objective-C 来创建游戏
使用 Cocos2d-Swift 背后的想法是转向新的编程语言,对吧?尽管 Cocos2d 的 Swift 版本仍然相对较新,而且在你阅读这本书的时候可能并不是所有功能都已实现,但它仍然可以作为创建游戏时的核心语言。所以,让我们在这里制作一个非常简单的游戏,基于你刚刚学到的核心概念,并结合你对 Cocos2d 引擎一般工作方式的了解。
为什么使用 Swift 而不是 Objective-C?因为,你应该跟上该领域最新技术的步伐,Swift 只是这一方向上的下一步。此外,随着时间的推移,苹果可能会逐渐弃用并停止支持 Objective-C(但这只是一个理论)。
通常,如果有机会,了解更多的语言是有帮助的。如果你对网页开发感兴趣,Swift 感觉与 JavaScript 非常相似,所以感觉像是同时学习两种语言。
那么为什么特别选择 Cocos2d-Swift 呢?原因和你刚才读到的相同。此外,用 Swift 编写代码通常比用 Objective-C 快,所以,如果你能更快地编写代码,理论上你也能更快地制作游戏,这真是太棒了!
游戏目标
我们将要制作的游戏包括一个位于中心的可以射击子弹的炮塔,一个得分计数器,以及从左右两边进入的正方形敌人。如果敌人到达中心,游戏就结束了!随着得分的增加,敌人的出生率也会增加,最终会超过炮塔的射击速度。
这里是我们将要制作的游戏的几个截图:


开始一个新的 Swift 项目
记住,现在开始 Cocos2d 项目的方式是通过 SpriteBuilder。即使我们不会在 SpriteBuilder 中使用任何代码,我们仍然需要在那里创建项目的过程。
因此,在 SpriteBuilder 中,转到文件 | 新建 | 项目。选择你想要创建项目的位置,将其命名为例如TurretGame,并确保选择Swift作为语言。以下截图供你参考:

然后,在Turret Game中,转到本书内容的Assets文件夹,并将Images文件夹拖到 SpriteBuilder 左侧的资源列表中,如下截图所示:

在将其导入到 SpriteBuilder 后,在 SpriteBuilder 中的Images文件夹上右键单击(或按Ctrl并单击),然后选择制作智能精灵图集。这是 TexturePacker 通常为我们做的事情。然而,我们可以使用 SpriteBuilder 的自动图集制作器来加快这个过程,因为这个项目只是一个示例。

一旦它变成了精灵图集,文件夹图标应该是带有微笑的粉红色。现在,转到文件 | 发布,以便 SpriteBuilder 可以为我们生成所需的项目文件,我们就可以继续了。
添加字体文件
不幸的是,截至本书编写时,SpriteBuilder 的表现不佳,并且不太擅长处理 BMFonts。因此,我们不会让 SpriteBuilder 处理尺寸/导出,而是创建我们自己的 BMFont 并将其手动添加到我们的文件列表中。这可能不是最有效的方法,但它有效,所以我们就这样做了。
在这本书的项目文件夹中,你应该在拖入 SpriteBuilder 的 Images 文件夹旁边看到一系列文件夹,如 resources-hd 等。将这四个文件夹全部复制并粘贴到你的项目文件的 iOS Resources 文件夹中。当它询问你时,确保你点击 Merge(以及 Apply to All)。

这将为每种尺寸类型添加字体文件到相应的文件夹,以便 Cocos2d 的目录搜索模式可以根据设备找到正确的尺寸。
导入 Bridging-Header 和加载 MainScene
当使用 Objective-C 与 Swift 文件一起工作时,Objective-C 需要 一个名为 Bridging-Header 的文件。这个文件会自动创建,所以我们只需要导入这个文件。文件的格式是 ProjectName-Swift.h,其中 ProjectName 是你的项目名称(例如,如果项目名为 TurretGame,我们将使用 TurretGame-Swift.h)。
现在,请随意打开 Xcode 项目。打开 AppDelegate.m 文件,并在文件顶部添加导入语句以添加 Bridging-Header 文件:
#import "TurretGame-Swift.h"
然后,在 StartScene 方法中,我们需要更改将过渡到主场景的代码行(以及添加读取我们的图像表的代码行):
- (CCScene*) startScene
{
// Capital "I"
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"Images.plist"];
//[CCBReader loadAsScene:@"MainScene"];
return [MainScene node];
}
我们在这里使用 node 而不是 Scene,因为我们正在调用一个 Swift 类。在这本书的前几个例子中,Scene 方法是我们创建的方法,而 node 是一个预定义的函数,它执行相同的功能。
创建背景、炮塔和得分标签
因为 Swift 旨在在可读性方面简洁明了,所以在 MainScene.swift 文件中,以下代码就足以获得白色背景、要显示的图像和屏幕底部的标签(注意 CCScene 继承;默认是 CCNode):
class MainScene: CCScene {
let winSize = CCDirector.sharedDirector().viewSize()
var background = CCNodeColor(color: CCColor.whiteColor())
//a value of "527" to make sure it's working. It'll get auto-set later, don't worry.
var lblScore = CCLabelBMFont(string: "527", fntFile: "bmFont.fnt")
//"Images/turret.png" because the turret image was located inside the Images folder
//also making sure to Downcast to a CCSprite from AnyObject!
//We want to downcast because the variable we're setting should be a sprite, so because the spriteFrameByName() method returns AnyObject (as an explicitly unwrapped optional), we must downcast the ambiguous type to CCSprite for better use of the variable later on.
var turret = CCSprite.spriteWithSpriteFrame(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("Images/turret.png")) as CCSprite
override init() {
super.init()
//simply add the background color
self.addChild(background)
//position and add the score label
self.lblScore.position = ccp(self.winSize.width/2, self.winSize.height * 0.1)
self.addChild(self.lblScore)
//position and add the turret (z:1 so the bullets can spawn underneath it)
self.turret.position = ccp(self.winSize.width/2, self.winSize.height/2)
self.addChild(self.turret, z: 1)
}
}
在这个阶段运行项目,你将看到我们描述的结果。如果你开始欣赏编写代码实现功能所需代码的简洁性,那么 Swift 可能正是你想要学习的语言。
现在让我们继续让炮塔旋转。
使用 touchMoved 旋转炮塔
注意到目前为止,几乎所有类和方法都命名得非常相似,Objective-C 和 Swift 之间的语法差异很小。我们的 touchBegan、touchMoved 和 touchEnded 方法也将如此。
首先,我们需要添加一个变量来存储玩家的触摸位置。因此,在 MainScene 的顶部添加以下变量:
class MainScene: CCScene {
...
var touchPos = ccp(0, 0)
...
}
然后,在 init() 方法中,将用户交互布尔值设置为 true:
//records touches
self.userInteractionEnabled = true
接下来,将以下方法添加到 MainScene 中:
// a method that will be called when a touch is registered on the device
override func touchBegan(touch: CCTouch!, withEvent event: CCTouchEvent!) {
// grab the touch's location within the scene, and set it to our variable
self.touchPos = touch.locationInNode(self)
// calculate the angle of the touch relative to the turret's current position
var angle = ccpToAngle( ccpSub( self.touchPos, self.turret.position ) )
// set the rotation of the turret based on the calculated angle (converted to degrees because Cocos2D doesn't use radians)
self.turret.rotation = CC_RADIANS_TO_DEGREES(Float(angle)) * -1
}
// a method called when a touch is dragged across the screen
override func touchMoved(touch: CCTouch!, withEvent event: CCTouchEvent!) {
// grab the location of the touch and set it again
self.touchPos = touch.locationInNode(self)
// calculate the angle again based on the new touch position
var angle = ccpToAngle( ccpSub( self.touchPos, self.turret.position ) )
// set the rotation of the turret again based on the new angle
self.turret.rotation = CC_RADIANS_TO_DEGREES(Float(angle)) * -1
}
override func touchEnded(touch: CCTouch!, withEvent event: CCTouchEvent!) {
// do nothing at the moment
}
如果你运行游戏,你将能够用手指拖动并使炮塔按照手指的方向旋转。
射击一些子弹
如果炮塔不射击子弹,那么它就没什么用了,所以让我们编写那段代码。
首先,我们想要一个可以调用(或安排)的方法。我们想要让它生成一个子弹,然后将其发射到我们的手指方向(炮塔所指的方向)。所以,请继续添加以下方法:
func shootBullet() {
//create the bullet. Again, "Images/bullet.png" because of the Images folder
var bullet = CCSprite.spriteWithSpriteFrame(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("Images/bullet.png")) as CCSprite
//position the bullet underneath the turret
bullet.position = ccp(self.winSize.width/2, self.winSize.height/2)
//calculate the distance to move based on similar triangles
let xDist = self.touchPos.x - self.turret.position.x;
let yDist = self.touchPos.y - self.turret.position.y;
let zDist = ccpDistance(self.touchPos, self.turret.position)
let newX = (xDist * winSize.width) / zDist;
let newY = (yDist * winSize.width) / zDist;
//assign that distance to a CGPoint variable
let moveDistance = ccp(newX, newY)
//create an action that will move the bullet, then after 0.5 seconds, it will remove it from the screen
var moveAndRemoveAction = CCActionSequence.actionOne(CCActionMoveBy.actionWithDuration(0.5, position: moveDistance) as CCActionFiniteTime, two: CCActionCallBlock.actionWithBlock({
self.removeChild(bullet)
}) as CCActionFiniteTime) as CCAction
//add the bullet
self.addChild(bullet)
//run the move action
bullet.runAction(moveAndRemoveAction)
}
虽然我们现在有一个创建子弹并沿着路径发射它的函数,但我们还没有办法测试它。所以,在我们的touchBegan方法中,我们添加了对安排选择器的调用:
override func touchBegan(touch: CCTouch!, withEvent event: CCTouchEvent!) {
...
//call it once, then schedule it
shootBullet()
self.schedule(Selector("shootBullet"), interval: 0.125)
}
此外,在我们的touchEnded方法中,我们添加了对unschedule选择器的调用(因为我们不希望炮塔持续射击):
override func touchEnded(touch: CCTouch!, withEvent event: CCTouchEvent!) {
self.unschedule(Selector("shootBullet"))
}
如果你想知道为什么选择器被引号包围,让我告诉你,那只是 Swift 的语法。你不需要明确地声明函数,你只需要将函数名作为字符串传递,Swift 会处理其余的部分。
如果你在这个时候运行游戏,你会看到当你的手指在屏幕上滑动时,那些子弹会从炮塔中火箭般射出。
生成敌人并将它们发送到中心
我们想要一些敌人来射击,所以让我们创建一个方法,它将在左墙或右墙上生成一个敌人,并在 3 秒内将其发送到屏幕中心(炮塔所在的位置):
func spawnEnemy() {
//create the enemy. Again, "Images/enemy.png" because of the Images folder
var enemy = CCSprite.spriteWithSpriteFrame(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("Images/enemy.png")) as CCSprite
//position the enemy randomly along the left or right wall
let yPos = arc4random() % UInt32(winSize.height)
let xPos = arc4random() % 2 == 0 ? -50 : winSize.width + 50
enemy.position = ccp(CGFloat(xPos),CGFloat(yPos))
//add the enemy to the screen
self.addChild(enemy)
//move to exactly 1 enemy-length away from the center (calculated with triangle ratios)
let distanceToCenter = ccpDistance(self.turret.position, enemy.position)
let xDistance = self.turret.position.x - xPos
let yDistance = self.turret.position.y - CGFloat(yPos)
let newDistanceToCenter = distanceToCenter - enemy.boundingBox().size.width
let newX = (newDistanceToCenter * xDistance) / distanceToCenter
let newY = (newDistanceToCenter * yDistance) / distanceToCenter
let centerPosDistance = ccp(newX,newY)
//create a move action that, after 3 seconds, will do something (nothing at the moment)
let moveAndEndGameAction = CCActionSequence.actionOne(CCActionMoveBy.actionWithDuration(3, position: centerPosDistance) as CCActionFiniteTime, two: CCActionCallBlock.actionWithBlock({
//do nothing at the moment
}) as CCActionFiniteTime) as CCAction
//run the move action
enemy.runAction(moveAndEndGameAction)
}
我们有一个生成敌人的方法真是太好了,但目前还没有生成。所以,就像我们安排的shootBullet函数一样,我们必须安排spawnEnemy函数。
在init()方法中,添加以下代码行来生成敌人:
override init() {
super.init()
...
self.schedule(Selector("spawnEnemy"), interval: 0.35)
}
好的!现在我们有了敌人的生成。但是即使它们到达了最终位置,也没有发生任何事情。让我们来修复这个问题。
过渡到 GameOver
当敌人接近中心(动作完成后),我们希望游戏结束,所以让我们创建一个GameOverScene类,并在敌人达到他们的最终位置时过渡到它。
首先,通过按Command + N(或文件 | 新建 | 文件)创建一个新文件。通过导航到iOS | 源选择Cocoa Touch Class。

然后将其命名为GameOverScene,并确保你选择Swift作为语言。

将其保存在项目的Source文件夹中,它将自动打开。
用以下内容替换你的GameOverScene类,这将设置背景为白色,在屏幕中心创建一个标签,启用触摸,并在屏幕被点击时返回到MainScene:
class GameOverScene: CCScene {
override init() {
super.init()
let winSize = CCDirector.sharedDirector().viewSize()
var background = CCNodeColor(color: CCColor.whiteColor())
self.addChild(background)
var label = CCLabelBMFont(string: "Tap anywhere to restart", fntFile: "bmFont.fnt")
label.position = ccp(winSize.width/2, winSize.height/2)
self.addChild(label)
self.userInteractionEnabled = true
}
override func touchBegan(touch: CCTouch!, withEvent event: CCTouchEvent!) {
CCDirector.sharedDirector().replaceScene(MainScene.node() as CCScene)
}
}
然后,为了测试这个场景,将以下replaceScene调用添加到MainScene.swift中敌人的移动动作的闭包(或代码块)中:
func spawnEnemy() {
...
//create a move action that, after 3 seconds, will do something (nothing at the moment)
let moveAndEndGameAction = CCActionSequence.actionOne(CCActionMoveBy.actionWithDuration(3, position: centerPosDistance) as CCActionFiniteTime, two: CCActionCallBlock.actionWithBlock({
//add this line – it will transition to the GameOverScene (in other words, the enemy was not killed)
CCDirector.sharedDirector().replaceScene(GameOverScene.node() as CCScene)
}) as CCActionFiniteTime) as CCAction
//run the move action
enemy.runAction(moveAndEndGameAction)
}
嗯!但是,现在虽然它正确地过渡了,但这不是一个很好的游戏,因为我们不能用我们射击的子弹阻止敌人。让我们来修复这个问题!
处理碰撞
处理碰撞的方法有很多种,但我们将通过使用子弹和敌人的边界框来处理它们。为此,我们将设置两个数组(一个用于敌人,一个用于子弹)和一个函数来遍历数组并检查边界框交叉。
首先,我们为数组添加两个变量。我们将使用NSMutableArray而不是 Swift 的数组,因为 Swift 的数组只允许我们通过索引移除项,而不是通过传递项本身。因此,使用NSMutableArray将更容易:
class MainScene: CCScene {
...
//arrays to hold our bullets and enemies
var bullets : NSMutableArray = []
var enemies : NSMutableArray = []
override init() {
...
现在我们需要将我们的子弹和敌人添加到它们各自的数组中(以及处理它们从数组中移除)。因此,在我们的shootBullet和spawnEnemy函数中,我们将对象添加到数组中,并在从场景中移除子弹之前将其从数组中移除:
func shootBullet() {
...
var moveAndRemoveAction = CCActionSequence.actionOne(CCActionMoveBy.actionWithDuration(0.5, position: moveDistance) as CCActionFiniteTime, two: CCActionCallBlock.actionWithBlock({
//remove the bullet from the array
//before removing it from the screen
self.bullets.removeObject(bullet)
self.removeChild(bullet)
}) as CCActionFiniteTime) as CCAction
//add the bullet
self.bullets.addObject(bullet)
self.addChild(bullet)
...
}
func spawnEnemy() {
...
//add the enemy to the enemies array before adding it to the screen
self.enemies.addObject(enemy)
//add the enemy to the screen
self.addChild(enemy)
...
}
接下来,我们需要创建一个函数,该函数将遍历我们的数组并检查碰撞。如果发现碰撞,我们将从数组(以及屏幕)中移除两个对象,并退出循环,以防止意外越界。
在MainScene.swift文件中创建以下函数:
func checkForCollisions() {
//check for collisions
for bullet in self.bullets {
for enemy in self.enemies {
//if the two bounding boxes are overlapping/intersecting/colliding
if CGRectIntersectsRect(bullet.boundingBox(),
enemy.boundingBox()) {
self.bullets.removeObject(bullet)
self.enemies.removeObject(enemy)
self.removeChild(bullet as CCSprite)
self.removeChild(enemy as CCSprite)
break;
}
}
}
}
最后,我们需要安排这个函数,以便在子弹与敌人交互时频繁调用,并且能够在正确的时间检测到碰撞。为此,只需在init()函数中安排它即可:
override init() {
...
self.schedule(Selector("checkForCollisions"), interval: 1.0/60.0)
}
在这个阶段运行游戏,你会看到子弹正确地发生碰撞。但是,没有分数计数器的话,这并不是一个真正的游戏。所以让我们把这个分数计数器添加到这个 Swift 示例游戏中。
计算分数
我们只需要一个跟踪分数的变量和一个显示标签。可以说,你也可以在游戏结束场景中这样做,但在这个示例中我们不会担心这一点。
因此,在MainScene类的顶部,我们创建两个变量,如描述所述,然后在init()方法中将标签添加到屏幕上:
class MainScene: CCScene {
...
//a variable to hold the score. The value of 0.0 sets it to a Float type by default, not Int
var score = 0.0
override init() {
...
}
}
然后我们实现update函数,该函数由 Cocos2d 自动调用:
override func update(delta: CCTime) {
//some obscure score increment over time...
self.score += 0.47
//set the label using String Interpolation
self.lblScore.setString("\(Int(self.score))")
}
我们还希望每消灭一个敌人就增加100分,所以在checkForCollisions函数中,我们只需将100加到分数变量上:
func checkForCollisions() {
...
if CGRectIntersectsRect(bullet.boundingBox(), enemy.boundingBox()) {
...
self.score += 100
break;
}
}
如果你现在运行游戏,你会看到随着时间的推移分数增加,以及当任何敌人被子弹击中时。唯一剩下要做的事情是随着时间的推移使游戏更难(因为目前来说相当无聊)。
提高难度
最后(尽管对于使示例工作并不完全重要),我们希望难度随时间增加,这样只有最好的玩家才能走得更远。我们将通过增加单位生成的速率(确切地说,每 2,000 分)来实现这一点。首先,我们想要一些变量来保存当前的生成速率以及自上次难度增加以来累积的点数:
class MainScene: CCScene {
...
//variables for enemy spawn rate (aka, difficulty)
var spawnRate = 0.35
var scoreSinceLastIncrease = 0.0
override init() {
然后,我们必须将硬编码的0.35值替换为spawnRate变量:
override init() {
...
self.schedule(Selector("spawnEnemy"), interval: self.spawnRate)
}
现在,每次我们增加score变量时,也必须增加scoreSinceLastIncrease变量。因此,在update和checkForCollisions中,我们需要给这两个变量都加上相同的数值:
func checkForCollisions() {
...
if CGRectIntersectsRect(bullet.boundingBox(), enemy.boundingBox()) {
...
self.score += 100
self.scoreSinceLastIncrease += 100
break;
}
}
override func update(delta: CCTime) {
//some random score increment over time...
self.score += 0.47
self.scoreSinceLastIncrease += 0.47
//set the label using String Interpolation
self.lblScore.setString("\(Int(self.score))")
}
最后,我们需要在我们的update方法内部检查,自上次难度增加以来的分数是否已经超过了我们的限制(在这种情况下,我们将在每获得 2,000 分后增加)。为此,我们只需取消spawnEnemy函数的调度,减少spawnRate(只减少到一定量),然后重新调度spawnEnemy函数:
override func update(delta: CCTime) {
...
if self.scoreSinceLastIncrease > 2000 {
//unschedule and re-schedule the spawnEnemy method
//using the new spawn rate
self.unschedule(Selector("spawnEnemy"))
spawnRate -= 0.025
if (spawnRate < 0.005) {
spawnRate = 0.005
}
self.schedule(Selector("spawnEnemy"), interval: self.spawnRate)
//subtract 2000, the amount of the difficulty
self.scoreSinceLastIncrease -= 2000
}
}
在这个阶段运行游戏,并观察越来越多的单位涌入视图,最终会压倒你。
就这样!我们刚刚用 Cocos2d 制作了一个非常简单的游戏,但这次有了 Swift 的帮助。这并不是世界上最好或最有意思的游戏,但它是有意义的。此外,这个例子更多的是展示如何使用 Swift 作为核心语言而不是 Objective-C 来制作游戏。
摘要
在本章中,你学习了如何使用 playgrounds,并使用 Cocos2d 和 Swift 创建一个基本游戏。
如前所述,如果你想要了解更多关于 Swift 的信息,有许多在线资源可供你使用——在线课程、苹果公司创建的内容,以及提供所需帮助的在线社区。
开心编码!




浙公网安备 33010602011771号