天天看點

iOS GameCenter 挑戰,排名

你也許曾聽說過Game Center,它是自打iOS 4.1被引入的線上多人社交遊戲網絡,支援玩家邀請好友一起玩兒遊戲,還可以建立一個多人遊戲的會話,追蹤成就系統,以及其他功能。

除了可以讓開發者更輕松的實作一些基本功能外,它還改善了另一個基礎問題:app推廣。如今App Store上有超過1百萬款app,單個使用者發現你的app的機率将會非常低。Game Center通過好友系統改善了此類問題,你可以檢視你的好友都在玩兒些什麼遊戲,是以你的遊戲的曝光率被增加了。

iOS 6.0為Game Center引入了一系列新的API,它們不僅可以增加你的遊戲曝光率,而且還能增加使用者粘性。其中一項功能是挑戰好友,即使你的Gamecenter好 友們沒有安裝此遊戲,也可以邀請向他們發送挑戰。例如,一個玩家在你的遊戲裡得到了高分,他可以向他的朋友發送一個挑戰邀請并且說:“嘿,來試試赢我 啊!”

當朋友接收到挑戰後,會立即看到消息中你的遊戲的連結。不難想象這一特性能夠成倍的增加使用者留存率!由于考慮到Game Center上龐大的使用者群體,這一點足夠說明你應該在遊戲中添加挑戰功能。

使用挑戰之前要先使用Game Center,是以本篇教程将首先帶你整體過一遍Game Center,包括設定Game Center并添加一個簡單的排行榜,同時會在過程中指出iOS 6新增的内容。

注意: 本篇教程要求你熟悉Cocos2D并且基于它制作過遊戲。如果你是Cocos2D的新手,可以先在本網站學習Cocos2D系列教程。

跳躍猴遊戲

首先下載下傳初始工程 – 跳躍猴!

MonkeyJump(跳躍猴)是一個簡單的橫向卷軸遊戲,它由我最喜歡的遊戲引擎Cocos2D制作的。它是基于一個由Cocos2D學習工作室制作的叫做CatJump的遊戲。我向其中加了一些有趣的元素,另外Vicki Wenderlich為遊戲制作了新的美術資源。

遊戲中的主要角色很明顯,是一隻猴子,遊戲的主要目标讓猴子達到最遠的距離同時躲避敵人。

MonkeyJump非常容易上手,即使是你的媽媽也能玩兒!隻需要輕點螢幕就可以讓猴子跳過敵人了。遊戲會記錄猴子跑的距離并以此為玩家打分。

iOS GameCenter 挑戰,排名

親自是玩兒一下吧!把starter項目解壓,在Xcode中編譯并運作。試試看你能跑多遠!:]

另外簡單過一遍代碼,看看這些層和場景之間是如何協調工作的。

配置Game Center

在做任何有關Game Center Challenges功能之前,首先要做的就是配置你的app使用Game Center!這個過程需要三個步驟:

  1. 建立并設定App ID。
  2. 在iTunes Coonect上注冊你的app。
  3. 啟用Game Center的功能,比如leaderboards(排行榜)

讓我們按順序過一遍這些步驟。對于許多有Game Center經驗的讀者來說,這會是相當熟悉的,但我保證我會很快的講完這一部分。

建立并設定APP ID

第一步,你需要建立一個App ID。登入到iOS Dev Center,選擇iOS Provisioning Portal。

在Provisioning Portal中,選擇App IDs,選擇create a new App ID。使用monkeyjump作為遊戲名字并輸入一個bundle identifier,通常這裡使用倒轉的DNS命名,比如com.ali.MonkeyJump(如果你沒有自己的域名,你可以使用你自己的名字代 替)。

iOS GameCenter 挑戰,排名

當你完成後,點選Submit按鈕。打開MonkeyJump Xcode工程,選擇project root,然後選擇MonkeyJump target,在Summary tab中把Bundle Identifier替換為你剛剛在Provisioning Portal中建立的那個。

iOS GameCenter 挑戰,排名

編譯并運作,在真機上運作試試看。如果一切配置都正确的話,遊戲應該立刻啟動。如果沒有,那麼clean一下項目并重新編譯一次。

在ITUNES CONNECT中注冊你的APP

接下來的步驟是iTunes Connect中建立一個新app。首先登入到iTunes Connect,切換到application management子頁面,點選位于左上角的Add New App按鈕。(如果你同時擁有Mac和iOS的開發者帳号,你可能需要選擇app的類型 – 當然要選擇iOS)。

在第一個螢幕中,輸入MonkeyJump作為遊戲名字,400作為SKU number(SKU number可以是任意的數字/單詞,你也可以設定成你想要的)并選擇上一步中建立的Bundle Identifier。

iOS GameCenter 挑戰,排名

當你輸入完所有值後,點選Continue。在彈出的提示框中輸入所有需要的資訊。因為你隻需要在本教程中使用此項目,是以一切從簡,隻填入強制要求填入的項目。☺

你需要上傳一個大的app icon 和一個截圖。為了讓過程更容易,我為你準備好了iTunes resources file。你可以解壓這個ZIP檔案,使用裡邊的圖檔來很快的完成這個煩人的注冊過程。

