《Learn IPhone andiPad Cocos2d Game Delevopment》的第5章。
## 一、使用多场景
很少有游戏只有一个场景。这个例子是这个样子的:![](https://box.kancloud.cn/2016-05-04_572a0b97c29b4.gif)
这个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。
这些东西可以丰富我们的理论知识,但没有必要细读。