使用UIKit製做卡牌遊戲(二)ios遊戲篇

轉自朋友Tommy 的翻譯,本身只翻譯了第三篇教程。html

 

譯者: Tommy | 原文做者: Matthijs Hollemans寫於2012/07/06 
原文地址: http://www.raywenderlich.com/12865/how-to-make-a-simple-playing-card-game-with-multiplayer-and-bluetooth-part-2數組


image

這篇文章是由iOS教程團隊成員Matthijs Hollemans發表的,一個經驗豐富的開發工程師和設計師。你能夠在Google+Twitter上找到他。服務器

歡迎回到使用UIKit經過藍牙或者Wi-Fi製做多人卡片遊戲系列教程。網絡

若是你以前沒有接觸過本系列教程,請先看這裏。這裏你能夠看到這個遊戲的一些視頻,接下來將邀請你進入本系列教程的學習。session

第一篇教程,你建立了主菜單和基本的Host Game and Join Game界面。app

你已經建立了一個能散播消息的server和可以偵測server的client,可是到目前爲止,很明顯還有些功能僅僅是在Xcode的輸出窗口中打印些log而已。框架

在第二部分中,也就是本篇教程,你將在屏幕上展現出一些可用的server和一些可以相連的client,而且完成卡片的配對。開始吧!dom

開始:將server展現給用戶


MatchmakingClient類有一個_availabelServers變量,一個NSMutableArray數組,這些是爲了儲存client偵測到的server的。當GKSession偵測到一個新的server時,你就把這個server的peer ID加到這個數組中。iphone

你怎麼能知道何時有新的server呢?MatchmakingClient是GKSession的Delegate,你能夠用它的delegate方法 session:peer:didChangeState: 來偵測server。用下面的方法替換MatchmakingClient.m中的那個方法:學習

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
    #ifdef DEBUG
    NSLog(@"MatchmakingClient: peer %@ changed state %d", peerID, state);
    #endif

    switch (state)
    {
        // The client has discovered a new server.
        case GKPeerStateAvailable:
            if (![_availableServers containsObject:peerID])
            {
                [_availableServers addObject:peerID];
                [self.delegate matchmakingClient:self serverBecameAvailable:peerID];
            }
            break;

        // The client sees that a server goes away.
        case GKPeerStateUnavailable:
            if ([_availableServers containsObject:peerID])
            {
                [_availableServers removeObject:peerID];
                [self.delegate matchmakingClient:self serverBecameUnavailable:peerID];
            }
            break;

        case GKPeerStateConnected:
            break;

        case GKPeerStateDisconnected:
            break;

        case GKPeerStateConnecting:
            break;
    }   
}

最新發現的server是經過peerID這個參數來標示的。這是一個相似@"663723729",包含一些數組的一些字符串。對於標示server,這些數字是很是重要的。

第三個參數"state",告訴你peer當前的狀態。通常狀況下,只有在狀態轉變爲GKPeerStateAvailable和GKPeerStateUnavailable的時候,咱們才處理。正如你從狀態的名字中看到的那樣,這些狀態預示着新的server被發現或者一個server斷開了鏈接(有多是用戶退出遊戲或者是他玩遊戲時走神兒了)。是把這個它的peer ID加到_availabelServers列表中,仍是從列表中刪除,視狀況而定。

如今還不能編譯,由於它還要通知它的delegate,這是個尚未定義的屬性。MatchmakingClient經過delegate方法讓JoinViewController知道有新的server可用了(或者server變的不可用)。將下面的代碼添加到MatchmakingClient.h文件的上方:

@class MatchmakingClient;

@protocol MatchmakingClientDelegate <NSObject>

- (void)matchmakingClient:(MatchmakingClient *)client serverBecameAvailable:(NSString *)peerID;
- (void)matchmakingClient:(MatchmakingClient *)client serverBecameUnavailable:(NSString *)peerID;

@end

將下面這個新屬性添加到@interface:

@property (nonatomic, weak) id <MatchmakingClientDelegate> delegate;

在.m文件中完成synthesize:

@synthesize delegate = _delegate;

如今JoinViewController要變成MatchmakingClient的delegate了,因此將這個protocol添加到JoinViewController.h文件中的@interface一行:

@interface JoinViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, MatchmakingClientDelegate>

在JoinViewController.m中的viewDidAppear方法中,當MatchmakingClient對象建立後添加以下一行代碼:

_matchmakingClient.delegate = self;

最後,實現delegate的方法:

#pragma mark - MatchmakingClientDelegate

- (void)matchmakingClient:(MatchmakingClient *)client serverBecameAvailable:(NSString *)peerID
{
    [self.tableView reloadData];
}

- (void)matchmakingClient:(MatchmakingClient *)client serverBecameUnavailable:(NSString *)peerID
{
    [self.tableView reloadData];
}

上面只是告訴tableview去從新加載,這就覺得着你還要在加載的data source方法裏去處理新數據,使得tableview可以顯示新數據。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (_matchmakingClient != nil)
        return [_matchmakingClient availableServerCount];
    else
        return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"CellIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];

    NSString *peerID = [_matchmakingClient peerIDForAvailableServerAtIndex:indexPath.row];
    cell.textLabel.text = [_matchmakingClient displayNameForPeerID:peerID];

    return cell;
}

