一旦设备接受了连接请求,对端立即会收到一个状态为GKPeerStateConnected 的Session 状态改变通知,然后这个设备会加到玩家列表中。
要测试这个 app,你需要运行两个 app 的拷贝:一个是服务器,一个是客户端。最简单的办法是用模拟器作为服务器,而用一台物理设备作为客户端。
如果你没有开发者账号,你将无法在真机上进行调试,这样你可能想在同一个机器上运行两个模拟器。这不是不可以,但就不是那么简单了。如果你想这样做,请参考 stack overflow 上的这个方法。
在模拟器上运行app,你会看到相同的界面:
现在在设备是运行 app。如果设备与计算机在同一网络,你将在模拟器上看到如下显示:
设备名称在“电视”上显示,同时模拟器上“Start Game”按钮显示了。而在设备上仍然显示“Waitingfor players!”。
后面,我们将在服务器/客户端之间加入更多的通信代码以便游戏能够进行。
和其他设备通信
现在,你已经运行了两份 app 拷贝了,是该用 GKSession 在两者间进行通信了。
GKSession有两个方法用于发送 p2p 数据,分别是:
-(BOOL)sendData:(NSData*)data toPeers:(NSArray *)peers withDataMode:(GKSendDataMode)modeerror:(NSError **)error;
-(BOOL)sendDataToAllPeers:(NSData *)data withDataMode:(GKSendDataMode)modeerror:(NSError **)error
两个方法分别用于向一个或多个端点发送 NSData。对于本项目,我们将使用的是第一个方法。第一个方法的好处是,你可以发送消息给自己。尽管这有些不可思议,但这有一个好处,即服务器能像一个客户端一样,能够发送数据触发自己响应。
服务器可能会有多个对端(包括自己),但客户端只会有一个对端:即服务器。
不论是哪种,用这个方法发送信息到所有端点都能兼顾。
一个 NSData 对象能容纳各种数据;因此 NSString 命令需要包装成NSData 发送,当收到数据时则反过来,以便打印出调试信息。
在 ATViewController.m: 最后加入以下方法:
#pragma mark - Peer communication - (void)sendToAllPeers:(NSString *)command { NSError *error = nil; [self.gkSession sendData:[command dataUsingEncoding:NSUTF8StringEncoding] toPeers:self.peersToNames.allKeys withDataMode:GKSendDataReliable error:&error]; if (error) { NSLog(@"Error sending command %@ to peers: %@", command, error); } } |
正如方法名称所示,这个方法发送一个 NSString 给所有已连接的端点。NSString的 dataUsingEncoding: 方法将字符串转换为空终止的 UTF8 字节编码的 NSData。
接收完毕,GKSession 委托方法receiveData:fromPeer:inSession:context:会被调用。它现在还是空的,需要你实现其中的逻辑。
在 receiveData:fromPeer:inSession:context:方法中加入代码:
NSString *commandReceived = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"Command %@ received from %@ (%@)", commandReceived, peer, self.peersToNames[peer]); |
将 data 数据反编码回 NSString,然后进行打印。
测试时,你可以发送一个命令然后在控制台中检测结果。
在 startGame方法(在ATViewController.m文件中)末尾加入:
[self sendToAllPeers:@"TEST COMMAND"]; |
这样,当用户轻触“Start Game”按钮,将会调用 startGame方法。这将导致服务器向所有端点发送 Test command 命令。
编译运行 app,首先在模拟器运行,然后在设备上运行。当设备启动时,控制台中会有输出,应该确保信息是从设备上输出的。
当模拟器上的 app 启动后,轻触“Start Game”按钮。可以看到控制台中显示如下输出:
是不是超简单?现在你已经可以发送信息了,剩下的工作就是添加新的命令并让游戏运行起来。
添加游戏逻辑
这是一个知识竞答游戏,你还需要准备一些试题和答案。我从 GeorgiaTech 找到了一个 CS (计算机科学)考试试卷,叫做 Trivia DatabaseStarter,超简单而且没有附加一堆乱七八糟的许可条款。
我将它从 CSV 格式转换成了格式良好的 plist,即项目中的 questions.plist包含了一个二维数组。数组中的每个数组中,第一个元素就是试题,正确答案是第2个元素(请勿偷看!),然后是错误的答案。
打开 ATViewController.m 添加如下属性:
@property (nonatomic, strong) NSMutableArray *questions; @property (nonatomic, strong) NSMutableDictionary *peersToPoints; @property (nonatomic, assign) NSInteger currentQuestionAnswer; @property (nonatomic, assign) NSInteger currentQuestionAnswersReceived; @property (nonatomic, assign) NSInteger maxPoints; |
这些属性分别说明如下:
- questions –存储试题和答案。它是可变的,因为每当问到一个试题,这个试题将从数组中移除。这样你就不会重复出题,同时也可以轻易知道游戏何时结束。
- peersToPoints – 当前成绩。存储每个端点的分数。
- currentQuestionAnswer – 当前问题的正确答案的索引。
- currentQuestionAnswersReceived – 收到了多少答案。
- maxPoints – 当前最高分,在 peerToPoints 数组中查找获胜者时会用到。
所有属性都准备好了,是时候添加开始游戏的代码了。删除在 startGame中的原来的那行代码,然后加入以下代码:
if (!self.gameStarted) { self.gameStarted = YES; self.maxPoints = 0; self.questions = [[NSArray arrayWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"questions" ofType:@"plist"]] mutableCopy]; self.peersToPoints = [[NSMutableDictionary alloc] initWithCapacity:self.peersToNames.count]; for (NSString *peerID in self.peersToNames) { self.peersToPoints[peerID] = @0; } } |
如果游戏尚未启动,将 gameStarted 设置为 YES 并将maxPoints 清零。然后从 plist 文件中加载试题。需要用 mutableCopy 方法获取可变数组,这样才能从数组中移除试题。然后初始化peersToPoints 数组,每个人的得分初始化为0。
这里还没有任何命令,因此游戏虽然已经准备好开始,但其实并没有真的开始。这是我们后续的工作。
首先,在 ATViewController.m 添加如下常量:
static NSString * const kCommandQuestion = @"question:"; static NSString * const kCommandEndQuestion = @"endquestion"; static NSString * const kCommandAnswer = @"answer:"; |
待会你就会明白怎么使用它们。
在 startGame 方法后添加如下方法:
- (void)startQuestion { // 1 int questionIndex = arc4random_uniform((int)[self.questions count]); NSMutableArray *questionArray = [self.questions[questionIndex] mutableCopy]; [self.questions removeObjectAtIndex:questionIndex]; // 2 NSString *question = questionArray[0]; [questionArray removeObjectAtIndex:0]; // 3 NSMutableArray *answers = [[NSMutableArray alloc] initWithCapacity:[questionArray count]]; self.currentQuestionAnswer = -1; self.currentQuestionAnswersReceived = 0; while ([questionArray count] > 0) { // 4 int answerIndex = arc4random_uniform((int)[questionArray count]); if (answerIndex == 0 && self.currentQuestionAnswer == -1) { self.currentQuestionAnswer = [answers count]; } [answers addObject:questionArray[answerIndex]]; [questionArray removeObjectAtIndex:answerIndex]; } // 5 [self sendToAllPeers:[kCommandQuestion stringByAppendingString: [NSString stringWithFormat:@"%lu", (unsigned long)[answers count]]]]; [self.scene startQuestionWithAnswerCount:[answers count]]; [self.mirroredScene startQuestion:question withAnswers:answers]; } |
开始出题的过程如下:
- 首先,从未出完的试题中随机抽选一道题。questionArray 保存了当前抽选的试题的一份拷贝,然后从试题数组中移除所选的试题。
- 试题内容位于 questionArray 的第一个元素,后面才是备选答案。首先取出试题内容另外保存,然后将它从 questionArray 中移除。现在,questionArray 中包含了所有答案,其中第一个答案为正确答案。
- 初始化一个 mutable 数组用于存储乱序后的答案,然后重置几个属性。
- 在循环中,对答案进行随机抽取。如果是第一个答案(即正确答案),将索引保存到currentQuestionAnswer 。然后将随机抽选的选项添加到 answers 数组,并从 questionArray 中移除。
- 最终,发送“question:”命令和备选答案的数量给所有端点。例如,“question:4”。然后更新界面,发送试题内容以及乱序后的备选答案给第二显示窗口。
接下来,在 startGame 方法最后,if 块之内加入:
[self startQuestion]; |
启动游戏,查看服务器的显示。