SpriteBuilder 學習筆記七

Chapter 8node

Selecting and Unlocking Levels編程

對於這個遊戲,你將給玩家一個選擇從哪一關開始的方法。由於levels都是分開的,你將使用一個Scroll View Node來讓玩家選擇。app

另外,你會但願有一個有彈性的方法去添加更多的levels,追蹤哪一level已經被玩家解鎖。GameState類在這時頗有幫助。less

固然,你必須添加額外的level CCB文件。你也會學習如何對當前app中得level文件計數。ide

你還會完成更多的menus。學習

 

Adding the Content Node測試

從設計Scroll View's的內容node開始。ui

Content node是一個容器,其內容能夠滾動。這樣的話,你將設計每一個world的level,做爲獨立的page,稍後,在Scroll View node中enable pagination,這樣每一個world menu就會在屏幕中居中。spa

在scrolling view中,Content node的內容尺寸決定了可滑動區域的尺寸。Scroll View的內容尺寸決定了用於能夠觸摸和拖動的區域。同時也決定了Conten node的邊界。設計

Note:內容會被繪製在Scroll View外面。若是你須要內容僅僅在Scroll View區域,你須要在上面繪製一個sprite。或者你能夠在程序中添加Scroll View到CCClippingNode。

內容的分頁要求你考慮the Scroll View automatically determines the number of pages based on the ratio of the Scroll View size in relation to the Content node size。

舉例來講,若是你須要五個獨立的,水平滾動的頁面,每一個寬爲100 points,Scroll View的寬必須是100,總Content node的尺寸必須是500.這假設了頁面沒有空白。若是在獨立的頁面中又空白,你還要考慮空白的尺寸。

建立一個新的CCB文件在UserInterface文件夾中,命名爲MainMenuLevelSelect.ccb。類型必須設置爲Node。選擇root node,在Item Properties,設置內容類型爲%。由於你須要在Scroll Views上添加3個頁面,而且只容許水平滾動,你必須設置content尺寸爲300%寬,100%高。這會讓Scroll View的Content Node三倍大於Scroll View。

你應該同時命名rool node爲levelSelect。在Item Code Connetions,須要設置一個自定義類,命名爲MainMenuLevelSelect。

在Tileless Editor View中,拖動W1_bg,W2_bg等背景圖片,一共須要拖動三個。這三幅圖都是411x290points尺寸的。

如今,這三幅圖極可能在相同位置,你須要在水平方向上把空間均勻出來。W1_bg的位置應該是0x0,W2_bg的位置應該是441x0,W3_bg的位置應該是882x0。圖片之間都添加了30-point-wide的空白。是否添加空白是可選的。另外,設置每一個background sprite的anchor point爲0x0。如圖:

Adding the Scroll View Node

打開MainScene.ccb,轉換到Node Library View,拖動一個Scroll View node到stage。選中Scroll View node,編輯position類型爲%,值爲50,anchor point爲0.5x0.5.

如今,重要的事情是:Scroll View的內容尺寸。每一個單獨的background圖片(page)是411points寬。你添加了30points的空白,這意味着一個單獨page是411 + 30 points寬,輸入441x290做爲Scroll View的內容尺寸。移動到CCScrollView屬性。

第一步應該是設置Content node值。設置爲UserInterface/MainMenuLevelSelect.ccb。保存後應該能直接看到效果,如圖:

Scroll View node應該僅僅能水平移動;所以,把Vertical scroll的勾去掉。Bounces設置決定了邊緣的表現。好比說,若是你在第一幅page中向左移動,或者在最後一幅page中向右移動,而且你勾選了Bounces,你能夠向這個方向稍微拖動一下node,可是它會反彈回來。若是沒有勾選Bounces,那麼效果僅僅是沒法拖動node。Bouncing讓用戶能知道滾動內容的邊界。最後,Paging enabled將每一頁滾動和snap到位。沒有paging的話,you can scroll through the Content node without any kind of snapping.

在Item Code Connection,你應該分配一個Doc root var。輸入_levelSelectScrollView。

由於空白的緣由,效果如上,這種狀況通常不是一個問題,由於它讓你看到右側有更多東西,鼓勵用戶去滑動。一樣當你到最後一頁時,空間會更清晰。可是,有時候,讓Scroll View頁在stage上居中更重要。爲了抵消這30point空白的效果,你必須再添加15points給Scroll View的X position。爲了這樣作,改變Scroll View的Xposition類型爲points,輸入299(284+15),再把position type改成%。

 

