Cocos2d开发系列(四)

《Learn IPhone andiPad Cocos2d Game Delevopment》的第5章。

一、使用多场景

很少有游戏只有一个场景。这个例子是这个样子的:

这个Scene中用到了两个Layer,一个Layer位于屏幕上方,标有”Herebe your Game Scores etc“字样的标签,用于模拟游戏菜单。一个Layer位于屏幕下方,一块绿色的草地上有一些随机游动的蜘蛛和怪物,模拟了游戏的场景。

1、加入新场景

一个场景是一个Scene类。加入新场景就是加入更多的Scene类。

有趣的是场景之间的切换。使用[CCDirectorreplaceScene]方法转场时,CCNode有3个方法会被调用:OnEnter、OnExit、onEnterTransitionDidFinish。

覆盖这3个方法时要牢记,始终要调用super的方法,避免程序的异常(比如内存泄露或场景不响应用户动作)。

-(void)onEnter {

// node的 init方法后调用.

// 如果使用CCTransitionScene方法,在转场开始后调用.

[superonEnter];

}

-(void )onEnterTransitionDidFinish {

// onEnter方法后调用.

// 如果使用CCTransitionScene方法,在转场结束后调用.

[superonEnterTransitionDidFinish];

}

-(void)onExit

{

// node的dealloc 方法前调用.

// 如果使用CCTransitionScene方法,在转场结束时调用.

[superonExit];

}

当场景变化时,有时候需要让某个node干点什么,这时这3个方法就派上用场了。

与在node的init方法和dealloc方法中做同样的事情不同,在onEnter方法执行时,场景已经初始化了;而在onExit方法中,场景的node仍然是存在的。

这样,在进行转场时,你就可以暂停动画或隐藏用户界面元素,一直到转场完成。这些方法调用的先后顺序如下(使用replaceScene 方法):

  1. 第2个场景的 scene方法

  2. 第2个场景的 init方法

  3. 第2个场景的 onEnter方法

  4. 转场

  5. 第1个场景的 onExit方法

  6. 第2个场景的 onEnterTransitionDidFinish方法

  7. 第1个场景的 dealloc方法

二、请稍候⋯⋯

切换场景时,如果场景的加载是一个比较耗时的工作,有必要用一个类似“Loading,please waiting…”的场景来过渡一下。用于在转场时过渡的场景是一个“轻量级”的Scene类,可以显示一些简单的提示内容:

typedefenum

{

TargetSceneINVALID = 0,

TargetSceneFirstScene,

TargetSceneOtherScene,

TargetSceneMAX,

} TargetScenes;

@interface LoadingScene : CCScene

{

TargetScenes targetScene_;

}

+(id)sceneWithTargetScene:(TargetScenes)targetScene;

-(id)initWithTargetScene:(TargetScenes)targetScene;

@end

#import "LoadingScene.h"

#import "FirstScene.h"

#import "OtherScene.h"

@interface LoadingScene(PrivateMethods)

-(void) update:(ccTime)delta;

@end

@implementation LoadingScene

+(id)sceneWithTargetScene:(TargetScenes)targetScene;

{

return [[[self alloc]initWithTargetScene:targetScene] autorelease];

}

-(id)initWithTargetScene:(TargetScenes)targetScene

{

if ((self = [super init]))

{

targetScene_ = targetScene;

CCLabel* label = [CCLabellabelWithString:@"Loading ..." fontName:@"Marker Felt" fontSize:64];

CGSize size = [[CCDirectorsharedDirector] winSize];

label.position =CGPointMake(size.width / 2, size.height / 2);

[self addChild:label];

[self scheduleUpdate];

}

returnself;

}

-(void) update:(ccTime)delta

{

[selfunscheduleAllSelectors];

switch (targetScene_)

{

case TargetSceneFirstScene:

[[CCDirector sharedDirector] replaceScene:[FirstScene scene]];

break;

case TargetSceneOtherScene:

[[CCDirector sharedDirector] replaceScene:[OtherScene scene]];

break;

default:

// NSStringFromSelector(_cmd) 打印方法名

NSAssert2(nil, @"%@: unsupported TargetScene %i", NSStringFromSelector(_cmd), targetScene_);

break;

}

}

-(void) dealloc

{

CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);

[super dealloc];

}

@end

首先,定义了一个枚举。这个技巧使LoadingScene能用于多个场景的转场,而不是固定地只能在某个场景的切换时使用。继续扩展这个枚举的成员,使LoadingScene能适用与更多目标Scene的转场。

sceneWithTargetScene方法中返回了一个autorelease的对象。在coco2d自己的类中也是一样的,你要记住在每个静态的初始化方法中使用autorelease。