這只是一些基本的tableview代碼。你只是簡單的告訴MatchmakingClient,tableview中的那些行應該從新顯示,咱們還須要一些新的輔助方法,將下面的方法聲明添加到MatchmakingClient.h文件中:

- (NSUInteger)availableServerCount;
- (NSString *)peerIDForAvailableServerAtIndex:(NSUInteger)index;
- (NSString *)displayNameForPeerID:(NSString *)peerID;

添加它們的實現到MatchmakingClient.m中:

- (NSUInteger)availableServerCount
{
    return [_availableServers count];
}

- (NSString *)peerIDForAvailableServerAtIndex:(NSUInteger)index
{
    return [_availableServers objectAtIndex:index];
}

- (NSString *)displayNameForPeerID:(NSString *)peerID
{
    return [_session displayNameForPeer:peerID];
}

這都是些簡單的方法,將_availabelServers_session對象封裝起來。在做爲客戶端的設備上啓動app,你應該可以看到下面這個界面:

image

成功了,client顯示出了server的名字(看,上面的截圖,我用個人ipod做爲server).

惋惜,界面看起來並非那麼漂亮。這很容易結局。添加一個新的類,繼承UITableViewCell,命名爲PeerCell。(我建議建立一個叫作"Views"的group,而後把剛建立好的類放進去。)

你能夠先把PeerCell.h放一放,用下面的內容替換PeerCell.m文件的內容:

#import "PeerCell.h"
#import "UIFont+SnapAdditions.h"

@implementation PeerCell

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    if ((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]))
    {
        self.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"CellBackground"]];
        self.selectedBackgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"CellBackgroundSelected"]];

        self.textLabel.font = [UIFont rw_snapFontWithSize:24.0f];
        self.textLabel.textColor = [UIColor colorWithRed:116/255.0f green:192/255.0f blue:97/255.0f alpha:1.0f];
        self.textLabel.highlightedTextColor = self.textLabel.textColor;
    }
    return self;
}

@end

PeerCell是一個正規的UITableViewCell,可是它改變了本來cell裏的textlabel的字體和顏色,而且換個一個新的背景。在JoinViewController的cellForRowAtIndexPath方法中,用下面一行代碼替換建立table view cell的代碼:

cell = [[PeerCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];

不要忘了導入PeerCell.h頭文件,如今table view cell看起來跟界面很是匹配了:

image

試試這樣:退出做爲server的設備上的app,client就會從它的列表中刪掉這個server的名字。若是你有多臺設備,不妨試試用多臺設備做爲server。這樣,client就會找到全部的server並在列表中顯示出它們的名字。

注意:當server出現或者消失的時候,client要等一會才能察覺到,須要幾秒的反應時間。因此若是列表沒有及時刷新,不要大驚小怪哦!

 

一個簡單的狀態機


下件要作的事情就是讓client和server鏈接。到目前爲止,你的app尚未作任何的通信-client已經可以顯示出可用的server,可是server還不知道client的任何信息。如今只是能看出,你點擊了哪一個server就代表client要鏈接哪一個server。

所以,MatchmakingClient要作兩件事情。第一,尋找server,鏈接你選擇的server。第二,若是鏈接成功,保持通信,這時,MatchmakingClient就再也不關心其它的server了。因此就沒有理由再去偵測其它新的server和更新_availabelServers列表了(不用將這些告訴它的delegate)。

MatchmakingClient的狀態能夠用狀態示意圖來展示出來。下面就是MatchmakingClient各類狀態的示意圖:

image

MatchmakingClient有四種狀態。開始是"idle"狀態,這是一開始的狀態,什麼都沒作。當調用startSearchingForServersWithSessionID:這個方法的時候,就會進入"Searching for Servers"狀態。這些也就是你代碼目前所作的。

當用戶決定鏈接一個server的時候,client就進入了"connecting"狀態,嘗試鏈接一個server。確保鏈接成功後就進入了"connected"狀態。若是在鏈接期間二者有一個斷開了(或者一塊兒消失),client就又進入"idle"狀態。

MatchmakingClient根據所處不一樣的狀態有不一樣的表現。在"searching for servers"狀態,它會從_availabelServers列表中添加或者刪除一個server,可是在"connecting"和"connected"狀態,是不會的。

用這樣的示意圖來描述對象各類可能的狀態,當狀態變化時,你能夠很明確地作出一些處理動做。在這邊教程中,還會使用一些相似的其它的一些示意圖,包括整個遊戲狀態的管理(這個要比你在這裏看到的複雜一些)。

狀態示意圖的實現叫作"state machine"。你能夠用一個enum和實例變量來監視MatchmakingClient的狀態。在MatchmakingClient.m中,@implementataion上方的添加以下代碼:

typedef enum
{
    ClientStateIdle,
    ClientStateSearchingForServers,
    ClientStateConnecting,
    ClientStateConnected,
}
ClientState;

這四個值表明者這個對象的四種不一樣的狀態。添加一個新的實例變量:

@implementation MatchmakingClient
{
    . . .
    ClientState _clientState;
}

這個狀態是這個對象的內部東西,沒有必要把它放進屬性裏。初始化時,這個狀態應該設置成"idle",因此添加這個初始化的方法到類中:

- (id)init
{
    if ((self = [super init]))
    {
        _clientState = ClientStateIdle;
    }
    return self;
}

如今咱們要完善先前寫過的方法來響應不一樣狀態的改變。首先是startSearchingForServersWithSessionID:,當MatchmakingClient進入idle狀態時,這個方法應該有所響應,改變以下:

- (void)startSearchingForServersWithSessionID:(NSString *)sessionID
{
    if (_clientState == ClientStateIdle)
    {
        _clientState = ClientStateSearchingForServers;
        // ... existing code goes here ...
    }
}

最後,改變session:peer:didChangeState:中的這兩個case語句:

// The client has discovered a new server.
case GKPeerStateAvailable:
    if (_clientState == ClientStateSearchingForServers)
    {       
        if (![_availableServers containsObject:peerID])
        {
            [_availableServers addObject:peerID];
            [self.delegate matchmakingClient:self serverBecameAvailable:peerID];
        }
    }
    break;

// The client sees that a server goes away.
case GKPeerStateUnavailable:
    if (_clientState == ClientStateSearchingForServers)
    {
        if ([_availableServers containsObject:peerID])
        {
            [_availableServers removeObject:peerID];
            [self.delegate matchmakingClient:self serverBecameUnavailable:peerID];
        }
    }
    break;

在ClientStateSearchingForServers狀態中,你只須要關心GKPeerStateAvailable和GKPeerStateUnavailable這兩種狀態就能夠了。注意狀態有兩種類型:一種是peer的狀態,就是delegate方法傳進來的,另外一種是MatchmakingClient狀態。爲了避免使那麼困惑,我稱後者爲_clientState。

鏈接Server


添加新的方法聲明到MatchmakingClient.h文件中:

- (void)connectToServerWithPeerID:(NSString *)peerID;

見名知意,你將用這個方法讓client鏈接特定的server。在.m中添加以下方法實現:

- (void)connectToServerWithPeerID:(NSString *)peerID
{
    NSAssert(_clientState == ClientStateSearchingForServers, @"Wrong state");

    _clientState = ClientStateConnecting;
    _serverPeerID = peerID;
    [_session connectToPeer:peerID withTimeout:_session.disconnectTimeout];
}

在"searching for servers"這個狀態,你只能調用上面這個方法。若是不調用此方法,就讓程序退出。這只是爲了程序更加健壯,確保狀態機正常工做。

當狀態改變爲"connecting"時,保存server的peer ID到一個新的實例變量_serverPeerID中,告訴GKSession對象這個client要和那個PeerID鏈接。對於timeout值-斷開鏈接,沒有響應時等待的時間。在這裏,你用GKSession默認的timeout時間就能夠了。

添加你個新的實例變量_serverPeerID:

@implementation MatchmakingClient
{
    . . .
    NSString *_serverPeerID;
}

這些就是MatchmakingClient的東西。如今你必須在某個地方調用connectToServerWithPeerID:這個方法。最合適的地方就是JoinViewController的tableview delegate。添加以下代碼到JoinViewController.m文件中:

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    if (_matchmakingClient != nil)
    {
        [self.view addSubview:self.waitView];

        NSString *peerID = [_matchmakingClient peerIDForAvailableServerAtIndex:indexPath.row];
        [_matchmakingClient connectToServerWithPeerID:peerID];
    }
}

這就十分明確了。首先,你要肯定server的peer ID(經過鎖定的indexPath.row屬性),而後調用這個新的方法去鏈接。注意,爲了蓋住table view和其它的控件,你還要顯示"waitView"這個界面。waitView是nib中的第二個top-level試圖,你就是用它來做爲loading頁面。

當你在客戶端運行app,並點擊一個server的名字,會看到以下界面:

image

MatchmakingClient已經進入"connecting"狀態,而且在等待這個server的迴應。你不想用戶在此時再進入其它的server,因此你顯示這個臨時的等待畫面.

若是你稍微看下server app的Debug輸出窗口,你會發現輸出了一些東西:

Snap[4503:707] MatchmakingServer: peer 1310979776 changed state 4
Snap[4503:707] MatchmakingServer: connection request from peer 1310979776

這些事GKSession的通知消息,告訴server,有個client(在這個例子中ID爲"1310979776")試圖鏈接進來。在下個部分,你將讓MatchmakingServer變得更聰明一些,讓它可以接受鏈接請求和顯示要鏈接的client到屏幕上。

注意:debug窗口打印出的"changed state 4",對應着GKPeerState常量中的一個:

  • 0 = GKPeerStateAvailable
  • 1 = GKPeerStateUnavailable
  • 2 = GKPeerStateConnected
  • 3 = GKPeerStateDisconnected
  • 4 = GKPeerStateConnecting

提示:若是你同時在Xcode中運行了多個設備,你能夠在debugger bar中切換debug輸出窗口:
image

 

在server端接受鏈接請求


如今你有一個正試圖鏈接的client,在後續事情完成以前,你必須先接受鏈接。這些都是在MatchmakingServer完成的。

在完成那些事情以前,要先在server設置一個state machine。添加以下的typedef到MatchmakingServer.m文件的上部:

typedef enum
{
    ServerStateIdle,
    ServerStateAcceptingConnections,
    ServerStateIgnoringNewConnections,
}
ServerState;

不像client,server只有三個狀態。

image

這真是太簡單了。當遊戲開始時,server就進入"ignoring new connections"狀態了。從那時起,新的client將被忽略。下面,添加一個新的實例變量來跟蹤這些狀態:

@implementation MatchmakingServer
{
    . . .
    ServerState _serverState;
}

就如當初的client,給server一個init方法,把狀態初始化爲idle:

- (id)init
{
    if ((self = [super init]))
    {
        _serverState = ServerStateIdle;
    }
    return self;
}

添加一個if語句到startAcceptingConnectionsForSessionID:這個方法中,用來檢測是不是"idle"狀態,而後將_serverState設置爲"accepting connections":

- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID
{
    if (_serverState == ServerStateIdle)
    {
        _serverState = ServerStateAcceptingConnections;

        // ... existing code here ...
    }
}

酷,如今爲何不讓GKSessionDelegate作點什麼呢。就像client發現有新的可用server被通知同樣,當發現有新的client請求鏈接時,應當通知server。在MatchmakingServer.m文件中,更改session:peer:didChangeState:這個方法:

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
    #ifdef DEBUG
    NSLog(@"MatchmakingServer: peer %@ changed state %d", peerID, state);
    #endif

    switch (state)
    {
        case GKPeerStateAvailable:
            break;

        case GKPeerStateUnavailable:
            break;

        // A new client has connected to the server.
        case GKPeerStateConnected:
            if (_serverState == ServerStateAcceptingConnections)
            {
                if (![_connectedClients containsObject:peerID])
                {
                    [_connectedClients addObject:peerID];
                    [self.delegate matchmakingServer:self clientDidConnect:peerID];
                }
            }
            break;

        // A client has disconnected from the server.
        case GKPeerStateDisconnected:
            if (_serverState != ServerStateIdle)
            {
                if ([_connectedClients containsObject:peerID])
                {
                    [_connectedClients removeObject:peerID];
                    [self.delegate matchmakingServer:self clientDidDisconnect:peerID];
                }
            }
            break;

        case GKPeerStateConnecting:
            break;
    }
}

是關注GKPeerStateConnected和GKPeerStateDisconnected這兩個狀態的時候了,這裏的邏輯跟先前設置client是同樣的:把peer ID加到數組裏,而後通知delegate。

固然,咱們如今尚未爲MatchmakingServer定義delegate protocol。如今就作,添加以下代碼到MatchmakingServer.h文件的上方:

@class MatchmakingServer;

@protocol MatchmakingServerDelegate <NSObject>

- (void)matchmakingServer:(MatchmakingServer *)server clientDidConnect:(NSString *)peerID;
- (void)matchmakingServer:(MatchmakingServer *)server clientDidDisconnect:(NSString *)peerID;

@end

你知道該怎麼作,添加一個屬性到@interface中:

@property (nonatomic, weak) id <MatchmakingServerDelegate> delegate;

在.m文件中synthesize這個屬性:

@synthesize delegate = _delegate;

可是誰來擔當MatchmakingServer的delegate呢?HostViewController,固然是它。跳轉至HostViewController.h文件而且添加MatchmakingServerDelegate到protocol列表中:

@interface HostViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, MatchmakingServerDelegate>

添加以下一行代碼到HostViewController.m的viewDidAppear方法中,由於還要給_matchmakingServer的delegate賦值,因此恰好放在MatchmakingServer alloc以後:

_matchmakingServer.delegate = self;

添加delegate實現方法:

#pragma mark - MatchmakingServerDelegate

- (void)matchmakingServer:(MatchmakingServer *)server clientDidConnect:(NSString *)peerID
{
    [self.tableView reloadData];
}

- (void)matchmakingServer:(MatchmakingServer *)server clientDidDisconnect:(NSString *)peerID
{
    [self.tableView reloadData];
}

就像你對MatchmakingClient和JoinViewController這兩個類作的同樣,你就是很簡單地刷新table view的內容。說到這兒,別忘了實現data source方法。替換numberOfRowsInSection和cellForRowAtIndexPath方法:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (_matchmakingServer != nil)
        return [_matchmakingServer connectedClientCount];
    else
        return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"CellIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
        cell = [[PeerCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];

    NSString *peerID = [_matchmakingServer peerIDForConnectedClientAtIndex:indexPath.row];
    cell.textLabel.text = [_matchmakingServer displayNameForPeerID:peerID];

    return cell;
}

這很是像你以前作的,不一樣的是,如今列表裏顯示的是鏈接的client,而不是可用的server。由於點擊table view cell在屏幕上是不須要任何效果的,因此添加以下方法來緊用選中的效果:

#pragma mark - UITableViewDelegate

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    return nil;
}

在文件的上方導入PeerCell的頭文件:

#import "PeerCell.h"

立刻就行了。你還須要添加這些缺乏的方法到MatchmakingServer。添加以下方法聲明到MatchmakingServer.h文件中:

- (NSUInteger)connectedClientCount;
- (NSString *)peerIDForConnectedClientAtIndex:(NSUInteger)index;
- (NSString *)displayNameForPeerID:(NSString *)peerID;

