首页 > 代码库 > Airplay 教程: 一个 Apple TV 多人竞答游戏(3)

Airplay 教程: 一个 Apple TV 多人竞答游戏(3)

一旦设备接受了连接请求,对端立即会收到一个状态为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];

}

开始出题的过程如下:

  1. 首先,从未出完的试题中随机抽选一道题。questionArray 保存了当前抽选的试题的一份拷贝,然后从试题数组中移除所选的试题。
  2. 试题内容位于 questionArray 的第一个元素,后面才是备选答案。首先取出试题内容另外保存,然后将它从 questionArray 中移除。现在,questionArray 中包含了所有答案,其中第一个答案为正确答案。
  3. 初始化一个 mutable 数组用于存储乱序后的答案,然后重置几个属性。
  4. 在循环中,对答案进行随机抽取。如果是第一个答案(即正确答案),将索引保存到currentQuestionAnswer 。然后将随机抽选的选项添加到 answers 数组,并从 questionArray 中移除。
  5. 最终,发送“question:”命令和备选答案的数量给所有端点。例如,“question:4”。然后更新界面,发送试题内容以及乱序后的备选答案给第二显示窗口。

接下来,在 startGame 方法最后,if 块之内加入:

  [self startQuestion];

启动游戏,查看服务器的显示。