Airplay Tutorial: An Apple TV Multiplayer Quiz Game

Learn how to make a multiplayer iOS quiz game that displays one thing to an Apple TV, and uses your device as a controller! By Gustavo Ambrozio.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Connecting Other Players

Now comes the fun part: getting more devices to connect and play the game.

The main class of GameKit’s peer-to-peer communication is GKSession. Quoting from the documentation: “A GKSession object provides the ability to discover and connect to nearby iOS devices using Bluetooth or Wi-fi.”

As with most communication protocols, a GameKit session has the concept of a “server” and a “client”. From the documentation: “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)”.

In your game, since there should be only one person connected to an external display, your device will start a session as a server whenever a device connects to an external display. If a device isn’t connected to a display, it starts a session as a client and starts looking for a server.

Why not just use straight peer-to-peer networking? If every device was a peer — that is, a client and a server simultaneously — it would be incredibly complex to manage all connected device and control gameplay. Having a single server to control the game greatly simplifies the game logic.

Add the following code to the bottom of ATViewController.m:

#pragma mark - GKSession master/slave

- (BOOL)isServer
{
    return self.mirroredScreen != nil;
}

The above code uses the mirroredScreen property that is set and cleared by the secondary screen notifications to determine if this device is a server or not.

Before you can start the GKSession, there’s a few more things that you’ll need. A GKSession reports peer discovery and communication using a delegate. Therefore, you need to implement the delegate protocol in a class that will receive all these events.

Since ATViewController is controlling your game, this is the best class to act as the delegate of the GKSession.

Open ATViewController.h and add the following header right after the SpriteKit header:

#import <GameKit/GameKit.h>

Now, find the following line:

@interface ATViewController : UIViewController

…and add the GKSessionDelegate protocol to it so that it looks like the line below:

@interface ATViewController : UIViewController <GKSessionDelegate>

Go back to ATViewController.m and add the following protocol method stubs to the end of the class:

#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
{
}

You’ll populate these methods later; you’ve simply added them now to avoid compilation errors.

For more details on the delegate methods of GKSessionDelegate, check out the official GKSessionDelegate Apple Docs.)

Before you set up a GKSession, you’ll need to add a few properties to store some several objects.

Add the following properties near the top of the file, in the same block as the other properties:

@property (nonatomic, strong) GKSession *gkSession;
@property (nonatomic, strong) NSMutableDictionary *peersToNames;
@property (nonatomic, assign) BOOL gameStarted;

The first property holds the GKSession; the second is a dictionary that stores the ID of your peers and their respective advertised names; and the last one is a boolean that indicates if the game has started yet. You’ll use all of these properties in the following steps.

Add the following code immediately after the stub methods you added above:

- (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;
    }
}

The first few lines are simply cleanup code for the case where the session is being restarted.

Next, you initialize the session object. SessionID is an ID unique to this app so that multiple devices with all kinds of GameKit apps can find each other.

The displayName parameter tells GKSession how this device should be identified to other peers. You can put whatever you like in this parameter, but here you’ll just use the device name for simplicity. The last parameter specifies the sessionMode, which indicates whether the device is a server or a client.

Once the GKSession is initialized, you tell your GKSession that ATViewController will be responsible for retrieving data from all peers and will also be the delegate for all events. Next, you set the session to be available; this signals GKSession to begin broadcasting over Wi-Fi and Bluetooth to try to find peers.

Finally, you initialize the peerToNames dictionary that tracks the other devices. If the current device is a server, it should be added to the dictionary to start.

Now you need to call this method in viewDidLoad to start the session when the game starts.

Add the following line to the end of viewDidLoad:

  [self startGKSession];

You also need to call this method when the device switches between client and server modes.

Add the same line to the end of setupMirroringForScreen::

  [self startGKSession];

Now that the Game Kit setup is complete, it’s time to start filling in those delegate methods!

Add the following code to 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 whenever a peer changes state. Possible states for peers are:

  • GKPeerStateAvailable: A new peer has been found and is available; in this case, you call connectToPeer:withTimeout: of GKSession. If the connection is successful you will get another state change callback with GKPeerStateConnected.
  • GKPeerStateConnected: The peer is now connected. In this case you add the peer name to the peerToNames dictionary and set the refresh flag to YES.
  • GKPeerStateDisconnected and GKPeerStateUnavailable: A peer has disconnected for some reason or has become unavailable. In this case you remove the name from the peerToNames dictionary and set the refresh flag to YES.

Finally, if the game has not started yet and the refresh flag is YES, send the updated peerToNames dictionary to the scene on the secondary screen and instruct scene on the device to enable the start game button if there are at least two connected players.

In order to establish a connection, one peer needs to ask the other to connect, as it’s being done in the method above when a peer is made available. The other side has to accept this connection request in order for the two to communicate.

Add the following code to 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];
    }

This delegate method executes on one device when the other device calls connectToPeer:withTimeout:. If the game has not yet started, accept the connection and report any errors that might occur. If the game has already started, refuse the connection.

When one device accepts the connection, the other will receive another state change notification of GKPeerStateConnected and the new device will be added to the list of players.

To test this, you’ll need to run two copies of the app: one as the server, and another as the client. The easiest way to do this is run the simulator as a server and have a physical device run another copy of the app as the client.

If you don’t have a paid developer account and can’t run apps on a device, you can try to run two simulators on the same machine in a pinch. It’s not impossible, but it’s not the most straightforward task, either. If you want to go this route, take a look at this Stack Overflow answer for ways to accomplish this.

Build and run the app on your simulator; you’ll see the same familiar starting screen:

iOS_Simulator_-_iPhone_Retina__3.5-inch____iOS_7.0.3__11B508_-5

Now start the app on your device. If your device and your computer are on the same network, you should see something similar to the following on your simulator:

iOS_Simulator_-_iPhone_Retina__4-inch____iOS_7.0.3__11B508_-3

The device name appears on the “TV” and the iOS Simulator now shows a “Start Game” button. Your device will still display the “Waiting for players!” label.

The next step is to add more communication between the server and the client so that the gameplay can start.

Gustavo Ambrozio

Contributors

Gustavo Ambrozio

Author

Over 300 content creators. Join our team.