Pretty image
Bill walks you through building a rudimentary multiplayer game for the iPhone using Apple’s Bonjour-based Game Kit technology.

A phone is a communication device. The iPhone is an application platform. Some of the best-selling applications are games. So what’s more natural than to create a multiplayer game that two or more people can play on their iPhones?

Unfortunately, adding networking to an app can be a major pain.

Fortunately, the iPhone has some great technology to make that job much easier. From the beginning (all 18 months ago) the iPhone SDK has included zero-configuration networking via a technology called Bonjour. With Bonjour you can broadcast services offered by your app, and other apps can find those services easily. But much of the code to get that started and send data back and forth between the two instances can be tedious and error-prone.

Enter iPhone SDK 3.0 and the Game Kit. Built on top of Bonjour, Game Kit removes the tedious setup and communication code required to connect two applications to each other. It makes building multi-player games much easier.

And in fact, the typical use case for the Game Kit is to connect two instances of a game together for multi-player action. Let’s do that.

We are going to build a really simple game that displays colored disks on two iPhones. When the user taps on one of these disks, the disk will spin and fade out on both devices. A really simple game, to be sure; but the stuff you learn here can easily be transferred to your real application to allow it to network your users together.

There are three phases to using Game Kit:

  1. First, you have to connect the two instances of the application.

  2. Next, you process data received from the other application to reflect state changes on the other instance of the application.

  3. Finally, state changes on the first instance of the application have to be packaged up and sent to the other instance.

Get Them Talking

There are a couple of ways to initiate the connection with Game Kit. The approach you take depends on how much control you need over the process or how much customization you want to do with the user interface. Of course the easiest way is to let Game Kit take care of the UI and all the details. We will take this approach in the application we build here.

Use the GKPeerPickerController to initiate the connection sequence. The code to kick it off is really simple: just create an instance, set the delegate, and tell the picker to show itself.

 - (void)startPeerPicker {
  GKPeerPickerController *picker =
  [[GKPeerPickerController alloc] init];
  picker.delegate = self;
  [picker show];
 }

When the picker appears, it will let the user know that it’s looking for other instances via an alert that looks like the first figure. Behind the scenes, the picker is doing all the tedious Bonjour configuration for us. I love how simple this is.

After we show the peer picker, it turns around and asks us (the delegate we provided) for the Game Kit session it should use to connect to other instances with.

 - (GKSession *)peerPickerController:(GKPeerPickerController *)picker
  sessionForConnectionType:(GKPeerPickerConnectionType)type {
  return [[[GKSession alloc]
  initWithSessionID:@"_com.prapprog.examples_"
  displayName:nil
  sessionMode:GKSessionModePeer]
  autorelease];
 }

The session id (_com.pragprog.examples_ in this case) is used by the application to find other instances that are compatible. So you will want to use unique names here for applications that would not understand each other. If you want to register your name to ensure that it is unique, you can do so on this site.

There are three modes the session can be in: client, server, or peer. You would use server mode if you wanted to connect up to four instances of the application, with one being the server and the other three being clients in client mode. Peer mode is better suited when you have just two instances of the application connecting. Since our game is just one-on-one, we’ll use the peer mode.

Now that we have the first instance of our application looking for peers, let’s start calling it instance A so we can distinguish it from the second instance, which we will imaginatively call B.

When instance B’s peer picker starts its game session, we will then have two instances of our application looking for each other. Since they both have the same session id, _com.pragprog.examples_, they can find each other. As each instance finds the other, the peer picker changes the UI to reflect peers that have been found. The peers will show up in a table view that looks like the second figure.

The next step in the two instances of the app getting connected is for one of the users to tap on one of the rows in this table view. When that happens, the other instance will get a prompt asking the user if they want to accept the connection. The prompt looks like the third figure.

