翻译人:hany3000 博客:http://blog.csdn.net/hany3000
这篇文章作者是ios教程团队成员Matthijs Hollemans, 他是一位ios开发人员、设计师,你能通过Google+ 和 Twitter找到他。
u欢迎回到我们的7部分使用wifi或者蓝牙建立一个多人纸牌游戏的魔鬼教程 。
如果对于这个章节你还是个新手的话,那么首先你最好去看之前的 第一部分.那里你可以看到这个系列的第一部分。
在第一部分中,你已经建立了主菜单和主机游戏界面、加入游戏界面。你也建立了一个可以广播Snap服务的服务器和一个可以检测服务器的客户端,但是只能在Xcode调试面板能看到这些功能。
现在在这一部分,你将显示服务器和客户端的属性,完成匹配功能。让我们开始吧!!
获得开始:给用户显示搜寻的服务器
客户端类MatchmakingClient 有一个变量_availableServers,它是一个动态数组,用来保存客户端在本地网络搜索到的服务器列表,当 GKSession检测到一个新的服务器的时候就会把它的ID添加到这个数组。
当这些发生的时候你是怎么知道的呢?客户端类 MatchmakingClient 是GKSession的一个委托,你能够使用session:peer:didChangeState: 委托函数. 在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;
}
} |
新被发现的服务器依靠peerID属性被其他设备检测出来,这是一个字符串包含一个号码,例如”663723729.” ,这个号码仅仅对辨认服务器很重要,
第三个属性是 “state,” 它告诉你那个点怎么样了,当前你做的仅仅是状态 GKPeerStateAvailable 和 GKPeerStateUnavailable. 从他们的名字你应该明白,他们的状态标明一个新的服务器被发现了,或者一个新的服务器关闭了(可能是因为用户退出了程序)这依赖当时的环境,你也会添加到服务器的ID到 _availableServers的列表,或者你从这个列表中移除它。
这个代码将不编译,因为它也调用了self.delegate, 这是一个你还没定义的属性,客户端类MatchmakingClient 需要让 JoinViewController 知道一个新的服务器可以连接(或者一个服务器不能连接了),然后你通过一些委托函数来做这些工作。添加下列代码到 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 delegate = _delegate; |
JoinViewController 现在需要成为客户端类MatchmakingClient的委托,因此添加一个协议到 @interface 行在头文及iinzai JoinViewController.h:
@interface JoinViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, MatchmakingClientDelegate> |
在 JoinViewController.m’s viewDidAppear 函数内,添加下列代码在 客户端类 MatchmakingClient 对象被生成的地方:
_matchmakingClient.delegate = self; |
最后实现新的委托函数
#pragma mark - MatchmakingClientDelegate
- (void)matchmakingClient:(MatchmakingClient *)client serverBecameAvailable:(NSString *)peerID
{
[self.tableView reloadData];
}
- (void)matchmakingClient:(MatchmakingClient *)client serverBecameUnavailable:(NSString *)peerID
{
[self.tableView reloadData];
} |
你简单告诉表视图要重新加载自己,那一丝是应高重新替换条数据函数来实现这些功能
- (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;
} |
这是最基本的表视图的代码,你简单的要求客户端类 MatchmakingClient 对象为表视图要显示的行的数据,使用一个新的函数,这个函数应该被添加到客户端类 MatchmakingClient中,添加他们的签名到 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];
} |
这些函数都很简单, 在_availableServers 和 _session 对象之间被使用,这些代码都被封装的很容易被使用。这就行了,重启这个程序在设备上,你会看到下列图像
![Join Game screen shows available servers (ugly) Join Game screen shows available servers (ugly)](http://cdn1.raywenderlich.com/wp-content/uploads/2012/04/Available-servers-ugly-480x255.png)
运行的很好!客户端显示了服务器的名字(在屏幕焦点上方,我使用我的iPod做服务器)
不幸的是,它看上去不是很漂亮,不过这很容易解决,添加一个新的 Objective-C 类到项目中,基类是 UITableViewCell, 命名为 PeerCell. (我建议把PeerCell 源文件到一个新的组中,命名为“Views.”)
替换下列代码到实现文件 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一个新的背景,在 JoinViewController’的cellForRowAtIndexPath 函数中,替换下列代码,这个是用来生成表视图的列:
cell = [[PeerCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; |
不要忘记添加一个头文件给PerCell.h,现在表视图的列看上去好看了很多把。
![Join Game screen shows available servers (pretty) Join Game screen shows available servers (pretty)](http://cdn5.raywenderlich.com/wp-content/uploads/2012/04/Available-servers-480x255.png)
试试下面的:在设备中退出程序,就是设定为服务器的那个,客户端现在应该从表视图中溢出了服务器的名字,如果你有足够的设备,试试让多于一个的设备做服务器,客户端应该能找到所有的服务器并在视图中显示他们的名字。
提示: 客户端辨别服务器出现或者消失可能需要几秒钟,因此如果表视图没有立刻重新加载数据的话不要惊恐。
一个简单的状态设备
下一件事情就是客户端连接服务器,到a目前为止,你的程序还没有做出任何的连接,客户端已经显示了可以连接的服务器,但是服务器不知道任何关于客户端的信息,仅仅在你按下了服务器的名字之后,客户端才会宣布它将连接那个服务器.
因此客户端类MatchmakingClient可以做两件不同的事情,首先它要寻找要加入的服务器,当你选择一个服务器的时候,它将会试着去连接那个服务器,在这一点上,假设它连接成功了,它就会保留这个连接,客户端类MatchmakingClient就不再对其他的服务器感兴趣,因此它不会再寻找其他的服务器或者刷新那个服务器列表 _availableServers(它也不会再告诉他的委托关于这些)
客户端类MatchmakingClient的这些不同的内部状态可以使用一个图表来说明,它的整个状态图表如下图:
![State diagram for MatchmakingClient State diagram for MatchmakingClient](http://cdn3.raywenderlich.com/wp-content/uploads/2012/04/State-diagram-MatchmakingClient-480x232.png)
客户端类MatchmakingClient能够有四个状态,它开始的时候处于空闲状态 “idle”,就是呆在那,什么都不做,当你调用函数 startSearchingForServersWithSessionID:的时候, 它就开始转移到“寻找服务器”状态,这就是目前为止你所写的代码。
当用户决定连接这个服务器的时候,客户端首先进入连接状态“connecting” ,这个时候它就视图连接服务器,当这个连接成功建立的时候就进入连接状态,在任何时候如果服务器抛弃了这个连接,客户端就会进入空闲状态。
客户端类MatchmakingClient处于不同的状态的表现是不同的,在寻找服务器的状态的时候,它将在服务器列表中添加或者移除服务器 ,但是在“连接中或者已经连接上了的时候,它不做这些。
使用图表来描述你的对象所处的可能状态,你马上就会明白了你的对象的是在不同的环境下要做什么,在这个教程中你将使用状态图表很多次(这里你面临的事情有点复杂)
状态图表的实现被称为”状态机“,你将保持客户端的状态轨迹,使用一个枚举和一个实例变量,添加下列代码到MatchmakingClient.m的顶部,在@implementation 的上方:
typedef enum
{
ClientStateIdle,
ClientStateSearchingForServers,
ClientStateConnecting,
ClientStateConnected,
}
ClientState; |
这四个值表明了这个对象的四个抓鬼太,添加一个实例变量:
@implementation MatchmakingClient
{
. . .
ClientState _clientState;
} |
状态是这个对象内部的事情,所以你不需要把它放在property中,初始化的时候,状态应该是空闲状态“idle,”,因此添加初始化函数到这个类中
- (id)init
{
if ((self = [super init]))
{
_clientState = ClientStateIdle;
}
return self;
} |
现在你将来改变这些函数的一些地方,就是之前你写的关于不同的状态那块。首先是 startSearchingForServersWithSessionID:. 这个函数应该仅仅是客户端类处于的空闲状态的时候。改变代码如下:
- (void)startSearchingForServersWithSessionID:(NSString *)sessionID
{
if (_clientState == ClientStateIdle)
{
_clientState = ClientStateSearchingForServers;
// ... existing code goes here ...
}
} |
最后改变两个基本状态,在session:peer:didChangeState::
// 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; |
你仅仅处理了GKPeerStateAvailable 和 GKPeerStateUnavailable 信息,如果你处于 ClientStateSearchingForServers 状态,这里提示你有两种状态,一种是点的状态,被委托函数来汇报,一种是客户端类的状态,我命名为 _clientState, 不要搞混了。
连接服务器
添加一个新的函数签名到MatchmakingClient.h:
- (void)connectToServerWithPeerID:(NSString *)peerID; |
从名字上能看出来,你将使用这个来连接客户端到指定的服务器,添加它的实现函数到 .m 文件中:
- (void)connectToServerWithPeerID:(NSString *)peerID
{
NSAssert(_clientState == ClientStateSearchingForServers, @"Wrong state");
_clientState = ClientStateConnecting;
_serverPeerID = peerID;
[_session connectToPeer:peerID withTimeout:_session.disconnectTimeout];
} |
你仅仅能在”搜寻服务器“状态下调用这个函数,如果你不这么做,你的程序将会出现一个失败的中断而退出。这是一点保护程序来确保状态机的正常运行。
你改变状态到连接状态 “connecting,” ,保存服务器的peerID到一个新的实例变量,命名为_serverPeerID, 告诉GKSession 对象连接客户端到这个peerID,对于连接超时-在它断开一个没有被回应的连接之前这个对话会等多久呢。你在GKSession中使用默认的断开连接超时
添加一个新的实例变量 _serverPeerID:
@implementation MatchmakingClient
{
. . .
NSString *_serverPeerID;
} |
这就是对客户端所做的,现在你必须调用这个新的函数connectToServerWithPeerID:T很明显的地方是加入游戏试图控制器的表视图委托,添加下列代码到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];
}
} |
这个应该是轻轻直接跳转,首先你决定了服务器的peerID(依靠看到的那行数据 indexPath.row), 然后调用一个新的方法来生成一个连接,提示你也添加了一个视图从 “waitView” 开关到屏幕上,这是为了覆盖表视图和其他控件重新调用之前的那个等待视图,这个等待视图是在nib的第二个顶级视图,你将使用它作为进度指示符。
现在你运行程序,按击服务的名字。就会出现下列显示 :
![The Connecting... screen The Connecting... screen](http://cdn4.raywenderlich.com/wp-content/uploads/2012/04/Connecting-screen-480x255.png)
客户端已经移到连接状态,等待服务器的确认,这个时候你不想让用户再试图加入其他服务器,因此你显示了一个临时等待屏幕。
如果你看一下调试输出面板针对服务器陈谷香,你会发现现在都发生了什么:
Snap[4503:707] MatchmakingServer: peer 1310979776 changed state 4
Snap[4503:707] MatchmakingServer: connection request from peer 1310979776 |
这些来自GKSession的通知告诉服务器,这个客户端(有一个id是“1310979776″在本例中)企图进行连接,在下一个章节中,你将使服务器类MatchmakingServer 变得有点只能,以致它将接受这些连接并显示连接的客户端在屏幕上。
提示:调试输出说 “changed state 4.” 这个数字的值代表着下列GKPeerState常量其中的一个数值
- 0 = GKPeerStateAvailable
- 1 = GKPeerStateUnavailable
- 2 = GKPeerStateConnected
- 3 = GKPeerStateDisconnected
- 4 = GKPeerStateConnecting
提示:如果你在Xcode中在几个设备上同时运行这个程序 ,你可以使用调试条来切换他们的调试输出。
![Switching debug output between devices Switching debug output between devices](http://cdn2.raywenderlich.com/wp-content/uploads/2012/04/Switching-debug-output-between-devices-480x138.png)
在服务器上接受连接
现在你有一个恩d客户端企图生成一个连接,但是在服务器那边,你在每一件事情都搞定之前你必须接受这些连接,这在服务器那边经常发生的事情。
但是在你接受之前,你要把状态机放到服务器那边,添加下列代码到 MatchmakingServer.m的顶部:
typedef enum
{
ServerStateIdle,
ServerStateAcceptingConnections,
ServerStateIgnoringNewConnections,
}
ServerState; |
不像客户端。服务器仅仅有三个状态。
![State diagram for MatchmakingServer State diagram for MatchmakingServer](http://cdn5.raywenderlich.com/wp-content/uploads/2012/04/State-diagram-MatchmakingServer-480x190.png)
那是很简单的,”忽略新的连接“状态是用来等你的主机开始纸牌游戏之后,从那个时候开始,新的客户端都不被允许进行连接,添加一个新的变量实例来保留当前的状态。
@implementation MatchmakingServer
{
. . .
ServerState _serverState;
} |
跟客户端一样。在初始化函数中初始化服务器的状态为空闲状态idle:
- (id)init
{
if ((self = [super init]))
{
_serverState = ServerStateIdle;
}
return self;
} |
添加一个声给 startAcceptingConnectionsForSessionID: 这个将用来检测状态是否是空闲状态,然后把它改成”接受连接“状态
- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID
{
if (_serverState == ServerStateIdle)
{
_serverState = ServerStateAcceptingConnections;
// ... existing code here ...
}
} |
非常酷,现在你为什么不声生成几个GKSessionDelegate 函数来做些事儿呢。像当新的服务器可以连接的时候客户端就会被通知到一样,当客户端试图连接服务器的时候服务器也会被通知到。在 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 这两个状态感兴趣了,这个逻辑是类似你在客户端上看到的:你简单的把PeerID添加到一个数组中,然后通知委托。
当然你还没有定义委托协议给服务器类 MatchmakingServer,现在开始做把,添加下列代码到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 delegate = _delegate; |
但是谁在服务器上来扮演这个委托的角色呢,当时主机游戏视图控制器了,切换到 HostViewController.h 然后添加MatchmakingServerDelegate 到实现协议列表中:
@interface HostViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, MatchmakingServerDelegate> |
添加下列行到HostViewController.m’s viewDidAppear, 在生成服务器类的之后,为了连接委托变量
_matchmakingServer.delegate = self; |
然后实现这个委托函数
#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所做的,你简单的刷新了表视图的内容,说到这,你仍然需要实现数据远函数,替换以下代码 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;
} |
这几乎是你之前所做的镜像,除了现在你显示的是连接客户端的列表,之前的是可以连接的服务器列表,因为在这个屏幕上按下视图中的某一行没有任何的印象,添加下列代码来禁止行选择。
#pragma mark - UITableViewDelegate
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
return nil;
} |
添加一个头文件来实现PeerCell到文件的顶部:
你快要完成啦。. 你需要添加小时的函数到服务器类中,添加下列函数签名到 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];
} |
哇,敲了很多代码!!现在你再来运行下程序,在设备上重启它,作为服务器(如果在客户端的设备上启动一下程序也性,但是你客户端代码什么都没改变,因为这是没什么必要的)
现在当你按下服务器的名字的时候在客户端断的设备上,客户端的名字将出现在服务器上的表视图中,试试。
除了那个。。。什么都没发生,因为我之前说的,在这个点上,客户端仍然试图生成一个跟服务器之间的连接,但是连接没有完成知道服务器接受它。
GKSession 有 其他的委托函数来做这些工作,名字为session:didReceiveConnectionRequestFromPeer:. 来接受到来的连接,服务器也有实现函数来调用 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];
}
} |
首先你要检查是否服务器的状态是“接受连接”状态,如果不是,显然你不想接受任何新的连接,因为你调用 denyConnectionFromPeer:. 当你已经连接客户端的数量达到最大额度时你也会调用这个,作为最大连接客户端数额,Snap!被设置为3.
如果每件事情都检查好了,你将调用acceptConnectionFromPeer:error:. 在那之后,其他的GKSession 委托函数将被调用,新的客户端将显示在表视图中,重启设备上的程序,再试一次。
服务器上的调试输出如下
Snap[4541:707] MatchmakingServer: Connection accepted from peer 1803140173
Snap[4541:707] MatchmakingServer: peer 1803140173 changed state 2 |
State 2 对应的是GKPeerStateConnected. 恭喜!你已经建立了一个连接在服务器和客户端之间,两个设备现在能发送消息了在彼此之间,当然是要通过 GKSession 对象来做这些(你将做很多小事情)
下面是我的iPod(做主机)的截屏,有三个客户端连接。
![Server with 3 clients Server with 3 clients](http://cdn3.raywenderlich.com/wp-content/uploads/2012/04/Server-with-3-clients.png)
提示:即使你能在这个屏幕顶部的文本区域中键入其他名字,在表视图中通常出现的依然是设备自己的名字
错误和断开连接的控制
当进行写网络的程序的时候这里有写事情要注意,就是异常extremely unpredictable. 在任何时候,连接都可能中断,你需要控制双方出现的异常,包括客户端和服务器。
这里说的是如何控制客户端,说客户端正在等待连接或者连接已经建好了,结果服务器突然断掉了,你的程序下一步该做什么?但是在Snap!中,你将让玩家返回主屏幕。
为了控制这个形势,你必须检查GKPeerStateDisconnected 状态,这个检查在你的GKSession 委托函数中,添加下列代码到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中实现任何事情,但是现在你已经把状态机移到”连接状态“了,之后你要调用新的函数 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 到空闲状态,清楚和销毁KSession 对象,你也调用了一个新的委托函数使得JoinViewController 知道客户端现在要断开连接了。
添加一个新的委托函到MatchmakingClient.h中的协议部分:
- (void)matchmakingClient:(MatchmakingClient *)client didDisconnectFromServer:(NSString *)peerID; |
这关心的是已经连接到服务器的客户端要断开连接的脚本,但是这根仍旧在连接进行中的客户端是不同的,那个形势是被其他的 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 ,什么时候都性,提示这个委托函数也被调用了当客户端试图连接而服务器调用 denyConnectionFromPeer:,例如已经有三个客户端连接的时候。
因为你添加了一个新的函数到MatchmakingClientDelegate 协议中,你也必须在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 知道玩家断开连接了,有不同的原因导致玩家断开连接,你需要让主屏幕知道原因,因此必要的时候它能显示一个警告视图 .
举例来说,如果玩家退出了游戏,然后没有警告被关闭,因为玩家已经知道它为什么要断开了,在那之后,它按下退出键,但是万一万一网络坏掉了,这就很容易出现一些异常。
,
这就意味着有两个以上的事情要去做:添加一个新的委托函数到 JoinViewControllerDelegate, 添加 _quitReason 变量.
首先,这个委托函数。添加一个声明头文件t JoinViewController.h中:
- (void)joinViewController:(JoinViewController *)controller didDisconnectWithReason:(QuitReason)reason; |
Xcode 现在将会抱怨因为它不知道这个 QuitReason符号是什么. 这是一个类型定义typedef是你即将在多行之间都要用的一种,因为把它添加到 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; |
这是Snap!能够辨认的四个原因,这个 JoinViewController 需要一个变量实例来存储退出的原因,你将设置这个变量在很多地方,然后等你客户端真的断开的时候,你将通过它到你自己的委托。
添加这个变量实例到JoinViewController:
@implementation JoinViewController
{
. . .
QuitReason _quitReason;
} |
你将在 viewDidAppear:中初始化这个变量
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (_matchmakingClient == nil)
{
_quitReason = QuitReasonConnectionDropped;
// ... existing code here ...
}
} |
_quitReason 的默认值是 “connection dropped.” ,除非用户是因为其他原因退出。例如依靠退出按钮退出,一个服务器的退出将被视为一个网络问题,而不是故意发生的事情。
因为你已经添加了一个新的函数到JoinViewController’s 的委托协议中,你也必须做些事情在MainViewController. 添加下列函数到MainViewController.m, 在这个 JoinViewControllerDelegate 区域:
- (void)joinViewController:(JoinViewController *)controller didDisconnectWithReason:(QuitReason)reason
{
if (reason == QuitReasonConnectionDropped)
{
[self dismissViewControllerAnimated:NO completion:^
{
[self showDisconnectedAlert];
}];
}
} |
如果是因为网络错误而导致的连接断开,你就爱那个关闭游戏屏幕然后显示一个警告,这个代码如下f 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];
} |
试试,连接一个客户端到服务器,在服务器的这个设备上按下Home 按钮 (或者完全退出这个程序)在一秒或者两秒之后,服务器将变得不能使用,客户端这边的连接将会被断开(在服务器这边也会被断开,但是因为服务器的程序现在被挂起来了,服务器将不会看到任何事情,一直到程序恢复)
客户端的调试输出如下:
Snap[98048:1bb03] MatchmakingClient: peer 1700680379 changed state 3
Snap[98048:1bb03] dealloc <JoinViewController: 0x9570ee0> |
状态 3 当然是GKPeerStateDisconnected. 程序带着一个警告信息返回到主屏幕。 :
![The disconnected alert The disconnected alert](http://cdn3.raywenderlich.com/wp-content/uploads/2012/04/Disconnected-alert-480x255.png)
正如你在调试输出看到的,这个 JoinViewController 这个变量被释放的时候,随着试图控制器,这个客户端对象 MatchmakingClient 也会被释放,如果你想确认这一点,添加一个NSLog() 到 dealloc:
- (void)dealloc
{
#ifdef DEBUG
NSLog(@"dealloc %@", self);
#endif
} |
这是非常的好。不过如果玩家连接服务器控制后按下退出按钮会怎么样呢,在这里,客户端应该是断开连接,没有警告提示,你可以生成一些发生在 exitAction:的事情。在 JoinViewController.m:中
- (IBAction)exitAction:(id)sender
{
_quitReason = QuitReasonUserQuit;
[_matchmakingClient disconnectFromServer];
[self.delegate joinViewControllerDidCancel:self];
} |
首先你设置了退出原因”用户退出“,然后你告诉客户端去断开连接,现在当你收到客户端的 matchmakingClient:didDisconnectFromServer: 回调信息。它将告诉主视图控制器原因是”用户退出“,不是其他警告信息。 .
Xcode 抱怨这个 “disconnectFromServer” 函数是未知的,但那仅仅是因为你没有把它放到头文件MatchmakingClient.h 中. 现在放
- (void)disconnectFromServer; |
再次运行这个程序,做一个连接,然后按下退出按钮在客户端,服务器的调试输出面板应该看到客户端自己断开了,客户端的名字也应该从表视图中消失。
提示,如果你在按下home按钮之后把程序再恢复这个服务器程序,你需要再次回到主屏幕按下主机游戏:,这个 GKSession 对象在程序被挂起之后就不再合法了。
“没有网络” 的错误
游戏套件仅仅让你在蓝牙或者一个waif网络之间建立点对点连接,如果蓝牙或者wifi都不可用,你应该给用户一个有好的警告信息,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, 那么网络就是简单的不能够使用,然后,你就告诉你的委托(一个新的函数)从服务器端断开。
添加一个新的委托函数到 MatchmakingClient.h中的委托协议里:
- (void)matchmakingClientNoNetwork:(MatchmakingClient *)client; |
添加它的实现函数到JoinViewController.m:
- (void)matchmakingClientNoNetwork:(MatchmakingClient *)client
{
_quitReason = QuitReasonNoNetwork;
} |
这其实非常简单:你只是设置了退出原因为”没有网络“,因为客户端类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];
} |
来测试这个代码。在飞机模式中的设备上运行这个程序(蓝牙和wifi都关掉)
提示: 在我的设备上,我必须到加入游戏屏幕Join Game screen (这里什么都没发生)中, 按下退出按钮回到主菜单,再次进入到加入游戏屏幕,首先我不确定为什么没有辨认出这个问题,可能更有力的办法是使用 底层API来检测蓝牙和Wifi是否可以使用。
调试输出面板将显示:
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.} |
然后屏幕上显示一个警告视图:
![The no-network alert The no-network alert](http://cdn3.raywenderlich.com/wp-content/uploads/2012/04/No-network-error.png)
为了这个”没有网络“错误,你不需要真的离开加入游戏界面Join Game screen, 及时你停用这个对话session 和任何网络活动。我认为对于用户来说跳回到主屏幕是有点混乱了。 .
提示: 显示警告视图的这段代码,实际上在这个程序中任何代码都可以显示文本,---使用 NSLocalizedString() 宏 及时你的程序只是首先做写英语方面的事情,它也是智能的准备你的代码来实现本地化,你之后将会做这些工作,想了解更多信息请看这个教程.
更多的情况需要你控制的是在客户端这一边,在我的测试中,我发现很多时候,一个当客户端正在连接服务器的时候这个服务器变得不能够使用,这里客户端会收到一个回调函数伴随着得到 状态GKPeerStateUnavailable.
如果你没有控制这种情况,客户端将超时,用户将得到一些错误信息,但是你能够写代码来让程序检查连接的种类。
在 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; |
控制服务器上的错误
在服务器上,处理断开连接和错误很类似,你已经做过一些代码来处理断开的客户端,所以这很简单。 t
首先。处理”无网络“情况,在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];
}
}
} |
这几乎跟你在客户端类MatchmakingClient中所做的几乎相同。出了现在你要调用一个新的函数名字叫endSession 来清理数据。添加 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];
} |
这没什么大的惊喜,你调用了两个新的委托函数 matchmakingServerNoNetwork: 和matchmakingServerSessionDidEnd:. 把他们添加到你 MatchmakingServer.h中的协议中去,实现部分写到HostViewController.
首先,新的函数签名要写在协议里。
- (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;
} |
And add a new method to its delegate protocol in HostViewController.h:
- (void)hostViewController:(HostViewController *)controller didEndSessionWithReason:(QuitReason)reason; |
最后在 MainViewController.m中实现这些函数:
- (void)hostViewController:(HostViewController *)controller didEndSessionWithReason:(QuitReason)reason
{
if (reason == QuitReasonNoNetwork)
{
[self showNoNetworkAlert];
}
} |
在飞机模式下在设备上运行程序,建立主机游戏,你应该得到“没有网络”的错误(如果你第一时间没有得到这个错误,退到主菜单,按下主机游戏)然后去设置里面关闭飞机模式,再回到Snap!按下主机游戏,现在客户端就可以找到服务器了。
为了让游戏玩起来比较友好,当用户按下推出按钮的时候在主机界面里,你也应该结束对话。因此替换 exitAction: 在 HostViewController.m文件中
- (IBAction)exitAction:(id)sender
{
_quitReason = QuitReasonUserQuit;
[_matchmakingServer endSession];
[self.delegate hostViewControllerDidCancel:self];
} |
当然endSession不是一个公用函数 ,添加他到@interface 当然在文件MatchmakingServer中
为了让服务器和客户端找到彼此我们做了很多工作!(相信我,如果你没有GKSession你将做更多更多的工作才能完成这些功能)
更酷的事情是,你可以把这个客户端和服务器类放到任何项目中去使用,所有这些功能都是免费的!!!!因为这些类被设计是完全独立那些视图控制器的,他们很容易在其他项目中被重用。
继续关注下一章节!!!