添加方法的實現部分到.m文件中:

- (NSUInteger)connectedClientCount
{
    return [_connectedClients count];
}

- (NSString *)peerIDForConnectedClientAtIndex:(NSUInteger)index
{
    return [_connectedClients objectAtIndex:index];
}

- (NSString *)displayNameForPeerID:(NSString *)peerID
{
    return [_session displayNameForPeer:peerID];
}

喔,敲了很多代碼啊!如今你能夠運行app了。從新啓動你的設備,它能夠做爲server起做用了(你能夠在client設備上啓動app,可是你尚未改變client端代碼,因此這真的沒有必要)。

如今,你點擊client設備上的一個server名稱,在這個server的table view列表裏就能夠看到這個client了。試試吧。

惋惜...什麼都沒有發生(哦,我知道了!)。就像先前我說的,若是一個client試圖鏈接server,直到這個server接受,二者才能創建起徹底的鏈接。

GKSession有另一個delegate方法作這個,就是session:didReceiveConnectionRequestFromPeer:方法。爲了接受新的鏈接,server必須實現這個方法而且向session對象發送acceptConnectionFromPeer:error: 消息。

在MatchmakingServer.m文件中,已經有了這個方法的框架,如今咱們要作的就是用下面的代碼完善它:

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
    #ifdef DEBUG
    NSLog(@"MatchmakingServer: connection request from peer %@", peerID);
    #endif

    if (_serverState == ServerStateAcceptingConnections && [self connectedClientCount] < self.maxClients)
    {
        NSError *error;
        if ([session acceptConnectionFromPeer:peerID error:&error])
            NSLog(@"MatchmakingServer: Connection accepted from peer %@", peerID);
        else
            NSLog(@"MatchmakingServer: Error accepting connection from peer %@, %@", peerID, error);
    }
    else  // not accepting connections or too many clients
    {
        [session denyConnectionFromPeer:peerID];
    }
}

首先,你要檢測server的狀態是否是"accepting connections"。若是不是,你就不能接受新的鏈接請求,拒絕請求你能夠用denyConnectionFromPeer:方法。當client鏈接數到達上限的時候,你也能夠調用那個方法來禁止鏈接,在Snap!中,咱們用maxClients這個屬性來控制最大鏈接數,值爲3。

若是一切準備就緒,你能夠調用acceptConnectionFromPeer:error:方法,以後,另外一個GKSession delegate方法將會被調用,而後就會有新的client顯示在table view列表中。再次試一下,在server設備上啓動app。

如今,在server的debug窗口應該輸出了:

Snap[4541:707] MatchmakingServer: Connection accepted from peer 1803140173
Snap[4541:707] MatchmakingServer: peer 1803140173 changed state 2

state 2就是GKPeerStateConnected狀態。恭喜!如今server和client已經鏈接成功。二者能夠經過GKSession對象相互發送消息了(這些也是你將要作的)。

下面就是個人iPod(服務器)截圖,裏面有三個已經鏈接進來的client:

image

注意:即便你能夠在屏幕上方的文本框中輸入另一個名稱,可是,在table view列表中顯示的永遠是設備的名稱(換句話說,也就是文本框的placeholder內容)。

 

錯誤和斷開鏈接的處理


立刻就要寫有關網絡處理的代碼,你要記住:事情老是不可預測的。在任什麼時候候,鏈接都有可能斷開,並且你還要妥善的處理好兩端,無論是client仍是server。

如何處理client端。好比說client等待被鏈接,或者鏈接已經被確認,而後server忽然離開。你如何處理要依據於你的app,可是在Snap!中,你將讓玩家退回主界面。

處理這樣的狀況,你必須在你GKSession的delegate方法檢查GKPeerStateDisconnected狀態,像下面這樣在MatchmakingClient.m文件中處理:

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
    . . .

    switch (state)
    {
        . . .

        // You're now connected to the server.
        case GKPeerStateConnected:
            if (_clientState == ClientStateConnecting)
            {
                _clientState = ClientStateConnected;
            }       
            break;

        // You're now no longer connected to the server.
        case GKPeerStateDisconnected:
            if (_clientState == ClientStateConnected)
            {
                [self disconnectFromServer];
            }
            break;

        case GKPeerStateConnecting:
            . . .
    }
}

先前,在GKPeerStateConnected和GKPeerStateDisconnected狀態中,你沒有實現任何東西,可是如今你在前者語句中將狀態機調整爲"connected"狀態,在後者語句中調用了一個新的方法disconnectFromServer。添加方法到類中:

- (void)disconnectFromServer
{
    NSAssert(_clientState != ClientStateIdle, @"Wrong state");

    _clientState = ClientStateIdle;

    [_session disconnectFromAllPeers];
    _session.available = NO;
    _session.delegate = nil;
    _session = nil;

    _availableServers = nil;

    [self.delegate matchmakingClient:self didDisconnectFromServer:_serverPeerID];
    _serverPeerID = nil;
}

這裏你又讓MatchmakingClient回到了"idle"狀態,而且清理和銷燬了GKSession對象。你還要調用一個新的delegate方法,讓JoinViewController知道這個client如今已經斷開鏈接了。