Assuming that the user clicks the Accept button, the peer picker will then pass ownership of the session back to the application by calling another delegate method. Here is the code for that method.

 - (void)peerPickerController:(GKPeerPickerController *)picker
  didConnectPeer:(NSString *)peerId
  toSession:(GKSession *)session {
  if([peerId hash] > [session.peerID hash]) {
  _status = kServerGameStartedClientStatus;
  } else {
  _status = kServerGameStartedServerStatus;
  }
  self.peerId = peerId;
  self.gameSession = session;
  self.gameSession.delegate = self;
  [self.gameSession setDataReceiveHandler:self
  withContext:NULL];
  [picker dismiss];
  picker.delegate = nil;
  [picker autorelease];
 }

The first part of this method is a really simple method to figure out which peer should be responsible for creating new disks to show up. The peer with the highest-hashing peer identifier will be placed in server status, the other will be placed in client status. Keep in mind that this does not affect the Game Kit session mode that we started with (peer in this case): the client and session states here are merely part of this simple game. In a more sophisticated game you’d probably want to have both peers responsible for the game state.

Next, we take ownership of the session by setting the gameSession property to the session passed into this delegate method. We also become the session’s delegate so we get notifications of interesting things happening on the session.

We also set this object as the data receiver handler. That means that as the other instance (A or B) sends data over the network, this object will get a message call with the data. We will look at that code in just a minute.

Finally we dismiss the picker and nil out its delegate. Congratulations! The two processes are now connected and ready to start sending data to each other.

Let the Games Begin!

Now that A and B are connected and ready to send data back and forth to each other, we need to listen to the session and inform the user if the connection is lost. Since we are the session’s delegate, we get the following callback if the connection is lost.

 - (void)session:(GKSession *)session
  peer:(NSString *)peerID
  didChangeState:(GKPeerConnectionState)state {
  if(_status != kServerPickerShownStatus) {
  if(GKPeerStateDisconnected == state) {
  // got disconnected from the other peer
  NSString *message =
  [NSString stringWithFormat:@"Could not reconnect with %@.",
  [session displayNameForPeer:peerID]];
  UIAlertView *alert = [[UIAlertView alloc]
  initWithTitle:@"Lost Connection"
  message:message
  delegate:self
  cancelButtonTitle:@"End Game"
  otherButtonTitles:nil];
  [alert show];
  [alert release];
  }
  }
 }

The session tells its delegate about all state changes for all connected sessions, so we could also use this callback to let the user know if new peers become available. In this simple game, though, we don’t care about anything but the other instance dropping. When the drop happens, we give the user an alert panel, and when they click the “End Game” button, the alert panel calls us back with this message:

 - (void)alertView:(UIAlertView *)alertView
 clickedButtonAtIndex:(NSInteger)buttonIndex {
  // index zero is the 'End Game' button, the only one
  if(buttonIndex == 0) {
  _status = kServerNotStartedStatus;
  }
 }

All we do it switch back to the “not started” status. When the game is in this state it will show the peer picker UI again.

Now that we have seen the code for the mechanics of getting connected, we need to take a look at the game’s execution loop.

The game loop is what drives the game. In our case we will push new disks into the game or take old hit or un-hit disks out of the game. It is important that our game loop does not execute faster than the refresh rate of the screen, as a lot of extra action could be going on that the user would never see.

In the iPhone SDK 3.0 release, Apple added a new class to Core Animation called CADisplayLink that makes it easy for us to coordinate with the display refresh. The code looks like this:

 - (id) init {
  self = [super init];
  if (self != nil) {
 ...
  self.link = [CADisplayLink displayLinkWithTarget:self
  selector:@selector(gameLoop)];
  self.link.frameInterval = 2;
 ...
  }
  return self;
 }
 
 - (void)start {
  [self.link addToRunLoop:[NSRunLoop mainRunLoop]
  forMode:NSDefaultRunLoopMode];
 }

We create the display link in the init method, then some time later our game object is told to start. On start, the link is placed onto the main run loop. Then its selector is fired on the main thread at the appropriate time.

