《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。
這些東西可以豐富我們的理論知識,但沒有必要細讀。
?