添加新的delegate方法聲明到MatchmakingClient.h文件中的protocol中:

- (void)matchmakingClient:(MatchmakingClient *)client didDisconnectFromServer:(NSString *)peerID;

這個delegate方法是處理一個已經鏈接了server的client失去鏈接的狀況,還有一種斷開鏈接的狀況是正在試圖鏈接server的client忽然斷開了。那麼後面這種狀況是另外一個GKSessionDelegate方法來處理的。用下面的方法替換MatchmakingClient.m中的方法:

- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error
{
    #ifdef DEBUG
    NSLog(@"MatchmakingClient: connection with peer %@ failed %@", peerID, error);
    #endif

    [self disconnectFromServer];
}

這裏沒什麼特別的。你只是在鏈接斷開的時候調用了disconnectFromServer方法。注意這個delegate方法在server明確調用denyConnectionFromPeer:方法拒絕client的鏈接請求時也會被調用,好比已經鏈接了3個client的時候。

由於你添加了一個新的方法聲明到MatchmakingClientDelegate protocol中,因此你要在JoinViewController.m中實現它:

- (void)matchmakingClient:(MatchmakingClient *)client didDisconnectFromServer:(NSString *)peerID
{
    _matchmakingClient.delegate = nil;
    _matchmakingClient = nil;
    [self.tableView reloadData];
    [self.delegate joinViewController:self didDisconnectWithReason:_quitReason];
}

除了最後一行,都太簡單了。由於你想要用戶回到主界面,那麼JoinViewController就必須讓MainViewController知道用戶失去鏈接了。用戶失去鏈接有不少不一樣的緣由,而且你須要讓主界面知道爲何,所以,在必要的時候能夠用alert view提示。

好比,若是用戶主動退出遊戲,那麼就沒有必要提示,由於用戶知道問什麼失去鏈接-畢竟是他本身按了退出按鈕。可是若是是網絡出錯致使的,作個友好的提示仍是不錯的。

這就意味着有兩件事情要作:添加新的delegate方法到JoinViewControllerDelegate中,添加_quitReason變量。

在JoinViewController.h文件適當的地方添加以下delegate方法聲明:

- (void)joinViewController:(JoinViewController *)controller didDisconnectWithReason:(QuitReason)reason;

Xcode將會警告,由於它並不知道有這變量QuitReason。這是一個在不少類中都要用到的結構,因此將它加入到Snap-Prefix.pch中,使得全部的代碼都能看到它。

typedef enum
{
    QuitReasonNoNetwork,          // no Wi-Fi or Bluetooth
    QuitReasonConnectionDropped,  // communication failure with server
    QuitReasonUserQuit,           // the user terminated the connection
    QuitReasonServerQuit,         // the server quit the game (on purpose)
}
QuitReason;

這裏有四個緣由,JoinViewController須要一個實例變量來儲存退出的緣由。你將會在幾個不一樣的地方給這個變量設置合適的值,在client真正失去鏈接的時候,你還要把這個消息傳遞給delegate。

添加實例變量到JoinViewController:

@implementation JoinViewController
{
    . . .
    QuitReason _quitReason;
}

在viewDidAppear:方法中初始化它:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    if (_matchmakingClient == nil)
    {
        _quitReason = QuitReasonConnectionDropped;

        // ... existing code here ...
    }
}

_quitReason默認值是"connection dropped"。除了用戶主動點擊退出按鈕,server都會認爲是網絡緣由,而不是一些故意的狀況。

由於你添加了一個新的方法到JoinViewController的delegate protocol中,所以你還要在MainViewController作一些工做。添加以下方法到MainViewController.m中的JoinViewControllerDelegate部分:

- (void)joinViewController:(JoinViewController *)controller didDisconnectWithReason:(QuitReason)reason
{
    if (reason == QuitReasonConnectionDropped)
    {
        [self dismissViewControllerAnimated:NO completion:^
        {
            [self showDisconnectedAlert];
        }];
    }
}

若是因爲網絡出錯斷開了鏈接,那麼你要關閉Join Game界面而且顯示alert。showDisconnectedAlert的代碼以下:

- (void)showDisconnectedAlert
{
    UIAlertView *alertView = [[UIAlertView alloc] 
        initWithTitle:NSLocalizedString(@"Disconnected", @"Client disconnected alert title")
        message:NSLocalizedString(@"You were disconnected from the game.", @"Client disconnected alert message")
        delegate:nil
        cancelButtonTitle:NSLocalizedString(@"OK", @"Button: OK")
        otherButtonTitles:nil];

    [alertView show];
}

試試吧。鏈接一個client到server,而後點擊server設備的home鍵(或者徹底退出)。一兩秒後,client將鏈接不到server。(server端也會丟失client的鏈接,可是由於server端如今是暫停狀態,在沒有從新回到遊戲界面以前是看不到任何東西的。)

client端debug窗口輸出:

Snap[98048:1bb03] MatchmakingClient: peer 1700680379 changed state 3
Snap[98048:1bb03] dealloc <JoinViewController: 0x9570ee0>

State 3固然就是GKPeerStateDisconnected狀態。app回到主界面會有一個提示信息:

image