當你完成後,點選Save按鈕,如果一切都OK的話,你會得到以下提示:

iOS GameCenter 挑戰,排名

歡呼!你已經在iTunes Connect中注冊了你的app并完成了最敷衍了事的部分,哈哈。接下來還需要幾個小步驟來激活Game Center。不要慌張,因為最麻煩的部分已經過去了。☺

啟用GAME CENTER的功能

點選藍色的Manage Game Center按鈕并點選Enable for Single Game Button。太棒了!你已經為你的遊戲啟用了Game Center了。這個步驟簡單到隻需要點選按鈕 – 不過别高興的太早,之後你還是需要寫很多的代碼才行哦。:]

iOS GameCenter 挑戰,排名

你還沒有完成本部分,還要添加一個leaderboard(排行榜)才算完。你可能會問這篇教程不是講challenges的嗎,跟leaderboard有什麼關系呢,别急,稍後你就明白了!

使用challenge要求添加一個leaderboard。點選Add Leaderboard按鈕并選擇Single Leaderboard類型。之後你會看到一個如下的表格:

iOS GameCenter 挑戰,排名

在leaderboard reference name欄輸入High Scores,leaderboard ID欄輸入HighScores。

注意: 一般來說,我推薦你使用包名字的擴充作為leaderboard或者achievement(成就)的ID。例如,以上的名字就是 com.ali.MonkeyJump.HighScores(你需要把com.ali替換為你自己的)。但是為了本篇教學的簡化,直接把它命名為 HighScores(而不是加上域名的字首)。

把Sort Order設定為High to Low,Score Format Type設定為Integer。最後,點選Add Language按鈕。為language details添入以下内容:

iOS GameCenter 挑戰,排名

此處添加圖檔不是雖然強制的,但是添加它是個很好的實踐機會。這裡你需要使用的資源是iTunes resource檔案夾中名字為icon_leaderboard_512.png,把它用作高分排行榜的icon。當你完成後,點選Save。

最後,點選Done按鈕。到這裡,一個leaderboard的配置就完成了,以後如果你想添加更多的,你就可以随心所欲了。

驗證本地玩家

在你開始寫代碼之前,你需要首先import(導入)GameKit framework。在 Xcode 4.5 中打開MonkeyJump工程并進入target設定。打開Build Phases子頁面,選擇Link Binary With Libraries部分。點選 “+” 按鈕,選擇導入GameKit framework到工程中。

iOS GameCenter 挑戰,排名

接下來你需要寫一些代碼來驗證使用者。注意如果你不驗證使用者,你是不能夠使用任何Game Center提供的很棒的功能的。

這裡的Player代表目前正在玩兒你的遊戲玩家。在Game Center的術語裡,這由GKLocalPlayer表示。

驗證過程簡單的分為兩個步驟:

  1. 首先你調用一個authenticate call 到 Game Center平台。
  2. 平台會異步的處理你的請求,結束後會調用一個回調函數。如果玩家已經登入了(95%的情況),一個歡迎的橫幅會彈出來,如果玩家沒登入,那麼一個允許玩家注冊的登入界面會彈出來。

我們這就寫些代碼。這裡我們使用一個單例模式,也就是說所有的Game Center的代碼都在一個類中。

在Xcode中,右鍵點選MonkeyJump group,選擇New Group。把新group命名為GameKitFiles。然後,右鍵點選新建立的這個group并選擇New File…,檔案模版選擇Objective-C Class template。把檔案命名為GameKitHelper,同時繼承NSObject。

iOS GameCenter 挑戰,排名

把GameKitHelper.h中的内容替換為以下内容:

//   Include the GameKit framework
#import <GameKit/GameKit.h>

//   Protocol to notify external
//   objects when Game Center events occur or
//   when Game Center async tasks are completed
@protocol GameKitHelperProtocol<NSObject>
@end

@interface GameKitHelper : NSObject

@property (nonatomic, assign)
    id<GameKitHelperProtocol> delegate;

// This property holds the last known error
// that occured while using the Game Center API's
@property (nonatomic, readonly) NSError* lastError;

+ (id) sharedGameKitHelper;

// Player authentication, info
-(void) authenticateLocalPlayer;
@end      

以上代碼自說明性很強并且有很詳細的注釋。你在此所做的無非就是聲明兩個方法和兩個屬性,其中一個屬性是delegate,另外一個會記錄下最近一次使用GameKit framework報出的錯誤。

切換到GameKitHelper.m并把檔案替換為以下内容:

#import "GameKitHelper.h"
#import "GameConstants.h"

@interface GameKitHelper ()
        <GKGameCenterControllerDelegate> {
    BOOL _gameCenterFeaturesEnabled;
}
@end

@implementation GameKitHelper

#pragma mark Singleton stuff

+(id) sharedGameKitHelper {
    static GameKitHelper *sharedGameKitHelper;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedGameKitHelper =
                [[GameKitHelper alloc] init];
    });
    return sharedGameKitHelper;
}

#pragma mark Player Authentication