Showing the Scroll View Popover

在你嘗試Scroll View以前,你必須建立MainMenuLevelSelect類和_levelSelectScrollView變量。

打開MainScene.m,添加代碼:

#import "MainScene.h"

@implementation MainScene {
    __weak CCScrollView *_levelSelectScrollView;
}
- (void)didLoadFromCCB {
    NSLog(@"scroll View:%@", _levelSelectScrollView);
}
- (void)ChangeToGameScene {
    CCScene *scene = [CCBReader loadAsScene:@"GameScene"];
    CCTransition *transition = [CCTransition transitionFadeWithDuration:1.5];
    [[CCDirector sharedDirector] presentScene:scene withTransition:transition];
}
@end

下一步,建立一個新的類,命名爲MainMenuLevelSelect,繼承自CCNode。在.h文件中,添加代碼:

#import "CCNode.h"
@class MainMenuButtons;
@interface MainMenuLevelSelect : CCNode
@property (weak) MainMenuButtons *mainMenuButtons;
@end

在MainMenuButtons.m中,添加代碼:

#import "MainMenuButtons.h"
#import "SettingsLayer.h"
#import "MainMenuLevelSelect.h"
@implementation MainMenuButtons {
    __weak MainMenuLevelSelect *_levelSelect;
}
- (void)didLoadFromCCB {
    _levelSelect = (MainMenuLevelSelect*)[self.parent getChildByName:@"levelSelect" recursively:YES];
    _levelSelect.parent.visible = NO;
    _levelSelect.mainMenuButtons = self;
}

首先輸出了一個新的MainMenuLevelSelect類頭文件,以便去添加類的變量_levelSelect.

在didLoadFromCCB中,你經過MainMenuLevelSelect實例的名字獲取引用。你之前常常這樣作,可是慢着,MainMenuButtons類和Scroll View或者Scroll View的Content node沒有關聯,意味着他們不是MainMenuButtons類的children。若是你仔細看getChildByName:方法,你會注意到它實際上發送給MainMenuButtons的parent node。由於MainMenuButtons是MainScene的child,self.parent是MainScene實例的引用。MainScene包含做爲child的Scroll View,而且Scroll View包含做爲child的Content node。於是,MainMenuLevelSelect實例能夠經過名字被遞歸找到。

隱藏CCScrollView實例而不是MainMenuLevelSelect實例是很重要的,由於使全部CCScrollView的觸摸事件失效。若是你僅僅讓_levelSelect invisible,它仍然有相同的視覺效果,可是用戶不能操做了。

最後,_levelSelect被分配給一個指向MainMenuButtons實例的引用,這樣它就能夠以後發送show message給MainMenuButtons實例,當能過戶關閉MainMenuLevelSelect popover時。

剩下的工做是更新shouldPalyGame方法,代碼以下:

- (void)shouldPlayGame {
    _levelSelect.parent.visible = YES;
    self.visible = NO;
    NSLog(@"Play");
}

這讓CCScrollView visible而且隱藏MainMenuButtons實例。

注意,不像SettingsLayer,CCScrollView實例並不從scen中移除,當應該顯示的時候才被重載;你僅僅是簡單地改變了它的visible狀態。相比於load一個CCB或者在程序中建立一個新的node實例,改變visible狀態更有效。

雖然改變visible狀態也須要耗費內存,可是,若是你頻繁須要一個node,你必須有足夠的類存讓它在任什麼時候間顯示出來。

也就是說,讓CCScrollView和它的Content node一直在scene中是一個實用的作法。你須要建立一個額外的CCB文件,它包含了Scroll View node,以即可以用CCBReader載入Scroll View和它的Content node。如今,當Scroll View node 被直接添加到MainScene.ccb中時,只有MainMenuLevelSelect.ccb(Scroll View的Content node)是一個單獨的CCB實例。因此僅須要用CCBReader載入MainMenuLevelSelect.ccb。

若是你想從它的parent node中移除CCScrollView,你必須在程序中建立另外一個CCScrollView的實例,用載入的Content node初始化它,或者把它分配Scroll View的contentNode屬性----可是僅僅在第二次之後,further complicating the code。

