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

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

这个方法并未完成,仍然还要加入代码。

将下列代码加到上述代码之后:

 

  // Setup window in external screen

   self.mirroredWindow = [[UIWindow alloc] initWithFrame:self.mirroredScreen.bounds];

   self.mirroredWindow.hidden = NO;

   self.mirroredWindow.layer.contentsGravity = kCAGravityResizeAspect;

   self.mirroredWindow.screen = self.mirroredScreen;

     self.mirroredScreenView = [[SKView alloc] initWithFrame:self.mirroredScreen.bounds];

     // Create and configure the scene.

   self.mirroredScene = [ATAirPlayScene sceneWithSize:self.mirroredScreenView.bounds.size];

   self.mirroredScene.scaleMode = SKSceneScaleModeAspectFill;

     // Present the scene.

   [self.mirroredScreenView presentScene:self.mirroredScene];

     [self.mirroredWindow addSubview:self.mirroredScreenView];

上述代码表明了向新的 screen 中显示内容是多么的容易。首先,用第2个screen 的大小创建了一个 UIWindow。默认 window 是隐藏的,我们必须将 hidden 属性设为 NO 以便显示它。

根据 CALayer 类参考,contentsGravity“用于制定layer 内容的摆放方式及缩放比例”。在 screenModeDidChange: 方法中,当 screen 发生变化,我们通过关闭然后打开来重置window,因此这里必须将 contentsGravity 设置为 aspect fill。然后将传入的 screen 设置为 window 的screen。

接下来,创建一个 window 同样大小的 SKView。SKView 是Sprite Kit 框架中的 UIView。如果你创建的项目未使用 Sprite Kit,你可以用 UIView 代替 SKView。

最终,创建一个  ATAirPlayScene,让 Sprite Kit 显示这个 scene,然后将视图加入 window。

现在还有一个 disableMirroringOnCurrentScreen方法未实现。

在 disableMirroringOnCurrentScreen:方法中加入代码:

  [self.mirroredScreenView removeFromSuperview];

   self.mirroredScreenView = nil;

   self.mirroredScreen = nil;

   self.mirroredScene = nil;

   self.mirroredWindow = nil;

  [self.scene enableStartGameButto?n:NO];

这个方法负责释放相关属性。 enableStartGameButton: 方法使“开始”按钮 disable,这是游戏逻辑的一部分,你暂时不用管它。

“开始” 按钮只会在设备连有第二显示器且玩家不止一个时可用。一旦第二显示器丢失,你需要禁用这个按钮。

最后一步是调用 setupOutputScreen 方法。在viewDidLoad 方法最后加入:

   [self setupOutputScreen];

编译运行项目,运行界面如下所示:

在模拟器菜单中,选择“硬件->电视输出->640x480”,然后一个新的电视模拟窗口会打开。

这时,一个模拟器的Bug 会导致程序崩溃。不用担心,这只会在模拟器上发生,在真实的 AppleTV 或显示器上则不会发生。不用退出模拟器,再次运行程序,你会看到两个都显示了:

如果你想在 AppleTV 上看看效果,在真实设备上运行程序,然后从控制中心的AirPlay 菜单中选择 AppleTV。

连接其他玩家

现在是最有意思的部分:让更多的设备连接进来,一起游戏。

GameKit 主要通过 GKSessionn 类实现 p2p 通信。根据这篇文档所述,“GKSession对象负责通过蓝牙或者 Wifi 来发现和连接邻近的 iOS 设备。”

类似其他大多数的通讯协议,GKSession 也存在“服务器”和“客户端”的概念。在文档中,“Session可以扮演成服务器(广播一个 session ID),主动查找其他客户端(广播该 session ID);也可以同时扮演服务器和客户端”。

在这个游戏中,因为只有一个人连接外接显示器,你的设备将以服务器的方式启动session,同时该设备应当连接到外接显示器。如果设备未连接外接显示器,则将启动客户端 session 并开始搜索服务器。

为什么不使用最简单的 p2p 网络?如果每个设备都是 p——即同时扮演服务器和客户端——那么连接设备的管理以及游戏控制会使游戏变得极其复杂。使用单服务器模式则会简单得多。

ATViewController.m 的最后加入:

#pragma mark - GKSession master/slave  

- (BOOL)isServer {

     return self.mirroredScreen != nil;

}

上面的代码通过 mirroredScreen 属性(该属性在收到外接显示器变更通知时改变)来判断设备是否为服务器。

在启动 Session 之前,有几点需要注意。GKSession 使用委托来通知端点(即p )发现及 p2p 通讯。因此,需要实现委托协议以接收所有 Session 事件。

ATViewController 是整个游戏的控制器,因此也是最适合扮演 GKSession 委托的地方。

打开 ATViewController.h 加入:

#import <GameKit/GameKit.h>

找到这行:

@interface ATViewController : UIViewController

声明对 GKSessionDelegate 的实现:

@interface ATViewController : UIViewController <GKSessionDelegate>

回到 ATViewController.m 在后面加入:

#pragma mark - GKSessionDelegate  

/* Indicates a state change for the given peer.  */

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { }  

/* Indicates a connection request was received from another peer.    Accept by calling -acceptConnectionFromPeer:  Deny by calling -denyConnectionFromPeer:  */

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID { }  

/* Indicates a connection error occurred with a peer, which includes connection request failures, or disconnects due to timeouts.  */

- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error { }  

/* Indicates an error occurred with the session such as failing to make available.  */

- (void)session:(GKSession *)session didFailWithError:(NSError *)error { }  

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context { }

后面再来填充这些方法。现在暂时空实现,避免编译错误。

关于 GKSessionDelegate 的细节,请参考官方文档 GKSessionDelegate 官方文档。

在创建 GKSession之前,还需要加几个属性声明,以便存储某些对象。

加入下列属性声明:

@property (nonatomic, strong) GKSession *gkSession;

@property (nonatomic, strong) NSMutableDictionary *peersToNames;

@property (nonatomic, assign) BOOL gameStarted;

第一个属性用于持有 GKSession,第二个用于存储每个端点的 ID 以及对应的广播名称;第三个用于表示游戏是否开始。 这些属性后面都会用到。

在上面的方法之后新增方法:

- (void)startGKSession {

     // Just in case we‘re restarting the session as server

     self.gkSession.available = NO;

     self.gkSession = nil;

       // Configure GameKit session.

     self.gkSession = [[GKSession alloc] initWithSessionID:@"AirTrivia"                             displayName:[[UIDevice currentDevice] name]                             sessionMode:self.isServer ? GKSessionModeServer : GKSessionModeClient];

     [self.gkSession setDataReceiveHandler:self withContext:nil];

     self.gkSession.delegate = self;

     self.gkSession.available = YES;

     self.peersToNames = [[NSMutableDictionary alloc] init];

     if (self.isServer)     {

         self.peersToNames[self.gkSession.peerID] = self.gkSession.displayName;

     }

}

前两行代码用于释放session,因为 session 即将重建。

接着,初始化 session 对象。SessionID 在 app 中是唯一的,以便所有设备(运行各种 GameKit app 的)能够互相识别。

displayName用于告诉 GKSession 如何当前设备和其他设备。你可以随便指定,但使用设备名称是最自然的做法。最后一个参数是 sessionMode,表明当前设备是服务器还是客户端。然后,将self 对象(ATViewController)指定为负责接收所有来自其他端点的数据,同时作为 session 事件的委托对象。接着,设置 session 的available 为 YES,这将导致 Session 开始在 Wifi 或者 Bluetooth 网络中广播,并开始查找端点。

最终,初始化一个 peerToNames 字典,用于记录其他设备。如果当前设备是一个服务器,应当将自己也加到字典中。

现在,可以在 viewDidLoad 方法中调用这个方法了:

  [self startGKSession];

当设备在客户端和服务器模式之间切换时,也需要调用这个方法。

在 setupMirroringForScreen:最后一行加入:

  [self startGKSession];

现在,关于 GameKit 的配置已经完成,让我们开始填充委托方法。

在 session:peer:didChangeState:中加入:

BOOL refresh = NO;  

switch (state)   {

     case GKPeerStateAvailable:

       if (!self.gameStarted)       {

         [self.gkSession connectToPeer:peerID withTimeout:60.0];

       }

       break;  

     case GKPeerStateConnected:

       if (!self.gameStarted)       {

         self.peersToNames[peerID] = [self.gkSession displayNameForPeer:peerID];

         refresh = YES;

       }

       break;  

     case GKPeerStateDisconnected:

     case GKPeerStateUnavailable:

       [self.peersToNames removeObjectForKey:peerID];

       refresh = YES;

       break;

     default:

       break;  

}    

if (refresh && !self.gameStarted)   {

     [self.mirroredScene refreshPeers:self.peersToNames];

     [self.scene enableStartGameButton:self.peersToNames.count >= 2];  

}

无论端点改变为什么状态,这个方法都会执行。端点可能的状态包括:

This method executes whenevera peer changes state. Possible states for peers are:

  • GKPeerStateAvailable: 发现新端点,并且该端点 availabel 是 YES。这时,可以调用 GKSession 的 connectToPeer:withTimeout: of GKSession 方法。如果连接成功,则会发来一个状态为 GKPeerStateConnected 的状态改变通知。
  • GKPeerStateConnected: 端点已连接。这时,你需要将端点名称添加到 peerToNames 字典,然后将 refresh 布尔变量标志为 YES。
  • GKPeerStateDisconnected 和 GKPeerStateUnavailable: 端点断开或者不再有效。这时,需要将端点名称从 peerToNames 中移除,然后将 refresh 标志为YES。

最终,如果游戏还未开始,同时 refresh 标志为 YES,将变更过的peerToNames 发送给第二显示窗口的 scene,当有两个以上的玩家连接时,使“开始”按钮可用。

为了建立连接,一个端点需要向另一个端点请求连接——这在上面代码中已经这样做了。然后对端必须接受连接请求,这样双方才能通讯。

在 session:didReceiveConnectionRequestFromPeer:方法中加入以下代码:

    if (!self.gameStarted)     {

         NSError *error = nil;

         [self.gkSession acceptConnectionFromPeer:peerID error:&error];

         if (error)         {

             NSLog(@"Error accepting connection with %@: %@", peerID, error);

         }

     }     else     {

         [self.gkSession denyConnectionFromPeer:peerID];

     }

当其他设备调用 connectToPeer:withTimeout: 方法连接一个设备时,该设备的这个委托方法被调用。如果游戏尚未开始,接受这个连接并打印可能发生的错误。如果游戏已经开始,拒绝该连接。