-(void) authenticateLocalPlayer {

  GKLocalPlayer* localPlayer =
    [GKLocalPlayer localPlayer];

    localPlayer.authenticateHandler =
    ^(UIViewController *viewController,
      NSError *error) {

        [self setLastError:error];

        if ([CCDirector sharedDirector].isPaused)
            [[CCDirector sharedDirector] resume];

        if (localPlayer.authenticated) {
            _gameCenterFeaturesEnabled = YES;
        } else if(viewController) {
            [[CCDirector sharedDirector] pause];
            [self presentViewController:viewController];
        } else {
            _gameCenterFeaturesEnabled = NO;
        }
    };
}
@end      

這裡你聲明了一個名為_gameCenterFeaturesEnabled的變量。這個BOOL類型的變量會辨別驗證是否成功。

iOS 6.0中驗證玩家的方式有所改變。所有你需要的就是設定GKLocalPlayer對象的authenticationHandler屬性,正如你在authenticateLocalPlayer方法中看到的。authenticationHandler block有兩個參數,它是被Game Center平台自動調用的。

這個block被系統在以下情形中被調用:

  • 當你設定了authenticationHandler并發出了驗證玩家的請求。
  • 當app進入foreground(前台)。
  • 在登入時,例如玩家在進入遊戲前沒有登入,進入時會彈出登入界面,在這個界面中的任何互動都會調用authenticationHandler。

authenticationHandler有兩個參數:

  • 第一個是 UIViewController ,它代表如果你未登入Game Center,需要你彈出的登入view controller。
  • 還有一個 NSError 表示驗證過程中發生的任何錯誤。

值得注意的是,在這個block中,你首先檢查玩家是否驗證過了,如果玩家已經被驗證了,你需要做的就是把_gameCenterEnabled變量置為true,然後就可以繼續遊戲了。

如果login view controller(authenticationHandler block中的第一個參數)不為nil,就意味着玩家還沒有登入Game Center。如果是這種情況,你先暫停遊戲,然後為玩家彈出登入的view controller。如果玩家在此界面登入成功或者點選Cancel按鈕,以上那個handler block還會被調用一次。

在老版本的Game Center中,開發者是沒法決定何時為玩家彈出登入界面的。這個新方法給予了開發者更多可控性,來決定在何時彈出此界面。

最終,如果驗證失敗了,你需要恰當的禁用所有Game Center的功能。這裡通過把_gameCenterFeaturesEnabled變量置為false來讓app無視Game Center的功能調用。

為了讓authenticateLocalPlayer起作用,你還需要一些代碼。在GameKitHelper.m中加入以下内容:

#pragma mark Property setters

-(void) setLastError:(NSError*)error {
    _lastError = [error copy];
    if (_lastError) {
        NSLog(@"GameKitHelper ERROR: %@", [[_lastError userInfo] 
          description]);
    }
}

#pragma mark UIViewController stuff

-(UIViewController*) getRootViewController {
    return [UIApplication 
      sharedApplication].keyWindow.rootViewController;
}

-(void)presentViewController:(UIViewController*)vc {
    UIViewController* rootVC = [self getRootViewController];
    [rootVC presentViewController:vc animated:YES 
      completion:nil];
}      

以上代碼實作了authenticateLocalPlayer需要的三個函數:

  1. lastError屬性被聲明為readonly。是以你不能在直接設定它的值,需要手動為其添加一個setter方法。這就是setLastError:的作用。
  2. Game Center登入controller需要真實顯示出來,這樣玩家才能做登入操作。presentViewController: 和 getRootViewController方法負責得到root view controller并且通過root view controller把登入界面顯示出來。

太棒了!現在是時候測試一下GameKitHelper了。打開Prefix.pch并加入必要的import:

#import "GameKitHelper.h"      

接下來,打開MenuLayer.m并在onEnter(緊跟在[super onEnter]語句之後)中加入如下内容。每當主界面顯示的時候都會驗證玩家。

[[GameKitHelper sharedGameKitHelper]
                authenticateLocalPlayer];      

編譯并運作。當主菜單顯示出來的時候你會看到以下的内容:

iOS GameCenter 挑戰,排名

左邊的圖示示範了登入view controller(玩家未登入Game Center的情況)。右邊的示範了welcome banner,每當驗證成功時就會彈出。

注意: 為了測試驗證過程,首先登出Game Center然後再在MonkeyJump app裡登入。隻有這樣才能在沙盒模式下運作Game Center。另外,在模拟器上運作也許行不通(至少在寫這篇教程時還不行)。你需要在真機上進行測試。

送出分數到Game Center

若送出一個分數到Game Center,需要使用GKScore類。這個類儲存着有關玩家分數和分數所屬類别的資訊。

類别指的是leaderboard ID。例如,你希望送出一個分數到High Scores排行榜,那麼GKScore對象的category就應該是你在iTues Connect設定的那個leaderboard ID,這裡就是HighScores。

打開GameKitHelper.h并加入以下方法聲明:

// Scores
-(void) submitScore:(int64_t)score
           category:(NSString*)category;      

接下來,在GameKitHelperProtocol中加入以下方法聲明:

-(void) onScoresSubmitted:(bool)success;      

打開GameKitHelper.m并加入以下代碼:

-(void) submitScore:(int64_t)score
        category:(NSString*)category {
    //1: Check if Game Center
    //   features are enabled
    if (!_gameCenterFeaturesEnabled) {
        CCLOG(@"Player not authenticated");
        return;
    }

    //2: Create a GKScore object
    GKScore* gkScore =
            [[GKScore alloc]
                initWithCategory:category];

    //3: Set the score value
    gkScore.value = score;

    //4: Send the score to Game Center
    [gkScore reportScoreWithCompletionHandler:
               ^(NSError* error) {

        [self setLastError:error];

        BOOL success = (error == nil);

        if ([_delegate
                respondsToSelector:
                @selector(onScoresSubmitted:)]) {

            [_delegate onScoresSubmitted:success];
        }
     }];
}      

以下是上面方法的分步說明:

  1. 檢查Game Center功能是否啟用了,隻有當啟用時再執行之後的代碼。
  2. 建立一個GKScore的執行個體。GKScore所需要的分數所屬類别作為init方法的參數傳入。
  3. 設定GKScore的分數值。
  4. 使用reportScoreWithCompletionHandler:方法把GKScore對象發送到Game Center端。當分數被發送後,平台會調用completion handler。completion handler是一個隻有一個參數的block,在這裡是一個NSError 對象,你可以通過它來檢視分數是否發送成功了。

現在你已經有了發送分數到Game Center的方法了,是時候使用它了。在使用該方法之前,打開GameConstants.h并在檔案末尾(但在最後的#endif之前)加入以下define語句:

#define kHighScoreLeaderboardCategory @"HighScores"      

然後,打開GameLayer.m并找到monkeyDead方法。根據此方法的名字透露的資訊,這個方法是在猴子挂掉時調用的。換句話說,就是遊戲結束的時候。

在該方法的開頭加入以下語句:

[[GameKitHelper sharedGameKitHelper]
        submitScore:(int64_t)_distance
        category:kHighScoreLeaderboardCategory];      

編譯并運作。玩兒一遍遊戲直到猴子挂掉。可憐的小家夥!

當你玩兒完後,你的分數會被發送到Game Center。為了驗證是否真的發送成功了,打開Game Center app,點選Games分頁并選擇MonkeyJump。這裡的排行榜會顯示你的分數。下邊是HighScores排行榜的截圖示例:

iOS GameCenter 挑戰,排名

你打敗我的分數了嗎?不要使用改代碼的作弊手段哦!:]

Game Center 挑戰

終于到了你期待已久的部分了!

Game Center 挑戰是iOS 6.0 的 Game Center中引入的最大的功能。挑戰可以讓你的遊戲病毒式地傳播,而且還可以極大的增加玩家的留存。

但是問題是,使用挑戰功能是一件異常艱難并且複雜的工作,因為它有着數量廣袤的的API而且非常複雜(作者開玩笑說的)。

iOS GameCenter 挑戰,排名

隻是開個玩笑啦!把挑戰功能內建到你的遊戲中,所有要做的僅僅是…完全不需要任何工作!☺如果你的遊戲支援leaderboards或者achievements,那麼你的遊戲就會自動的支援challenges(挑戰),而不需要做任何額外的工作。

iOS GameCenter 挑戰,排名

為了測試這個,打開Game Center程式(確定你是在沙盒模式下)。進入Games分頁并打開MonkeyJump遊戲。

如果你已經玩兒了有幾次遊戲,從leaderboard中選擇你的高分。你會看到在一個Challenge Friends的按鈕出現在詳細資訊界面。點選它,輸入想要挑戰的好友的名字,點選Send。當challenge被成功發送後,你的好友會收到一個 push notification。

注意: 為了測試challenges,你需要兩台運作iOS 6.0的裝置,每台都需要登入不同的Game Center帳号,并且互相之間要加為好友。

Challenges絕不僅僅是push notifications而已。讓我通過一個例子來詳細地為你說明它。

假如我給Ray發送了一個500米成績的挑戰。Ray會在他的裝置上接收到一個通知他此挑戰的push notification。我們假設Ray在回應挑戰的遊戲中得到了1000米的成績。也就是說Ray挑戰成功了。那麼他當然想讓我知道這件事兒。

由于遊戲是把所有分數發送到Game Center上的,Game Center自動地擷取到Ray挑戰成功了,是以它會同時發送一個挑戰完成的push notification到兩個人的裝置上去。Ray之後還可以以1000米這個分數向我發起挑戰。他一定不知道我再夢裡也能跑1000米吧。

iOS GameCenter 挑戰,排名

這個過程可以無窮盡的持續下去,每一次一方都會超過另一方的分數。這樣就會讓人很上瘾,這種自我延續的特性使得每個遊戲開發者都應該在他/她的遊戲中內建challenge。

到現在為止,你已經在Game Center應用中測試了challenge,但是怎麼樣才能允許玩家在遊戲内也能想他/她的好友發起挑戰呢?

這就是接下來要做的。:]你将要在你的遊戲中添加一個朋友選擇器,允許玩家選擇他/她的好友,并發送挑戰。

打開GameKitHelper.h并加入一個新的屬性。

@property (nonatomic, readwrite)
        BOOL includeLocalPlayerScore;      

接下來在GameKitHelperProtocol加入以下方法聲明:

-(void) onScoresOfFriendsToChallengeListReceived:
            (NSArray*) scores;
-(void) onPlayerInfoReceived:
            (NSArray*)players;      

同時在GameKitHelper中加入以下方法聲明:

-(void) findScoresOfFriendsToChallenge;

-(void) getPlayerInfo:(NSArray*)playerList;

-(void) sendScoreChallengeToPlayers:
        (NSArray*)players
        withScore:(int64_t)score
        message:(NSString*)message;      

然後在GameKitHelper.m中定義以上的方法。讓我們從findScoresOfFriendsToChallenge開始。添加以下内容:

-(void) findScoresOfFriendsToChallenge {
    //1
    GKLeaderboard *leaderboard =
            [[GKLeaderboard alloc] init];

    //2
    leaderboard.category =
            kHighScoreLeaderboardCategory;

    //3
    leaderboard.playerScope =
            GKLeaderboardPlayerScopeFriendsOnly;

    //4
    leaderboard.range = NSMakeRange(1, 100);

    //5
    [leaderboard
        loadScoresWithCompletionHandler:
        ^(NSArray *scores, NSError *error) {

        [self setLastError:error];

        BOOL success = (error == nil);

        if (success) {
            if (!_includeLocalPlayerScore) {
                NSMutableArray *friendsScores =
                        [NSMutableArray array];

                for (GKScore *score in scores) {
                    if (![score.playerID
                          isEqualToString:
                          [GKLocalPlayer localPlayer]
                          .playerID]) {
                        [friendsScores addObject:score];
                    }
                }
                scores = friendsScores;
            }
            if ([_delegate
                 respondsToSelector:
                 @selector
                 (onScoresOfFriendsToChallengeListReceived:)]) {

              [_delegate
               onScoresOfFriendsToChallengeListReceived:scores];
            }
        }
    }];
}      

這個方法負責擷取玩家的所有好友的分數。通過查詢HighScores leaderboard擷取玩家的好友分數。

每次你查詢分數,Game Center都會預設添加本地玩家的分數進去。例如上邊的方法,當你擷取所有好友的分數的同時,Game Center傳回的數組不但包含所有玩家好友的,也會包含玩家自身的分數。是以這裡你使用了includeLocalPlayerScore屬性來決定是 否要添加玩家自己的分數到傳回結果中,預設的這個值是NO(不包含玩家的分數)。

現在添加以下方法:

-(void) getPlayerInfo:(NSArray*)playerList {
    //1
    if (_gameCenterFeaturesEnabled == NO)
        return;

    //2
    if ([playerList count] > 0) {
        [GKPlayer
            loadPlayersForIdentifiers:
            playerList
            withCompletionHandler:
                 ^(NSArray* players, NSError* error) {

                 [self setLastError:error];

                 if ([_delegate
                          respondsToSelector:
                          @selector(onPlayerInfoReceived:)]) {

                     [_delegate onPlayerInfoReceived:players];
            }
         }];
     }
}      

此方法通過傳入一個玩家ID的數組來獲得這些玩家的資訊。

還有最後一個方法 – 添加以下代碼:

-(void) sendScoreChallengeToPlayers:
    (NSArray*)players
    withScore:(int64_t)score
    message:(NSString*)message {

    //1
    GKScore *gkScore =
        [[GKScore alloc]
            initWithCategory:
            kHighScoreLeaderboardCategory];
    gkScore.value = score;

    //2
    [gkScore issueChallengeToPlayers:
            players message:message];
}      

此方法向一組玩家發送一個分數挑戰,同時還跟随着一條玩家發送的消息。

很好!接下來,你需要一個friend picker(玩家選擇器)。friend picker将會允許玩家輸入一條自定義的消息并選擇他/她想要發送挑戰的玩家們。預設情況下,它會選擇那些目前分數比你低的玩家,因為這些人玩家理所應 當向他們發送挑戰。畢竟每個玩家都希望赢!☺

在Xcode中建立一個group并命名為ViewControllers。然後建立一個繼承自UIViewController的檔案并将其命名為FriendsPickerViewController。注意這裡要選中“With XIB for user interface”。如下所示:

iOS GameCenter 挑戰,排名

打開FriendsPickerViewController.xib檔案,設定view’s orientation為landscape,拖拽進來一個UITableView,一個UITextField和一個UILabel到canvas中,設定label的text屬性為“Challenge message”。

另外,為了讓次界面看起來和遊戲的其他界面相吻合,添加bg_menu.png作為背景圖檔。最終的view controller看起來如下圖所示:

iOS GameCenter 挑戰,排名

打開FriendsPickerViewController.h并在@interface添加如下語句:

typedef void
        (^FriendsPickerCancelButtonPressed)();
typedef void
        (^FriendsPickerChallengeButtonPressed)();      

這兩個新的資料類型,FriendsPickerCancelButtonPressed 和 FriendsPickerChallengeButtonPressed,是你将要使用的兩種block。block類似C函數,它有傳回類型(這裡是 void)和零個或多個參數。typedef定義使之後在代碼中使用此block更為簡化。

添加如下屬性到@interface部分:

//1
@property (nonatomic, copy)
    FriendsPickerCancelButtonPressed
    cancelButtonPressedBlock;

//2
@property (nonatomic, copy)
    FriendsPickerChallengeButtonPressed
    challengeButtonPressedBlock;      

這些屬性是将來Cancel或者Challenge按鈕按下時所執行的block。