在方法中,构造了一个CCLabel,然后调用scheduleUpdate方法。scheduleUpdate方法会在下一个时间(约一帧)后调用update方法。在update方法中,我们根据sceneWithTargetScene方法中指定的枚举参数,切换到另一个scene。在这个scene的加载完成之前,LoadingScene会一直显示并且冻结用户的事件响应。

我们不能直接在初始化方法initWithTargetScene中直接切换scene,这会导致程序崩溃。记住,在一个Node还在初始化的时候,千万不要在这个scene上调用CCDirector的replaceScene方法。

LoadingScene的使用很简单,跟一般的scene一样:


CCScene* newScene = [LoadingScenesceneWithTargetScene:TargetSceneFirstScene];

[[CCDirectorsharedDirector] replaceScene:newScene];

三、使用Layer

Layer类似Photoshop中层的概念,在一个scene中可以有多个Layer:

typedefenum

{

LayerTagGameLayer,

LayerTagUILayer,

} MultiLayerSceneTags;

typedefenum

{

ActionTagGameLayerMovesBack,

ActionTagGameLayerRotates,

}MultiLayerSceneActionTags;

@classGameLayer;

@classUserInterfaceLayer;

@interface MultiLayerScene :CCLayer 

{

boolisTouchForUserInterface;

}

+(MultiLayerScene*) sharedLayer;

@property (readonly) GameLayer* gameLayer;

@property (readonly) UserInterfaceLayer*uiLayer;

+(CGPoint) locationFromTouch:(UITouch*)touch;

+(CGPoint) locationFromTouches:(NSSet *)touches;

+(id) scene;

@end

@implementation MultiLayerScene

static MultiLayerScene* multiLayerSceneInstance;

+(MultiLayerScene*) sharedLayer

{

NSAssert(multiLayerSceneInstance != nil, @"MultiLayerScenenot available!");

returnmultiLayerSceneInstance;

}

-(GameLayer*) gameLayer

{

CCNode* layer = [selfgetChildByTag:LayerTagGameLayer];

NSAssert([layer isKindOfClass:[GameLayerclass]], @"%@: not aGameLayer!", NSStringFromSelector(_cmd));

return (GameLayer*)layer;

}

-(UserInterfaceLayer*) uiLayer

{

CCNode* layer = [[MultiLayerScenesharedLayer] getChildByTag:LayerTagUILayer];

NSAssert([layer isKindOfClass:[UserInterfaceLayerclass]], @"%@: not aUserInterfaceLayer!", NSStringFromSelector(_cmd));

return (UserInterfaceLayer*)layer;

}

+(CGPoint) locationFromTouch:(UITouch*)touch

{

CGPoint touchLocation = [touchlocationInView: [touch view]];

return [[CCDirectorsharedDirector] convertToGL:touchLocation];

}

+(CGPoint) locationFromTouches:(NSSet*)touches

{

return [selflocationFromTouch:[touches anyObject]];

}

+(id) scene

{

CCScene* scene = [CCScenenode];

MultiLayerScene* layer = [MultiLayerScenenode];

[scene addChild:layer];

return scene;

}

-(id) init

{

if ((self = [superinit]))

{

NSAssert(multiLayerSceneInstance == nil, @"anotherMultiLayerScene is already in use!");

multiLayerSceneInstance = self;

GameLayer* gameLayer = [GameLayernode];

[selfaddChild:gameLayerz:1tag:LayerTagGameLayer];

UserInterfaceLayer* uiLayer = [UserInterfaceLayernode];

[selfaddChild:uiLayerz:2tag:LayerTagUILayer];

}

returnself;

}

-(void) dealloc

{

CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);

[superdealloc];

}

@end

MultiLayerScene 中使用了多个Layer:一个GameLayerh 和一个UserInterfaceLayer 。

MultiLayerScene 使用了静态成员multiLayerSceneInstance 来实现单例。 MultiLayerScene也是一个Layer,其node方法实际上调用的是实例化方法init——在其中,我们加入了两个Layer,分别用两个枚举LayerTagGameLayer 和LayerTagUILayer 来检索,如属性方法gameLayer和uiLayer所示。

uiLayer是一个UserInterfaceLayer,用来和用户交互,在这里实际上是在屏幕上方放置一个菜单,可以把游戏的一些统计数字比如:积分、生命值放在这里:


typedefenum

{

UILayerTagFrameSprite,

}UserInterfaceLayerTags;

@interface UserInterfaceLayer :CCLayer 

{

}

-(bool) isTouchForMe:(CGPoint)touchLocation;

@end

@implementation UserInterfaceLayer

-(id) init

