转自朋友Tommy 的翻译,自己只翻译了第三篇教程。

译者: 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制作多人卡片游戏系列教程。

如果你之前没有接触过本系列教程,请先看这里。这里你可以看到这个游戏的一些视频,接下来将邀请你进入本系列教程的学习。

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

你已经创建了一个能散播消息的server和能够侦测server的client,但是到目前为止,很明显还有些功能仅仅是在Xcode的输出窗口中打印些log而已。

在第二部分中,也就是本篇教程,你将在屏幕上展示出一些可用的server和一些能够相连的client,并且完成卡片的配对。开始吧!

开始:将server展示给用户


MatchmakingClient类有一个_availabelServers变量,一个NSMutableArray数组,这些是为了储存client侦测到的server的。当GKSession侦测到一个新的server时,你就把这个server的peer ID加到这个数组中。

你怎么能知道什么时候有新的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。

连接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的名字,会看到如下界面:

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输出窗口:

在server端接受连接请求


现在你有一个正试图连接的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端处理错误


在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可以相互发送信息!

期间,你有任何有关这篇教程的问题或者评论,都可以在下面进行讨论!

使用UIKit制作卡牌游戏(二)ios游戏篇的更多相关文章

  1. 使用UIKit制作卡牌游戏(三)ios游戏篇

    译者: Lao Jiang | 原文作者: Matthijs Hollemans写于2012/07/13 转自朋友Tommy 的翻译,自己只翻译了这第三篇教程. 原文地址: http://www.ra ...

  2. 使用UIKit制作卡牌游戏(一)ios游戏篇

    转自朋友Tommy 的翻译,自己只翻译了第三篇教程. 译者: Tommy | 原文作者: Matthijs Hollemans写于2012/06/29 原文地址: http://www.raywend ...

  3. 从零开始开发一款H5小游戏(二) 创造游戏世界,启动发条

    本系列文章对应游戏代码已开源 Sinuous game 上一节介绍了canvas的基础用法,了解了游戏开发所要用到的API.这篇文章开始,我将介绍怎么运用这些API来完成各种各样的游戏效果.这个过程更 ...

  4. [Firefly引擎][学习笔记二][已完结]卡牌游戏开发模型的设计

    源地址:http://bbs.9miao.com/thread-44603-1-1.html 在此补充一下Socket的验证机制:socket登陆验证.会采用session会话超时的机制做心跳接口验证 ...

  5. TCG卡牌游戏研究:《炉石战记:魔兽英雄传》所做的改变

    转自:http://www.gameres.com/665306.html TCG演进史 说到卡牌游戏,大家会联想到什么呢? 是历史悠久的扑克牌.风靡全球的<MTG 魔法风云会>与< ...

  6. Unity3D_(游戏)卡牌04_游戏界面

        启动屏界面.主菜单界面.选关界面.游戏界面 卡牌01_启动屏界面 传送门 卡牌02_主菜单界面 传送门 卡牌03_选关界面 传送门 卡牌04_游戏界面    传送门 选关界面效果 (源代码在文 ...

  7. 游戏制作之路:一个对我来说可实现的High-end的Mac/iOS游戏制作大概计划

    对于学习一些东西,我比较习惯任务驱动式的学习,也就是说,要事先订好一个目标,要做什么东西,达到什么效果,然后根据自己了解的知识作一个可以实现这个目标的计划. 现在要学的是游戏制作,而且是High-en ...

  8. JLOI 2013 卡牌游戏

    问题描述: N个人坐成一圈玩游戏.一开始我们把所有玩家按顺时针从1到N编号.首先第一回合是玩家1作为庄家.每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡片上的数字为X,则庄家首先 ...

  9. [JLOI2013]卡牌游戏

    [题目描述 Description] N个人坐成一圈玩游戏.一开始我们把所有玩家按顺时针从1到N编号.首先第一回合是玩家1作为庄家.每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡 ...

随机推荐

  1. [JS1] 如何嵌入

    <html> <head> <title>在HTML文档中嵌入JavaScript代码是如何嵌入到HTML文档中的.</title> <scrip ...

  2. paip.消除 Java 的冗长try/catch/finally

    paip.消除 Java 的冗长try/catch/finally 作者Attilax 艾龙,  EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://blo ...

  3. 看2015年TFC游戏大会,云计算何以唱主角

    日前,第10界TFC游戏大会浩浩荡荡地在北京国际会议中心成功举办了.与往届不同的是,这一次TFC的金苹果奖被四家云计算公司夺走,它们分别是金山云.阿里云.腾讯云和首都在线.四家云计算公司夺走了游戏大会 ...

  4. VS2013编译经常卡在正在从以下位置加载xxx.dll的符号

    换了系统后,重新下载了一个vs2013 with update2安装,编译的时候总是卡在 正在从以下位置加载xxx.dll的符号 如图: 解决方法: 进入VS---工具---选项----调试----符 ...

  5. hdu1875 畅通工程再续 最小生成树并查集解决---kruskal

    http://acm.hdu.edu.cn/showproblem.php?pid=1875 New~ 欢迎“热爱编程”的高考少年——报考杭州电子科技大学计算机学院关于2015年杭电ACM暑期集训队的 ...

  6. Prototype in JavaScript

    声明 本文旨在入门,简单了解下何为prototype & __proto__ 原型对象 我们创建每个函数都有个prototype(原型)属性,该属性是一个指针,指向一个对象,而这对象的用途是包 ...

  7. Java中Atomic包的实现原理及应用

    1. 同步问题的提出 假设我们使用一个双核处理器执行A和B两个线程,核1执行A线程,而核2执行B线程,这两个线程现在都要对名为obj的对象的成员变量i进行加1操作,假设i的初始值为0,理论上两个线程运 ...

  8. Apache Solr查询语法

    常用: q - 查询字符串,必须的. fl - 指定返回那些字段内容,用逗号或空格分隔多个. start - 返回第一条记录在完整找到结果中的偏移位置,0开始,一般分页用. rows - 指定返回结果 ...

  9. Unity3D Shader入门指南(二)

    关于本系列 这是Unity3D Shader入门指南系列的第二篇,本系列面向的对象是新接触Shader开发的Unity3D使用者,因为我本身自己也是Shader初学者,因此可能会存在错误或者疏漏,如果 ...

  10. 使用 dbms_xplan.display 按照 plan_hash_value 查执行计划的方法

    dbms_xplan.display_* 能按照 plan_hash_value 只有 display_awr 方法,如果这个SQL PLAN 刚刚生成,没有写入到AWR怎么办呢? 可以将 V$SQL ...