接下來添加Cancel和Challenge按鈕到view controller中。打開FriendsPickerViewController.m并替換viewDidLoad為以下代碼:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIBarButtonItem *cancelButton =
        [[UIBarButtonItem alloc]
         initWithTitle:@"Cancel"
         style:UIBarButtonItemStylePlain
         target:self
         action:@selector(cancelButtonPressed:)];

    UIBarButtonItem *challengeButton =
        [[UIBarButtonItem alloc]
         initWithTitle:@"Challenge"
         style:UIBarButtonItemStylePlain
         target:self
         action:@selector(challengeButtonPressed:)];

    self.navigationItem.leftBarButtonItem =
        cancelButton;
    self.navigationItem.rightBarButtonItem =
        challengeButton;
}      

此方法添加了兩個UIBarButtonItems到view controller中,分别是Cancel和Challenge按鈕。現在添加當這兩個按鈕被按下時所觸發的方法。

- (void)cancelButtonPressed:(id) sender {
    if (self.cancelButtonPressedBlock != nil) {
        self.cancelButtonPressedBlock();
    }
}

- (void)challengeButtonPressed:(id) sender {
    if (self.challengeButtonPressedBlock) {
            self.challengeButtonPressedBlock();
    }
}      

上邊的方法很好了解,你所做的就是在函數中執行challenge和cancel的block。

在你把此view controller內建到遊戲中并驗證一切正常之前,你需要先寫一個初始化方法來擷取本地玩家的分數。在完成這一步之前,你先要定義一個變量存儲此分數。

在FriendsPickerViewController.m中的類extension塊兒中添加以下變量 – 記得要在變量之間插入花括号,最終的類extension看起來如下所示:

@interface FriendsPickerViewController () {
    int64_t _score;
}
@end      

現在添加如下的初始化方法:

- (id)initWithScore:(int64_t) score {
    self = [super
            initWithNibName:
            @"FriendsPickerViewController"
            bundle:nil];

    if (self) {
        _score = score;
    }
    return self;
}      

在FriendsPickerViewController.h中添加該方法聲明,如下所示:

-(id)initWithScore:(int64_t) score;      

現在你就可以測試以下此view controller了,看看它是不是如預期一樣工作正常。打開GameKitHelper.h并定義一個如下方法:

-(void)
    showFriendsPickerViewControllerForScore:
    (int64_t)score;      

然後打開GameKitHelper.m并添加如下import語句:

#import "FriendsPickerViewController.h"      

然後,添加如下方法:

-(void)
    showFriendsPickerViewControllerForScore:
    (int64_t)score {

    FriendsPickerViewController
        *friendsPickerViewController =
                [[FriendsPickerViewController alloc]
                 initWithScore:score];

    friendsPickerViewController.
        cancelButtonPressedBlock = ^() {
        [self dismissModalViewController];
    };

    friendsPickerViewController.
        challengeButtonPressedBlock = ^() {
        [self dismissModalViewController];
    };

    UINavigationController *navigationController =
        [[UINavigationController alloc]
            initWithRootViewController:
            friendsPickerViewController];

    [self presentViewController:navigationController];
}      

此方法會模态地彈出FriendPickerController。它還定義了當Challeng和Cancel按鈕被按下時觸發的block。目前它們隻是簡單的使該界面消失。

打開GameOverLayer.m并把menuButtonPressed:中的CCLOG(@”Challenge button pressed”);行替換為以下内容:

[[GameKitHelper sharedGameKitHelper]
            showFriendsPickerViewControllerForScore:_score];      

到了關鍵時刻了!編譯并運作,玩兒一局MonkeyJump,在game over屏點選Challenge Friends按鈕,你會看到FriendsPickerViewController彈出來了。如果你點選不論Challenge還是Cancel按鈕 都會使該界面消失。

iOS GameCenter 挑戰,排名

很好!你的遊戲現在有了好友選擇的界面了。但是這個界面還沒顯示任何的好友,這樣是不行的。

沒必要感覺孤單,我們這就加入此功能!

打開FriendsPickerViewController.m并把類extension替換為以下内容:

@interface FriendsPickerViewController ()
        <UITableViewDataSource,
        UITableViewDelegate,
        UITextFieldDelegate,
        GameKitHelperProtocol> {

    NSMutableDictionary *_dataSource;
    int64_t _score;
}
@property (nonatomic, weak)
        IBOutlet UITableView *tableView;
@property (nonatomic, weak)
        IBOutlet UITextField *challengeTextField;
@end      

注意這裡的interface部分實作了很多的protocol。同時,它還有兩個IBOutlet,一個是UITableView的,另一個是UITextfield的。使用Interface Builder把它們和相應的view關聯起來,如下所示:

iOS GameCenter 挑戰,排名

接下來設定UITableView的delegate和data source,還有UITextField的delegate,在Interface Builder中把它們都設定為File’s Owner。

為了完成這個,首先選擇UITableView,在Connections inspector中,把data source和delegate outlet分别拖拽到左側的File’s Owner,如下圖所示:

iOS GameCenter 挑戰,排名

為UITextField重複此步驟。

切換到FriendsPickerViewController.m并在initWithScore:方法的if語句中的_score = score;行之後添加如下代碼:

