如何制作一个简单的蓝牙网络多人扑克游戏第二部分

翻译人: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)

运行的很好!客户端显示了服务器的名字(在屏幕焦点上方,我使用我的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)

试试下面的:在设备中退出程序,就是设定为服务器的那个,客户端现在应该从表视图中溢出了服务器的名字,如果你有足够的设备,试试让多于一个的设备做服务器,客户端应该能找到所有的服务器并在视图中显示他们的名字。 

提示: 客户端辨别服务器出现或者消失可能需要几秒钟,因此如果表视图没有立刻重新加载数据的话不要惊恐。 

一个简单的状态设备

下一件事情就是客户端连接服务器,到a目前为止,你的程序还没有做出任何的连接,客户端已经显示了可以连接的服务器,但是服务器不知道任何关于客户端的信息,仅仅在你按下了服务器的名字之后,客户端才会宣布它将连接那个服务器.  

因此客户端类MatchmakingClient可以做两件不同的事情,首先它要寻找要加入的服务器,当你选择一个服务器的时候,它将会试着去连接那个服务器,在这一点上,假设它连接成功了,它就会保留这个连接,客户端类MatchmakingClient就不再对其他的服务器感兴趣,因此它不会再寻找其他的服务器或者刷新那个服务器列表 _availableServers(它也不会再告诉他的委托关于这些)

客户端类MatchmakingClient的这些不同的内部状态可以使用一个图表来说明,它的整个状态图表如下图: 

State diagram for MatchmakingClient

客户端类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

客户端已经移到连接状态,等待服务器的确认,这个时候你不想让用户再试图加入其他服务器,因此你显示了一个临时等待屏幕。 

如果你看一下调试输出面板针对服务器陈谷香,你会发现现在都发生了什么: 

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

在服务器上接受连接

现在你有一个恩d客户端企图生成一个连接,但是在服务器那边,你在每一件事情都搞定之前你必须接受这些连接,这在服务器那边经常发生的事情。 

但是在你接受之前,你要把状态机放到服务器那边,添加下列代码到 MatchmakingServer.m的顶部:

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

不像客户端。服务器仅仅有三个状态。 

State diagram for MatchmakingServer

那是很简单的,”忽略新的连接“状态是用来等你的主机开始纸牌游戏之后,从那个时候开始,新的客户端都不被允许进行连接,添加一个新的变量实例来保留当前的状态。 

@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到文件的顶部:

#import "PeerCell.h"

你快要完成啦。. 你需要添加小时的函数到服务器类中,添加下列函数签名到 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

提示:即使你能在这个屏幕顶部的文本区域中键入其他名字,在表视图中通常出现的依然是设备自己的名字 

错误和断开连接的控制

当进行写网络的程序的时候这里有写事情要注意,就是异常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

正如你在调试输出看到的,这个 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

为了这个”没有网络“错误,你不需要真的离开加入游戏界面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中

- (void)endSession;

为了让服务器和客户端找到彼此我们做了很多工作!(相信我,如果你没有GKSession你将做更多更多的工作才能完成这些功能) 

更酷的事情是,你可以把这个客户端和服务器类放到任何项目中去使用,所有这些功能都是免费的!!!!因为这些类被设计是完全独立那些视图控制器的,他们很容易在其他项目中被重用。 

继续关注下一章节!!!  


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以帮您解答这个问题。 首先,我们需要定义一个 Card 类,用于表示一张扑克牌。Card 类需要包含属性 suit(花色)和 rank(点数),以及方法用于获取和设置这些属性。 ```python class Card: def __init__(self, suit, rank): self.suit = suit self.rank = rank def get_suit(self): return self.suit def get_rank(self): return self.rank def __str__(self): return f"{self.rank} of {self.suit}" ``` 接下来,我们需要定义一个 Deck 类,用于表示一副扑克牌。Deck 类需要包含一组 Card 对象,以及方法用于初始化、洗牌和抽牌。 ```python import random class Deck: def __init__(self): self.cards = [] for suit in ["Hearts", "Diamonds", "Clubs", "Spades"]: for rank in range(1, 14): self.cards.append(Card(suit, rank)) def shuffle(self): random.shuffle(self.cards) def deal_card(self): return self.cards.pop() def __str__(self): return f"Deck with {len(self.cards)} cards" ``` 然后,我们需要定义一个 Player 类,用于表示一个玩家。Player 类需要包含一个手牌,以及方法用于添加和移除牌。 ```python class Player: def __init__(self, name): self.name = name self.hand = [] def add_card(self, card): self.hand.append(card) def remove_card(self, card): self.hand.remove(card) def __str__(self): return f"{self.name} has {len(self.hand)} cards" ``` 最后,我们需要定义一个 Game 类,用于表示一个扑克游戏。Game 类需要包含一个 Deck 对象和一组 Player 对象,以及方法用于发牌和判断胜负。 ```python class Game: def __init__(self, players): self.deck = Deck() self.players = players def deal_cards(self): for _ in range(2): for player in self.players: card = self.deck.deal_card() player.add_card(card) def determine_winner(self): winner = None highest_score = 0 for player in self.players: score = sum(card.get_rank() for card in player.hand) if score > highest_score: winner = player highest_score = score return winner def play(self): self.deck.shuffle() self.deal_cards() winner = self.determine_winner() print(f"{winner.name} wins with a score of {sum(card.get_rank() for card in winner.hand)}") ``` 这样,我们就完成了一个简单扑克游戏,包括 Card、Deck、Player 和 Game 类。您可以根据自己的需求进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值