Tip:永遠記住:Keep it simple stupid,或者KISS原則。

運行APP,你如今能夠滑動3個背景圖片,注意到你如今還能夠敲擊下方的buttons。

 

Designing the Scroll View Content Node

你如今沒法作的事情是真正的敲擊一個level而且玩,你也不能關閉Scroll View的popover。爲了作到這些,設計level-selection pages,這樣每一個page都表明一個world。

從添加每一個page上得close button開始。打開MainMenuLevelSelect.ccb。拖動CloseButton.ccb到每一個W#_bg sprite上。最好是直接拖動每一個button到W#_bg sprite中,這樣它就變成了page background image的child了。

選擇每一個CloseButton,改變它的Position類型爲%,值爲100.

如今,添加loge和title。在Tileless Editor View中,拖動W1_logo到W1_bg sprite上,拖動W1_title到W1_logo sprite上。W1_title應該是W1_logo的child,W1_logo應該是W1_bg的child。對W2和W3重複這一過程。如圖:

選擇每一個logo和titleimage,改變它們的位置。logo position類型應該是%,值是50 x 85,title position

類型應該是percent,值是50x0。對於3個level buttons,你最好使用Box Layout node來水平對齊它們。拖動一個Box Layout到每一個W#_bg sprite上,改變Box Layout node的位置類型爲%,值爲50x35.同時改變anchor point爲0.5x0.5,這樣child nodes會在Box layout node的位置處居中,Spacing 應該是30。

你應該拖動W#_l1,W#_l2和W#_l3圖片,按順序添加到CCLayout Box node中,這樣,每一個Box Layout node都有3個sprites children。如圖:暫時忽略CCButton nodes。

改變全部W#_l# sprite(除了W1_l1)爲light gray---color code爲999999。由於第一關固然是永遠解鎖過的。

而後,添加一個button座位每一個W#_l# sprite的child,一共9個buttons。可是你如今僅僅應該添加一個button,編輯屬性,而且copy和paste它8次。

button的位置和anchor point 應該都是0x0。改變preferred size type爲%,值爲100x100.清楚Title field,改變Sprite frame屬性爲Normal和Highlighted State爲NULL。

在Button的Item Code Connections中,輸入shouldLoadLevel:座位selector。注意後面的:,由於你將做爲一個參數接收button。

如今你能夠copy和paste button8次。而後拖動到W#_l#sprite中。

最後,你須要一個方法去給 buttons肯定身份,這樣當button被敲擊的時候,你就知道你應該loading哪一Level了。

一個方法是對第一個button輸入名字爲1,第二個爲2,最後一個是9.確保輸入的都是數字。

最後的視覺效果以下:

 

Unlocking Levels

在你對level-selection buttons編程以前,你必須更新GameState類,以記錄哪些levels已經解鎖,和最近玩的level。

打開GameStat.h,添加以下代碼:

#import <Foundation/Foundation.h>

@interface GameState : NSObject
+ (GameState*)sharedGameState;
@property CGFloat musicVolume;
@property CGFloat effectsVolume;

@property int currentLevel;
@property (readonly)int highstUnlockedLevel;
- (BOOL)unlockNextLevel;
@end

currentLevel是正在被玩的level。在unlockNextLevel方法中將要用到它。highestUnlockedLevel表示玩家能夠玩的level的最高編號。自由unlockNextLevel方法能夠改變它。在GameState.m中添加以下代碼:

static NSString *keyForUnlockedLevel = @"unlockedLevel";
- (void)setHighstUnlockedLevel:(int)level {
    int totalLevelCount = 9;
    if(_currentLevel > 0 && _currentLevel <= totalLevelCount) {
        [[NSUserDefaults standardUserDefaults]setInteger:level forKey:keyForUnlockedLevel];
    }
}
- (int)highstUnlockedLevel {
    NSNumber *number = [[NSUserDefaults standardUserDefaults] objectForKey:keyForUnlockedLevel];
    return (number? [number intValue] : 1);
}

setter方法定義了level的最大值---一共9levels。currentLevel屬性在存儲到NSUserDefaults前先被檢測是否在1到9之間。getter方法獲取存儲過的NSNumber,而且返回它的intValue或者1(若是KeyForUnlockedLevel還咩有key的話)。返回1是由於first level應該永遠是解鎖過的。