dataSource = [NSMutableDictionary dictionary];

GameKitHelper *gameKitHelper = [GameKitHelper sharedGameKitHelper];

gameKitHelper.delegate = self;
[gameKitHelper findScoresOfFriendsToChallenge];      

此方法初始化了data source,設定其自身為GameKitHelper的delegate并調用findScoresOfFriendsToChallenge。如果你 還記得,這個方法是用來擷取本地玩家所有好友的分數的。接下來需要實作 onScoresOfFriendsToChallengeListReceived:代理方法,來處理當玩家的分數擷取到後的事件:

-(void)
    onScoresOfFriendsToChallengeListReceived:
    (NSArray*) scores {
    //1
    NSMutableArray *playerIds =
        [NSMutableArray array];

    //2
    [scores enumerateObjectsUsingBlock:
        ^(id obj, NSUInteger idx, BOOL *stop){

        GKScore *score = (GKScore*) obj;

        //3    
        if(_dataSource[score.playerID]
                                == nil) {
            _dataSource[score.playerID] =
                    [NSMutableDictionary dictionary];
            [playerIds addObject:score.playerID];
        }

        //4
        if (score.value < _score) {
            [_dataSource[score.playerID]
                    setObject:[NSNumber numberWithBool:YES]
                    forKey:kIsChallengedKey];
        }

        //5
        [_dataSource[score.playerID]
                    setObject:score forKey:kScoreKey];
    }];

    //6
    [[GameKitHelper sharedGameKitHelper]
                    getPlayerInfo:playerIds];
    [self.tableView reloadData];
}      

以上代碼有很強的自說明性,不過還是按步驟解釋一下:

  1. 建立一個名為playerIds的數組用來存儲本地玩家所有好友的ID。
  2. 然後此方法開始周遊傳回的分數。
  3. 對每一個分數,都在data source中建立相應的資料,并且在playerIds數組中儲存player ID。
  4. 如果這個分數比本地玩家的分數低,該分數對應的在data source中的資料會被标記。
  5. 分數被儲存在data source字典中。
  6. GameKitHelper的getPlayerInfo:方法調用時使用playerIds數組作為參數。該方法會傳回每個好友的詳細資訊,比如好友的名字和頭像。

以上代碼需要一些#define語句才能正常工作,在檔案頭#import行之後,加入以下内容(有些是以後會用到的):

#define kPlayerKey @"player"
#define kScoreKey @"score"
#define kIsChallengedKey @"isChallenged"

#define kCheckMarkTag 4      

然後你需要實作onPlayerInfoReceived:代理方法。這個方法會在本地玩家所有好友的資訊擷取到之後調用。

-(void) onPlayerInfoReceived:(NSArray*)players {
    //1

    [players
        enumerateObjectsUsingBlock:
        ^(id obj, NSUInteger idx, BOOL *stop) {

        GKPlayer *player = (GKPlayer*)obj;

        //2
        if (_dataSource[player.playerID]
                                == nil) {
            _dataSource[player.playerID] =
                    [NSMutableDictionary dictionary];
        }
        [_dataSource[player.playerID]
                    setObject:player forKey:kPlayerKey];

        //3
        [self.tableView reloadData];
    }];
}      

這個方法也非常直接了當,因為你有每個玩家的詳細資訊,你隻需要更新每個玩家的_dataSource字典即可。

_dataSource字典用來作為table view的資料源。接下來實作table view的data source方法,如下所示:

- (NSInteger)tableView:(UITableView *)tableView
        numberOfRowsInSection:(NSInteger)section {
    return _dataSource.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell identifier";
    static int ScoreLabelTag = 1;
    static int PlayerImageTag = 2;
    static int PlayerNameTag = 3;

    UITableViewCell *tableViewCell =
        [tableView
            dequeueReusableCellWithIdentifier:
            CellIdentifier];

    if (!tableViewCell) {

        tableViewCell =
            [[UITableViewCell alloc]
             initWithStyle:UITableViewCellStyleDefault
             reuseIdentifier:CellIdentifier];
        tableViewCell.selectionStyle =
            UITableViewCellSelectionStyleGray;
        tableViewCell.textLabel.textColor =
            [UIColor whiteColor];

        UILabel *playerName =
            [[UILabel alloc] initWithFrame:
             CGRectMake(50, 0, 150, 44)];
        playerName.tag = PlayerNameTag;
        playerName.font = [UIFont systemFontOfSize:18];
        playerName.backgroundColor =
            [UIColor clearColor];
        playerName.textAlignment =
            UIControlContentVerticalAlignmentCenter;
        [tableViewCell addSubview:playerName];

        UIImageView *playerImage =
                [[UIImageView alloc]
                initWithFrame:CGRectMake(0, 0, 44, 44)];
        playerImage.tag = PlayerImageTag;
        [tableViewCell addSubview:playerImage];

        UILabel *scoreLabel =
                [[UILabel alloc]
                initWithFrame:
                 CGRectMake(395, 0, 30,
                        tableViewCell.frame.size.height)];

        scoreLabel.tag = ScoreLabelTag;
        scoreLabel.backgroundColor =
                    [UIColor clearColor];
        scoreLabel.textColor =
                    [UIColor whiteColor];
        [tableViewCell.contentView
                    addSubview:scoreLabel];

        UIImageView *checkmark =
                [[UIImageView alloc]
                 initWithImage:[UIImage
                 imageNamed:@"checkmark.png"]];
        checkmark.tag = kCheckMarkTag;
        checkmark.hidden = YES;
        CGRect frame = checkmark.frame;
        frame.origin =
               CGPointMake(tableView.frame.size.width - 16, 13);
        checkmark.frame = frame;
        [tableViewCell.contentView
                addSubview:checkmark];
    }
    NSDictionary *dict =
        [_dataSource allValues][indexPath.row];
    GKScore *score = dict[kScoreKey];
    GKPlayer *player = dict[kPlayerKey];

    NSNumber *number = dict[kIsChallengedKey];

    UIImageView *checkmark =
            (UIImageView*)[tableViewCell
                           viewWithTag:kCheckMarkTag];

    if ([number boolValue] == YES) {
        checkmark.hidden = NO;
    } else {
        checkmark.hidden = YES;
    }

    [player
        loadPhotoForSize:GKPhotoSizeSmall
        withCompletionHandler:
        ^(UIImage *photo, NSError *error) {
        if (!error) {
            UIImageView *playerImage =
            (UIImageView*)[tableView
                           viewWithTag:PlayerImageTag];
            playerImage.image = photo;
        } else {
            NSLog(@"Error loading image");
        }
    }];

    UILabel *playerName =
        (UILabel*)[tableViewCell
                      viewWithTag:PlayerNameTag];
    playerName.text = player.displayName;

    UILabel *scoreLabel =
        (UILabel*)[tableViewCell
                      viewWithTag:ScoreLabelTag];
    scoreLabel.text = score.formattedValue;
    return tableViewCell;
}      