As I said earlier, the gameLoop method is responsible for the action of the game. Here is the code for our really simple game loop:

 - (void)gameLoop {
  _loopId++;
  if(kServerNotStartedStatus == _status) {
  _status = kServerPickerShownStatus;
  [self startPeerPicker];
  } else if(kServerGameStartedServerStatus == _status) {
  if(0 == (_loopId % _diskFrequency)) {
  srandom(_loopId);
  long x = random() % 300 + 10;
  long y = random() % 460 + 10;
  CGPoint location =
  CGPointMake((CGFloat)x, (CGFloat)y);
  long colorIndex = random() % _colors.count;
  NSMutableData *data =
  [NSMutableData dataWithBytes:&location
  length:sizeof(location)];
  [data appendBytes:&colorIndex
  length:sizeof(colorIndex)];
  [self sendMessageTypeID:kServerNewDiskMessageType
  data:data];
  UIColor *color =
  [_colors objectAtIndex:colorIndex];
  [self.delegate diskAtPoint:location
  withColor:color];
  }
  }
 }

If the game is in “not started” status, then we display the peer picker. If the game is in “server” status, then we create a new randomly located disk, push it to the other instances, and display it.

Sending the data is easy, thanks again to Game Kit. Here is the code for pushing data to the other peer:

 - (void)sendMessageTypeID:(ServerPacketTypeId)packetTypeId data:
  (NSData *)data {
  NSMutableData *sentData = [NSMutableData data];
  [sentData appendBytes:&packetTypeId
  length:sizeof(packetTypeId)];
  [sentData appendData:data];
  [self.gameSession sendData:sentData
  toPeers:[NSArray arrayWithObject:self.peerId]
  withDataMode:GKSendDataUnreliable
  error:nil];
 }

First we put the type of packet into the data we are going to send. On the other side, we use this first entry to determine how to decipher the data and what to do with it once it’s unpacked. We then add the data to send to the package and push it over the game session to the other peers. (Or peer: in this case it’s just one.)

We’re sending the data in the unreliable mode simply to show how it’s done. In unreliable mode the data goes out over UDP, which means that it might not ever arrive or might arrive in a different order than it was sent. The other possibility is to send the data in reliable mode, which uses TCP, ensuring that the data arrives and that it arrives in the correct order.

Now that we’re sending data, let’s look at the code that processes that data as it’s received on the other side of the connection:

 - (void) receiveData:(NSData *)data
  fromPeer:(NSString *)peer
  inSession:(GKSession *)session
  context:(void *)context {
  ServerPacketTypeId typeId;
  [data getBytes:&typeId length:sizeof(typeId)];
  if(kServerNewDiskMessageType == typeId) {
  CGPoint point;
  NSRange range;
  range.location = sizeof(typeId);
  range.length = sizeof(point);
  [data getBytes:&point range:range];
  NSUInteger colorIndex = 0;
  range.location = range.location + range.length;
  range.length = sizeof(colorIndex);
  [data getBytes:&colorIndex range:range];
  [self.delegate diskAtPoint:point
  withColor:
  [_colors objectAtIndex:colorIndex]];
  } else if(kServerSmashMessageType == typeId) {
 ...
  }
 }

Recall that our game object is the data receiver for the session. Each time the session receives data, it packages it up and sends it to us via this method.

We decode the message by looking at the type of the message. When a new disk message is received, we pull the location and color from the data, then pass that off to our delegate to create a new instance of the disk for this application.

And that’s it. As you can see, getting two instances of an application connected is simple with Game Kit. Looking at the bigger picture, this handy framework provides us with a simple abstraction that makes it easy to send and receive data between iPhones.

Bill Dudney is co-author of iPhone SDK Development. He is a software developer and entrepreneur currently building software for the Mac. Bill started his computing career on a NeXT cube with a magneto-optical drive running NeXTStep 0.9. Over the years Bill migrated into the Java world where he worked for years on building cool enterprise software. But he never forgot his roots and how much fun it was to write software that did cool things for normal people. Bill is back to AppKit to stay. You can follow him on his blog.