书名:Cocos2d 跨平台游戏开发指南(第2版)
ISBN:978-7-115-43713-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [印度] Siddharth Shekar
译 武传海
责任编辑 胡俊英
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright ©2016 Packt Publishing. First published in the English language under the title Cocos2d Cross-Platform Game Development Cookbook, Second Edition.
All rights reserved.
本书由英国Packt公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
Cocos2d是一个开源框架,可用于构建游戏和应用程序,它可以让用户在创建自己的多平台游戏时节省很多的时间。
本书介绍了使用Cocos2d进行跨平台游戏开发的相关知识。全书内容共分为11章,分别介绍了精灵与动画、场景与菜单、各类交互方法、物理引擎、声音、AI与A*寻路、数据存储与取回、游戏效果、辅助工具、Swift/SpriteBuilder,同时还介绍了如何将所开发的游戏移植到Android平台下。
本书经过了精心编排和设计,包含丰富且实用的开发示例,能够让读者轻松获取知识并掌握开发技巧,非常适合对Cocos2d有一些了解的读者阅读,也适合有相关游戏开发经验的人员阅读。
Siddharth Shekar是一位游戏开发者,拥有超过5年的游戏行业开发经验。他开发了多款游戏,并把它们发布到iOS、Android、Amazon、Windows Phone App Stores中。
在过去的11年间,Siddharth一直从事编程工作,熟悉C++、C#、Objective-C、Java、JavaScript、LUA、Swift。在使用Flash开发网页游戏和手机游戏,以及使用Cocos2d、Cocos2d-x、Unity 3D、Unreal Engine开发游戏方面拥有丰富的经验。Siddharth的工作也涉及一些图形库,例如DirectX、OpenGL、OpenGL ES。
除了开发游戏之外,他还在较大的工程院校开设游戏开发讲习班,在游戏开发机构担任客座讲师。
Siddharth也是《Learning Cocos2d-x Game Development》和《Learning iOS 8 Game Development Using Swift》两本书的作者,它们均由Packt出版发行。
目前,他在奥克兰、新西兰的Media Design School的游戏院系担任讲师,讲授图形编程和PlayStation 4/PS Vita本地游戏开发,并指导准毕业生们。
在闲暇时,Siddharth喜欢尝试使用最新的游戏开发框架和工具。他不仅是个狂热的游戏玩家,还对动画、计算机图形学、音乐感兴趣,也是个纯粹的影迷。
关于Media Design School与Siddharth Shekar的更多信息,你可以从www.mediadesign school.com了解到。
首先,我要一如既往地感谢我的母亲(Shanti Shekar)和父亲(R.Shekar),他们总是毫无条件地给予我爱与支持。
其次,我要感谢Media Design School的Fiona Scott-Milligan(首席学术负责人)与Syed Fawad Mustafa Zaidi(项目协调员),他们不断鼓励我写作本书。还要感谢Bachelor of Software Engineering Department的同事们,他们是Asma、Salwan、Bindu、Imran、Shilpa和Michael,在本书的写作过程中,他们一直支持着我。
对Andreas(CodeAndWeb)和Tom(71Squared)的感激之情更是溢于言表,感谢他们开发这么棒的工具,让我可以把大量时间与精力放在编写游戏上,而不是开发工具上。
我也要感谢Packt出版社,他们最终把书稿整理成书。感谢Adrian Raposo、Akshay Shetty和Samantha Gonsalves,他们帮助并指导我写作本书。感谢技术审稿人提供的技术反馈与修改意见,我真的从中学到了很多新东西。
最后,我还要感谢我的朋友们、粉丝和支持者,他们已成为我生活的一部分,这些年他们一直对我宽容有加,催我不断奋进前行。
Hsiao Wei Chen使用Unity、Cocos2d和本地代码为移动设备开发游戏。她也参加了Game jams活动,并尝试在48~72小时内开发一款游戏。Hsiao喜欢写作,她加入了NaNoWriMo,尝试在一个月内写50000字小说。她也是一个博主,经常写辅导教程与书评。
我要感谢两个姐妹与父母,他们总是支持我做任何自己想干的事情。
Tim Park最初学习编程是在VIC-20机器上使用Basic语言完成,后来他在Amiga机器上使用C语言编写程序。然后又学习了其他语言与框架,这些积累让他如鱼得水,能够完成自己想做的事情。于是,他开始专门编写会计系统,后来又开发游戏机,目前在Arctic Empire从事移动开发和Web开发工作。
自从2007年创始以来,Apple App Store一直保持着持续增长的势头,每天平均约有500个App提交。其中,大约80%的App是游戏。形成这种局面的部分原因是Apple构建了一个非常棒的生态系统,免费提供操作系统和IDE开发环境,便于普通开发者接触并使用它们。另一部分原因在于Cocos2d框架,它是目前应用最广泛的免费iOS游戏开发框架之一,借助它,开发者能够更方便地开发游戏和App应用。
SpriteBuilder把Cocos2d集成到其中,这让使用Cocos2d开发游戏变得更有吸引力。因为现在人们可以使用SpriteBuilder更容易地开发简单的游戏原型,而不用编写任何代码。而且,开发者只需付出很小的努力就能让同一款App或游戏运行在具有不同分辨率的设备上运行。
此外,我们也可以轻松地把这些游戏移植到Android上,而无需重写游戏代码。跨平台iOS游戏开发多年来一直是开发者的梦想,如今这一梦想变成了现实。
自从第一台iOS设备诞生以来,Cocos2d就一直存在着,相关社区也非常活跃,各种资源丰富且十分有用。此外,还有许多由第三方开发者开发的各种好用的工具,让Cocos2d成为更棒的开发框架。
本书带你学习开发一款游戏所需的各种知识。每个章节都按照逻辑顺序进行组织安排,这样每当你学完一部分内容,就离游戏开发更近了一步。在本书的最后部分,还讲解了如何把开发好的游戏移植到Android设备上。
希望不久的将来能在App Store中看到自己的游戏大作!
第1章 精灵与动画,教你如何在场景中绘制精灵,添加颜色精灵,以及使用primitives渲染2D形状。本章也将展示如何让精灵动起来,如何移动精灵,以及如何向精灵添加各种动作。另外还要学习如何实现视差效果,让场景更具动感。
第2章 场景与菜单,介绍如何在游戏中添加场景(例如gameplay场景),讲解如何在场景中创建按钮与标签,以及如何使用各种过渡效果在两个场景之间进行切换。
第3章 手势、触屏与加速度传感器,讨论各种用户交互方法,例如单击、按住、轻松等手势,还讲解把加速度传感器输入与手势添加到场景的方法。
第4章 物理引擎(Physics),通过示例演示如何把物理引擎添加到场景并使对象对其做出相应反应。本章也会讲解body的不同类型、body属性,添加精灵到body,向body应用作用力与动量,进行碰撞检测,以及通过把物理刚体联合在一起创建对象,例如篮子。
第5章 声音,演示如何编辑音轨以创建音乐与音效,添加背景音乐与音效到游戏,向游戏添加暂停与继续按钮,在选项菜单中添加音量滑块,以控制音量大小。
第6章 游戏AI与A*寻路,介绍向游戏中的敌人添加人工智能的方法。本章也讲解创建巡逻、追击、射弹敌人的方法。此外,也讲解了A*寻路方法,使用网格为敌人创建更高级的AI逻辑。
第7章 数据存储与取回,教你如何使用NSUserDefault在设备上保存和加载游戏,并且讲解使用JSON、Plist、XML文件为客户存储与取回数据创建与访问文件。
第8章 效果,教你添加动态照明到游戏,使用触摸控制管理位置以及光源颜色,向游戏添加光源产生的2D投影。在本章中,你也能学到使用Cocos2d的CCEffects类添加效果到游戏,以及在游戏中添加有趣效果的方法。
第9章 游戏开发辅助工具,讲解使用工业级标准工具辅助游戏开发的方法,这些工具包括TexturePacker、Glyph Designer、PhysicsEditor、Particle Designer、Sprite Illuminator,借助这些工具可以开发出更具视觉吸引力和性能更优的游戏。
第10章 Swift/SpriteBuilder基础,展示如何使用苹果公司发布的编程语言Swift来开发游戏。在本章中,我们会看到Objective-C与Swift语言之间的不同,学习如何把Objective-C类导入到Swift中以减少代码重复编写。本章也讲解使用SpriteBuilder开发更健壮游戏的有关内容。
第11章 移植到Android,本章讲解如何把为iOS开发的游戏移植到Android设备上。我们将学习如何安装Android Xcode插件,为部署准备硬件设备,最后在实际的Android设备上运行项目。
在使用Cocos2d开发游戏时,所有需要做的是准备好最新版本的OS X EI Capitan、SpriteBuilder和Xcode。所有这些应用程序都可以在苹果App Store中看到,并且免费供用户下载使用。本书内容是基于iPad的,所以你最好准备一台iPad 3供测试代码使用。此外,开发运行在Android平台上的应用,需要你有一台Android设备,并且最好使用Galaxy或Nexus系列手机,它们十分可靠且经过严格测试。
本书的章节结构经过精心安排与设计,读者可以轻松从书中获取特定部分以及所必需的信息。每个部分的内容都相对独立,并且容易跟学。本书针对中到高级用户,即那些对Cocos2d工作原理有基本了解的用户。尽管本书也讲解一些基本概念,但并不做深入讲解,因此阅读本书需要读者有一定的相关经验。
在本书中,你将经常看到几个标题,包括“准备工作”“操作步骤”“工作原理”“更多内容”和“另见”。
关于如何把握一节内容,为了给出明确的指示,我们使用如下几个版块组织相关内容。
本部分指出学习本节内容需要做的准备,也包含安装一些必需的软件或做一些预先设置的内容。
本部分包含要做的具体步骤。
本部分通常讲解与前一部分内容相关的更多细节,并呈现最终运行结果。
本部分讲解与前面内容相关的更多知识,通过阅读本部分内容,读者将掌握更多相关知识。
本部分提供了一些有用的页面链接,读者可以从中获取更多与主题相关的有用内容。
在本书中,在不同类型的信息之间,读者将看到大量不同文本类型的区分。下面给出了一些类型示例,以及对它们所代表含义的说明。
正文中出现的代码用语、数据表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入、推特标签显示如下:“然后,我们创建_sprite1
与_sprite2
变量。在spriteWithImageNames
中,我们传入文件名变量,它持有文件名字符串。”
代码块设置如下:
#import "CCSprite.h"
@interface ParallaxSprite :CCSprite{
CGSize _winSize;
CGPoint _center;
CCSprite *_sprite1, *_sprite2;
float _speed;
}
-(id)initWithFilename:(NSString *)filename Speed:(float)speed;
-(void)update:(CCTime)delta;
@end
当我们希望你特别关注代码块的某个部分时,就使用粗体把相关代码行表示出来,如下所示。
#import "Hero.h"
#import "ParallaxSprite.h"
@interface MainScene :CCNode{
CGSizewinSize;
Hero* hero;
ParallaxSprite* pSprite;
}
命令行输入或输出写成如下形式:
./install.sh –i
正文中的新术语与关键词以粗体形式标识出来。你在屏幕上看到的单词,例如在菜单或对话框中,出现在正文中的形式如下:“接下来,我们给它一个类名。在Subclass of中选择CCSprite,在Language中选择Objective-C”。
警告或重要注释出现在这里
提示与技巧出现在这里
我们总是热切期望各位读者的反馈,让我们知道你对本书的看法,你喜欢哪部分以及不喜欢哪部分。你的反馈对我们至关重要,它能够帮助我们开发出对你真正有用的选题。
要给我们发送反馈意见,你可以发送电子邮件到feedback@packpub.com
,并在信息标题中请注明书名。
如果你对某个主题特别熟悉,并且对写作出书感兴趣,请到www.packtpub.com/authors
阅读我们的作者投稿指南。
如果你有幸拥有了一本Packt图书,我们将会做大量工作以帮助你从购买的图书中获得最大的受益。
访问http://www.packtpub.com
站点,使用自己的账户登录之后,你就可以下载本书的示例代码。如果你在别处购买了本书,可以访问http://www.packtpub.com/support
页面,并进行注册,稍后我们会通过电子邮件把示例代码发送给你。
你可以遵循以下步骤下载示例代码。
1.使用电子邮件地址与密码登录我们的网站,或者注册为新用户。
2.把鼠标指针置于顶部的SUPPORT选项卡之上。
3.单击Code Downloads & Errata。
4.在Search框中输入书名。
5.选择想下载示例代码的图书。
6.从下拉菜单中选择购买图书的位置。
7.单击Code Download,完成示例代码的下载。
示例代码下载完成后,请使用下列软件的最新版本解压缩文件。
我们也以PDF文件的形式向你提供书中用到的截屏、图表。这些彩色图像将帮助你更好地理解输出中的变化。你可以从https://www.packtpub.com/sites/default/files/downloads/Cocos2d
CrossPlatformGameDevelopmentCookbookSecond Edition_ColorImages.pdf
下载PDF文件。
尽管我们已经做出了最大努力来最大限度地保证本书内容的准确性,但是难免出现疏漏,导致错误偶尔发生。如果你在我们的图书中发现了错误,可能会是文字上的,也可能是代码上的,请把错误提交给我们,对此我们充满感激。如果你这样做,就能帮助其他读者避免这些错误,也能帮助我们在下一版图书中进行改正。如果你发现任何错误,请访问http://packtpub.com/submit-errata
,选择相应图书并单击Errata Submission Form链接,并输入错误细节,向我们提交。一旦你发现的错误得到确认,提交将被接受,错误将被上传到我们的网站或者添加到Errata版块之下的勘误列表中。
如果你想浏览已经提交到的错误,请转到https://www.packtpub.com/books/content/support
,在搜索中输入书名,相关信息将出现在Errata版块之下。
在网络上享有版权保护的作品不断遭受盗版行为侵害的事件不断发生,这对所有媒体都是非常严重的问题。我们要尽一切努力严格保护我们作品的版权与许可政策。如果你在网络上发现我们作品的任何形式的非法拷贝,请立刻向提供给我们准确的地址或者域名,我们将尽一切努力补救,并维护自己的合法权利。
请联系我们,并把可疑盗版材料的链接发送到copyright@packtpub.com
。
对于读者提供的相关信息,我们深表谢意,感谢读者为保护我们作者的合法权利做出的努力,我们将尽己所能为读者出版更多更有价值的作品。
对于本书的内容,如果有什么疑问,你可以联系我们,把相关问题发送至question@packtpub.com
,我们将尽最大努力帮你解决。
本章涵盖主题如下:
在本章中,我们将介绍有关Cocos2d框架的一些基本知识,以便帮助各位了解相关概念。首先,了解下载并安装SpriteBuilder/Cocos2d的过程,然后讲解Cocos2d中使用的2D坐标系统。
在学完基础内容之后,接着介绍精灵的基本属性以及如何把它们添加到场景之中。我们将了解一下如何把一幅图像添加到精灵对象,并讨论如何创建一个占位精灵,以便在游戏原型阶段测试基本的游戏机制与冲突。然后,再学习如何使用gIPrimitives创建基本形状。在此之后,我们将讨论如何使用动作对精灵进行移动、旋转、缩放操作,以及如何把动作绑定到精灵对象上。接下来,我们将学习如何使用精灵帧来让一个角色动起来。最后,我们向场景添加视差滚动效果,赋予它更多动感。
在创建并运行Cocos2d项目之前,必须先安装SpriteBuilder与Xcode。在本部分中,我们将简单地介绍一下如何安装它们。
首先到http://cocos2d.spritebuilder.com
下载Coscos2d,并进行安装。
目前Spritebuilder已成为Cocos2d的官方安装程序。单击Cocos2d-SpriteBuilder installer链接,随后打开Mac App Store Preview页面,而后完成安装。
如果想下载较早版本的Coscos2d,你也可以在如图1-1所示页面的“Archived Releases”区域中查找并下载它们。
图1-1
如图1-2所示,当出现提示时,单击View in Mac App Store链接,然后单击Launch Application。
图1-2
如图1-3所示,打开App Store之后,单击Install,启动安装过程。请注意,下载文件时需要先使用账户进行登录。
图1-3
安装完成后,SpritesBuilder将出现在应用程序文件夹中。打开Launchpad,单击SpritesBuilder程序即可打开它,如图1-4所示。
图1-4
SpritesBuilder启动之后,将邀请你加入SpritesBuilder邮件列表。如图1-5所示,添加E-mail地址,单击Continue按钮,或者单击Sign Up Later按钮。
图1-5
在本书后面的部分,我们将学习如何使用SpritesBuilder创建一个小项目。目前,我们已经做好创建一个新项目的准备。在菜单中,依次单击File-New Project,选择一个位置用于创建项目文件夹。
如图1-6所示,在打开的Save as窗口中,可以选择主要的编码语言。由于我们要使用Objective-C语言,所以请在窗口底部的Primary Language中确保已选择了Objective-C语言。
图1-6
在为项目选定保存位置之后,在顶部的Save As文本框中,输入项目名称。请记住项目名称与保存位置,后面在Xcode中打开项目时需要使用这些信息。
接下来,我们需要在Xcode中打开项目。首先,我们要了解如何使用Xcode编写代码,这是编写一款复杂游戏所必需的。在本书后面的章节中,我们将继续讨论如何使用SpriteBuilder简化游戏开发过程。
现在,可以关闭SpriteBuilder项目了,我们已经不再需要它。
如果你尚未安装Xcode,请先从Mac App Store下载它,如图1-7所示。
图1-7
安装Xcode与安装其他应用非常类似。
执行如下步骤。
1.安装好Xcode之后,进入存储项目的文件夹中,双击projectname.xcodeproj
文件。这里,由于我把项目命名为Sprite
,因此要双击Sprite.xcodeproj
文件,如图1-8所示。
图1-8
2.打开项目之后,你将看到如图1-9所示的界面。
图1-9
单击左上角的play按钮,运行项目,如图1-10所示。
图1-10
这将在模拟器中运行并显示默认项目。祝贺你!你已经成功运行Coscos2d。
在2D游戏开发中,我们只需考虑两种坐标系统,一种是屏幕坐标系统,另一种是对象坐标系统。
在2D中,无论何时,当我们把一个对象放置到屏幕上时,总是要考虑对象离屏幕的左下角有多远。这是因为坐标原点位于屏幕的左下角,而非屏幕的中心。正因如此,如果把一个精灵放置到屏幕上,并且未修过它的位置时,它将在屏幕的左下角被创建出来。请记住,屏幕坐标原点[(0,0)]位于屏幕的左下角。如图1-11所示,如果你想把精灵放置到屏幕的中心,需要把精灵位置设置为位置属性中宽与高的一半。由于所有对象的位置都基于屏幕的左下角确定,因此我们把这种坐标系统称为屏幕坐标系统。
图1-11
对象坐标系统只涉及精灵自身,精灵中心与对象中心重合,这与坐标原点位于屏幕左下角的屏幕坐标系不同。精灵的中心称为锚点。当你旋转一个精灵时,精灵将围绕其中心进行旋转,这是因为坐标原点位于精灵中心。通过修改精灵的锚点属性,你可以更改精灵的原点。
当启动应用时,将默认加载在SpriteBuilder中创建好的场景。下面,我们将打开MainScene
文件并且稍微做一些修改,而后加载它以取代默认场景。
到目前为止,MainScene.h
和MainScene.m
两个文件中没有任何代码。接下来,打开它们,并添加如下代码。
首先,打开MainScene.h
文件,添加如下代码。
@interface MainScene :CCNode{
CGSizewinSize;
}
+(CCScene*)scene;
@end
接着,在MainScene.m
文件中,添加如下代码。
#import "MainScene.h
@implementation MainScene
+(CCScene*)scene{
return[[self alloc]init];
}
-(id)init{
if(self = [super init]){
winSize = [[CCDirectorsharedDirector]viewSize];
}
return self;
}
@end
然后,转到AppDelegate.m
文件,它位于Source/Platforms
下的iOS文件夹中,如图1-12所示。
图1-12
在startScene
函数中修改代码(粗体部分)如下:
- (CCScene*) startScene
{
//Comment or delete line below as shown
//return [CCBReaderloadAsScene:@"MainScene"];
//add below line instead
return [MainScene scene];
}
至此,我们已经创建好一个完整的空白项目,供练习使用。
此时,如果你编译并运行空白项目,将只能看到一个黑色屏幕。为了确保我们的确为绘制做好了准备,并且保证绘制的内容能够显示在屏幕上,让我们添加一些用于更改背景颜色的基本代码。
在MainScene.m
文件的init
函数中,添加如下代码。
-(id)init{
if(self = [super init]){
winSize = [[CCDirectorsharedDirector]viewSize];
CGPoint center = CGPointMake(winSize.width/2,
winSize.height/2);
//Background
CCNode* backgroundColorNode = [CCNodeColor
nodeWithColor:[CCColor
colorWithRed:0.0f
green:1.0
blue:0.0]];
[selfaddChild:backgroundColorNode];
}
return self;
}
在init
函数中,首先初始化super init
文件,而后获取当前设备的屏幕尺寸,再创建一个CGPoint
类型的辅助变量,用来计算屏幕中心。
接着,我们创建一个新的CCNode
,命名为backgroundColorNode
,并调用CCNodeColor
类和nodeWithColor
函数。在其内,我们将传递红色、绿色、蓝色值。由于我想要绿色背景,所以我把绿色值设置为1
,其他均设置为0
。
最后,我们把backgroundColorNode
添加到场景中。
运行项目,观察刚才所做的更改,如图1-13所示。
图1-13
此时,将只能看到一个绿色屏幕,它就是刚刚被添加到场景中的节点。如果你只想一个普通背景,而不想往项目中导入一幅图像,这是一种非常有用且快捷的方式,它让你能够轻松得到带有任何颜色的背景。
为了在屏幕上显示图像,并对图像进行处理,你需要使用CCSprite类把图像添加到场景中。与普通图像不同,精灵拥有多种属性,例如移动、缩放、旋转等,它们可以用来对图像进行处理。
为了把精灵添加到场景中,我们需要先把背景图像导入到项目中。
前面我们已经在init
函数中添加了有关backgroundColorNode
的代码,紧接其下,添加如下代码。
//Basic CCSprite - Background Image
CCSprite* backgroundImage = [CCSpritespriteWithImageNamed:@"Bg.png"];
backgroundImage.position = CGPointMake(winSize.width/2,
winSize.height/2);
[selfaddChild:backgroundImage];
这里,我们将获取Bg
图像,并且将其作为子成员添加到当前场景中。从本章的Resources
文件夹,把如图1-14所示的Bg-ipad.png
与Bg-ipadhd.png
文件拖动到项目的Resources/Published-iOS
文件夹中。
图1-14
我们还必须对CCBReader.m
文件稍微做一下改动。在Search中,输入CCFileUtilsSearchMode
文本,进行搜索。然后用CCFileUtilsSearchModeSuffix
取代CCFileUtilsSearchModeDirectory
,如图1-15所示。
图1-15
这将更改searchmode
文件,使其从目录变为后缀模式。
此时,如果已经编译并运行项目,你将会看到如图1-16所示的一幅图像。通过这种方式,我们可以在场景中显示出精灵。
图1-16
RenderTexture
用来创建占位精灵,它们可以用来构建游戏原型。如果你想测试精灵的移动、跳跃代码,但又不想访问精灵,使用RenderTexture
是创建精灵既快速又直接的方式。
为了创建RenderTexture
精灵,我们将编写一个新函数,当我们给出要创建精灵的尺寸与颜色时,它将创建并返回创建好的精灵。
在MainScene.h
文件中,之前我们创建了scene
函数,在该函数之下,添加如下粗体代码。
+(CCScene*)scene;
-(CCSprite *)spriteWithColor:(ccColor4F)bgColor
textureWidth:(float)textureWidth
textureHeight:(float)textureHeight;
@end
这个函数将根据所给出的颜色、宽度、高度创建并返回CCSprite
精灵。
在MainScene.m
文件中,在init
函数之下,添加前面函数定义如下:
-(CCSprite *)spriteWithColor:(ccColor4F)bgColor
textureWidth:(float)textureWidth
textureHeight:(float)textureHeight {
CCRenderTexture *rt =
[CCRenderTexture
renderTextureWithWidth:textureWidth
height:textureHeight];
[rtbeginWithClear:bgColor.r
g:bgColor.g
b:bgColor.b
a:bgColor.a];
[rt end];
return [CCSpritespriteWithTexture:rt.sprite.texture];
}
在该函数中,我们创建了一个新变量rt
,它是CCRenderTexture
类型,并且把传入函数的宽度与高度传递给它。
然后,使用传入的颜色清空RenderTexture
,再调用rt
的end
函数。
最后,我们将通过传入rt
精灵纹理创建CCSprite
,并作为函数返回值进行返回。
为了使用RenderTexture
函数,需要先在添加背景到场景的代码之下添加如下代码。
//rtSprite
CCSprite* rtSprite = [self spriteWithColor:ccc4f(1.0, 1.0, 0.0, 1.0)
textureWidth:150textureHeight:150];
rtSprite.position = CGPointMake(winSize.width/2,
winSize.height/2);
[selfaddChild:rtSprite];
在上述代码中,我们创建了一个新变量rtSprite
,它是CCSprite
类型,并把调用我们的函数所创建出的精灵赋给它。
调用函数时,将通过传入的r、g、b、a
值,创建ccc4f
类型的颜色。为了获得黄色,把红色、绿色全部设置为1
。此外,我们还分别把宽度、高度值设置150
。
然后,把rtSpirte精灵的位置设置到场景中心,最后将其添加到场景中。运行场景,你将看到如图1-17所示的结果。
图1-17
通过修改rgba颜色值,可以更改精灵颜色。
比如,这里我把rgba颜色值修改为(1.0,0.0,1.0,1.0)
(品红色—注:原书所述的黄色是错误的,应为品红色),你将看到如图1-18所示的结果。
//rtSprite
CCSprite* rtSprite = [self spriteWithColor:ccc4f(1.0, 0.0, 1.0, 1.0)
textureWidth:150textureHeight:150];
rtSprite.position = CGPointMake(winSize.width/2, winSize.height/2);
[selfaddChild:rtSprite];
图1-18
前面我们只是考虑如何把一个精灵添加到场景中,然而,以后可能想要一个单独的精灵类,以便你能为它添加自己的行为。在这一部分,我们将讨论如何通过扩展基本的CCSprite
类来创建自定义的精灵类。
让我们一起看一下如何创建一个自定义精灵类,它拥有自己的运动,并且更新函数。
为此,我们要先创建新文件。
1.在菜单栏中,依次选择File-New-File,在iOS的Source下,选择Cocoa Touch Class,单击Next按钮,如图1-19所示。
图1-19
2.接着,我们输入类名,在Subclass of中选择CCSprite,选择Language为Objective-C,单击Next按钮,如图1-20所示。
图1-20
3.然后,如图1-21所示,单击Create按钮,将文件添加到项目中。
图1-21
新建好文件之后,让我们对它们做一些改动,使其可以使用字符串作为文件名,且从文件创建精灵。
在Hero.h
文件中,做如下修改:
#import "CCSprite.h"
@interface Hero :CCSprite{
CGSizewinSize;
}
-(id)initWithFilename:(NSString *) filename;
@end
接着,修改Hero.m
文件,如下:
#import "Hero.h"
@implementation Hero
-(id)initWithFilename:(NSString *)filename
{
if (self = [super initWithImageNamed:filename]) {
}
return self;
}
@end
为了创建自定义精灵类的实例,打开MainScene.h文件,导入Hero.h文件,创建Hero类的一个新实例,命名为hero,代码如下:
#import "Hero.h"
@interface MainScene :CCNode{
CGSizewinSize;
Hero* hero;
}
在MainScene.m
文件中,在rtSprite
代码之下,添加如下代码:
hero = [[Hero alloc]initWithFilename:@"hero.png"];
hero.position = CGPointMake(center.x - winSize.width/4,
winSize.height/2);
[selfaddChild:hero];
在上述代码中,我们使用hero.png
文件初始化hero
。在Resources
文件夹中,我们必须把hero-ipad.png
和hero-ipadhd.png
文件导入到项目中,导入方式与前面添加Bg
图像文件时一样。
接着,如图1-22所示,我们把hero
放置到屏幕宽度的左四分之一处,位于屏幕中心的左侧,并且在屏幕高度的二分之一处。最后,把hero
对象添加到场景中。
图1-22
接下来,让我们一起看一下如何使hero
动起来。
在这一部分,我们将讨论如何让精灵动起来。我们将修改自定义精灵类,让角色动起来。通过提供带有许多图像的Cocos2D,并使之循环通过这些图像,即可产生动画效果。
为了让精灵动起来,我们将添加 4 帧动画,并将其应用到 hero 精灵类,通过使用repeatForever
动作让图像循环动起来。在下一部分,我们将详细讲解有关动作的内容。
在本章的Resources
文件夹中,含有hero
帧的普通、ipad
、ipadhd
版本图像。我们把所有这些图像文件导入到项目中,如图1-23所示。
图1-23
在Hero.m
文件中,修改initWithFilename
文件,如下:
-(id)initWithFilename:(NSString *)filename
{
if (self = [super initWithImageNamed:filename]) {
NSMutableArray *animFramesArray = [NSMutableArray array];
for (inti=1; i<=4; i++){
[animFramesArrayaddObject:
[CCSpriteFrameframeWithImageNamed:
[NSStringstringWithFormat:@"hero%d.png",i ]]];
}
CCAnimation* animation =
[CCAnimation
animationWithSpriteFrames:animFramesArraydelay:0.3];
CCActionInterval *animate=
[CCActionAnimateactionWithAnimation:animation];
CCAction* repeateAnimation =
[CCActionRepeatForeveractionWithAction:animate];
[selfrunAction:repeateAnimation];
}
return self;
}
@end
在上述代码中,我们先是创建了一个NSMutableArray
类型的新变量,命名为animFramesArray
。然后,创建一个for
循环,循环变量i
从1
到4
,这是因为总共有4幅图像。通过传入想遍历的4幅图像的名称,我们把动画帧保存到数组之中。
接着,我们创建一个CCAnimation
类型的变量,命名为animation
,在4个动画帧中进行传递,并且添加延时,设置动画播放时间。
然后,我们创建一个CCActionInterval
类型的变量animate
,用来通过动画创建一个循环。接着,创建CCAction
,调用repeat forever
动作,用来不断循环遍历动画。
最后,在当前类上运行动作。
现在,你应该能够看到人物角色动起来了,如图1-24所示。动画的播放速度由CCAnimation
类的延时进行控制。
图1-24
我们将看到自定义精灵让人物角色动起来了,并且没有对MainScene
中的实例做任何修改。
在前面的动画制作中,我们已经学习了一些有关Actions
的内容。除此之外,Cocos2d中还有更多动作供你使用。并且,你也可以把多种动作组成一个动作序列,集中应用到目标对象上。
首先,让我们一起看一个简单的动作,它用来把hero沿着x轴移动屏幕宽度的一半,并沿y轴方向从中心向下移动屏幕高度的四分之一。
在把hero添加到MainScene之后,在MainScene.m
文件中添加如下代码:
CGPointfinalPos = CGPointMake(center.x + winSize.width/4, center.y -
winSize.height/4);
CCActionFiniteTime* actionMove = [CCActionMoveToactionWithDuration
:1.0position:finalPos];
[herorunAction:actionMove];
为了方便起见,我创建了一个CGPoint
,命名为finalPos
,用来存储最终位置。然后,创建一个CCActionFiniteTime
类型的变量actionMove
,调用CCMoveTo
函数,指定动作的持续时间为1.0
秒,并且给出想把hero
移动到的目的位置。最后,调用hero
的runAction
函数,传入创建好的动作。
当你运行项目时,hero起初位于黄色渲染精灵的左侧,而后慢慢开始向右下角移动,(注:原文中if the render sprite is over a period of 1second一句,建议删除,因为并未对render精灵施加动作,它是一直存在的)经过1秒之后,hero到达目标位置,移动动作停止,hero将再次静止不动,如图1-25所示。
图1-25
接下来,让我们创建更多动作,然后把这些动作放入一个动作序列中,依次执行这些动作。为此,我们将添加如下代码,替换掉之前的动作代码:
//Actions
CGPointinitPos = hero.position;
CGPointfinalPos = CGPointMake(center.x + winSize.width/4, center.y -
winSize.height/4);
CCActionFiniteTime* actionMove = [CCActionMoveToactionWithDuration:
1.0position:finalPos];
CCAction *rotateBy = [CCActionRotateByactionWithDuration:2.0 angle:
180];
CCAction *tintTo= [CCActionTintToactionWithDuration:1.0
color:[CCColorcolorWithRed:0.0fgreen:1.0blue:0.0]];
CCAction *delay = [CCActionDelayactionWithDuration:1.0];
CCAction *moveToInit = [CCActionMoveToactionWithDuration:
1.0position:initPos];
CCAction *rotateBack = [CCActionRotateByactionWithDuration:2.0 angle:
180];
CCAction *tintBlue= [CCActionTintToactionWithDuration:1.0
color:[CCColorcolorWithRed:0.0fgreen:0.0blue:1.0]];
CCAction *sequence = [CCActionSequenceactions:actionMove,
rotateBy,tintTo, moveToInit, delay, rotateBack, tintBlue, nil];
[herorunAction:sequence];
在上面代码中,在把最终位置保存到finalPos变量之后,我又把hero的初始位置保存到名称为initPos
的CGPoint
类型变量中,后面我们会用到它。
第一个动作是moveTo
动作,用来把角色移动到指定的位置。
接着,我们将使用rotateBy
动作,对角色进行旋转,并指定持续时间与旋转角度。
随后,我们会使用tintTo
动作,它用来改变角色对象的颜色,并再次给出持续时间与想改变的颜色。本示例中,我们把角色的颜色更改为绿色。
然后,我们调用延时动作,用来在执行下一个动作之前暂停一段时间。在示例中,我们把延时时间设置为1
秒。
接下来,我们要把角色对象移动到最初位置,改变对象颜色为蓝色,再次把对象旋转180度。
然后,创建CCSequence
动作,把所有动作放入其中,以便依次播放这些动作。动作添加完之后,再添加一个nil
,表示动作列表结束。
最后,我们调用hero的runAction
函数,执行动作序列。
现在,人物角色将从起始位置开始执行一系列动作,当他返回起始位置时,将变为蓝色。
代码产生的效果如图1-26所示。
图1-26
Cocos2d使用openGLES,它是一个图形库,用来把对象显示在屏幕上。其实,到目前为止我们所有的绘图工作都依赖于这个图形库。Cocos2d也允许你访问gIPrimitives,使用它可以创建基本形状,如圆形、正方形、矩形等。
现在,让我们一起看几个示例。我们将从创建一个简单的圆形开始。
在添加好hero
节点之后,添加如下代码:
//drawDotNode
CCDrawNode* dotNode = [CCDrawNode node];
CCColor* red = [CCColorcolorWithRed:1.0fgreen:0.0fblue:0.0f];
[dotNodedrawDot:CGPointMake(winSize.width/2, winSize.height/2) radius:
10.0fcolor:red];
[selfaddChild:dotNode];
gIPrimitives使用CCDrawNode
类创建出来。示例中,我们先新建一个CCDrawNode
实例,命名为dotNode,然后创建一个CCColor
类型的变量red
,指定RGBA值为red
。
接着,调用CCDrawNode
的drawDot
函数,传入圆形的创建位置,并传入圆形半径与颜色。最后,我们把dotNode
添加到场景中。
当你运行项目时,将在屏幕中心看到一个红色圆点。
在示例中,我们指定了圆心位置与圆形半径,Cocos2d会据此绘制圆形,并且根据我们指定的颜色填充圆形。
绘制圆形只是示例之一,我们也可以使用同样的方法绘制出其他形状,如图1-27所示。
图1-27
接下来,我们将看一下如何使用CCDrawNode
类的drawWithPolyVerts
函数绘制任意多边形。添加如下代码,替换或者注释掉DrawDot
节点。
// DrawSquareNode
CCDrawNode *squareNode = [CCDrawNode node];
CGPointsquareVerts[4] =
{
CGPointMake(center.x - 50, center.y - 50),
CGPointMake(center.x - 50, center.y + 50),
CGPointMake(center.x + 50, center.y + 50),
CGPointMake(center.x + 50, center.y - 50)
};
CCColor* green = [CCColorcolorWithRed:0.0fgreen:1.0fblue:0.0f];
[squareNodedrawPolyWithVerts:squareVerts
count:4
fillColor:red
borderWidth:1
borderColor:green];
[selfaddChild:squareNode];
在上述代码中,我们先创建一个CCDrawNode
类型的新节点。然后,创建一个CGPoint
数组,通过squareVerts
名称进行引用,数组中存储着正方形的4个顶点坐标。接下来,创建一个CCColor
类型的变量green
,使用RGBA值将其指定为绿色。
然后,调用drawPolyLineWithVerts
,传入顶点数组,给出要绘制的顶点数,指定填充颜色为红色、边框线宽为1
、边框颜色为green
,green
是我们之前刚刚创建出的CCColor
类型变量。
最后,我们把squareNode
添加到场景之中。
运行项目,我们将看到如图1-28所示的运行结果。
图1-28
我们也可以使用同样的代码创建三角形。如果我们让函数绘制3个顶点,而非4个顶点,一个三角形就被绘制出来,而不是之前的正方形。
为了绘制三角形,修改代码如下,即在代码中,我们把顶点数由原来的4
个改为3
个。请注意,并不需要修改顶点数组。
CCColor* green = [CCColorcolorWithRed:0.0fgreen:1.0fblue:0.0f];
[squareNodedrawPolyWithVerts:squareVerts
count: 3
fillColor:red
borderWidth:1
borderColor:green];
[selfaddChild:squareNode];
再次运行项目,我们将看到如图1-29所示的变化。
图1-29
事实上,借助CCDrawNode
类,我们也可以在两点之间绘制线段。
为此,我们需要在绘制多边形的代码的下方,添加如下代码:
//segment node
CCColor* blue = [CCColorcolorWithRed:0.0fgreen:0.0fblue:1.0f];
CCDrawNode* segmentNode = [CCDrawNode node];
[segmentNodedrawSegmentFrom:center
to:CGPointMake(center.x + 40, center.y + 40)
radius: 2.0
color: blue];
[selfaddChild:segmentNode];
在上面代码中,我们先创建了一个CCColor
类型的变量blue
,用来把线段着为蓝色。然后,我们又创建了一个CCDrawNode
类型的变量,命名为segmentNode
。
针对segmentNode
,我们调用drawSegment
函数,设置绘制起点为屏幕中心,终点距离x轴为40个单位,距离y轴也是40个单位,并且设置半径为2.0,它是指线条粗细,指定线条颜色为blue
。
最后,我们把节点添加到场景中。
请注意,在如图1-30所示的屏幕截图中,我修改了折线,绘制出了一个正方形,而非三角形。
图1-30
在本部分,我们将向游戏中添加视差效果(背景滚动效果),它是游戏中非常流行的一种效果。在视差效果中,相比于背景中的对象,前景中的对象移动得更快,背景中的对象移动得要慢很多,借此产生立体感与运动错觉。
回想一下以前的电影片段,其中的英雄或主角保持静止不动,他们看上去就像在骑马一样,背景不断循环,让人产生错觉,以为英雄在场景中真地向前移动,如图1-31所示。
图1-31
下面我们将实现一个非常简单的视差效果,其中所有的背景对象(例如,树、灌木、草)都以相同的速度进行移动。为了实现这一效果,我们只要获取背景图像,并让它在一个循环中不断移动即可。
视差效果实现如下:针对背景图像,我们将使用两个精灵,而不是一个精灵,在游戏开始时把它们沿水平方向并排放在一起,如图1-31中的第一幅图所示。第一个精灵可见,第二个精灵在屏幕之外,最初玩家并不能看到它。
当游戏开始时,两个精灵将以一定的速度朝x轴的负方向移动,即向屏幕左侧移动。两个精灵以相同的速度移动,因此当游戏开始时,精灵1将慢慢地向左逐渐移出屏幕,随之精灵2将一点点地在屏幕显现出来。
一旦精灵1完全移出屏幕,它将快速移到精灵2的右侧,即精灵2在游戏开始时所处的位置上。
上述过程将在一个循环中不断重复进行。两个精灵总是向屏幕左侧移动。当一个精灵从屏幕左侧移出屏幕之后,它将立即移动到屏幕右侧,并且继续向左一点点地移动。
在为视差滚动创建资源,编写视差效果代码时,有几点需要各位牢记。首先,当为视差效果创建资源时,所使用的图像应该是连续的。例如,当你观看前面的第二幅图像时,会看到背景中的山脉好像是连续的。即使Sprite 1
与Sprite 2
是两幅不同的图像,当把它们放在一起时,它们看上去就像单独的一张图像。同样的现象也出现在山脚下的淡绿色灌木丛上。灌木丛的左半部分位于Sprite 1
中,右半部分位于Sprite 2
中,当把它们并排在一起时,它们就会一起组成一棵完整的灌木,让人产生一种它们本来就是一棵单独灌木的错觉。
第二点要注意的是图像之间的接缝。即使把图像无缝衔接在一起,并且让精灵以相同的速度移动,有时在精灵之间仍然可能会观察到有缝隙存在。尽管这不是一个非常普遍的问题,但是在一些框架中它可能会出现。为了防止出现这一问题,你可以把图像稍微拉伸一点点,使图像精灵彼此略微发生重叠,通常玩家觉察不到这种细微的变化。另一个方法是采用手工方式把精灵放置到屏幕精灵的末端,并且必要时做适当的调整,把精灵之间的接缝弥合。
上面这些就是视差滚动效果背后涉及到的主要理论。接下来,让我们一起编写代码,实现简单的视差滚动效果。
首先,采用类似于创建Hero
类的方式,创建CocosTouchClass
类型的文件,并且将其命名为ParallaxSprite
。
打开ParallaxSprite.h
文件,添加如下代码。
#import "CCSprite.h"
@interface ParallaxSprite :CCSprite{
CGSize _winSize;
CGPoint _center;
CCSprite *_sprite1, *_sprite2;
float _speed;
}
-(id)initWithFilename:(NSString *)filename Speed:(float)speed;
-(void)update:(CCTime)delta;
@end
在上述代码中,我们先创建了几个变量,这些变量后面会用到,例如变量_winSize
和_center
,前一个变量用来获取游戏运行设备的屏幕分辨率的大小,后一个用来计算屏幕中心。
接着,我们又创建了两个CCSprite类型的变量,持有两张图像,在视差效果中用来不断循环。
然后,我们添加了一个_speed
变量,用来指定图像移动与循环的速度。
类似于Hero
类,在ParallaxSprite
类中,我们也创建了一个initWithFilename
函数,它使用给定的文件名对类进行初始化。另外,我们也添加了一个float
类型变量,用来指定精灵的速度。
此外,我们还需要一个update
函数,它在 1 秒内会被调用60次,用来在类中更新两个精灵的位置。
以上就是ParallaxSprite.h
文件的所有代码,接下来,转到并打开ParallaxSprite.m
文件。
在ParallaxSprite.m
文件中,添加如下代码:
#import "ParallaxSprite.h"
@implementation ParallaxSprite
-(id)initWithFilename:(NSString *)filename Speed:(float)speed;{
if(self = [super init]){
NSLog(@"[parallaxSprite] (init) ");
_winSize = [[CCDirectorsharedDirector]viewSize];
_center = CGPointMake(_winSize.width/2, _winSize.height/2);
_speed = speed;
_sprite1 = [CCSpritespriteWithImageNamed:filename];
_sprite1.position = _center;
[selfaddChild:_sprite1];
_sprite2 = [CCSpritespriteWithImageNamed:filename];
_sprite2.position = CGPointMake(_sprite1.position.x + _winSize.
width
, _center.y);
[selfaddChild:_sprite2];
}
return self;
}
在上述代码中,我们首先实现initWithFilename
函数。在initWithFilename
函数中,先初始化超类,获取_winSize
。接着,通过把窗口的宽度与高度分别除以2计算出屏幕中心,再把speed
的值赋给_speed
变量。
然后,创建_sprite1
和_sprite2
两个变量,在spriteWithImageNames
中,通过filename
变量传入文件名字符串。
请注意,_sprite1
被放置到屏幕中心,_sprite2
被设置到屏幕之外,横坐标与_spirte1
相差一个屏幕宽度,纵坐标与_spirte1
相同。
最后,把两个精灵添加到类中。
接下来,我们开始实现update
函数,添加代码如下:
-(void)update:(CCTime)delta{
floatxPos1 = _sprite1.position.x - _speed;
floatxPos2 = _sprite2.position.x - _speed;
_sprite1.position = CGPointMake(xPos1, _sprite1.position.y);
_sprite2.position = CGPointMake(xPos2, _sprite1.position.y);
if(xPos1 + _winSize.width/2 <= 0){
_sprite1.position = CGPointMake(_sprite2.position.x +
_winSize.width, _center.y);
}else if(xPos2 + _winSize.width/2 <= 0){
_sprite2.position = CGPointMake(_sprite1.position.x + _winSize.
width
, _center.y);
}
}
@end
首先,我们分别为两个精灵计算它们在x轴上的新位置,计算时先获取精灵当前位置的x值,再用它减去精灵的移动速度。之所以这样做,是因为我们希望在每次调用update
函数时让精灵沿着x轴的负方向进行移动。
接着,我们把新坐标分别指派给两个精灵,其中x值为上面计算得到的值,y值保持原值不变。
然后,检测图像的右边缘对于玩家是否仍然可见,还是已经移出屏幕左侧之外。如果是这样,我们就把精灵放到脱屏位置上,即纵坐标不变,横坐标与另一个精灵相距一个屏幕宽度,以确保两个精灵之间不会出现缝隙。
在代码中,我们使用了if-else
语句,这是因为每次只会有一个精灵移出屏幕左侧边界。
下面让我们一起看一下如何使用ParallaxSprite
类。在MainScene.h
类中,引入ParallaxSprite.h
文件,创建一个ParallaxSprite
类型的变量pSprite
,代码如下:
#import "Hero.h"
#import "ParallaxSprite.h"
@interface MainScene :CCNode{
CGSizewinSize;
Hero* hero;
ParallaxSprite* pSprite;
}
然后,在MainScene.m
文件中,移除本章开始时用来添加背景精灵的代码,添加如下代码:
//Basic CCSprite - Background Image - REMOVE
//CCSprite* backgroundImage = [CCSpritespriteWithImageNamed:@"Bg.
png"];
//backgroundImage.position = CGPointMake(winSize.width/2,
winSize.height/2);
//[self addChild:backgroundImage];
//Parallax Background Sprite - ADD
pSprite = [[ParallaxSpritealloc]initWithFilename:@"Bg.png" Speed:5];
[selfaddChild:pSprite];
正如前面我们所做的那样,我们把Bg.png
文件指派给pSprite,此外,我们又指定了速度值为5
。
请注意,不必手工调用ParallaxSprite
类的update
函数,每一帧它都会被自动调用执行。而且,你也不必像以前那样调度它,开始时update
函数会被自动初始化。
到此为止,我们已经编写好了所有代码,运行代码,我们将会看到如图1-32所示的背景滚动效果。
图1-32
本章涵盖主题如下:
在上一章中,我们讨论了如何添加与操控精灵。在本章中,我们将学习如何在游戏中创建场景,它们用来在游戏中创建菜单的基本元素。
场景是游戏的基本构件。通常,在一款游戏中会有一个主菜单场景,从主菜单场景玩家可以切换到其他不同场景中,例如游戏场景、选项场景、得分场景等。在每一个场景中都会有菜单。
类似地,在MainScene
中,有一个play按钮,它是菜单的一部分,当游戏玩家单击它时,就会切换到游戏场景,并开始运行游戏代码。
在创建主菜单场景之前,需要先创建一个新项目,并且在加载游戏时要根据第1章中的操作步骤加载MainScene
文件。当然,也不要忘记修改CCBReader.m
文件,还要修改文件夹搜索参数。
其实,这一步并不需要什么其他操作。在本章的后面部分,我们将讨论如何使用自定义的init
函数创建更为复杂的场景。
目前,我们将把MainScene用作MainMenu
场景,并且使用文本、按钮、场景过渡函数进一步完善它。
到目前为止,我们已经有了一个MainMenu.h
文件,它拥有如下类似代码:
@interface MainScene : CCNode
+(CCScene*)scene;
@end
也有一个MainMenu.m
文件,代码如下:
#import "MainScene.h"
@implementation MainScene
+(CCScene*)scene{
return[[self alloc]init];
}
-(id)init{
if(self = [super init]){
winSize = [[CCDirector sharedDirector]viewSize];
}
return self;
}
@end
上面代码中没有什么新东西。我们可以像在第1章中所做的那样在init
函数中添加背景图像,让场景显得更好看一些。我们将在init
函数中添加如下代码:
-(id)init{
if(self = [super init]){
winSize = [[CCDirector sharedDirector]viewSize];
CCSprite* backgroundImage =
[CCSprite spriteWithImageNamed:@"Bg.png"];
backgroundImage.position =
CGPointMake(winSize.width/2,
winSize.height/2);
[self addChild:backgroundImage];
}
return self;
}
编译并运行代码,确保代码中没有错误发生,因为接下来我们要把这个文件用作主菜单场景。
运行场景之后,你将看到背景图像,与第1章中的背景图像一模一样。
场景是创建游戏界面的基本构件。一个场景可以包含任意数量的精灵、文本标签、菜单,以及它们的任意组合等,并且它能满足开发者的需要,允许他们使用这些元素搭建场景。
以上代码只是演示了如何创建场景,以及基本游戏场景看上去是什么样子,如图2-1所示。在本章后面部分,我们将讨论如何定制我们自己的场景。
图2-1
接下来,我们将添加文本标签、按钮、菜单,进一步完善我们的主菜单场景。
在这一部分,我们将学习如何向场景中添加文本。在Cocos2d中,有两种方法可以用来向场景中添加文本:一种是使用CCLabelTTF
类,另一种是使用CCLabelBMFont
类。我们将在本书第9章的Glyph Designer一节中讲解CCLabelBMFont
,本部分我们只讲解CCLabels,了解一下它们是如何工作的。
CCLabelTTF使用Mac系统中现有的系统字体。在使用CCLabelTTF
类时,我们只需指定要使用的字体名称、希望显示的文本以及字体大小,就能轻松地以指定的字体、大小显示给出的文本。
请注意,有些字体虽然已经安装到系统中,但是仍然无法在游戏中使用。这是因为Cocos2d只支持一部分系统字体。如果你想添加列表中没有的系统字体,就需要手动添加它。
添加好背景图像之后,紧接着添加如下代码:
CCLabelTTF *mainmenuLabel =
[CCLabelTTF labelWithString:@"Main Menu"
fontName:@"AmericanTypewriter-Bold"
fontSize: 36.0f];
mainmenuLabel.position =
CGPointMake(winSize.width/2,
winSize.height * 0.8);
[self addChild:mainmenuLabel];
在上述代码中,我们先创建了一个CCLabelTTF
类型的mainmenuLabel
变量,调用labelWithString
函数,并传入3个参数,分别是要显示的文本、字体名称以及字体大小。
然后,设置文本位置,横坐标为宽度的一半,纵坐标是高度的80%(从屏幕底部算起),所以给定的文本最终出现在整个屏幕中间偏上的位置上。
最后,我们把mainmenuLabel
添加到场景之中。
在示例代码中,我们使用了AmericanTypeWriter-Bold字体。完整的字体列表可以在SpriteBuilder.app
的FontListTTF.plist
文件中找到,在我们程序文件中的具体位置为/Applications/SpriteBuilder.app/Contents/Resources/FontListTTF.plist
。
CCLabelTTF
的工作方式类似于精灵,你可以修改它的位置或旋转它,甚至对它进行缩放操作(见图2-2)。
图2-2
你也可以向文本字体添加阴影与描边。在把文本添加到场景之后,紧接着添加如下代码:
//adding shadow
mainmenuLabel.shadowColor =
[CCColor colorWithRed:0.0 green:0.0 blue:1.0];
mainmenuLabel.shadowOffset = ccp(1.0, 1.0);
//adding outline
mainmenuLabel.outlineColor =
[CCColor colorWithRed:1.0 green:0.0 blue:0.0];
mainmenuLabel.outlineWidth = 2.0;
阴影颜色属性用来为阴影添加颜色,在示例代码中,我们使用阴影颜色属性把阴影设置为蓝色。此外,我们还需要设置阴影偏移,否则阴影将会被文本覆盖掉而变得不可见。
类似地,描边颜色属性(outlineColor
)用来设置文本描边颜色(见图2-3),描边粗细属性(outlineWidth
)用来设置文本轮廓线的粗细,默认值为1.0f
。
图2-3
按钮是用来在不同场景之间进行导航的主要方式。在本部分中,我们将学习如何向场景中添加按钮。
按钮与普通精灵类似,但是制作时,需要使用两张图片。一张图片用来在按钮常态下进行显示,即在按钮未按下时显示,另一张图片在按钮按下时显示。
学习本部分时,所需要的资源在本章的资源文件夹中有提供,针对于iPad与iPad HD共有4张图片,其中两张图片用来在按钮正常状态下(未按下)显示,另外两张在按钮按下时显示。复制4张图片到项目的Resource/Publishes-iOS
文件夹下。
并且,在MenuScene.m
文件顶部添加如下代码,导入Cocos2d-ui.h
文件。当添加按钮与布局类时,需要导入该文件。
#import "cocos2d-ui.h"
前面我们已经在MenuScene.m
文件中添加了文本描边代码,紧随其后,添加如下代码:
CCButton *playBtn = [CCButton buttonWithTitle:nil
spriteFrame:[CCSpriteFrame
frameWithImageNamed:@"playBtn_normal.png"]
highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:@
"playBtn_pressed.png"]
disabledSpriteFrame:nil];
制作按钮时,我们必须使用CCButton
类,它带有4个参数,分别为按钮标题、常态下的spriteFrame
参数、高亮或按下时的精灵帧,以及按钮不可用时的精灵帧。
本示例中,我们只传入两张图片:一张是按钮在常态下显示的图片,另一张是按钮按下时要显示的图片。
此时,如果运行项目,我们不会在屏幕上看到按钮。这是因为我们还没有把按钮添加到场景中。在把按钮添加到场景之前,需要先把它们添加到一个CCLayout
类型的按钮菜单中,然后再把它们添加到场景之中。
为此,让我们先创建一个CCLayout
类型的变量,代码如下:
CCLayoutBox * btnMenu;
btnMenu = [[CCLayoutBox alloc] init];
btnMenu.anchorPoint = ccp(0.5f, 0.5f);
btnMenu.position = CGPointMake(winSize.width/2,
winSize.height * 0.5);
[btnMenu addChild:playBtn];
[self addChild:btnMenu];
在上面代码中,我们创建了一个名称为btnMenu
的CCLayout
类型的变量,而后为它分配内存并进行初始化。接着,我们把按钮的锚点设置为center
,不然所有按钮都会被定位到按钮菜单的左下位置。然后,把按钮菜单放置到屏幕中心。
最后,我们把playBtn
按钮添加到btnMenu
中,再把btnMenu
本身添加到场景中,如图2-4所示。
图2-4
现在,我们就可以在屏幕上看到添加好的按钮了。并且,当我们按下按钮时,按钮上的显示图片将被替换为按钮按下时的图片。
为了让按钮执行某个动作,我们需要在按钮按下且被释放时调用一个函数。
首先,在playBtn
变量之后,添加如下粗体代码。
CCButton *playBtn = [CCButton buttonWithTitle:nil
spriteFrame:[CCSpriteFrame frameWithImageNamed:@"playBtn_normal.
png"]
highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:@
"playBtn_pressed.png"]
disabledSpriteFrame:nil];
[playBtn setTarget:self selector:@selector(playBtnPressed:)];
然后,再添加如下代码,当按下并释放按钮时将执行它。
-(void)playBtnPressed:(id)sender{
CCLOG(@"play button pressed");
}
在上述代码中,我们把playBtnPressed
函数指派给了playBtn
按钮。这样一来,当按下并释放按钮时,playBtnPressed
函数就会被调用执行,目前它只是在控制台上输出一段文字,用来告诉我们按钮被按下了。
当按下并释放按钮时,你将看到控制台高亮显示,并且在其中看到输出如图2-5所示的文本内容。
图2-5
当按下play按钮时,游戏应该切换到另一个场景,为此我们需要创建一个新场景,以便从原来的场景切换到新场景。首先,让我们学习一下如何创建一个场景。
现在,让我们添加gameplay场景。在第1章中,我们已经学习过如何创建文件,采用相同方法,创建GamePlayScene类文件。
如上,我创建了一个名为GamePlayScene
类。GamePlayScene.h
文件中包含的代码如下:
#import "CCScene.h"
@interface GameplayScene : CCNode
+(CCScene*)scene;
-(id)initWithLevel:(NSString*)lvlNum
@end
GamePlayScene.m
文件包含如下代码:
#import "GameplayScene.h"
#import "cocos2d-ui.h"
@implementation GameplayScene
+(CCScene*)scene{
return[[self alloc]initWithLevel:lvlNum];
}
-(id)initWithLevel:(NSString*)lvlNum{
if(self = [super init]){
CGSize winSize = [[CCDirector sharedDirector]viewSize];
//Basic CCSprite - Background Image
CCSprite* backgroundImage = [CCSprite spriteWithImageNamed:@
"Bg.png"];
backgroundImage.position = CGPointMake(winSize.width/2,
winSize.height/2);
[self addChild:backgroundImage];
CCLabelTTF *mainmenuLabel = [CCLabelTTF labelWithString:@"Gameplay
Scene" fontName:@"AmericanTypewriter-Bold" fontSize: 36.0f];
mainmenuLabel.position = CGPointMake(winSize.width/2, winSize.
height
* 0.8);
self addChild:mainmenuLabel];
CCLabelTTF *levelNumLabel = [CCLabelTTF labelWithString:lvlNum
fontName:@"AmericanTypewriter-Bold" fontSize: 24.0f];
levelNumLabel.position = CGPointMake(winSize.width/2, winSize.
height
* 0.7);
[self addChild:levelNumLabel];
CCButton *resetBtn = [CCButton buttonWithTitle:nil
spriteFrame:[CCSpriteFrame frameWithImageNamed:@
"resetBtn_normal.png"]
highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:@
"resetBtn_pressed.png"]
disabledSpriteFrame:nil];
[resetBtn setTarget:self selector:@selector(resetBtnPressed:)];
CCLayoutBox * btnMenu;
btnMenu = [[CCLayoutBox alloc] init];
btnMenu.anchorPoint = ccp(0.5f, 0.5f);
btnMenu.position = CGPointMake(winSize.width/2, winSize.height *
0.5);
[btnMenu addChild:resetBtn];
[self addChild:btnMenu];
}
return self;
}
-(void)resetBtnPressed:(id)sender{
CCLOG(@"reset button pressed");
}
@end
GamePlayScene
类与MainScene
类相似,但是在其中,我们添加了自定义场景和init
函数,以便向类中传入字符串形式的难度级别数字。
此外,我们也添加了一个用于显示当前游戏难度级别的标签。
同样地,目前GamePlayScene
类不做任何事。但是,在接下来的部分中,我们将学习如何从一个场景过渡到gameplay场景,其中会显示我们当前选择的游戏难度级别。
在本部分中,我们将讨论如何在场景之间进行过渡切换。
绝大部分准备工作已经在前面完成,接下来,让我们专心编写代码。由于我们希望从一个场景过渡到GameplayScene场景,所以必须先把GameplayScene
类导入到MainScene.m
文件,代码如下:
#import "GameplayScene.h"
接着,在MainScene.m
文件的playBtnPressed
函数中,添加如下粗体代码:
-(void)playBtnPressed:(id)sender{
CCLOG(@"play button pressed");
[[CCDirector sharedDirector] replaceScene:
[[GameplayScene alloc]
initWithLevel:@"1"]];
}
当我们按play按钮时,游戏将会加载GameplayScene
场景,显示“GameplayScene
”文本,并且显示当前所选的游戏难度级别1
,如图2-6所示。
图2-6
接下来,我们添加一个重置按钮,以及按下重置按钮要调用的函数,它会让我们从GameplayScene
返回到MainMenu
场景中。在GameplayScene.m
文件中,修改resetButtonPressed
函数,代码如下。这样一来,当我们按重置按钮时,就会从当前场景切换回MainMenu
场景之中。
-(void)resetBtnPressed:(id)sender{
CCLOG(@"reset button pressed");
[[CCDirector sharedDirector]
replaceScene:[[MainScene alloc] init]];
}
运行代码,按下重置按钮,将返回主菜单场景中。
如果你认为添加过渡效果十分酷炫,你可以在Cocos2d中添加切换场景时的过渡效果,这十分简单。
我们真正需要做的是,在playBtnPressed
函数中,使用设置过渡效果的代码代替前面所写的代码。创建过渡效果时,需要使用CCTransition
类。
在playBtnPressed
函数中,使用如下粗体代码代替上一节编写的代码。
-(void)playBtnPressed:(id)sender{
CCLOG(@"play button pressed");
//[[CCDirector sharedDirector] replaceScene:[[GameplayScene alloc]
initWithLevel:@"1"]];
CCTransition *transition = [CCTransition
transitionPushWithDirection:
CCTransitionDirectionLeft duration:0.20];
[[CCDirector sharedDirector]replaceScene:[[GameplayScene alloc]
initWithLevel:@"1"] withTransition:transition];
}
运行应用程序,当切换场景时,你会看到一个漂亮的淡入淡出效果。
在创建过渡效果时,主要用到的类是CCTransition
类。通过它,你可以指定要使用的过渡效果类型,也可以指定过渡效果的持续时间。在上面的示例代码中,我把过渡时长设置为0.2秒,你也可以根据游戏的实际需要增加或缩短过渡效果的持续时间(见图2-7)。
图2-7
在添加效果时,Cocos2d为你提供了许多选择。如图2-8所示,你可以浏览效果列表,通过尝试添加它们,观察每种效果的作用。
图2-8
修改过渡效果,如下所示。再次运行应用,观察应用的新效果,如图2-9所示。
CCTransition *transition = [CCTransition
transitionRevealWithDirection:
CCTransitionDirectionLeft duration:0.2];
图2-9
在本部分中,我们将学习如何添加难度选择场景,其中包含多种难度选择按钮,当你按下某个按钮时,相应难度水平的游戏就会被加载进来。
为了创建难度级别选择场景,你需要一个自定义精灵,用来显示按钮背景图片以及表示难度级别的数字。首先,我们要创建这些按钮。
在创建好按钮精灵之后,接下来我们要创建一个新场景,用来存放背景图像、场景名称、按钮数组,以及变换场景到指定游戏难度的逻辑。
首先,我们创建一个新的 Cocoa Touch 类,命名为LevelSelectionBtn
,它以CCSprite
类作为父类。
然后,打开LevelSelectionBtn.h
文件,在其中添加如下代码:
#import "CCSprite.h"
@interface LevelSelectionBtn : CCSprite
-(id)initWithFilename:(NSString *) filename
StartlevelNumber:(int)lvlNum;
@end
上面代码中,我们创建了一个自定义的init
函数,它带有两个参数,一个是图像文件名,用来指定按钮背景图像,另一个是整数,显示在按钮背景图像之上,表示难度级别。
这就是LevelSelectionBtn.h
文件中的所有代码。在LevelSelectionBtn.m
文件中,添加如下代码:
#import "LevelSelectionBtn.h"
@implementation LevelSelectionBtn
-(id)initWithFilename:(NSString *) filename StartlevelNumber:
(int)lvlNum;
{
if (self = [super initWithImageNamed:filename]) {
CCLOG(@"Filename: %@ and levelNUmber: %d",
filename,
lvlNum);
CCLabelTTF *textLabel =
[CCLabelTTF labelWithString:[NSString
stringWithFormat:@"%d",lvlNum ]
fontName:@"AmericanTypewriter-Bold"
fontSize: 12.0f];
textLabel.position =
ccp(self.contentSize.width / 2,
self.contentSize.height / 2);
textLabel.color = [CCColor colorWithRed:0.1f
green:0.45f
blue:0.73f];
[self addChild:textLabel];
}
return self;
}
@end
在自定义的init
函数中,首先把参数传入的文件名与游戏难度级别数字在控制台中输出,而后创建一个文本标签,在把整数转换为字符串之后传递给它。
随后,把文本标签放置到当前精灵背景图像的中央,通过把图像的宽度与高度分别除以2得到图像中心点坐标。
由于图像背景与文本都是白色,所以需要把文本颜色修改为蓝色,以便把文本显示出来。
最后,我们把文本添加到当前的类中。
以上就是LevelSelectionBtn
类的所有代码。接下来,我们将创建LevelSelectionScene
类,并向其中添加精灵按钮与按下按钮所要执行的逻辑。
创建好LevelSelectionScene
类之后,在头文件(LevelSelectionScene.h)中添加如下代码:
#import "CCScene.h"
@interface LevelSelectionScene : CCScene{
NSMutableArray *buttonSpritesArray;
}
+(CCScene*)scene;
@end
请注意,在LevelSelectionScene.h
中,除了常见代码之外,我们还创建了一个NSMutableArray
类型的变量buttonsSpritesArray
,在后面的代码中将会用到它。
接着,在LevelSelectionScene.m
文件中,添加如下代码:
#import "LevelSelectionScene.h"
#import "LevelSelectionBtn.h"
#import "GameplayScene.h"
@implementation LevelSelectionScene
+(CCScene*)scene{
return[[self alloc]init];
}
-(id)init{
if(self = [super init]){
CGSize winSize = [[CCDirector sharedDirector]viewSize];
//Add Background Image
CCSprite* backgroundImage = [CCSprite spriteWithImageNamed:@
"Bg.png"];
backgroundImage.position = CGPointMake(winSize.width/2,
winSize.height/2);
[self addChild:backgroundImage];
//add text heading for file
CCLabelTTF *mainmenuLabel = [CCLabelTTF labelWithString:@
"LevelSelectionScene" fontName:@"AmericanTypewriter-Bold"
fontSize:
36.0f];
mainmenuLabel.position = CGPointMake(winSize.width/2, winSize.
height
* 0.8);
[self addChild:mainmenuLabel];
//initialize array
buttonSpritesArray = [NSMutableArray array];
int widthCount = 5;
int heightCount = 5;
float spacing = 35.0f;
float halfWidth =
winSize.width/2 - (widthCount-1) * spacing * 0.5f;
float halfHeight =
winSize.height/2 + (heightCount-1) * spacing * 0.5f;
int levelNum = 1;
for(int i = 0; i < heightCount; ++i){
float y = halfHeight - i * spacing;
for(int j = 0; j < widthCount; ++j){
float x = halfWidth + j * spacing;
LevelSelectionBtn* lvlBtn =
[[LevelSelectionBtnalloc]
initWithFilename:@"btnBG.png"
StartlevelNumber:levelNum];
lvlBtn.position = CGPointMake(x,y);
lvlBtn.name =
[NSString stringWithFormat:@"%d",levelNum];
[self addChild:lvlBtn];
[buttonSpritesArray addObject: lvlBtn];
levelNum++;
}
}
}
return self;
}
在上述代码中,我们先添加了背景图像、场景标题文本,并对NSMutableArray
进行了初始化。
然后,我们创建了6个变量,如下所示(见图2-10)。
widthCount
:列数。heightCount
:行数。spacing
:精灵按钮之间的距离,防止它们重叠在一起。halfWidth
:指从屏幕中心到第一个精灵按钮左上角的距离在x
轴上的投影而得到的距离。halfHeight
:指从屏幕中心到第一个精灵按钮左上角的距离在y
轴上的投影而得到的距离。lvlNum
:指显示在精灵按钮上表示难度级别的数字,默认值为1
,每次创建一个按钮,其值就会增加1。图2-10
在双重循环中,我们将获取每个按钮精灵的x与y坐标。首先,为了获取y值,我们要用halfHeight减去spacing与循环变量i的乘积。由于i的初始值为0
,所以最顶行的y值为halfHeigh。
随后,计算按钮位置的x值,计算时我们使用halfWidth加上spacing与j的乘积。每次x值都会被spacing增大。
在获取x与y值之后,创建一个新的LevelSelectionBtn
精灵,传入btnBG.png
图像,以及lvlNum
值,生成按钮精灵。
然后,把之前计算得到的x与y值赋给按钮精灵的position
属性。
为了通过数字引用按钮,我们先把levelNum
转换为字符串,而后将其赋给按钮精灵的name
属性,也就是说引用按钮的数字与其上显示的代表难度级别的数字是一致的。
接下来,把按钮添加到场景之中,同时按钮也会被添加到之前创建的全局按钮精灵数组之中,这是因为后面我们需要对这些图像进行循环。
最后,把levelNum
值增加1
。
然而,目前我们还没有向精灵按钮添加任何交互行为,当添加交互行为后,每次按下按钮,就会加载相应难度级别的游戏场景。
为了添加触摸交互行为,我们将使用Cocos2d内置的touchBegan
函数。在本书的后面章节中,我们将创建更复杂的游戏界面。而这里,我们只使用基本的touchBegan
函数。
在同一个文件中,在init
函数与@end
之间添加如下代码:
-(void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event{
CGPoint location = [touch locationInNode:self];
for (CCSprite *sprite in buttonSpritesArray)
{
if (CGRectContainsPoint(sprite.boundingBox, location)){
CCLOG(@" you have pressed: %@", sprite.name);
CCTransition *transition =
[CCTransition transitionCrossFadeWithDuration:0.20];
[[CCDirector sharedDirector]replaceScene:[[GameplayScene
alloc]initWithLevel:sprite.name] withTransition:transition];
self.userInteractionEnabled = false;
}
}
}
每次当我们触碰屏幕时,touchBegan
函数都会被调用执行。
因此,当我们触碰屏幕时,应用程序就会获取触碰的位置,并将其保存到location
变量中。
而后,使用for in
循环遍历添加到buttonSpritesArray数组中的所有按钮精灵。
并且,调用RectContainsPoint
函数,检测我们触碰的位置是否位于任一个按钮精灵的矩形框之内。
若是,则在控制台中输出相关信息,告知用户单击了哪个按钮,这样一来,我们就能知道是否加载了正确难度级别的场景。
接着,我们创建了一个淡入淡出过渡效果,并且从当前场景切换到GameplayScene
,并使用所单击的按钮精灵名称进行初始化。
最后,我们需要把Boolean
型变量userInteractionEnabled
设置为false
,禁止当前类监听用户的触屏行为。
当然,这需要我们在init
函数开始的时候先把userInteractionEnabled
设置为TRUE
,即在init
函数中添加如下粗体代码。
if(self = [super init]){
self.userInteractionEnabled = TRUE;
CGSize winSize = [[CCDirector sharedDirector]viewSize];
至此,我们已经编写好了LevelSelectionScene
类。但是,我们还需要向MainScene
添加一个按钮以便打开LevelSelectionScene
。
在MainScene
的init
函数中添加如下粗体代码。我们主要添加了menuBtn
按钮,以及单击它时要调用的函数。
CCButton *playBtn =
[CCButton buttonWithTitle:nil
spriteFrame:[CCSpriteFrame frameWithImageNamed:@
"playBtn_normal.png"]
highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:@
"playBtn_pressed.png"]
disabledSpriteFrame:nil];
[playBtn setTarget:self
selector:@selector(playBtnPressed:)];
CCButton *menuBtn = [CCButton buttonWithTitle:nil
spriteFrame:[CCSpriteFrame
frameWithImageNamed:@"menuBtn.png"]
highlightedSpriteFrame:[CCSpriteFrame
frameWithImageNamed:@"menuBtn.png"]
disabledSpriteFrame:nil];
[menuBtn setTarget:self selector:@selector(menuBtnPressed:)];
CCLayoutBox * btnMenu;
btnMenu = [[CCLayoutBox alloc] init];
btnMenu.anchorPoint = ccp(0.5f, 0.5f);
btnMenu.position =
CGPointMake(winSize.width/2, winSize.height * 0.5);
btnMenu.direction = CCLayoutBoxDirectionVertical;
btnMenu.spacing = 10.0f;
[btnMenu addChild:menuBtn];
[btnMenu addChild:playBtn];
[self addChild:btnMenu];
请不要忘记把menuBtn.png
文件从本章的资源文件夹复制到项目中,否则会出现编译错误。
接下来,添加menuBtnPressed
函数,一旦menuBtn
按钮按下且被释放,它就会被调用执行,代码如下:
-(void)menuBtnPressed:(id)sender{
CCLOG(@"menu button pressed");
CCTransition *transition = [CCTransition transitionCrossFadeWith
Duration:0.20];
[[CCDirector sharedDirector]replaceScene:[[LevelSelectionScene
alloc]init] withTransition:transition];
}
现在,MainScene
场景如图2-11所示。
图2-11
单击play按钮之下的菜单按钮,你将看到如下LevelSelectScreen
场景,里面罗列出了所有的难度级别,如图2-12所示。
图2-12
此时,单击任意一个按钮,即可切换到GameplayScene场景,并且把你所单击的代表难度级别的数字一同显示出来。
由于我单击了18号按钮,所以在切换到GameplayScene场景后,显示出的数字为18,如图2-13所示。
图2-13
假如你的游戏有多个难度级别,例如有20个等级,那么只用一个单独的难度级别选择场景来显示所有的级别选择按钮是可以的。但是,要是有更多等级呢?在本部分中,我们将修改前面编写的代码,创建一个节点,并进行初始化,从而产生一个可以滚动的难度级别选择场景。
我们将创建一个新类,将其命名为LevelSelectionLayer
,它继承自CCNode
类。然后,把我们在前面添加到LevelSelectionScene
中的所有代码复制到其中。这样一来,我们就有了一个单独的类,在游戏中可以根据实际需要的次数对其进行多次实例化。
在LevelSelectionLayer.h
文件中,修改代码如下:
#import "CCNode.h"
@interface LevelSelectionLayer : CCNode {
NSMutableArray *buttonSpritesArray;
}
-(id)initLayerWith:(NSString *)filename
StartlevelNumber:(int)lvlNum
widthCount:(int)widthCount
heightCount:(int)heightCount
spacing:(float)spacing;
@end
在上述代码中,我们修改了init
函数,替换掉了硬编码值,使得我们能够创建出一个更富弹性的游戏难度选择层。
在LevelSelectionLayer.m
文件中,添加如下代码:
#import "LevelSelectionLayer.h"
#import "LevelSelectionBtn.h"
#import "GameplayScene.h"
@implementation LevelSelectionLayer
- (void)onEnter{
[super onEnter];
self.userInteractionEnabled = YES;
}
- (void)onExit{
[super onExit];
self.userInteractionEnabled = NO;
}
-(id)initLayerWith:(NSString *)filename StartlevelNumber:(int)lvlNum
widthCount:(int)widthCount heightCount:(int)heightCount spacing:
(float)spacing{
if(self = [super init]){
CGSize winSize = [[CCDirector sharedDirector]viewSize];
self.contentSize = winSize;
buttonSpritesArray = [NSMutableArray array];
float halfWidth = self.contentSize.width/2 - (widthCount-1) *
spacing *
0.5f;
float halfHeight = self.contentSize.height/2 + (heightCount-1) *
spacing
* 0.5f;
int levelNum = lvlNum;
for(int i = 0; i < heightCount; ++i){
float y = halfHeight - i * spacing;
for(int j = 0; j < widthCount; ++j){
float x = halfWidth + j * spacing;
LevelSelectionBtn* lvlBtn = [[LevelSelectionBtn alloc]
initWithFilename:filename StartlevelNumber:levelNum];
lvlBtn.position = CGPointMake(x,y);
lvlBtn.name = [NSString stringWithFormat:@"%d",levelNum];
[self addChild:lvlBtn];
[buttonSpritesArray addObject: lvlBtn];
levelNum++;
}
}
}
return self;
}
-(void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event{
CGPoint location = [touch locationInNode:self];
CCLOG(@"location: %f, %f", location.x, location.y);
CCLOG(@"touched");
for (CCSprite *sprite in buttonSpritesArray)
{
if (CGRectContainsPoint(sprite.boundingBox, location)){
CCLOG(@" you have pressed: %@", sprite.name);
CCTransition *transition = [CCTransition transitionCross
FadeWithDuration:0.20];
[[CCDirector sharedDirector]replaceScene:[[GameplayScene
alloc]initWithLevel:sprite.name] withTransition:transition];
}
}
}
@end
其中,粗体代码是主要的修改内容。首先,我们通过onEnter
与onExit
函数来启用、禁用触摸功能。另一个主要的改变是,我们把节点的contentsize
值设置为winSize
。并且,在指定按钮的左上角坐标时,我们并没有使用winSize
,而使用了节点的contentsize
值。
下面,让我们转到LevelSelectionScene
类,在LevelSelectionScene.h
文件中添加如下代码:
#import "CCScene.h"
@interface LevelSelectionScene : CCScene{
int layerCount;
CCNode *layerNode;
}
+(CCScene*)scene;
@end
如上所示,我们修改了头文件,在其中添加了两个全局变量。
layerCount
变量保存你添加的所有层与节点。layerNode
变量是一个空节点,方便我们能把所有图层节点添加到它,这样就可以向后或向前移动它,而不用分别移动每个层节点。接着,在LevelSelectionScene.m
文件中,添加如下代码:
#import "LevelSelectionScene.h"
#import "LevelSelectionBtn.h"
#import "GameplayScene.h"
#import "LevelSelectionLayer.h"
@implementation LevelSelectionScene
+(CCScene*)scene{
return[[self alloc]init];
}
-(id)init{
if(self = [super init]){
CGSize winSize = [[CCDirector sharedDirector]viewSize];
layerCount = 1;
//Basic CCSprite - Background Image
CCSprite* backgroundImage = [CCSprite spriteWithImageNamed:
@"Bg.png"];
backgroundImage.position = CGPointMake(winSize.width/2,
winSize.height/2);
[self addChild:backgroundImage];
CCLabelTTF *mainmenuLabel = [CCLabelTTF labelWithString:
@"LevelSelectionScene" fontName:@"AmericanTypewriter-Bold"
fontSize:
36.0f];
mainmenuLabel.position = CGPointMake(winSize.width/2, winSize.
height
* 0.8);
[self addChild:mainmenuLabel];
//empty node
layerNode = [[CCNode alloc]init];
[self addChild:layerNode];
int widthCount = 5;
int heightCount = 5;
float spacing = 35;
for(int i=0; i<3; i++){
LevelSelectionLayer* lsLayer = [[LevelSelectionLayer
alloc]initLayerWith:@"btnBG.png"
StartlevelNumber:widthCount * heightCount * i + 1
widthCount:widthCount
heightCount:heightCount
spacing:spacing];
lsLayer.position = ccp(winSize.width * i, 0);
[layerNode addChild:lsLayer];
}
CCButton *leftBtn = [CCButton buttonWithTitle:nil
spriteFrame:[CCSpriteFrame frameWithImageNamed:@"left.png"]
highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:
@"left.png"]
disabledSpriteFrame:nil];
[leftBtn setTarget:self selector:@selector(leftBtnPressed:)];
CCButton *rightBtn = [CCButton buttonWithTitle:nil
spriteFrame:[CCSpriteFrame frameWithImageNamed:@"right.png"]
highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:
@"right.png"]
disabledSpriteFrame:nil];
[rightBtn setTarget:self selector:@selector(rightBtnPressed:)];
CCLayoutBox * btnMenu;
btnMenu = [[CCLayoutBox alloc] init];
btnMenu.anchorPoint = ccp(0.5f, 0.5f);
btnMenu.position = CGPointMake(winSize.width * 0.5, winSize.height
*
0.2);
btnMenu.direction = CCLayoutBoxDirectionHorizontal;
btnMenu.spacing = 300.0f;
[btnMenu addChild:leftBtn];
[btnMenu addChild:rightBtn];
[self addChild:btnMenu z:4];
}
return self;
}
-(void)rightBtnPressed:(id)sender{
CCLOG(@"right button pressed");
CGSize winSize = [[CCDirector sharedDirector]viewSize];
if(layerCount >=0){
CCAction* moveBy = [CCActionMoveBy actionWithDuration:0.20
position:ccp(-winSize.width, 0)];
[layerNode runAction:moveBy];
layerCount--;
}
}
-(void)leftBtnPressed:(id)sender{
CCLOG(@"left button pressed");
CGSize winSize = [[CCDirector sharedDirector]viewSize];
if(layerCount <=0){
CCAction* moveBy = [CCActionMoveBy actionWithDuration:0.20
position:ccp(winSize.width, 0)];
[layerNode runAction:moveBy];
layerCount++;
}
}
@end
在上述代码中,重要的代码都已经使用粗体标识出来。除了添加通用的背景与文本以外,我们还把layerCount
初始化为1
,并且也对空节点layerNode
进行了初始化。
接着,我们创建了一个for
循环,在其中,通过传递btnBg
图像中每个选择层的初值、宽度值、高度值与按钮间的spacing值,我们添加了3个难度级别选择层。
并且,请注意这些层是如何以屏幕宽度单位进行定位的。第一个层对玩家是可见的。其他连续层被添加到屏幕之外,这与创建视差效果时添加第二张脱屏图像所采用的方式一样。
然后,把每个级别选择层添加到layerNode
之中。
此外,我们还创建了左移与右移按钮,每次单击它们,都能把layerNode
向左或向右移动。相应地,我们创建了leftBtnPressed
和rightBtnPressed
两个函数,当按下左移按钮或右移按钮时,它们就会被调用执行。
首先,让我们一起看一下rightBtnPressed
函数。一旦按下右移按钮,rightBtnPressed
函数就会被调用执行,先向控制台输出相应信息,而后获取窗口大小,接着检查layerCount
值是否大于0,由于前面我们把layerCount
设置为1
,所以判断结果为真,创建moveBy
动作,指定沿x轴负方向移动窗口宽度大小,沿y轴方向移动为0
,因为我们只想让移动沿着x轴进行,而沿y轴方向不做移动。最后,设置移动动作的持续时间为0.20f
。
然后,layerNode
执行moveBy动作,并且把layerCount
值减1
。
而在leftBtnPressed
函数中,会把层向相反方向,即往左移动。运行游戏,观察LevelSelectionScene
中的变化,如图2-14所示。
图2-14
由于不能往左移,所以按左移按钮不会有任何作用。然而,如果按右移按钮,你将会看到场景层发生滚动,显示出下一部分按钮,如图2-15所示。
图2-15