轉自朋友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數組
這篇文章是由iOS教程團隊成員Matthijs Hollemans發表的,一個經驗豐富的開發工程師和設計師。你能夠在Google+和Twitter上找到他。服務器
歡迎回到使用UIKit經過藍牙或者Wi-Fi製做多人卡片遊戲系列教程。網絡
若是你以前沒有接觸過本系列教程,請先看這裏。這裏你能夠看到這個遊戲的一些視頻,接下來將邀請你進入本系列教程的學習。session
在第一篇教程,你建立了主菜單和基本的Host Game and Join Game界面。app
你已經建立了一個能散播消息的server和可以偵測server的client,可是到目前爲止,很明顯還有些功能僅僅是在Xcode的輸出窗口中打印些log而已。框架
在第二部分中,也就是本篇教程,你將在屏幕上展現出一些可用的server和一些可以相連的client,而且完成卡片的配對。開始吧!dom
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,你應該可以看到下面這個界面:
成功了,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看起來跟界面很是匹配了:
試試這樣:退出做爲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各類狀態的示意圖:
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。
添加新的方法聲明到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的名字,會看到以下界面:
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輸出窗口:
如今你有一個正試圖鏈接的client,在後續事情完成以前,你必須先接受鏈接。這些都是在MatchmakingServer完成的。
在完成那些事情以前,要先在server設置一個state machine。添加以下的typedef到MatchmakingServer.m文件的上部:
typedef enum { ServerStateIdle, ServerStateAcceptingConnections, ServerStateIgnoringNewConnections, } ServerState;
不像client,server只有三個狀態。
這真是太簡單了。當遊戲開始時,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:
注意:即便你能夠在屏幕上方的文本框中輸入另一個名稱,可是,在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回到主界面會有一個提示信息:
就像你在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的界面:
對於"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端,處理斷開鏈接和錯誤與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能夠相互發送信息!
期間,你有任何有關這篇教程的問題或者評論,均可以在下面進行討論!