{

if ((self = [superinit]))

{

CGSize screenSize = [[CCDirectorsharedDirector] winSize];

CCSprite* uiframe = [CCSpritespriteWithFile:@"ui-frame.png"];

uiframe.position = CGPointMake(0, screenSize.height);

uiframe.anchorPoint = CGPointMake(0, 1);

[selfaddChild:uiframe z:0tag:UILayerTagFrameSprite];

// 用Label模拟UI控件( 这个Label没有什么作用,仅仅是演示).

CCLabel* label = [CCLabellabelWithString:@"Here be yourGame Scores etc"fontName:@"Courier"fontSize:22];

label.color = ccBLACK;

label.position = CGPointMake(screenSize.width / 2, screenSize.height);

label.anchorPoint = CGPointMake(0.5f, 1);

[selfaddChild:label];

self.isTouchEnabled = YES;

}

returnself;

}

-(void) dealloc

{

CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);

[superdealloc];

}

-(void)registerWithTouchDispatcher

{

[[CCTouchDispatchersharedDispatcher] addTargetedDelegate:selfpriority:-1swallowsTouches:YES];

}

// 判断触摸是否位于有效范围内.

-(bool) isTouchForMe:(CGPoint)touchLocation

{

CCNode* node = [selfgetChildByTag:UILayerTagFrameSprite];

returnCGRectContainsPoint([node boundingBox], touchLocation);

}

-(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event

{

CGPoint location = [MultiLayerScenelocationFromTouch:touch];

bool isTouchHandled = [selfisTouchForMe:location];

if (isTouchHandled)

{

// 颜色改变为红色,表示接收到触摸事件.

CCNode* node = [selfgetChildByTag:UILayerTagFrameSprite];

NSAssert([node isKindOfClass:[CCSpriteclass]], @"node is not a CCSprite");

((CCSprite*)node).color = ccRED;

// Action:旋转+缩放.

CCRotateBy* rotate = [CCRotateByactionWithDuration:4angle:360];

CCScaleTo* scaleDown = [CCScaleToactionWithDuration:2scale:0];

CCScaleTo* scaleUp = [CCScaleToactionWithDuration:2scale:1];

CCSequence* sequence = [CCSequenceactions:scaleDown, scaleUp, nil];

sequence.tag = ActionTagGameLayerRotates;

GameLayer* gameLayer = [MultiLayerScenesharedLayer].gameLayer;

// 重置GameLayer 属性,以便每次动画都是以相同的状态开始

[gameLayer stopActionByTag:ActionTagGameLayerRotates];

[gameLayer setRotation:0];

[gameLayer setScale:1];

// 运行动画

[gameLayer runAction:rotate];

[gameLayer runAction:sequence];

}

return isTouchHandled;

}

-(void) ccTouchEnded:(UITouch*)touch withEvent:(UIEvent *)event

{

CCNode* node = [selfgetChildByTag:UILayerTagFrameSprite];

NSAssert([node isKindOfClass:[CCSpriteclass]], @"node is not aCCSprite");

// 色彩复原

((CCSprite*)node).color = ccWHITE;

}

@end

 

为了保证uiLayer总是第一个收到touch事件,我们在 registerWithTouchDispatcher 方法中使用-1的priority。并且用 isTouchForMe 方法检测touch是否处于Layer的范围内。如果在,touchBegan方法返回YES,表示“吃掉”touch事件(即不会传递到下一个Layer处理);否则,返回NO,传递给下一个Layer(GameLayer)处理。

而在GameLayer中,registerWithTouchDispatcher 的priority是0

以下是GameLayer代码:

@interface GameLayer : CCLayer 

{

CGPointgameLayerPosition;

CGPointlastTouchLocation;

}

@end

@interface GameLayer(PrivateMethods)

-(void) addRandomThings;

@end

@implementation GameLayer

-(id) init

{

if ((self = [superinit]))

{

self.isTouchEnabled = YES;

gameLayerPosition = self.position;

CGSize screenSize = [[CCDirectorsharedDirector] winSize];

CCSprite* background = [CCSpritespriteWithFile:@"grass.png"];

background.position = CGPointMake(screenSize.width / 2, screenSize.height / 2);

[selfaddChild:background];

CCLabel* label = [CCLabellabelWithString:@"GameLayer"fontName:@"MarkerFelt"fontSize:44];

label.color = ccBLACK;

label.position = CGPointMake(screenSize.width / 2, screenSize.height / 2);

label.anchorPoint = CGPointMake(0.5f, 1);

[selfaddChild:label];

[selfaddRandomThings];

self.isTouchEnabled = YES;

}

returnself;

}

// 为node加上一个MoveBy的动作(其实就是在围绕一个方框在绕圈)

-(void)runRandomMoveSequence:(CCNode*)node

{

float duration = CCRANDOM_0_1() * 5 + 1;

CCMoveBy* move1 = [CCMoveByactionWithDuration:duration position:CGPointMake(-180, 0)];

CCMoveBy* move2 = [CCMoveByactionWithDuration:duration position:CGPointMake(0, -180)];

CCMoveBy* move3 = [CCMoveByactionWithDuration:duration position:CGPointMake(180, 0)];

CCMoveBy* move4 = [CCMoveByactionWithDuration:duration position:CGPointMake(0, 180)];

CCSequence* sequence = [CCSequenceactions:move1, move2, move3,move4, nil];

CCRepeatForever* repeat = [CCRepeatForeveractionWithAction:sequence];

[node runAction:repeat];

}

// 模拟一些游戏对象,为每个对象加上一些动作(绕圈).

-(void) addRandomThings

{

CGSize screenSize = [[CCDirectorsharedDirector] winSize];

for (int i = 0; i < 4; i++)

{

CCSprite* firething = [CCSpritespriteWithFile:@"firething.png"];

firething.position = CGPointMake(CCRANDOM_0_1() * screenSize.width, CCRANDOM_0_1() * screenSize.height);

[selfaddChild:firething];

[selfrunRandomMoveSequence:firething];

}

for (int i = 0; i < 10; i++)

{

CCSprite* spider = [CCSpritespriteWithFile:@"spider.png"];

spider.position = CGPointMake(CCRANDOM_0_1() * screenSize.width, CCRANDOM_0_1() * screenSize.height);

[selfaddChild:spider];

[selfrunRandomMoveSequence:spider];

}

}

-(void) dealloc

{

CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);

//don"t forget to call "super dealloc"

[superdealloc];

}

