Cocos2d开发系列(四)

最后更新于:2022-04-01 11:38:59

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