就像你在debug窗口看到的那樣,JoinViewController已經deallocate了。隨着這個view controller一塊兒的還有MatchmakingClient對象。若是你要確認,能夠添加NSLog()到dealloc中:

- (void)dealloc
{
    #ifdef DEBUG
    NSLog(@"dealloc %@", self);
    #endif
}

很是好,可是若是用戶鏈接server成功後,點擊了退出按鈕再怎麼辦?這種狀況,client應該斷開鏈接而且不要顯示alert。在JoinViewController.m中完成exitAction:方法來作這些事情。

- (IBAction)exitAction:(id)sender
{
    _quitReason = QuitReasonUserQuit;
    [_matchmakingClient disconnectFromServer];
    [self.delegate joinViewControllerDidCancel:self];
}

首先,你設置了退出緣由爲"user quit",而後告訴client失去鏈接。當你接收到matchmakingClient:didDisconnectFromServer:回調消息時,它會告訴MainViewController退出的緣由是"user quit",並且沒有提示信息。

Xcode會提示"disconnectFromServer"方法沒有找到,這只是由於你咩有把它放到MatchmakingClient.h中。將下面的一行加進去:

- (void)disconnectFromServer;

再次運行app,鏈接,而後在client端點擊退出按鈕。你將會在server debug輸出窗口看到client已經失去鏈接的輸出,client的名字也將會在server端的列表中消失。

若是你在server端點擊home鍵進入後臺以後又恢復server app,以後你就須要從新回到主界面,並再次按下Host Game。可是在app暫停以後,原來的GKSession對象將再也不有效。

 

"無網絡"錯誤


GameKit只是容許你經過藍牙或者Wi-Fi來實現peer-to-peer鏈接。若是在鏈接的過程當中,任何一方藍牙和Wi-Fi不可用了,你應該給一個友好的錯誤提示。GKSession的嚴重錯誤有,好比在session:didFailWithError:通知的錯誤,因此在matchmakingClient.m中用下面的方法替換原方法:

- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
    #ifdef DEBUG
    NSLog(@"MatchmakingClient: session failed %@", error);
    #endif

    if ([[error domain] isEqualToString:GKSessionErrorDomain])
    {
        if ([error code] == GKSessionCannotEnableError)
        {
            [self.delegate matchmakingClientNoNetwork:self];
            [self disconnectFromServer];
        }
    }
}

真正的錯誤被封裝到NSError對象中,若是是一個GKSessionCannotEnableError錯誤,那麼僅僅網絡不可用。這種狀況你要告訴你的delegate(帶有一個新的方法)而且與server斷開鏈接。

添加新的delegate方法到MatchmakingClient.h中的protocol裏:

- (void)matchmakingClientNoNetwork:(MatchmakingClient *)client;

在JoinViewController.m中添加實現:

- (void)matchmakingClientNoNetwork:(MatchmakingClient *)client
{
    _quitReason = QuitReasonNoNetwork;
}

很簡單吧:你只是設置退出的緣由爲"no network",由於MatchmakingClient調用了disconnectFromServer方法,JoinViewController也獲得了didDisconnectFromServer消息,並且你還要把這些告訴MainViewController。你如今要作的就是讓MainViewController可以收到退出緣由的消息。

在MainViewController.m中實現以下方法:

- (void)joinViewController:(JoinViewController *)controller didDisconnectWithReason:(QuitReason)reason
{
    if (reason == QuitReasonNoNetwork)
    {
        [self showNoNetworkAlert];
    }
    else if (reason == QuitReasonConnectionDropped)
    {
        [self dismissViewControllerAnimated:NO completion:^
        {
            [self showDisconnectedAlert];
        }];
    }
}

showNoNetworkAlert的代碼:

- (void)showNoNetworkAlert
{
    UIAlertView *alertView = [[UIAlertView alloc] 
        initWithTitle:NSLocalizedString(@"No Network", @"No network alert title")
        message:NSLocalizedString(@"To use multiplayer, please enable Bluetooth or Wi-Fi in your device's Settings.", @"No network alert message")
        delegate:nil
        cancelButtonTitle:NSLocalizedString(@"OK", @"Button: OK")
        otherButtonTitles:nil];

    [alertView show];
}

測試一下這些代碼吧,在飛行模式下運行app(在這種模式下,Wi-Fi和藍藍牙被關閉了)。

注意:在個人設備上,我必需要先進入Join Game界面(這兒什麼都沒有發生),點擊退出按鈕回到主界面,而後再次進入Join Game界面。我不清楚爲何GameKit第一次沒有意識到這個問題。或許Reachability API中有更準確的方法來檢測藍牙和Wi-Fi的可用性吧。

debug窗口輸出:

MatchmakingClient: session failed Error Domain=com.apple.gamekit.GKSessionErrorDomain Code=30509 "Network not available." UserInfo=0x1509b0 {NSLocalizedFailureReason=WiFi and/or Bluetooth is required., NSLocalizedDescription=Network not available.}

顯示alert view的界面:

image

對於"no network"錯誤,確實沒有必要離開Join Game界面,即使你停用了session和任何的網絡活動。我以爲跳到主界面會使用戶感到迷惑。

