cocos2d的单例
cocos2d有一个备受争议的设计模式:大量使用了【单例】,原则上讲,单例是程序生命周期中只能被实例化一次的类。
你可以认为他是导演(一部电影的拍摄过程中只有一个导演),他是蚁后(一个蚁巢中只有一个蚁后)等等。
为了确保该对象只被实例化一次,我们利用类的一个静态方法来生成和访问类,在cocos2d中,这个方法通常以shared开头,以下是一些常用的cocos2d单例类的实例化语句:
CCActionManager* sharedManager = [CCActionManager sharedManager];
CCDirector* sharedDirector = [CCDirector sharedDirector];
CCSpriteFrameCache* sharedCache = [CCSpriteFrameCache sharedSpriteFrameCache];
CCTextureCache* sharedTexCache = [CCTextureCache sharedTextureCache];
CCTouchDispatcher* sharedDispatcher = [CCTouchDispatcher sharedDispatcher];
CDAudioManager* sharedManager = [CDAudioManager sharedManager];
SimpleAudioEngine* sharedEngine = [SimpleAudioEngine sharedEngine];
下面让我们来思考一下为什么cocos2d的作者会将他们设计成单例模式。
单例的好处是你可以在任何时间,任何类中的任何地方直接调用他。
没错,这相当于全局类或者全局变量,它具备全局变量的所有优点,我们不用再将他们以参数的形式传来传去。从第一关到第二关进行场景切换的时候我们可以不用担心相关数据的传递。
当然,它也具备全局变量的缺点,这毋庸置疑,从游戏开始到游戏结束他都将常驻内存。
我相信cocos2d的作者是经过深思熟虑以后才做的这个决定。
声音、角色信息,都可以被存到单例中。
OK,我们来玩一个单例:
static MyRole *_sharedRole = nil;
+(MyRole *) sharedRole
{
if (_sharedManager == nil)
{
_sharedManager = [[MyRole alloc] init];
}
return _sharedManager;
}
我想这段代码已经非常清晰了,不再赘述。
注意
在使用单例模式的时候,除了考虑内存的因素以外,还有一点是需要事先明了的:你确定这个家伙永远不可能【多例】么?
刚才我说了Role(角色)适合用作单例,这可能是一个误导。
你可能认为在游戏中主角只有一个,于是使用单例类来抽象我们的游戏主角,这就可能为游戏的扩展性埋下一个致命的隐患:如果一个月后你打算将这个游戏升级为对战模式的,那么代码将很难扩展。
因此除非你确信这个东西现在、将来都不会被实例化多个,否则不要使用单例。这是昆仑神提醒我的,我又提醒你:-)
几个常用单例类
The Director
一部戏中,导演永远只有一个,他是cocos2d的核心,游戏资源的调度、出场、退场都要听导演的。
具体作用如下:
1.访问和改变场景(Scene)
2.访问cocos2d的配置细节(Config)
3.访问视图(View,UIWindow)
4.暂停恢复和结束游戏
5.其他全局功能(比如在UIKit和OpenGL中转换坐标)
6.其他我不知道的功能:-)
The Scene Graph(场景)
场景,是由所有目前活跃的cocos2d节点所组成的,这些节点通常是层(CCLayer)。
节点是cocos2d中一个很简单但是非常重要的概念,Scene、Layer、Sprite(精灵)、Menu、Lable等都是节点。节点类都继承自CCNode这个抽象类,其中定义了通用的属性和方法。
CCNode
正如上面说的那样,CCNode类是所有节点的基类,没有视觉表现,它定义了所有节点都通用的属性和方法。
所有的节点都可以通过调用addChild方法来添加子节点,以下是一个示意图:
你对一个节点施加的影响将会影响到他的所有子节点。
比如父节点在移动,子节点在旋转,那么在玩家的眼中子节点是旋转着移动,这是一个很诱人的东东。
想象一下,玩家操纵一架战斗母机,周围还可以围绕着几个负责警卫的战斗机,就可以用它来实现。
CCNode的几个使用示例
1.生成节点
CCNode* childNode = [CCNode node];
2.为节点添加子节点
[myNode addChild:childNode z:0 tag:123];
addChild的该种重载中有3个参数,第一个参数是要添加的节点,参数tag相当于C#中的Id,参数z指的是在z轴上的次序,也就是谁上谁下的问题:-)。
如果不指定z轴的顺序的话,将按照添加的先后顺序来绘制节点,先加入的先绘制,也就是说后添加的节点会遮盖住先添加的节点。当然,这是对于有显示效果的节点(CCSprite、CCMenu、CCLabelTTF)而言的,很多节点(CCScene、CCLayer)是没有显示效果的。
3.访问子节点
CCNode* retrievedNode = [myNode getChildByTag:123];
getChildByTag,类似于Javascript中的getElementByID方法,很明显,在用childNode添加子节点的时候tag是不能重复的。
4.通过tag来删除子节点
[myNode removeChildByTag:123 cleanup:YES];
我查阅了一下cocos2d的文档,原文是这样说的:
Removes a child from the container by tag value. It will also cleanup all running actions depending on the cleanup parameter.
cleanup这个参数如果设置为YES,将会停止正发生在该节点上的动作。
5.通过节点指针删除节点
[myNode removeChild:retrievedNode];
6.删除节点所有的子节点
[myNode removeAllChildrenWithCleanup:YES];
cleanup的含义不再赘述。
7.从本节点的父节点删除自身
[myNode removeFromParentAndCleanup:YES];
这个很有意思,哈,大多数语言的集合操作中是不存在这个方法的。这是由CCNode的数据结构决定的。
关于tag
如果有多个节点拥有相同的tag数值,getChildByTag将把找到的第一个节点返回。其它节点将不能够再被访问。
所以你要确保为你的节点指定独有的tag数值。
动作(Actions)也有tag。不过,节点和动作的tag不会冲突,所以拥有相同tag数值的动作和节点可以和平共处。关于Action,稍后讲解。
To初学者:看开发类的文档,一定要能适应一个如下习惯:很多知识点都是后面详解的,当你第一次看到他的时候作者可能只是简单的提了一下。千万不要在这里放弃,你可以尝试着阅读完所有的章节,然后再回头重读,感觉就不一样了。
Actions
关于Action,不在这里详细讲解,后文可能会有专门的章节来说他,先给出一张图,图中展示了cocos2d为我们提供的所有基于节点的动作。
来看一个简单的动作的声明:
CCAction* myAction = [CCBlink actionWithDuration:10 blinks:20];
myAction.tag = 234;
通过CCAction构造了一个动作,这个动作是CCBlink(闪烁),在游戏中如果你想让玩家注意某个区域,节点执行此动作将是一个不错的选择。
CCBlink继承自CCAction,actionWithDuration方法负责构造一个对象,这个对象会在10秒钟内闪烁20次。
正如上文所说,Action也是可以有tag的,这个tag可以让我们在需要的时候控制该动作,很明显,这是一个好消息。
myAction被实例化以后,并不会被执行,因为你还没有指定这个Action要在哪一个节点上播放。通过以下代码指定:
[myNode runAction:action];
如果你想在以后使用此动作,可以通过tag来获取:
CCAction *thisAction = [MyNode getActionByTag:234];
当然,你可以通过以下两种方式中的一种来停止该动作:
myNode stopActionByTag:234];
或者
[myNode stopAction:thisAction];
也可以通过以下代码来停止本节点上的所有动作:
[myNode stopAllActions];
预订消息
有时候需要在节点上每隔一段时间执行一些操作,比如每个0.01秒执行一次碰撞检测。在Objective-C中我们称之为【预定消息】,他其实远没有你听起来这么复杂:
首先需要在节点(本例中是HelloWorldLayer)的.h文件中声明一个方法:
-(void) updateInfo;
现在在.m文件中实现这个方法,该方法就是要被反复调用的东东:
static int i = 0;
-(void)updateInfo
{
[lbl setString:[NSString stringWithFormat:@"%d",i++]];
}
关键代码出现在init方法中:
// on "init" you need to initialize your instance
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super" return value
if( (self=[super init])) {
// create and initialize a Label
lbl = [CCLabelTTF labelWithString:@"Hello Cocos" fontName:@"Marker Felt" fontSize:64];
// ask director the the window size
CGSize size = [[CCDirector sharedDirector] winSize];
// position the label on the center of the screen
lbl.position = ccp( size.width /2 , size.height/2 );
// add the label as a child to this Layer
[self addChild: lbl];
[self schedule:@selector(updateInfo) interval:0.01f];
}
return self;
}
节点的schedule方法负责启动一个【预定消息】,updateInfo方法将每隔0.01秒执行一次。
你可以通过schedule方法启动多个预定消息。
你可以通过:
[self unscheduleAllSelectors];
来终止所有的预订的消息。
也可以通过如下代码来终止某一个预订的消息:
[self unschedule:@selector(updateInfo)];
怎么样,很简单把。
场景和层
这句话这重要:CCNode、CCScene、CCLayer这些类是没有视觉表现的,他们是在内部作为场景图的抽象来使用的。
下面是现阶段的我对他们的理解:
CCScene(场景):盛放当前活跃节点,通常是CCLayer,CCMenu一类的东西。任何时候,【当前】场景只有一个。游戏中的场景和影视作品中的场景有很多相同点:
从摄像师的镜头中看,有布景,道具、演员,这些活跃的节点构成了当前Scene。
大多数情况下,场景对象本身并不包含任何游戏相关的代码,而且很少会被子类化。所以他一般都是在CCLayer对象+(id)scene这个静态方法中被实例化的,代码如下:
+(id) scene
{
CCScene *scene = [CCScene node];
CCLayer* layer = [HelloWorld node];
[scene addChild:layer];
return scene;
}
注意,这个代码是CCLayer中的,该Layer负责构造一个CCScene,并且把自己作为子节点放入其中。也就是在儿子中构造爹。
第一个场景是在AppDelegate.m的applicationDidFinishLaunching方法中构造的:
[[CCDirector sharedDirector] runWithScene: [HelloWorldLayer scene]];
这句话可以这样理解:
导演([CCDirector sharedDirector])拿着话筒喊道:第一场戏准备,前景先进入啊(HelloWorldLayer),那个谁,紫薇(CCSprite)骑马上招手(CCAction),你先站到镜头前,Action!
导演有一个重要的职能就是切换场景,代码如下:
[[CCDirector sharedDirector] replaceScene:[NewScene scene]];
当然cocos2d提供了很多很炫的切换场景效果(淡入淡出、翻转进出等等⋯⋯),这些读者可以去自学。
两个场景的相互切换
如果是两个场景相互切换(例如游戏场景和设置场景),可以使用:
[[CCDirector sharedDirector] pushScene:[SettingScene scene]];
来将当前游戏场景压栈,然后把SettingScene的示例压栈,此时设置场景处于场景栈顶,被显示。
当玩家点了"Back to game"以后,可以通过如下代码返回游戏场景
[[CCDirector sharedDirector] popScene];
pushScene和PopScene只适合做两个场景的切换用,我们再也不用在setting场景中为返回哪一个游戏场景烦恼了。堆栈是个好东西。
很明显的是,pushScene和PopScene方法都没有被重载,也就是说,他们不提供场景的切换效果。、
注意
在场景的切换瞬间,对内存是一个考验,因为cocos2d是先生成并加载新场景,然后再释放就场景,在这里应重点测试,尤其是你使用了切换效果的时候。
CCLayer(层)
层是构成场景的主要要素,还记得愤怒的小鸟的这一幕么?
这至少涉及到3个层:
布景层(远山、大树、ANGRY BIRD)
精灵层(飞来飞去的红色小鸟)
控制层(PLAY、离开、设置等按钮,用来接受玩家输入)
和场景一样的是,层没有大小,比如精灵层,并不是只容纳一只小鸟就可以了,你甚至不能说层和显示设备的尺寸一样大。再说一遍,层没有大小,你能说出【科学发展观】的尺寸么?哈。
层只是一个组织的概念,如果你对层使用Action,那么该层上所有的物体都会受到影响,这能为我们的开发带来极大的便利。你可以让两只小红鸟从左边飞入屏幕,同时让三只黄鸟从右边飞入屏幕,你可以把相同或者有共同运动特征的元素放到一个层里面。
层有z轴的顺序,那么用户的触摸事件是如何接收的呢?它并不像你想想的那样,只有最上层的层才能接收事件。事实上在默认情况下,所有的层都是不能接收用户的触摸的,除非你在init方法中用
[self setIsTouchEnabled:YES];
让层接收Touch事件。
可能会有人和你说:“hi,伙计,层用的多的话影响游戏效率!”,这是没有根据的一种论断,曾并不会增加额外的系统开销。但是接受事件的层就不一样了。
事件的本质其实就是高频率的侦听。你应该尽量减少接收用户输入事件的层。事实上最好是只有一个层接收用户事件。我们通常称这种层为ControlLayer,当然这又涉及到一种新的技术,那就是在层与层之间传递控制消息,有一个叫做Pusher的小游戏在这方面做得非常好,如果可能的话,我会在后续的章节中讲解他。
接收触摸事件
触摸事件是IOS设备非常重要的事件,我认为至少70%以上的用户是通过触摸来向设备输入信息的。因此对于触摸事件一定要重点学习。
整体而言,触摸事件分为标准触摸和目标触摸两大类。
标准触摸事件针对整个层而言,支持多点触摸,层的子元素不会接收到这个事件,举这样一个例子把,如果是winform编程的话,这个触摸事件只能被Form接收,而不能被Form上的按钮接收。
如果你想要让层的子节点接收触摸事件,就要使用目标触摸。比如愤怒的小鸟中,当你在屏幕上滑动手指的时候,触发的是标准触摸,背景会跟着手指的移动而移动。
但是当你的时候点到弹弓上小鸟这个Sprite上的时候,就要用目标触摸了。这个事件只能被该目标接收,在该目标意外会被忽略,而且在接收了这个事件以后,你可以选择吃掉这个触摸事件,中断他继续向其他对象发送。
关于触摸事件只说到这里,关于标准触摸和目标触摸会有独立的章节来讲解。
CCSprite
CCSprite是最常用到的类,我们称之为精灵,还以愤怒的小鸟为例,每一只小鸟,每一头猪,每一个玻璃块砖块石头块,白鸟下的蛋蛋,在地上飘落的羽毛,手指放上去会改变样子的菜单按钮⋯⋯这些东西都是精灵。游戏,最终是由精灵来实现的。
精灵往往需要多张图片按照某种频率来循环切换,这些资源必需放置在Resources中,否则你的应用程序将无法找到它。
精灵的位置:
精灵不是一个像素点,哈哈,你是不是觉得我在说梦话,我说的不对么?精灵确实不是一个像素,他有尺寸!那么有一个问题我们需要思考:
既然精灵是有尺寸的,那么如果我们通过sprite.position= CGPointMake(20, 30)为其设置位置的时候,应该将精灵的哪一个位置移动到(20,30)呢?这个精灵上的点,我们称之为定位点(anchor)
默认情况下,节点的点位点是处于节点的中心点的,如下图所示:
我们可以通过:
mySprite.anchorPoint = ccp(0.5, 0.5);
来设置精灵的定位点。
假设图中的矩形是精灵节点,他默认的定位点是中心点,也就是A点。我们可以将ABCDE5个点中的任何一个设置为定位点,代码分别如下:
//A点
mySprite.anchorPoint = ccp(0.5, 0.5);
//B点
mySprite.anchorPoint = ccp(0, 0);
//C点
mySprite.anchorPoint = ccp(1, 0);
//D点
mySprite.anchorPoint = ccp(1, 1);
//E点
mySprite.anchorPoint = ccp(0, 1);
当然,你还可以通过mySprite.anchorPoint = ccp(0.2, 0.4);来将anchor点设置到精灵节点中的其他位置,但是我实在是找不到这样设置的理由:)
贴图大小一个bird精灵之所以能眨眼睛,是因为有很多张贴图在快速的切换。在设计这些贴图素材的时候,尺寸的是定是有讲究的,这要从贴图的存储机制说起。
假设我们设计了一张257X257的贴图,使用32位色(每个像素点的颜色值占4个字节),那么他的存储空间应该是多大呢?这看似是一个简单的计算机基础题:257*257*4=258K,但事实情况是这张图片刚好占用了1M的存储空间。这对于IOS设备有限的内存而言太浪费了!!!
来分析原因:这是因为iOS设备要求任何贴图的尺寸必须符 合“2的n次方”规定。257x257像素的贴图到了iOS设备中以后,系统会自动生 成一张与257x257尺寸最相近的符合“2的n次方”规定的图片(一张512x512像 素的图片),以便于把原贴图放进这个符合规定的“容器”中。而这张512x512 像素的图片占用了1MB的内存空间。
所以在设计贴图的时候应该保证宽度和高度都是2的n次方。比如刚才的例子,应该把图片改为255*255的尺寸,你只需要改变一下其中图片的边距就可以轻松实现。
CCLabel
游戏中总有一些需要显示文字的地方,比如生命数量,得分,剧情提示等等,这些都是使用CCLable的地方。
构造CCLable的过程非常简单,他其实就是一个CCNode的派生类。
CCLabel* label = [CCLabel labelWithString:@"text" fontName:@"AppleGothic" fontSize:32];
[self addChild:label];
你可以改变字体,改变尺寸。但是需要说明的是并非所有的字体都是被支持的,因为从本质上讲游戏中的字体都是用纹理贴图实现的,cocos2d仅仅为我们提供了有限的一些字体:
Family name: AppleGothic
Font name: AppleGothic
Family name: Hiragino Kaku Gothic ProN
Font name: HiraKakuProN-W6
Font name: HiraKakuProN-W3
Family name: Arial Unicode MS
Font name: ArialUnicodeMS
Family name: Heiti K
Font name: STHeitiK-Medium
Font name: STHeitiK-Light
Family name: DB LCD Temp
Font name: DBLCDTempBlack
Family name: Helvetica
Font name: Helvetica-Oblique
Font name: Helvetica-BoldOblique
Font name: Helvetica
Font name: Helvetica-Bold
Family name: Marker Felt
Font name: MarkerFelt-Thin
Family name: Times New Roman
Font name: TimesNewRomanPSMT
Font name: TimesNewRomanPS-BoldMT
Font name: TimesNewRomanPS-BoldItalicMT
Font name: TimesNewRomanPS-ItalicMT
Family name: Verdana
Font name: Verdana-Bold
Font name: Verdana-BoldItalic
Font name: Verdana
Font name: Verdana-Italic
Family name: Georgia
Font name: Georgia-Bold
Font name: Georgia
Font name: Georgia-BoldItalic
Font name: Georgia-Italic
Family name: Arial Rounded MT Bold
Font name: ArialRoundedMTBold
Family name: Trebuchet MS
Font name: TrebuchetMS-Italic
Font name: TrebuchetMS
Font name: Trebuchet-BoldItalic
Font name: TrebuchetMS-Bold
Family name: Heiti TC
Font name: STHeitiTC-Light
Font name: STHeitiTC-Medium
Family name: Geeza Pro
Font name: GeezaPro-Bold
Font name: GeezaPro
Family name: Courier
Font name: Courier
Font name: Courier-BoldOblique
Font name: Courier-Oblique
Font name: Courier-Bold
Family name: Arial
Font name: ArialMT
Font name: Arial-BoldMT
Font name: Arial-BoldItalicMT
Font name: Arial-ItalicMT
Family name: Heiti J
Font name: STHeitiJ-Medium
Font name: STHeitiJ-Light
Family name: Arial Hebrew
Font name: ArialHebrew
Font name: ArialHebrew-Bold
Family name: Courier New
Font name: CourierNewPS-BoldMT
Font name: CourierNewPS-ItalicMT
Font name: CourierNewPS-BoldItalicMT
Font name: CourierNewPSMT
Family name: Zapfino
Font name: Zapfino
Family name: American Typewriter
Font name: AmericanTypewriter
Font name: AmericanTypewriter-Bold
Family name: Heiti SC
Font name: STHeitiSC-Medium
Font name: STHeitiSC-Light
Family name: Helvetica Neue
Font name: HelveticaNeue
Font name: HelveticaNeue-Bold
Family name: Thonburi
Font name: Thonburi-Bold
Font name: Thonburi
应该尽量少的减少对CCLabel显示文本的改变([lable setString:@"newTxt"]),因为每次改变都会导致系统重新渲染一遍,这非常耗时。
CCMenu(菜单)
菜单是游戏中必不可少的东东,cocos2d为我们设计好了一整套Menu。他一样是通过节点的方式来管理的。
看一下这段代码:
CGSize size = [[CCDirector sharedDirector] winSize];
// 设置CCMenuItemFont的默认属性
[CCMenuItemFont setFontName:@"Helvetica-BoldOblique"];
[CCMenuItemFont setFontSize:26];
// 生成几个文字标签并指定它们的选择器
CCMenuItemFont* item1 = [CCMenuItemFont itemFromString:@"Go Back!" target:self selector:@selector(menuItem1Touched:)];
// 使用已有的精灵生成一个菜单项
CCSprite* normal = [CCSprite spriteWithFile:@"Icon.png"];
normal.color = ccRED;
CCSprite* selected = [CCSprite spriteWithFile:@"Icon.png"];
selected.color = ccGREEN;
CCMenuItemSprite* item2 = [CCMenuItemSprite itemFromNormalSprite:normal selectedSprite:selected target:self selector:@selector(menuItem2Touched:)];
// 用其它两个菜单项生成一个切换菜单(图片也可以用于切换)
[CCMenuItemFont setFontName:@"STHeitiJ-Light"];
[CCMenuItemFont setFontSize:18];
CCMenuItemFont* toggleOn = [CCMenuItemFont itemFromString:@"I'm ON!"];
CCMenuItemFont* toggleOff = [CCMenuItemFont itemFromString:@"I'm OFF!"];
CCMenuItemToggle* item3 = [CCMenuItemToggle itemWithTarget:self selector:@selector(menuItem3Touched:) items:toggleOn, toggleOff, nil];
// 用菜单项生成菜单
CCMenu* menu = [CCMenu menuWithItems:item1, item2, item3, nil];
menu.position = CGPointMake(size.width / 2, size.height / 2);
[self addChild:menu];
// 排列对齐很重要,这样的话菜单项才不会叠加在同一个位置
[menu alignItemsVerticallyWithPadding:40];
注释写得很清楚了,我就不再多说了,其中涉及到了:普通文本菜单、精灵菜单、toggle菜单。
恩,文章有点长,如果你是初学者可能只能看懂一小部分,但是只要通过努力,可能一周后你再来阅读本文的时候就会觉得他是小菜一碟了,相信自己,伙计。
本节课所涉及到的代码,我上传了,放出链接:源码
回见。