好多的代碼啊。:]但是你之前使用過UITableView,這些代碼對你并不陌生。tableView:cellForRowAtIndex:建立一個新的UITableViewCell。每個table view中的cell都會包含一個頭像,玩家的名字和分數。

現在添加tableView:didSelectRowAtIndex:來處理使用者選擇table view中每一行的事件:

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:
    (NSIndexPath *)indexPath {

    BOOL isChallenged = NO;

    //1
    UITableViewCell *tableViewCell =
            [tableView cellForRowAtIndexPath:
                indexPath];

    //2
    UIImageView *checkmark =
            (UIImageView*)[tableViewCell
                viewWithTag:kCheckMarkTag];

    //3
    if (checkmark.isHidden == NO) {
        checkmark.hidden = YES;
    } else {
        checkmark.hidden = NO;
        isChallenged = YES;
    }
    NSArray *array =
        [_dataSource allValues];

    NSMutableDictionary *dict =
        array[indexPath.row];

    //4
    [dict setObject:[NSNumber
                     numberWithBool:isChallenged]
                     forKey:kIsChallengedKey];
    [tableView deselectRowAtIndexPath:indexPath
               animated:YES];
}      

這個方法所做的是設定_dataSource的entry為YES或者NO。

編譯并運作。到這裡FriendsPickerViewController就可以顯示出帶有本地玩家的好友資訊的UITableView了。每個好友的詳細資訊,比如名字和頭像,也會被顯示在每個cell中。如下圖所示:

iOS GameCenter 挑戰,排名

最後一件要做的事就是實際發送挑戰了。把FriendsPickerViewController.m中的challengeButtonPressed:替換為以下内容:

- (void)challengeButtonPressed:
                (id) sender {

    //1
    if(self.challengeTextField.text.
                        length > 0) {

        //2
        NSMutableArray *playerIds =
                    [NSMutableArray array];
        NSArray *allValues =
                    [_dataSource allValues];

        for (NSDictionary *dict in allValues) {
            if ([dict[kIsChallengedKey]
                            boolValue] == YES) {

                GKPlayer *player =
                    dict[kPlayerKey];
                [playerIds addObject:
                    player.playerID];
            }
        }
        if (playerIds.count > 0) {

            //3
            [[GameKitHelper sharedGameKitHelper]
                sendScoreChallengeToPlayers:playerIds
                withScore:_score message:
                    self.challengeTextField.text];
        }

        if (self.challengeButtonPressedBlock) {
            self.challengeButtonPressedBlock();
        }
    } else {
        self.challengeTextField.layer.
                borderWidth = 2;
        self.challengeTextField.layer.
                borderColor =
                    [UIColor redColor].CGColor;
    }
}      

以下是上面方法的詳細步驟分解:

  1. 此方法首先檢查玩家是否輸入了消息。如果沒有,就把challengeTextField的邊框設為紅色。
  2. 如果使用者輸入了文本,此方法擇查找所有被選中的玩家ID,并把它們儲存到playerIds數組中。
  3. 如果使用者選擇了一個火一個以上的玩家挑戰的話,則使用玩家ID作為參數調用GameKitHelper的sendScoreChallengeToPlayers:withScore:方法。此方法會發送挑戰給所有已選擇的玩家。

編譯并運作遊戲。現在當你點選FriendsPickerViewController界面的Challenge Friends按鈕時,它會發送一個分數挑戰。如果你有兩台裝置,你就可以輕易地測試它們是否工作正常了。