未解鎖的levels在unlockNextLevel方法中被完成了,添加方法:

- (BOOL)unlockNextLevel {
    int highest = self.highstUnlockedLevel;
    if(_currentLevel >= highest ) {
        [self setHighstUnlockedLevel:_currentLevel + 1];
    }
    return (highest < self.highstUnlockedLevel);
}

當前的highestUnlockedLevel是經過self.highestUnlockedLevel,從屬性setter中獲得的,而後分配個highest變量。若是currentLevel大於等於highest number。setHighestUnlockedLevel:setter被調用,這樣就能夠解鎖當前level的下一level。最後,該方法返回一個BOOL值,代表下一level是否被解鎖,(經過比較前highest數和當前highestUnlockedLevel)

 

Highlighting Level Buttons

level buttons須要被解鎖----也就是說,它們的顏色被設置到白色----若是它們的level目前能夠被訪問。「可訪問」意味着按鈕的level 數字小於等於highestUnlockedLevel。你同時想要啓動GameScene,而且載入button的對應level文件。

在MainMenuLevelSelect.m中添加以下代碼。在didLoadFromCCB中的代碼會列舉全部的buttons,和當前的highestUnlockedLevel相比較。若是button的level應該被解鎖,button的parent sprite 就設置顏色爲白色,移除昏暗效果。代碼以下:

#import "MainMenuLevelSelect.h"
#import "MainMenuButtons.h"
#import "SceneManager.h"
#import "GameState.h"
@implementation MainMenuLevelSelect
- (void)didLoadFromCCB {
    int count = 1;
    int highest = [GameState sharedGameState].highstUnlockedLevel;
    CCNode *button;
    while((button = [self getChildByName:@(count).stringValue recursively:YES])) {
        if (button.name.intValue <= highest) {
            CCSprite *sprite = (CCSprite*)button.parent;
            sprite.color = [CCColor whiteColor];
        }
        count++;
    }
}
@end

 注意@(count).stringValue,在OC中,你能夠用這種語法初始化arrays,dictionaries,numbers。

好比:

NSArray * array = @[obj1,obj2,obj3];

NSDictionary *dict = @{key1:obj1,key2:obj2,key3:obj3};

NSNumber *number = @(1234);

NSNumber *number = @(YES);

NSNumber *number = @(count);

這種語法比使用常規方法要更簡潔更短。

Note:爲何不使用NSNumber呢?很簡單:NSNumber是一個不變類。你必須建立一個新的NSNumber object,若是值必須改變的話。一樣也沒有NSMutableNumber類。

 

Closing the Level-Selection Popover

level-selection popover能夠用CloseButton.ccb實例關閉。

在MainMenuLevelSelect.m中添加以下代碼:

- (void)shouldClose {
    self.parent.visible = NO;
    [_mainMenuButtons show];
}

Content node的parent實例是Scroll View。因此讓parent的visible狀態爲NO,而後讓MainMenuButtons自身顯示。

 

Loading Levels

剩下爲每一關注冊button presses,而且測試level有沒有unlocked。若是有,就load level;不然,你能夠添加一個拒絕的sound effect。

在MainMenuLevelSelect.m中添加代碼:

// loading a level or not
- (void)shouldLoadLevel:(CCButton*)sender {
    GameState *gameState = [GameState sharedGameState];
    int levelNumber = sender.name.intValue;
    if (levelNumber <= gameState.highstUnlockedLevel) {
        gameState.currentLevel = levelNumber;
        [SceneManager presentGameScene];
    }else {
        //maybe play a 'access denied' sound effect here
    }
}
@end

像以前同樣,button的name屬性經過intValue被轉化爲int。如今打開ScenenManeger.m,首先import GameScene和GameState 頭文件。以下:

#import "SceneManager.h"
#import "GameScene.h"
#import "GameState.h"

找到presentGameScene方法,用下面內容替換:

+ (void)presentGameScene {
    //id s = [CCBReader loadAsScene:@"GameScene"];
    //id t = [CCTransition transitionMoveInWithDirection:CCTransitionDirectionRight duration:1.0];
    //[[CCDirector sharedDirector] presentScene:s withTransition:t];
    CCScene *scene = [CCBReader loadAsScene:@"GameScene"];
    GameScene *gameScene = (GameScene*)scene.children.firstObject;
    int levelNumber = [GameState sharedGameState].currentLevel;
    NSString *level = [NSString stringWithFormat:@"Levels/Level%i",levelNumber];
    [GameScene loadLevelNamed:level];
    id t = [CCTransition transitionPushWithDirection:CCTransitionDirectionLeft duration:1.0];
    [[CCDirector sharedDirector]presentScene:scene withTransition:t];
}

GameScene像往常同樣,使用CCBReader的loadAsScene方法載入。loadAsScene方法返回一個通用CCScene對象,它的惟一child永遠是載入的CCB的root node。當前level number轉變爲一個string。

Tip:開發者們仍然頻繁的用額外的0填充文件名,以對抗:數字排序。好比說,Level0001,由於你也許有上千個level。請不要這麼作!現代OS知道numeric sorting問題。string-formatting代碼段:Level%i適用全部數字。

 

使用GameScene實例的引用,你能夠發送LoadLevelNamed:方法,傳遞生成的level string。你應該在顯示scene以前作完這些,這樣scene在渲染以前就所有準備好了。loadLevelNamed:方法也是你以前用過的,除了你尚未從其餘類中使用它。爲了不編譯器抱怨沒有找到selector,你必須打開GameScene.h,添加以下:

- (void)loadLevelNamed:(NSString*)levelCCB;

在GameScene.m中,你須要移除[self loadLevelNamed:nil];

儘管它如今能夠成功的載入第一level,但這是錯誤的。畢竟,第一level已經經過GameScene.ccb引用了,至今,你尚未載入特殊的level。你如今應該改變這一點,經過更新loadLevelNamed:方法。以下:

- (void)loadLevelName:(NSString*)levelCCB {
    [_levelNode removeFromParent];
    CCNode * level = [CCBReader load:levelCCB];
    [self addChild:level];
    _levelNode = level;

_levelNode是一個Sub File node的引用,如今時Level1.ccb。載入一個level首先應該移除當前存在的level。而後CCBReader載入由MainMenuLevelSelect.m中得shouldLoadLevelNamed:方法生成的levelCCB string。而後level做爲一個child加載如GameScene。

最後,_levelNode引用被新的level替換。注意由於_levelNode是一個__weak引用,因此你不能直接把CCBReader load:方法返回的node分配給它。在OC運行時,它會被置爲nil。

若是你如今運行,按下Play,而後敲擊first-level button,你應該能進入GameScene。可是如今沒有pause button了。

至今,GameScene必須依靠SpriteBuilder中得nodes順序決定繪製的順序。level content最早被繪製,而後是GameMenuLayer在level上面一層被繪製。

可是,如今你移除了_levelNode.而且載入了一個新的,而且做爲child node加入了GameScene。添加一個新的node永遠會被放在list的末尾;所以,繪製順序如今就反了,你就再也不可以看到pause button。

爲了修復這一點,用下面的代碼取代addChild這一行:

    level.zOrder = -1;
    [self addChild:level];

zOrder屬性決定了node得繪製順序。有着更低的zOrder的nodes在有着更高的zOrder的nodes以前繪製,可是它們必須有同一個parent。注意這兩行代碼也能夠寫成:[self addChild:level z:-1];

如今,載入levels能夠工做!可是還有要考慮的,如今GameScene在GameScene.ccb中引用了Level1.ccb。因此當你載入GameScene,它會載入Level1.ccb,而後用新的level替換。這有些不效率。

 

Adding a Dummy Level

爲了修復這一點,在SpriteBuilder中,右鍵點擊Levels文件夾,選擇New File,重命名爲DummyLevel.ccb,設置類型爲Node。這就能夠了,你如今僅僅須要一個空的CCB。

如今打開GameScene.ccb,選擇level Content node。在Item Properties,點擊CCB File下拉菜單。選擇Levels/DummyLevel.ccb。

 

Winning Levels

若是你通關而且到達了exit會發生什麼呢?如今是時候解決這個問題而且解鎖下一level了。

在SpriteBuilder中,右鍵點擊UserInterface/Popovers文件夾,添加一個New File,命名爲LevelCompleteLayer.ccb,設置類型爲layer。改變root node 的content size 類型爲%,值爲100x100.轉到Item Code Connections,輸入GameMenuLayer做爲類名。你須要一個background image,label和button。

從Tileless Editor View中,拖動以_bg結尾的圖片到stage上。必須,我使用W2_bg。改變background sprite的位置類型爲%,值爲50x50.

而後,轉到Node Library View,拖動一個LabelTTF Node

到background sprite,這樣label成爲sprite的child。改變label的位置類型爲%,值爲50x66。設置label的title爲「Level Complete」.

最後,添加一個Button,拖動到background sprite上,做爲一個child。改變button的位置類型爲%,值爲50x30.

 首選尺寸爲170x50。button的Title應該是Next Level。改變Normal State和Highlighted State的SpriteFrame,爲P_resume.png,改變Highlighted State的Background和Label color爲99999.

別忘記設置selector,輸入shouldLoadNextLevel。視覺效果以下圖:

Tip:若是你想要讓這個popover更加功能齊全,考慮添加retry和exit button。畢竟,popover和其餘的popovers使用相同的GameMenuLayer類,因此只要把button的selectors鏈接到shouldRestartGame和shouldExitGame便可。

回到Xcode,打開GameScene.m,import GameState頭文件:

#import "GameScene.h"
#import "Trigger.h"
#import "GameMenuLayer.h"
#import "GameState.h"

而後,定位到參數是player何exit的physics collision方法,替換爲下面的代碼:

- (BOOL)ccPhysicsCollisionBegin:(CCPhysicsCollisionPair *)pair player:(CCNode *)player exit:(CCNode *)exit {
    [[GameState sharedGameState]unlockNextLevel];
    [[GameState sharedGameState] synchronize];
    [self showPopoverNamed:@"UserInterface/Popovers/LevelCompleteLayer"];
    return NO;
}

這解鎖了下一個level。你能夠在任何一個level完成的時候調用這個方法。甚至當你從新玩level1,它也不會從新設置highest unlocked level爲level2.GameState是同步的。

注意到synchronize方法多是未定義的,若是沒有定義,你能夠添加在GameState.h和GameState.m,或者用下面的代碼替換synchronize:

[[NSUserDefaults standardUserDefaults] synchronize];。另外,固然了,LevelCompleteLayer被顯示爲一個popup。那麼,在GameMenuLayer.m中,添加一個import「GameState.h」以下:

#import "GameMenuLayer.h"
#import "GameScene.h"
#import "SceneManager.h"
#import "GameState.h"

而後,添加以下的selector:

- (void)shouldLoadNextLevel {
    [GameState sharedGameState].currentLevel += 1;
    [SceneManager presentGameScene];
}

這段代碼很直接。由於player即將進入下一level,因此當前level增長1。presentGameScene方法則呈現一個新的GameScene實例,而且載入當前level。

 

Adding More Levels

有一個簡單的方法添加更多的levels。打開Finder,找到Packages/SpriteBuilder Resources.sbpack文件夾。你能夠看Level1.ccb和DummyLevel.ccb文件。

你能夠複製Level1.ccb。

 

Counting Level Files

你將必須計算Level#.ccb文件的數量。在GameState.m中添加代碼是徹底可選的。記住,however,that if you do add it,unlocking more levels will work only if you have consecutively named Level#.ccb files in SpriteBuilder.

添加代碼:

- (int)levelCount {
    NSBundle *mainBundle = [NSBundle mainBundle];
    NSString *path;
    int count = 0;
    do {
        count++;
        NSString *level = [NSString stringWithFormat:@"Level%i",count];
        path = [mainBundle pathForResource:level ofType:@"ccbi" inDirectory:@"Published-iOS/Levels"];
    }while(path != nil);
    count --;
    return count;
}

爲了使用levelCount方法,編輯setHighestUnlockedLevel:方法,替換第一行代碼,以下:

- (void)setHighstUnlockedLevel:(int)level {
    int totalLevelCount = [self levelCount];
    //int totalLevelCount = 9;
    if(_currentLevel > 0 && _currentLevel <= totalLevelCount) {
        [[NSUserDefaults standardUserDefaults]setInteger:level forKey:keyForUnlockedLevel];
    }
}

 

Showing the Correct Level-Selection Page

最後,可是也一樣重要的,讓咱們返回到ScrollView。當你完成

相關文章
相關標籤/搜索