* 交互游戏平台Game Center,
* P2P设备通讯功能
* In-Game Voice。
本文主要介绍的就是我在开发一个对战游戏时使用到的设备通讯功能。在游戏中采用的是“Master-Client”形式的网路拓扑。即有机器作为主机Host一个游戏,然后其他设备以玩家的身份加入,同时主机也是其中一个玩家。UI界面大概就是下面这个样子:
Host界面:
![](http://dl2.iteye.com/upload/attachment/0086/4435/51adb885-79ce-3d3c-92c3-27cb24d73d92.png)
玩家界面:
![](http://dl2.iteye.com/upload/attachment/0086/4437/402b3da0-e08d-3c4e-a2ee-eb691ee1883e.png)
整个网络的连接过程为:
* 主机向自己所在的局域网发出信号:“我是擎天柱,我现在可以接受连接了。”,等待连接
* 玩家进入搜寻主机界面,发现主机,则把主机的信息显示在自己的可连接游戏列表中。
* 玩家点击主机名,连接到主机上。
* 主机接受到其他设备的连接,把接入设备信息显示到自己的接入玩家列表中。
这样,就完成了局域网设备中,设备之间的相互发现功能。这一切在iOS中是怎么实现的呢?
iOS中P2P设备的通讯功能都是基于GKSession来实现的,关于GKSession,苹果的官网上描述如下:
“A GKSession object provides the ability to discover and connect to nearby iOS devices using Bluetooth or Wi-fi.
Sessions primarily work with peers. A peer is any iOS device made visible by creating and configuring a GKSession object. Each peer is identified by a unique identifier, called a peer id (peerID) string. Your application can use a peerID string to obtain a user-readable name for a remote peer and to attempt to connect to that peer. Similarly, your session’s peer ID is visible to other nearby peers. After a connection is established, your application uses the remote peer’s ID to address data packets that it wants to send.
Peers discover other peers by using a unique string to identify the service they implement, called a session ID (sessionID). Sessions can be configured to broadcast a session ID (as a server), to search for other peers advertising with that session ID (as a client), or to act as both a server and a client simultaneously (as a peer.Your application controls the behavior of a session through a delegate that implements the GKSessionDelegate protocol. The delegate is called when remote peers are discovered, when those peers attempt to connect to the session, and when the state of a remote peer changes. Your application also provides a data handler to the session so that the session can forward data it receives from remote peers. The data handler can be a separate object or the same object as the delegate.”
简单来说,就是如下几点:
* 1)这个类就是用来连接附近的iOS设备的,这个近怎么算呢?就是蓝牙和WiFi可达;
* 2)在GKSession中每台设备都有一个自己的Peer ID,这是整个网络连接中最重要的信息。连接哪个设备,数据传送到那个设备,设备名称配置等都是通过Peer ID来做的;
* 3)设备之间的识别,连接,状态变化是通过实现GKSessionDelegate来确认具体行为的;
* 4)设备之间的数据的接收则是通过指定Data Receiver Handler来实现,这个Data Receive Handler可以随便指定。
好,接下来就看一下游戏中的设备发现功能是怎么做的,在Host界面的ViewController中,我使用了一个MatchingServer类来处理所有和网络连接相关的代码,避免ViewController过于庞大。在ViewController启动完之后之后,调用了MatchingServer的代码来启动一个Session,向外传递信息;“本主机可以接收连接了…”
[_matchingServer startAcceptingConnectionsForSessionID:SESSION_ID];
在MatchingServer中,保存了一个GKSession的属性,同时该类实现了GKSessionDelegate protocol。
@interface MatchingServer : NSObject<GKSessionDelegate> ... @property (nonatomic, strong, readonly) GKSession *session; ...
在实现类中,MatchingServer创建了一个新的GKSession,创建GKSession给了Session的名字,如果给nil的话,或自动使用应用的Bundle Name,然后主机想要展示的名字,最后是这是一个这个Session的模式。
_session = [[GKSession alloc] initWithSessionID:sessionId displayName:nil sessionMode:GKSessionModeServer]; _session.delegate = self; _session.available = YES;
在玩家界面时,同样的,我使用了一个MatchingClient来处理所有网络连接的事。
[_client startSearchForServersWithSessionID:SESSION_ID];
@interface MatchingClient : NSObject<GKSessionDelegate> ... @property (nonatomic, strong, readonly) GKSession *session; ...
_session=[[GKSession alloc] initWithSessionID:SESSION_ID displayName:nil sessionMode:GKSessionModeClient]; _session.delegate = self; _session.available = YES;
上面的代码中,主机以Server模式创建了一个GKSession并设置为available,Client以Client模式创建了一个GKSession同样设置为available,他们都使用了相同的Session ID,因为GKSession只能发现具有相同Session ID的设备。GKSession一创建,设备就开始查找附近的设备,发现设备了怎么处理呢?
设备连接
这就是GKSessionDelegate的控制领域,GKSession会根据自己探测到的设备的状态调用对应的GKSessionDelegate的方法。
GKSessionDelegate有4个方法:
检测到设备发生状态改变时,调用:
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state;
收到其它设备的连接请求时,调用:
- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID;
和其他设备连接发生错误时,调用:
- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error;
GKSession内部发生错误时,比方说无法初始化GKSession,调用:
- (void)session:(GKSession *)session didFailWithError:(NSError *)error;
因此,在MatchingServer中,主要就是等待和接受Client连接,需要关注的状态改变就是一个Client的连接和断开,当和一个Client建立连接时,把接入的client放入connectedClients,断开连接则移除:
-(void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { NSLog(@"MatchmakingServer: peer %@ changed state %d", peerID, state); switch (state) { case GKPeerStateConnected: if (_serverState == ServerStateAcceptingConnection) { if (![_connectedClients containsObject:peerID]) { [_connectedClients addObject:peerID]; [self.delegate matchingServer:self clientDidConnected:peerID]; } } break; case GKPeerStateDisconnected: if (_serverState != ServerStateIdle) { if ([_connectedClients containsObject:peerID]) { [_connectedClients removeObject:peerID]; [self.delegate matchingServer:self clientDidDisconnected:peerID]; } } break; default: break; } }
当Server收到一个连接请求时,如果主机尚未开始游戏,且游戏未达最大人数限制,则接受连接,反之,则拒绝连接。
-(void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID { NSLog(@"MatchmakingServer: connection request from peer %@", peerID); if (_serverState == ServerStateAcceptingConnection && [_connectedClients count] < _maxClients) { NSError *error; if ([_session acceptConnectionFromPeer:peerID error:&error]) { NSLog(@"MatchmakingServer: connected to peer %@", peerID); } else { NSLog(@"MatchmakingServer: Error accepting connection from peer %@, %@", peerID, error); } } else { [session denyConnectionFromPeer:peerID]; } }
在MatchingClient中,则主要关注的是主机设备的状态,不同的状态,Client则需要对自身状态做出调整,同时更新一些界面参数:
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { #ifdef DEBUG NSLog(@"MatchmakingClient: peer %@ changed state %d", peerID, state); #endif switch (state) { case GKPeerStateAvailable: if(_clientState == ClientStateSearchingForServers){ if (![_availableServers containsObject:peerID]) { [_availableServers addObject:peerID]; [self.delegate matchingClient:self serverBecameAvailable:peerID]; } } break; case GKPeerStateUnavailable: if(_clientState == ClientStateSearchingForServers){ if([_availableServers containsObject:peerID]) { [_availableServers removeObject:peerID]; [self.delegate matchingClient:self serverBecameUnavailable:peerID]; } } if (_clientState == ClientStateConnecting && [_serverPeerID isEqualToString:peerID]) { [self disconnectFromServer]; } break; case GKPeerStateConnected: if (_clientState == ClientStateConnecting) { _clientState = ClientStateConnected; [self.delegate matchingClient:self didConnectToServer:peerID]; } break; case GKPeerStateDisconnected: if (_clientState == ClientStateConnected) { [self disconnectFromServer]; } default: break; } }
通过上面所有的代码,游戏具备了基本的发现设备,连接设备的能力,接下来说说,如何在设备之间传递数据。
数据传输
在GKSession中,数据传输非常简单。只需要准备好数据,然后调用相关的session的方法发送即可。与数据传输相关的方法有3个:
设定接受数据的Delegate:
– setDataReceiveHandler:withContext:
发送数据到指定的节点:
– sendData:toPeers:withDataMode:error:
发送数据给所有节点:
– sendDataToAllPeers:withDataMode:error:
前面提到GKSession对象的Data Receive Handler可以设定为任何对象,没有限制。但是被设为Data Receive Handler的对象必须实现方法:
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context
否则,会导致整个应用的崩溃。
参考: http://developer.apple.com/library/ios/#documentation/GameKit/Reference/GKSession_Class/Reference/Reference.html