注意:顯示alertview的代碼-事實上,在app上顯示文本的任何代碼-都應該使用NSLocalizedString()宏命令來進行國際化。即便你的app前期只須要English,爲你項目之後的國際化作準備是很是明智的。更多有關國際化信息,看這裏

這裏還有一個你要在client端處理的問題。在個人測試中,我發現有時候server變的不可用時,client仍然很執着地去嘗試鏈接。這種狀況,client會收到一個狀態改變爲GKPeerStateUnavailable的回調。

若是你沒有處理這種狀況,client端的鏈接最終會timeout,用戶也會獲得一些錯誤的提示信息。可是你也是能夠在代碼中檢測這種鏈接斷開錯誤的。

在MatchmakingClient.m,改變GKPeerStateUnavailable的case語句:

// The client sees that a server goes away.
case GKPeerStateUnavailable:
    if (_clientState == ClientStateSearchingForServers)
    {
        // ... existing code here ...
    }

    // Is this the server we're currently trying to connect with?
    if (_clientState == ClientStateConnecting && [peerID isEqualToString:_serverPeerID])
    {
        [self disconnectFromServer];
    }           
break;

 

在server端處理錯誤


在server端,處理斷開鏈接和錯誤與client端的很是類似,由於你已經在client端有處理的相關代碼了。因此很是簡單。

首先,處理"no network"問題。在MatchmakingServer.m文件中,改變session:didFailWithError:方法:

- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
    #ifdef DEBUG
    NSLog(@"MatchmakingServer: session failed %@", error);
    #endif

    if ([[error domain] isEqualToString:GKSessionErrorDomain])
    {
        if ([error code] == GKSessionCannotEnableError)
        {
            [self.delegate matchmakingServerNoNetwork:self];
            [self endSession];
        }
    }
}

除了如今你要調用一個叫endSession的方法用來清理以外,這裏跟你在MatchmakingClient作的幾乎相同。添加endSession:

- (void)endSession
{
    NSAssert(_serverState != ServerStateIdle, @"Wrong state");

    _serverState = ServerStateIdle;

    [_session disconnectFromAllPeers];
    _session.available = NO;
    _session.delegate = nil;
    _session = nil;

    _connectedClients = nil;

    [self.delegate matchmakingServerSessionDidEnd:self];
}

這裏沒什麼驚奇的。你還要調用兩個新的delegate方法,matchmakingServerNoNetwork:和matchmakingServerSessionDidEnd:,添加它們到MatchmakingServer.h的protocol中,而後在HostViewController.m中實現它們。

首先,添加聲明到protocol中:

- (void)matchmakingServerSessionDidEnd:(MatchmakingServer *)server;
- (void)matchmakingServerNoNetwork:(MatchmakingServer *)server;

而後,在HostViewController.m文件中添加對應的實現方法:

- (void)matchmakingServerSessionDidEnd:(MatchmakingServer *)server
{
    _matchmakingServer.delegate = nil;
    _matchmakingServer = nil;
    [self.tableView reloadData];
    [self.delegate hostViewController:self didEndSessionWithReason:_quitReason];
}

- (void)matchmakingServerNoNetwork:(MatchmakingServer *)server
{
    _quitReason = QuitReasonNoNetwork;
}

再一次,你在以前見到過一樣地邏輯。來吧,添加_quitReason實例變量到HostViewController中:

@implementation HostViewController
{
    . . .
    QuitReason _quitReason;
}

在HostViewController.h中添加新的方法到它的delegate protocol中:

- (void)hostViewController:(HostViewController *)controller didEndSessionWithReason:(QuitReason)reason;

最後,在MainViewController.m中實現這個方法:

- (void)hostViewController:(HostViewController *)controller didEndSessionWithReason:(QuitReason)reason
{
    if (reason == QuitReasonNoNetwork)
    {
        [self showNoNetworkAlert];
    }
}

在飛行模式下啓動app,而後試着開一局遊戲。你將會獲得"no network"錯誤。(若是第一次你沒有獲得錯誤提示,那麼退到主界面,再次點擊Host Game按鈕試試。)進入設置,關閉飛行模式,而後在切換回Snap!再一次,點擊Host Game按鈕,新的client應該可以找到這個server了。

爲了完整一些,在用戶點擊Host Game界面退出按鈕的時候,你還應該結束會話。因此替換HostViewController的exitAction:方法:

- (IBAction)exitAction:(id)sender
{
    _quitReason = QuitReasonUserQuit;
    [_matchmakingServer endSession];
    [self.delegate hostViewControllerDidCancel:self];
}

固然,endSession不是一個public方法,因此仍是把它加在MatchmakingServer的@interface中比較好:

- (void)endSession;

哎呀,僅僅是讓server和client相互找到對方就作了這麼多工做!(相信我,若是沒有GKSession,你還有大量的工做要作!)

很是酷的事情是,你能夠把MatchmakingServer和matchmakingClient類免費引用到其它的工程中!由於設計的這些類獨立於全部的view controller,它們在其它項目中很容易重用。

 

下一步該作什麼?


這是到目前爲止教程的範例工程

準備着手處理第三部分吧,在那部分,client和server能夠相互發送信息!

期間,你有任何有關這篇教程的問題或者評論,均可以在下面進行討論!

相關文章
相關標籤/搜索