-(void)registerWithTouchDispatcher

{

[[CCTouchDispatchersharedDispatcher] addTargetedDelegate:selfpriority:0swallowsTouches:YES];

}

-(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event

{

// 记录开始touch时的位置.

lastTouchLocation = [MultiLayerScenelocationFromTouch:touch];

//先停止上一次动作,以免对本次拖动产生干扰.

[selfstopActionByTag:ActionTagGameLayerMovesBack];

//吃掉所有touche

returnYES;

}

-(void) ccTouchMoved:(UITouch*)touch withEvent:(UIEvent *)event

{

//记录手指移动的位置

CGPoint currentTouchLocation =[MultiLayerScenelocationFromTouch:touch];

//计算移动的距离

CGPoint moveTo = ccpSub(lastTouchLocation,currentTouchLocation);

//上面的计算结果要取反.因为接下来是移动前景,而不是移动背景

moveTo = ccpMult(moveTo,-1);

lastTouchLocation =currentTouchLocation;

//移动前景——修改Layer的位置,将同时改变Layer所包含的nodeself.position = ccpAdd(self.position, moveTo);

}

-(void) ccTouchEnded:(UITouch*)touch withEvent:(UIEvent *)event

{

//最后把Layer的位置复原.Action:移动+渐慢

CCMoveTo* move = [CCMoveToactionWithDuration:1position:gameLayerPosition];

CCEaseIn* ease = [CCEaseInactionWithAction:move rate:0.5f];

ease.tag =ActionTagGameLayerMovesBack;

[selfrunAction:ease];

}

@end


为了让程序运行起来更有趣,GameLayer中加入了一张青草的背景图,以及一些游戏对象,并让这些对象在随机地移动。这部分内容不是我们关注的,我们需要关注的是几个touch方法的处理。

1、ccTouchBegan :

由于GameLayer是最后收到touch事件的Layer,我们不需要检测touch是否在Layer范围(因为传给它的都是别的Layer“吃剩下”的touch)。所以GameLayer的touchBegan方法只是简单的返回YES(“吃掉”所有touch)。

2、ccTouchMoved:

在这里我们计算手指移动的距离,然后让Layer作反向运动。为什么要作“反向”运动?因为我们想制造一种屏幕随着手指划动的感觉,例如: 当手向右划动时,屏幕也要向右运动。当然,iPhone不可能真的向右运动。要想模拟屏幕向右运动,只需让游戏画面向左运动即可。因为当运动物体在向前移动时,如果假设运动物体固定不动,则可以认为是参照物(或背景)在向后运动。

3、ccTouchEnded:

在这里,我们把Layer的位置恢复到原位。

 

四、其他

这一章还讨论了很多有用的东西,比如“关卡”。是使用Scene还是Layer作为游戏关卡?

作者还建议在设计Sprite时使用聚合而不要使用继承。即Sprite设计为不从CCNode继承,而设计为普通的NSObject子类(在其中聚合了CCNode)。

此外还讨论了CCTargetToucheDelegate、CCProgressTimer、CCParallaxNode、vCCRibbon和CCMotionStreak。

这些东西可以丰富我们的理论知识,但没有必要细读。

 

文章导航