转自朋友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能够相互发送信息!
期间,你有任何有关这篇教程的问题或者评论,均可以在下面进行讨论!