Chapter 14. Going Head to Head

In this chapter, you will learn how to create a game that lets two people play against each other using two iPhones or iPod touches. To accomplish this, we will use the GameKit framework, which allows iPhone apps to communicate using a wireless technology called Bluetooth.

Hello Pong!

A great place to begin our journey is the "Hello World" of computer games: Pong. It's a perfect example to use in this chapter for two reasons. If you have never played this game before, this is your chance to get acquainted with and be inspired by one of the great classics. And what's even more important, the simplicity of its rules allows us to avoid spending too much time on trying to understand the game mechanics, so we can focus on the networking aspects instead.

Note

In Pong, each player controls a paddle, which is used to bounce the ball to the opposite side of the field. Your goal is to make your opponent miss the ball. Each time that happens, the ball gets moved to the center of the screen and the game begins again. In the original game, you got a point every time your opponent missed. For the sake of simplicity, we are going to skip that feature. However, I encourage you to be creative and expand the game to make it as interesting and engaging as possible. After all, that's what game development is all about.

We are going to start with a version of the game where you control one paddle and the computer controls the other, as shown in Figure 14-1. We will then gradually add code that lets you play against another human player. By the end of this chapter, you will be able to install a copy of the game on a friend's iPhone or iPod touch and finally determine which one of you is better at Pong by battling it out in front of a cheering crowd.

Pong—visually, this game is as simple as they come.

Figure 14.1. Pong—visually, this game is as simple as they come.

Download the source code that accompanies this book from the Apress web site, and unzip the archive to a folder on your computer. Locate the GKPong project and open it in Xcode. Run the game to get a better understanding of how it works and what it looks like.

Before we start changing it, let's quickly go through the classes that comprise this app:

  • Paddle is responsible for loading the image of the Pong paddle and keeping track of the paddle's velocity and position.

  • Ball encapsulates all of the logic for displaying and moving the ball on the screen.

  • GKPongViewController is responsible for handling user input and managing the interactions between various views and game objects.

  • GKPongAppDelegate doesn't do much work beyond displaying the main view right after the app launches.

What makes this game tick? Touch event handling implemented in GKPongViewController allows the player to control one of the paddles. The gameLoop method, which is executed approximately 30 times a second, is responsible for moving the ball and the other paddle.

Using Peer Picker to Find a Human Opponent

So, what did you think about competing against a computer? "Boring," you say? That's what I thought. Let's fix it by dismantling the very code that controls that other paddle, and adding the ability to find people around us who are also running GKPong and would like to play.

Instead of jumping into the game right away after tapping through the welcome screen, our player now needs to be presented with some UI that allows searching for an opponent. Luckily for us, Apple has already created a standardized UI that does exactly that. All we need to do is initialize an instance of GKPeerPickerController and put it up on the screen. This class is part of the GameKit framework, which needs to be added to our project: right-click Frameworks, select Add

Using Peer Picker to Find a Human Opponent

Let's go ahead and make the necessary changes to GKPongViewController.m:

#import "GKPongViewController.h"

#define INITIAL_BALL_SPEED 4.5
#define INITIAL_BALL_DIRECTION (0.3 * M_PI)

@implementation GKPongViewController

@synthesize gameLoopTimer;

- (void)processOneFrameComputerPlayer {
  float distance = ball.center.x - topPaddle.center.x;
  static const float kMaxComputerDistance = 10.0;

  if ( fabs(distance) > kMaxComputerDistance ) {
distance = kMaxComputerDistance * (fabs(distance) / distance);
}

  [topPaddle moveHorizontallyByDistance:distance inViewFrame:self.view.frame];
}

- (void)gameLoop {
  if ( gameState != gameStatePlaying ) {
    return;
  }
  [bottomPaddle processOneFrame];
  [topPaddle processOneFrame];
  [self processOneFrameComputerPlayer];
  [ball processOneFrame];
}

- (void)ballMissedPaddle:(Paddle*)paddle {
  if ( paddle == topPaddle ) {
    didWeWinLastRound = YES;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Your opponent missed!


        Tap to serve the ball."];
  }
  else {
    didWeWinLastRound = NO;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Looks like you missed...


        Tap to serve the ball."];
  }
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  if ( gameState == gameStatePlaying ) {
    UITouch *touch = [[event allTouches] anyObject];
    paddleGrabOffset = bottomPaddle.center.x - [touch locationInView:touch.view].x;
  }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  if ( gameState == gameStatePlaying ) {
    UITouch *touch = [[event allTouches] anyObject];
    float distance = ([touch locationInView:touch.view].x + paddleGrabOffset) - bottomPaddle.center.x;
    [bottomPaddle moveHorizontallyByDistance:distance inViewFrame:self.view.frame];
  }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  UITouch *touch = [[event allTouches] anyObject];

  if ( gameState == gameStateLaunched && touch.tapCount > 0 ) {
    [self hideAnnouncement];
    gameState = gameStatePlaying;
    [self startGame];
    gameState = gameStateLookingForOpponent;
    GKPeerPickerController *picker;
    picker = [[GKPeerPickerController alloc] init];
    picker.delegate = self;
    [picker show];
  }
  else if ( gameState == gameStateWaitingToServeBall && touch.tapCount > 0 ) {
    [self hideAnnouncement];
    gameState = gameStatePlaying;
    [self resetBall];
  }
}

- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker {
  picker.delegate = nil;
  [picker autorelease];

  [self showAnnouncement:@"Welcome to GKPong!


      Please tap to begin."];
  gameState = gameStateLaunched;
}

- (void)startGame {
  topPaddle.center = CGPointMake(self.view.frame.size.width/2, topPaddle.frame.size.height);
  bottomPaddle.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height - bottomPaddle.frame.size.height);
  [self resetBall];

  [self.view addSubview:topPaddle.view];
  [self.view addSubview:bottomPaddle.view];
  [self.view addSubview:ball.view];
self.gameLoopTimer = [NSTimer scheduledTimerWithTimeInterval:0.033 target:self selector:@selector(gameLoop) userInfo:nil repeats:YES];
}

- (void)viewDidLoad {
  [super viewDidLoad];

  sranddev();

  announcementLabel = [[AnnouncementLabel alloc] initWithFrame:self.view.frame];
  announcementLabel.center = CGPointMake(announcementLabel.center.x, announcementLabel.center.y - 23.0);

  topPaddle = [[Paddle alloc] init];
  bottomPaddle = [[Paddle alloc] init];

  ball = [[Ball alloc] initWithField:self.view.frame topPaddle:topPaddle bottomPaddle:bottomPaddle];
  ball.delegate = self;

  [self showAnnouncement:@"Welcome to GKPong!


      Please tap to begin."];
  didWeWinLastRound = NO;
  gameState = gameStateLaunched;
}

- (void)showAnnouncement:(NSString*)announcementText {
  announcementLabel.text = announcementText;
  [self.view addSubview:announcementLabel];
}

- (void)hideAnnouncement {
  [announcementLabel removeFromSuperview];
}

- (void)resetBall {
  [ball reset];
  ball.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height/2);
  ball.direction = INITIAL_BALL_DIRECTION + ((didWeWinLastRound)? 0: M_PI);
  ball.speed = INITIAL_BALL_SPEED;
}

- (void)dealloc {
  [topPaddle release];
  [bottomPaddle release];
  [ball release];
  [announcementLabel removeFromSuperview];
  [announcementLabel release];
  [gameLoopTimer invalidate];
  self.gameLoopTimer = nil;
  [super dealloc];
}
@end

In order to communicate user's actions to our application, peer picker needs a delegate that implements the GKPeerPickerControllerDelegate protocol. In our case, GKPongViewController takes on that responsibility.

Whenever the user dismisses the peer picker dialog by tapping the Cancel button, we would like to switch the view back to the welcome screen. We will do this by implementing the peerPickerControllerDidCancel: delegate method.

Also notice that we are keeping track of what the player is up to by changing the gameState variable. It is used throughout the code to determine the correct course of action whenever we need to respond to something that the user does. For example, we display peer picker in response to a tap on the screen only if gameState is equal to gameStateLaunched; that is, the welcome message is being shown.

Let's now make all of the necessary changes to GKPongViewController.h:

#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
#import "Ball.h"
#import "Paddle.h"
#import "AnnouncementLabel.h"

typedef enum {
  gameStateLaunched,
  gameStateLookingForOpponent,
  gameStateWaitingToServeBall,
  gameStatePlaying
} GameState;

@interface GKPongViewController : UIViewController <BallDelegate> {
@interface GKPongViewController : UIViewController <BallDelegate, GKPeerPickerControllerDelegate> {
  Paddle *topPaddle;
  Paddle *bottomPaddle;
  float paddleGrabOffset;

  Ball *ball;
  float initialBallDirection;

  AnnouncementLabel *announcementLabel;

  BOOL didWeWinLastRound;
  NSTimer *gameLoopTimer;
  GameState gameState;
}

@property (retain, nonatomic) NSTimer *gameLoopTimer;

- (void)showAnnouncement:(NSString*)announcementText;
- (void)hideAnnouncement;
- (void)startGame;
- (void)resetBall;
@end

What Does It Look Like?

Go ahead and fire up two instances of the game. Tap through the welcome screen in both apps, and you should see the peer picker. It will take a few moments for the devices to discover each other. Then you should be able to select your opponent on one device and confirm or decline the invitation to play on the other.

Note

If you are using two iPhone OS devices to run the app, make sure to always redeploy the code to both of your test devices every time you want to run a new version of the game. That way, you won't run into strange problems that sometimes result from the fact that the two apps don't work the same way due to one of them being out of date. You can also use an iPhone OS device in conjunction with the iPhone simulator to run GKPong. Just make sure that both your computer and the iPhone/iPod touch are connected to the same wireless network, as mentioned in the previous chapter.

Figures 14-2 through 14-7 show what this sequence looks like on my second-generation iPod touch and iPhone 3G, called Petro iPod 2G and Petro 3G, respectively.

The GKPong app has been launched. As soon as user taps the screen, the peer picker will appear.

Figure 14.2. The GKPong app has been launched. As soon as user taps the screen, the peer picker will appear.

The GameKit framework makes sure that Bluetooth is enabled before search for peers can begin.

Figure 14.3. The GameKit framework makes sure that Bluetooth is enabled before search for peers can begin.

Once Bluetooth is enabled, both devices start looking for peers.

Figure 14.4. Once Bluetooth is enabled, both devices start looking for peers.

After some time, devices detect each other's presence, and users are finally able to choose their opponent.

Figure 14.5. After some time, devices detect each other's presence, and users are finally able to choose their opponent.

The user of the Petro iPod 2G chose Petro 3G as his opponent. The invitation to play must be accepted by the other party before game can begin.

Figure 14.6. The user of the Petro iPod 2G chose Petro 3G as his opponent. The invitation to play must be accepted by the other party before game can begin.

The user is notified when his invitation is declined.

Figure 14.7. The user is notified when his invitation is declined.

How Does It Work?

Are you curious as to what actually happens when the peer picker goes to work? If so, let's step back from the code for a moment and take a look behind the scenes. You don't necessarily need to understand this part before moving on to the next topic, so feel free to skip this section if you're not interested.

It turns out that GKPeerPickerController does quite a few things on our behalf. First and foremost, it advertises our application's presence to everyone who is in the vicinity and is willing to listen. That includes all other GameKit-enabled applications that are running on Bluetooth-capable iPhones or iPod touches located within a couple dozen feet from us and actively looking for peers.

There can be several different types of apps running at the same time, and the framework makes sure that our GKPong game is not going to accidentally discover and connect to a chess application, for example. In order to accomplish all of this, GameKit employs a networking protocol called Bonjour (introduced in the previous chapter and detailed in the next chapter), which was adopted to work over Bluetooth as of version 3.0 of the iPhone SDK.

Once a peer of the same type (another GKPong app) is found, the peer picker displays its name in the list of available opponents. As soon as the user taps that row, Bonjour does one last thing: It figures out what exactly needs to be done in order to connect to that device. When that is done, we can finally start exchanging data.

Making the Connection

Notice how nothing happens when you click the Accept button in our game? That's because we don't actually have any handling for it. Switch to GKPongViewController.m and add the following code:

#import "GKPongViewController.h"

#define INITIAL_BALL_SPEED 4.5
#define INITIAL_BALL_DIRECTION (0.3 * M_PI)

@implementation GKPongViewController

@synthesize gameLoopTimer;
@synthesize gkPeerID, gkSession;

- (void)gameLoop {
  if ( gameState != gameStatePlaying ) {
    return;
  }
  [bottomPaddle processOneFrame];
  [topPaddle processOneFrame];
  [ball processOneFrame];
}

- (void)ballMissedPaddle:(Paddle*)paddle {
  if ( paddle == topPaddle ) {
    didWeWinLastRound = YES;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Your opponent missed!


       Tap to serve the ball."];
  }
  else {
    didWeWinLastRound = NO;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Looks like you missed...


        Tap to serve the ball."];
  }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  if ( gameState == gameStatePlaying ) {
    UITouch *touch = [[event allTouches] anyObject];
    paddleGrabOffset = bottomPaddle.center.x - [touch locationInView:touch.view].x;
  }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  if ( gameState == gameStatePlaying ) {
    UITouch *touch = [[event allTouches] anyObject];
    float distance = ([touch locationInView:touch.view].x + paddleGrabOffset) - bottomPaddle.center.x;
    [bottomPaddle moveHorizontallyByDistance:distance inViewFrame:self.view.frame];
  }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[event allTouches] anyObject];

  if ( gameState == gameStateLaunched && touch.tapCount > 0 ) {
    [self hideAnnouncement];
    gameState = gameStateLookingForOpponent;

    GKPeerPickerController *picker;
    picker = [[GKPeerPickerController alloc] init];
    picker.delegate = self;
    [picker show];
  }
  else if ( gameState == gameStateWaitingToServeBall && touch.tapCount > 0 ) {
    [self hideAnnouncement];
    gameState = gameStatePlaying;
    [self resetBall];
  }
}

- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker {
                picker.delegate = nil;
  [picker autorelease];

  [self showAnnouncement:@"Welcome to GKPong!


      Please tap to begin."];
  gameState = gameStateLaunched;
}

- (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session {
    self.gkPeerID = peerID;
    self.gkSession = session;

    [picker dismiss];
    picker.delegate = nil;
    [picker autorelease];
}

- (void)startGame {
  topPaddle.center = CGPointMake(self.view.frame.size.width/2, topPaddle.frame.size.height);
  bottomPaddle.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height - bottomPaddle.frame.size.height);
  [self resetBall];

  [self.view addSubview:topPaddle.view];
  [self.view addSubview:bottomPaddle.view];
  [self.view addSubview:ball.view];

  self.gameLoopTimer = [NSTimer scheduledTimerWithTimeInterval:0.033 target:self selector:@selector(gameLoop) userInfo:nil repeats:YES];
}

- (void)viewDidLoad {
  [super viewDidLoad];

  sranddev();
announcementLabel = [[AnnouncementLabel alloc] initWithFrame:self.view.frame];
  announcementLabel.center = CGPointMake(announcementLabel.center.x, announcementLabel.center.y - 23.0);

  topPaddle = [[Paddle alloc] init];
  bottomPaddle = [[Paddle alloc] init];

  ball = [[Ball alloc] initWithField:self.view.frame topPaddle:topPaddle bottomPaddle:bottomPaddle];
  ball.delegate = self;

  [self showAnnouncement:@"Welcome to GKPong!


      Please tap to begin."];
  didWeWinLastRound = NO;
  gameState = gameStateLaunched;
}

- (void)showAnnouncement:(NSString*)announcementText {
  announcementLabel.text = announcementText;
  [self.view addSubview:announcementLabel];
}

- (void)hideAnnouncement {
  [announcementLabel removeFromSuperview];
}

- (void)resetBall {
  [ball reset];
  ball.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height/2);
  ball.direction = INITIAL_BALL_DIRECTION + ((didWeWinLastRound)? 0: M_PI);
  ball.speed = INITIAL_BALL_SPEED;
}

- (void)dealloc {
  [topPaddle release];
  [bottomPaddle release];
  [ball release];
  [announcementLabel removeFromSuperview];
  [announcementLabel release];
  [gameLoopTimer invalidate];
  self.gameLoopTimer = nil;
  [super dealloc];
}
@end

By calling peerPickerController:didConnectPeer:toSession, the picker tells its delegate that it has found an opponent, or peer, identified by the string peerID, and that a connection (also known as session) with that peer has been established. From this point on, gkSession object is responsible for all of the communication between the two players/applications.

Let's not forget to add the necessary declarations to GKPongViewController.h:

#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
#import "Ball.h"
#import "Paddle.h"
#import "AnnouncementLabel.h"

typedef enum {
  gameStateLaunched,
  gameStateLookingForOpponent,
  gameStateWaitingToServeBall,
  gameStatePlaying
} GameState;

@interface GKPongViewController : UIViewController <BallDelegate, GKPeerPickerControllerDelegate> {
  Paddle *topPaddle;
  Paddle *bottomPaddle;
  float paddleGrabOffset;

  Ball *ball;
  float initialBallDirection;

  AnnouncementLabel *announcementLabel;

  BOOL didWeWinLastRound;
  NSTimer *gameLoopTimer;
  GameState gameState;

  NSString *gkPeerID;
  GKSession *gkSession;
}

@property (retain, nonatomic) NSTimer *gameLoopTimer;
@property (retain, nonatomic) NSString *gkPeerID;
@property (retain, nonatomic) GKSession *gkSession;

- (void)showAnnouncement:(NSString*)announcementText;
- (void)hideAnnouncement;
- (void)startGame;
- (void)resetBall;
@end

Sending and Receiving Messages

Now that we found another player who is willing to interact with us, it's time to see what else goes into making a multiplayer game.

Whenever you play a game that's confined to one device, all movements and changes to in-game objects happen inside the same application's memory space. In GKPong, when a user touches the screen, we process that event and change the paddle's position accordingly. When the ball bounces off a paddle, we recalculate the ball's coordinates, speed, and direction, in addition to moving its image on the screen. If we are playing against a computer, no one else in the world needs to know anything about how those variables change.

Now, imagine that our friend Bob wants to play the game with us, and he will be using his own iPhone. Obviously, Bob would like to see our paddle move on his screen every time we move it on ours. The same is true for the ball—every time it bounces, both players need to see the change in the ball's trajectory as soon as possible.

One of the biggest challenges in designing multiplayer games is determining what information needs to be communicated between different instances of the app. You might be tempted to just send all of the data that you have as often as you can, but that would quickly overwhelm the device's communication channels and drain its battery. Therefore, we must carefully pick what information needs to be shared with our peer and what can be omitted. As we continue building the game, you will notice that this theme comes up again and again.

Let's take it one step at a time, starting with what happens when two people agree to play against each other.

Rolling the Dice

Think back to the original version of our application. Who tapped the screen to serve the ball every time a new round started? That's right—the human player. But now that we have not one but two human players, who will get to go first? That's easy—whoever loses a round gets to start the next one. But what if we haven't played a single round yet? In real-world sports, they sometimes toss a coin to determine who is going to start the match. That sounds like a good way to resolve the question (if the coin is fair, that is). In our case, a random-number generator can serve the same purpose. And so, we are going to play a mini game that will determine who gets to kick off the Pong match. Instead of being a coin toss, it's more akin to rolling dice. Figure 14-8 shows what that algorithm looks like.

Dice roll algorithm involves two apps generating and exchanging random numbers in order to determine who will start the game. This diagram comes into play after user of the Petro 3G iPhone accepts the invitation to play.

Figure 14.8. Dice roll algorithm involves two apps generating and exchanging random numbers in order to determine who will start the game. This diagram comes into play after user of the Petro 3G iPhone accepts the invitation to play.

Keep in mind that we can't actually start playing Pong before we figure out which player will kick off the match. Therefore, rolling the dice is the first thing that needs to happen after the connection is established. Let's open GKPongViewController.m and add the following code:

#import "GKPongViewController.h"

#define INITIAL_BALL_SPEED 4.5
#define INITIAL_BALL_DIRECTION (0.3 * M_PI)
enum {
  gkMessageDiceRolled
};

@implementation GKPongViewController

@synthesize gameLoopTimer;
@synthesize gkPeerID, gkSession;

- (void)gameLoop {
  if ( gameState != gameStatePlaying ) {
    return;
  }
  [bottomPaddle processOneFrame];
  [topPaddle processOneFrame];
  [ball processOneFrame];
}

- (void)ballMissedPaddle:(Paddle*)paddle {
  if ( paddle == topPaddle ) {
    didWeWinLastRound = YES;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Your opponent missed!


        Tap to serve the ball."];
  }
  else {
    didWeWinLastRound = NO;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Looks like you missed...


        Tap to serve the ball."];
  }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  if ( gameState == gameStatePlaying ) {
    UITouch *touch = [[event allTouches] anyObject];
    paddleGrabOffset = bottomPaddle.center.x - [touch locationInView:touch.view].x;
  }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  if ( gameState == gameStatePlaying ) {
    UITouch *touch = [[event allTouches] anyObject];
    float distance = ([touch locationInView:touch.view].x + paddleGrabOffset) - bottomPaddle.center.x;
    [bottomPaddle moveHorizontallyByDistance:distance inViewFrame:self.view.frame];
  }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  UITouch *touch = [[event allTouches] anyObject];

  if ( gameState == gameStateLaunched && touch.tapCount > 0 ) {
    [self hideAnnouncement];
    gameState = gameStateLookingForOpponent;
GKPeerPickerController *picker;
    picker = [[GKPeerPickerController alloc] init];
    picker.delegate = self;
    [picker show];
  }
  else if ( gameState == gameStateWaitingToServeBall && touch.tapCount > 0 ) {
    [self hideAnnouncement];
    gameState = gameStatePlaying;
    [self resetBall];
  }
}

- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker {
                picker.delegate = nil;
  [picker autorelease];

  [self showAnnouncement:@"Welcome to GKPong!


      Please tap to begin."];
  gameState = gameStateLaunched;
}

- (void)diceRolled {
  NSMutableData *data = [NSMutableData dataWithCapacity:1+sizeof(int)];
  char messageType = gkMessageDiceRolled;
  [data appendBytes:&messageType length:1];
  myDiceRoll = rand();
  [data appendBytes:&myDiceRoll length:sizeof(int)];

  [gkSession sendDataToAllPeers:data withDataMode:GKSendDataReliable error:nil];
}

- (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session {
    self.gkPeerID = peerID;
    self.gkSession = session;
    [gkSession setDataReceiveHandler:self withContext:NULL];

    [picker dismiss];
    picker.delegate = nil;
    [picker autorelease];

    gameState = gameStateRollingDice;
    [self diceRolled];
}

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context {
  const char *incomingPacket = (const char *)[data bytes];
  char messageType = incomingPacket[0];


  switch (messageType) {
    case gkMessageDiceRolled: {
      int peerDiceRoll = *(int *)(incomingPacket + 1);
if ( peerDiceRoll == myDiceRoll ) {
        [self diceRolled];
        return;
      }
      else if ( myDiceRoll > peerDiceRoll ) {
        [self showAnnouncement:@"The game is about to begin.


            Tap to serve the ball!"];
        gameState = gameStateWaitingToServeBall;
        didWeWinLastRound = NO;
      }
      else {
        [self showAnnouncement:@"The game is about to begin.


            Waiting for the opponent..."];
        gameState = gameStateWaitingForOpponentToServeBall;
        didWeWinLastRound = YES;
      }

      [self startGame];
      break;
    }
  }
}

- (void)startGame {
  topPaddle.center = CGPointMake(self.view.frame.size.width/2, topPaddle.frame.size.height);
  bottomPaddle.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height - bottomPaddle.frame.size.height);
  [self resetBall];

  [self.view addSubview:topPaddle.view];
  [self.view addSubview:bottomPaddle.view];
  [self.view addSubview:ball.view];

  self.gameLoopTimer = [NSTimer scheduledTimerWithTimeInterval:0.033 target:self selector:@selector(gameLoop) userInfo:nil repeats:YES];
}

- (void)viewDidLoad {
  [super viewDidLoad];

  sranddev();

  announcementLabel = [[AnnouncementLabel alloc] initWithFrame:self.view.frame];
  announcementLabel.center = CGPointMake(announcementLabel.center.x, announcementLabel.center.y - 23.0);

  topPaddle = [[Paddle alloc] init];
  bottomPaddle = [[Paddle alloc] init];

  ball = [[Ball alloc] initWithField:self.view.frame topPaddle:topPaddle bottomPaddle:bottomPaddle];
  ball.delegate = self;

  [self showAnnouncement:@"Welcome to GKPong!

Please tap to begin."];
  didWeWinLastRound = NO;
  gameState = gameStateLaunched;
}

- (void)showAnnouncement:(NSString*)announcementText {
  announcementLabel.text = announcementText;
  [self.view addSubview:announcementLabel];
}

- (void)hideAnnouncement {
  [announcementLabel removeFromSuperview];
}

- (void)resetBall {
  [ball reset];
  ball.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height/2);
  ball.direction = INITIAL_BALL_DIRECTION + ((didWeWinLastRound)? 0: M_PI);
  ball.speed = INITIAL_BALL_SPEED;
}

- (void)dealloc {
  [topPaddle release];
  [bottomPaddle release];
  [ball release];
  [announcementLabel removeFromSuperview];
  [announcementLabel release];
  [gameLoopTimer invalidate];
  self.gameLoopTimer = nil;
  [super dealloc];
}
@end

The new diceRolled method does two things: comes up with a random number, and then sends that number over to our opponent. Whenever we want to send some data using GKSession, we first need to pack it into NSData object. We are using NSMutableData in order to make the process of putting our message together a bit easier:

NSMutableData *data = [NSMutableData dataWithCapacity:1+sizeof(int)];

We are anticipating that this is not the only kind of message that we'll be sending to our opponent. Therefore, we are introducing a notion of message type, which is an enum that will help us figure out what to do with a particular message when it arrives.

char messageType = gkMessageDiceRolled;
[data appendBytes:&messageType length:1];

We also need to actually roll the dice, store the resulting value for later use, and include it in the network message:

myDiceRoll = rand();
[data appendBytes:&myDiceRoll length:sizeof(int)];

After we put the message together in this way, it will look something like Figure 14-9.

The resulting "dice rolled" message is 5 bytes long. The body of the message is an integer that contains the value of our dice roll.

Figure 14.9. The resulting "dice rolled" message is 5 bytes long. The body of the message is an integer that contains the value of our dice roll.

Finally, we are ready to ship it out. Since our app might not function correctly if the message is dropped, we are asking for a guaranteed delivery by specifying GKSendDataReliable:

[gkSession sendDataToAllPeers:data withDataMode:GKSendDataReliable error:nil];

Now, look back at how we initialized the NSMutableData object. See that dataWithCapacity part? This is a bit of an optimization on our part. We know exactly how much data will be sent, even before we start putting the message together: it's 1 byte for the messageType and sizeof(int) bytes for the one integer that we want to include. By using dataWithCapacity, we are telling NSMutableData how much memory we will need in advance, therefore possibly avoiding unnecessary memory allocations later on. Strictly speaking, this step might not be entirely necessary for such a small amount of memory. This is just an example of an optimization that you might want to think about when working on your own apps.

Let's go through the other changes that we made to the code just now. First, whenever a new connection is established, we want to make sure that any messages that arrive via that session are delivered directly to us. We tell GKSession that GKPongViewController is responsible for handling the incoming data:

[gkSession setDataReceiveHandler:self withContext:NULL];

Whenever the data does arrive, GKSession will attempt to call the receiveData:fromPeer:inSession:context method on the object that was designated as the handler. It is then up to us to make sense of that data. Remember how we put our "dice roll" message together? The first byte indicated the type. Keep in mind that we will have more kinds of messages in the future. That's why we are putting the switch/case statement in from the very beginning:

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context {
  const char *incomingPacket = (const char *)[data bytes];
  char messageType = incomingPacket[0];

  switch (messageType) {
    case gkMessageDiceRolled: {

If the message is indeed of type gkMessageDiceRolled, we will try to extract the value of our opponent's dice roll by interpreting the group of bytes that comes after the message type byte as an integer:

int peerDiceRoll = *(int *)(incomingPacket + 1);

If you aren't too sure what that line means, here is an expanded version:

const char *bytesAfterMessageType = incomingPacket + 1;
      int *peerDiceRollPointer = (int *)bytesAfterMessageType;
      int peerDiceRoll = *peerDiceRollPointer;

We need to resolve possible ties. If both players come up with the same value, we roll the dice again, by calling diceRolled:

if ( peerDiceRoll == myDiceRoll ) {
        [self diceRolled];
        return;
      }

Note that by the time this code gets called, the myDiceRoll variable will already contain results of our own dice roll attempt, because diceRolled is guaranteed to be called before receiveData:fromPeer:inSession:context. Think about why that is.

If we happen to win this mini game of dice, we get to serve the ball first. If not, we will wait for the opponent to do so. Once that is settled, we are finally ready to start the game:

else if ( myDiceRoll > peerDiceRoll ) {
        [self showAnnouncement:@"The game is about to begin.


            Tap to serve the ball!"];
        gameState = gameStateWaitingToServeBall;
        didWeWinLastRound = NO;
      }
      else {
        [self showAnnouncement:@"The game is about to begin.


            Waiting for the opponent..."];
        gameState = gameStateWaitingForOpponentToServeBall;
        didWeWinLastRound = YES;
      }

      [self startGame];

Before compiling, don't forget to modify GKPongViewController.h accordingly:

#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
#import "Ball.h"
#import "Paddle.h"
#import "AnnouncementLabel.h"

typedef enum {
  gameStateLaunched,
  gameStateLookingForOpponent,
  gameStateRollingDice,
  gameStateWaitingForOpponentToServeBall,
  gameStateWaitingToServeBall,
  gameStatePlaying
} GameState;

@interface GKPongViewController : UIViewController <BallDelegate, GKPeerPickerControllerDelegate> {
  Paddle *topPaddle;
  Paddle *bottomPaddle;
  float paddleGrabOffset;
Ball *ball;
  float initialBallDirection;

  AnnouncementLabel *announcementLabel;

  BOOL didWeWinLastRound;
  NSTimer *gameLoopTimer;
  GameState gameState;

  NSString *gkPeerID;
  GKSession *gkSession;

  int myDiceRoll;
}

@property (retain, nonatomic) NSTimer *gameLoopTimer;
@property (retain, nonatomic) NSString *gkPeerID;
@property (retain, nonatomic) GKSession *gkSession;

- (void)showAnnouncement:(NSString*)announcementText;
- (void)hideAnnouncement;
- (void)startGame;
- (void)resetBall;
@end

Congratulations! If this chapter were a hill, we would be at the top of it right now. From this point on, the going should get easier. Make sure to go back and review any material that might seem a little fuzzy.

This is a good spot to take a break. Come back when you are ready to continue.

Ready...Set...Go!

Let's try to run the app in its current state and figure out what else needs to be done. It seems like our dice rolling is working, so one of the players gets to start the match by tapping the screen. But the other player never finds out that the game has actually begun, and his screen just keeps saying, "Waiting for opponent...." Well, if you think about it, it's not really surprising, since we don't have any code that would notify our opponent that the ball has been served. In order to fix that, let's edit GKPongViewController.m and create a new message type:

enum {
  gkMessageDiceRolled,
  gkMessageBallServed
};

When is it a good time to send such a message to our peer? Whenever we tap the screen to begin the game, of course:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  UITouch *touch = [[event allTouches] anyObject];

  if ( gameState == gameStateLaunched && touch.tapCount > 0 ) {
    [self hideAnnouncement];
gameState = gameStateLookingForOpponent;

    GKPeerPickerController *picker;
    picker = [[GKPeerPickerController alloc] init];
    picker.delegate = self;
    [picker show];
  }
  else if ( gameState == gameStateWaitingToServeBall && touch.tapCount > 0 ) {
    [self hideAnnouncement];
    gameState = gameStatePlaying;
    [self resetBall];

    char messageType = gkMessageBallServed;
    [gkSession sendDataToAllPeers:[NSData dataWithBytes:&messageType length:1] withDataMode:GKSendDataReliable error:nil];
  }
}

In addition, we also need to interpret and process the message when it comes in:

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context {
  const char *incomingPacket = (const char *)[data bytes];
  char messageType = incomingPacket[0];

  switch (messageType) {
    case gkMessageDiceRolled: {
      int peerDiceRoll = *(int *)(incomingPacket + 1);
      if ( peerDiceRoll == myDiceRoll ) {
        [self diceRolled];
        return;
      }
      else if ( myDiceRoll > peerDiceRoll ) {
        [self showAnnouncement:@"The game is about to begin.


            Tap to serve the ball!"];
        gameState = gameStateWaitingToServeBall;
        didWeWinLastRound = NO;
      }
      else {
        [self showAnnouncement:@"The game is about to begin.


            Waiting for the opponent..."];
        gameState = gameStateWaitingForOpponentToServeBall;
        didWeWinLastRound = YES;
      }

      [self startGame];
      break;
    }

    case gkMessageBallServed:
      didWeWinLastRound = YES;
      [self resetBall];
      [self hideAnnouncement];
      gameState = gameStatePlaying;
      break;
  }
}

At this point, you might be wondering how the ball knows to start or stop moving without us calling any kind of a stop or start method. The mechanism is quite simple: As soon as the dice roll is executed and we call startGame, a timer is scheduled. This timer starts calling the gameLoop method 30 times a second or so. In turn, gameLoop calls [ball processOneFrame], which makes the ball recalculate and update its position; however, this happens only if our gameState variable is equal to gameStatePlaying. That's why whenever we change the value of gameState to something else, the objects on the screen stop moving. When we set it to gameStatePlaying, the game comes alive again.

Hits and Misses

Launch the game. If we've done everything right, the ball should be getting served correctly now, on both screens. But we still have a problem: Whenever the ball misses or bounces off our paddle, the other player doesn't know about it, and the game gets out of sync until the next time the ball is served. By now, you should have a pretty good idea about how to correct this. Let's first handle the easier of the two cases: communicating when the ball misses our paddle.

GKPongViewController.m needs a few changes, such as a new message type:

enum {
  gkMessageDiceRolled,
  gkMessageBallServed,
  gkMessageBallMissed
};

Since all of the ball-movement processing happens inside of a class called Ball, how does GKPongViewController know when the ball misses one of the paddles? Why, through delegation, of course! ballMissedPaddle: is called, and that's how we find out. Let's also tell our peer about it:

- (void)ballMissedPaddle:(Paddle*)paddle {
  if ( paddle == topPaddle ) {
    didWeWinLastRound = YES;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Your opponent missed!


        Tap to serve the ball."];
  }
  else {
    didWeWinLastRound = NO;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Looks like you missed...


        Tap to serve the ball."];
    char messageType = gkMessageBallMissed;
    [gkSession sendDataToAllPeers:[NSData dataWithBytes:&messageType length:1] withDataMode:GKSendDataReliable error:nil];
  }
}

And last, but not least, we process the incoming message:

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context {
  const char *incomingPacket = (const char *)[data bytes];
char messageType = incomingPacket[0];

  switch (messageType) {
    case gkMessageDiceRolled: {
      int peerDiceRoll = *(int *)(incomingPacket + 1);
      if ( peerDiceRoll == myDiceRoll ) {
        [self diceRolled];
        return;
      }
      else if ( myDiceRoll > peerDiceRoll ) {
        [self showAnnouncement:@"The game is about to begin.


            Tap to serve the ball!"];
        gameState = gameStateWaitingToServeBall;
        didWeWinLastRound = NO;
      }
      else {
        [self showAnnouncement:@"The game is about to begin.


            Waiting for the opponent..."];
        gameState = gameStateWaitingForOpponentToServeBall;
        didWeWinLastRound = YES;
      }

      [self startGame];
      break;
    }

    case gkMessageBallServed:
      didWeWinLastRound = YES;
      [self resetBall];
      [self hideAnnouncement];
      gameState = gameStatePlaying;
      break;

    case gkMessageBallMissed:
      didWeWinLastRound = YES;
      [self showAnnouncement:@"You won the last round!


          Waiting for the opponent..."];
      gameState = gameStateWaitingForOpponentToServeBall;
      break;
  }
}

That was easy enough—a little too easy, in fact. Something doesn't feel right. Take a look at the ballMissedPaddle: method again. We seem to still be trying to detect and handle the case when our ball flies past the paddle that's located at the top of the screen, which is our local representation of the opponent's paddle. Why would we need to do that if we just added the code that makes our peer tell us when the ball misses their paddle?

Also notice that we still didn't add the code that communicates our peer's paddle position changes to us, which means that at any given point in time, we cannot be certain where our opponent's paddle is. This, in turn, means that the code that tries to detect whether or not the ball flew past the paddle located on the top of the screen might produce incorrect results. This leaves us with only one sure way to find out about our opponent's failures to bounce the ball back to us: listen for gkMessageBallMissed messages.

The two pieces of code that we are about to remove do, of course, make sense if both paddles are under our complete control, as was the case when we were playing against a computer.

Let's get rid of that conditional statement:

- (void)ballMissedPaddle:(Paddle*)paddle {
  if ( paddle == topPaddle ) {
    didWeWinLastRound = YES;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Your opponent missed!


        Tap to serve the ball."];
  }
  else {
    didWeWinLastRound = NO;
    gameState = gameStateWaitingToServeBall;
    [self showAnnouncement:@"Looks like you missed...


        Tap to serve the ball."];
    char messageType = gkMessageBallMissed;
    [gkSession sendDataToAllPeers:[NSData dataWithBytes:&messageType length:1] withDataMode:GKSendDataReliable error:nil];
  }
}

Let's also remove the logic that is responsible for actually detecting the miss. It's located in Ball.m:

- (void)processOneFrame {
  // Recalculate our position
  CGPoint ballPosition = view.center;

  ballPosition.x -= speed * cos(direction);
  ballPosition.y -= speed * sin(direction);
  view.center = ballPosition;

  // Are we hitting the wall on the right?
  if ( ballPosition.x >= (fieldFrame.size.width - view.frame.size.width/2) ) {
    if ( !alreadyBouncedOffWall ) {
      self.direction = M_PI - direction;
      alreadyBouncedOffWall = YES;
    }
  }
  // Are we hitting the wall on the left?
  else if ( ballPosition.x <= view.frame.size.width/2 ) {
    if ( !alreadyBouncedOffWall ) {
      self.direction = M_PI - direction;
      alreadyBouncedOffWall = YES;
    }
  }
  else {
    alreadyBouncedOffWall = NO;
  }

  // If we have moved out of the bouncing zone, reset "already bounced" flag
if ( alreadyBouncedOffPaddle && ballPosition.y + view.frame.size.height/2 < bottomPaddle.frame.origin.y &&
      ballPosition.y - view.frame.size.height/2 > topPaddle.frame.origin.y + topPaddle.frame.size.height) {
    alreadyBouncedOffPaddle = NO;
  }

  // Are we moving past bottom paddle?
  if ( ballPosition.y + view.frame.size.height/2 >= (bottomPaddle.frame.origin.y) && ! alreadyBouncedOffPaddle ) {
    // Bounce or miss?
    if ( ballPosition.x + view.frame.size.width/2 > bottomPaddle.frame.origin.x &&
        ballPosition.x - view.frame.size.width/2 < bottomPaddle.frame.origin.x + bottomPaddle.frame.size.width ) {
      [self bounceBallOffPaddle:bottomPaddle];
    }
    else {
      // We missed the paddle
      [delegate ballMissedPaddle:bottomPaddle];
    }
  }

  // Are we moving past top paddle?
  if ( ballPosition.y - view.frame.size.height/2 <= topPaddle.frame.origin.y + topPaddle.frame.size.height && ! alreadyBouncedOffPaddle ) {
    // Bounce or miss?
    if ( ballPosition.x + view.frame.size.width/2 > topPaddle.frame.origin.x &&
        ballPosition.x - view.frame.size.width/2 < topPaddle.frame.origin.x + topPaddle.frame.size.width ) {
      [self bounceBallOffPaddle:topPaddle];
    }
    else {
      // We missed the paddle
      [delegate ballMissedPaddle:topPaddle];
    }
  }
}

That does it for the "ball missed" case. What happens when the ball bounces? We need to go through a similar process there, with one caveat: As things stand right now, the ball handles the bounces but doesn't tell GKPongViewController when they happen. Let's add another method to the BallDelegate protocol, located in Ball.h:

@protocol BallDelegate
- (void)ballMissedPaddle:(Paddle*)paddle;
- (void)ballBounced;
@end

And add the code to call the new delegate method, also located in Ball.m:

- (void)processOneFrame {
  // Recalculate our position
  CGPoint ballPosition = view.center;

  ballPosition.x -= speed * cos(direction);
  ballPosition.y -= speed * sin(direction);
  view.center = ballPosition;
// Are we hitting the wall on the right?
  if ( ballPosition.x >= (fieldFrame.size.width - view.frame.size.width/2) ) {
    if ( !alreadyBouncedOffWall ) {
      self.direction = M_PI - direction;
      alreadyBouncedOffWall = YES;
    }
  }
  // Are we hitting the wall on the left?
  else if ( ballPosition.x <= view.frame.size.width/2 ) {
    if ( !alreadyBouncedOffWall ) {
      self.direction = M_PI - direction;
      alreadyBouncedOffWall = YES;
    }
  }
  else {
    alreadyBouncedOffWall = NO;
  }

  // If we have moved out of the bouncing zone, reset "already bounced" flag
  if ( alreadyBouncedOffPaddle && ballPosition.y + view.frame.size.height/2 < bottomPaddle.frame.origin.y &&
      ballPosition.y - view.frame.size.height/2 > topPaddle.frame.origin.y + topPaddle.frame.size.height) {
    alreadyBouncedOffPaddle = NO;
  }

  // Are we moving past bottom paddle?
  if ( ballPosition.y + view.frame.size.height/2 >= (bottomPaddle.frame.origin.y) && ! alreadyBouncedOffPaddle ) {
    // Bounce or miss?
    if ( ballPosition.x + view.frame.size.width/2 > bottomPaddle.frame.origin.x &&
        ballPosition.x - view.frame.size.width/2 < bottomPaddle.frame.origin.x + bottomPaddle.frame.size.width ) {
      [self bounceBallOffPaddle:bottomPaddle];
      [delegate ballBounced];
    }
    else {
      // We missed the paddle
      [delegate ballMissedPaddle:bottomPaddle];
    }
  }
}

Now that GKPongViewController possesses all of the necessary information, let's share some of it with our peer.

Add the following code to GKPongViewController.m:

enum {
  gkMessageDiceRolled,
  gkMessageBallServed,
  gkMessageBallMissed,
  gkMessageBallBounced
};

typedef struct {
  CGPoint position;
float direction;
  float speed;
} BallInfo;

@implementation GKPongViewController

@synthesize gameLoopTimer;
@synthesize gkPeerID, gkSession;

- (void)gameLoop {
  if ( gameState != gameStatePlaying ) {
    return;
  }
  [bottomPaddle processOneFrame];
  [topPaddle processOneFrame];
  [ball processOneFrame];
}

- (void)ballMissedPaddle:(Paddle*)paddle {
  didWeWinLastRound = NO;
  gameState = gameStateWaitingToServeBall;
  [self showAnnouncement:@"Looks like you missed...


      Tap to serve the ball."];

  char messageType = gkMessageBallMissed;
  [gkSession sendDataToAllPeers:[NSData dataWithBytes:&messageType length:1] withDataMode:GKSendDataReliable error:nil];
}

- (void)ballBounced {
  NSMutableData *data = [NSMutableData dataWithCapacity:1+sizeof(BallInfo)];
  char messageType = gkMessageBallBounced;
  [data appendBytes:&messageType length:1];

  BallInfo message;
  message.position = ball.center;
  message.direction = ball.direction;
  message.speed = ball.speed;
  [data appendBytes:&message length:sizeof(BallInfo)];

  [gkSession sendDataToAllPeers:data withDataMode:GKSendDataReliable error:nil];
}

Scroll down and add another block of code:

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context {
  const char *incomingPacket = (const char *)[data bytes];
  char messageType = incomingPacket[0];

  switch (messageType) {
    case gkMessageDiceRolled: {
      int peerDiceRoll = *(int *)(incomingPacket + 1);
      if ( peerDiceRoll == myDiceRoll ) {
        [self diceRolled];
        return;
}
      else if ( myDiceRoll > peerDiceRoll ) {
        [self showAnnouncement:@"The game is about to begin.


            Tap to serve the ball!"];
        gameState = gameStateWaitingToServeBall;
        didWeWinLastRound = NO;
      }
      else {
        [self showAnnouncement:@"The game is about to begin.


            Waiting for the opponent..."];
        gameState = gameStateWaitingForOpponentToServeBall;
        didWeWinLastRound = YES;
      }

      [self startGame];
      break;
    }

    case gkMessageBallServed:
      didWeWinLastRound = YES;
      [self resetBall];
      [self hideAnnouncement];
      gameState = gameStatePlaying;
      break;

    case gkMessageBallMissed:
      didWeWinLastRound = YES;
      [self showAnnouncement:@"You won the last round!


          Waiting for the opponent..."];
      gameState = gameStateWaitingForOpponentToServeBall;
      break;

    case gkMessageBallBounced: {
      BallInfo peerBallInfo = *(BallInfo *)(incomingPacket + 1);
      ball.direction = peerBallInfo.direction + M_PI;
      ball.speed = peerBallInfo.speed;
      ball.center = CGPointMake(self.view.frame.size.width - peerBallInfo.position.x, self.view.frame.size.height - peerBallInfo.position.y);
      break;
    }
  }
}

These latest modifications follow the same pattern that we've applied before:

  • Define a new message type.

  • Whenever an event that interests us happens, send a message of the new type to our peer.

  • Make sure to properly handle incoming messages of the new type.

There is one slight deviation, however. In order to make constructing the network message easier, we are adding a new struct that will hold all of the variables that are necessary to accurately describe the position, speed, and direction of the ball after it bounces off our paddle.

typedef struct {
  CGPoint position;
  float direction;
  float speed;
} BallInfo;

Now, instead of adding several variables to the NSMutableData one by one, we can add everything in one shot, without needing to worry about their order:

- (void)ballBounced {
  NSMutableData *data = [NSMutableData dataWithCapacity:1+sizeof(BallInfo)];
  char messageType = gkMessageBallBounced;
  [data appendBytes:&messageType length:1];

  BallInfo message;
  message.position = ball.center;
  message.direction = ball.direction;
  message.speed = ball.speed;
  [data appendBytes:&message length:sizeof(BallInfo)];

  [gkSession sendDataToAllPeers:data withDataMode:GKSendDataReliable error:nil];
}

It also makes our life a little easier on the message-processing end. We can access parts of the structure by their names instead of needing to juggle pointers. The code comes out much cleaner:

case gkMessageBallBounced: {
      BallInfo peerBallInfo = *(BallInfo *)(incomingPacket + 1);
      ball.direction = peerBallInfo.direction + M_PI;
      ball.speed = peerBallInfo.speed;
      ball.center = CGPointMake(self.view.frame.size.width - peerBallInfo.position.x, self.view.frame.size.height - peerBallInfo.position.y);
      break;
    }

A side question for you to think about: Why can't we use the values sent to us by our peer without first changing them in particular ways (see ball.direction and ball.center in the preceding code)?

Let's step back for a moment. Why does the peer have to send us all of the updated variables instead of just telling us the fact that the ball has bounced and letting us calculate the rest? The answer is exactly the same as it was when we asked the question about detecting the ball misses locally versus waiting for gkMessageBallMissed messages: We might not have enough information to correctly calculate the outcome of the bounce at any given point in time.

For example, in order to make the game a bit more realistic, we have some code in there that continuously calculates the velocity of the paddle and uses that value whenever the paddle comes in contact with the ball and we need to calculate trajectory of bounce. And as far as our opponent's paddle is concerned, we don't really have up-to-date velocity information at this point. And even after we add the code to receive and process our peer paddle's movements, such an approach wouldn't necessarily work. You'll see why shortly.

The Paddle Comes Alive

Our game is almost there. The ball bounces correctly now, and the misses seem to get registered pretty accurately. But it still doesn't work quite right, since the paddle located on the top of the screen never moves—it looks like we are simply bouncing the ball against a wall.

Let's write a bit more code to make it right. Open GKPongViewController.m and add the new message type:

enum {
  gkMessageDiceRolled,
  gkMessageBallServed,
  gkMessageBallMissed,
  gkMessageBallBounced,
  gkMessagePaddleMoved
};

Next, let's compose and send the message to our peer:

- (void)paddleMoved {
  NSMutableData *data = [NSMutableData dataWithCapacity:1+sizeof(int)+sizeof(float)];
  char messageType = gkMessagePaddleMoved;
  [data appendBytes:&messageType length:1];
  myLastPaddleUpdateID++;
  [data appendBytes:&myLastPaddleUpdateID length:sizeof(int)];
  float x = bottomPaddle.center.x;
  [data appendBytes:&x length:sizeof(float)];

  [gkSession sendDataToAllPeers:data withDataMode:GKSendDataUnreliable error:nil];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  if ( gameState == gameStatePlaying ) {
    UITouch *touch = [[event allTouches] anyObject];
    paddleGrabOffset = bottomPaddle.center.x - [touch locationInView:touch.view].x;
  }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  if ( gameState == gameStatePlaying ) {
    UITouch *touch = [[event allTouches] anyObject];
    float distance = ([touch locationInView:touch.view].x + paddleGrabOffset) - bottomPaddle.center.x;

    float previousX = bottomPaddle.center.x;
    [bottomPaddle moveHorizontallyByDistance:distance inViewFrame:self.view.frame];
    if ( bottomPaddle.center.x != previousX ) {
      [self paddleMoved];
    }
  }
}

And finally, let's not forget to process the message when it comes in:

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context {
  const char *incomingPacket = (const char *)[data bytes];
  char messageType = incomingPacket[0];

  switch (messageType) {
    case gkMessageDiceRolled: {
      int peerDiceRoll = *(int *)(incomingPacket + 1);
      if ( peerDiceRoll == myDiceRoll ) {
        [self diceRolled];
        return;
      }
      else if ( myDiceRoll > peerDiceRoll ) {
        [self showAnnouncement:@"The game is about to begin.


            Tap to serve the ball!"];
        gameState = gameStateWaitingToServeBall;
        didWeWinLastRound = NO;
      }
      else {
        [self showAnnouncement:@"The game is about to begin.


            Waiting for the opponent..."];
        gameState = gameStateWaitingForOpponentToServeBall;
        didWeWinLastRound = YES;
      }

      [self startGame];
      break;
    }

    case gkMessageBallServed:
      didWeWinLastRound = YES;
      [self resetBall];
      [self hideAnnouncement];
      gameState = gameStatePlaying;
      break;

    case gkMessageBallMissed:
      didWeWinLastRound = YES;
      [self showAnnouncement:@"You won the last round!


          Waiting for the opponent..."];
      gameState = gameStateWaitingForOpponentToServeBall;
      break;

    case gkMessageBallBounced: {
      BallInfo peerBallInfo = *(BallInfo *)(incomingPacket + 1);
      ball.direction = peerBallInfo.direction + M_PI;
      ball.speed = peerBallInfo.speed;
      ball.center = CGPointMake(self.view.frame.size.width - peerBallInfo.position.x, self.view.frame.size.height - peerBallInfo.position.y);
      break;
    }

    case gkMessagePaddleMoved: {
      int paddleUpdateID = *(int *)(incomingPacket + 1);
      if ( paddleUpdateID <= peerLastPaddleUpdateID ) {
        return;
      }
peerLastPaddleUpdateID = paddleUpdateID;

      float x = *(float *)(incomingPacket + 1 + sizeof(int));
      topPaddle.center = CGPointMake(self.view.frame.size.width - x, topPaddle.center.y);
      break;
    }
  }
}

- (void)startGame {
  topPaddle.center = CGPointMake(self.view.frame.size.width/2, topPaddle.frame.size.height);
  bottomPaddle.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height - bottomPaddle.frame.size.height);
  [self resetBall];

  myLastPaddleUpdateID = 0;
  peerLastPaddleUpdateID = 0;

  [self.view addSubview:topPaddle.view];
  [self.view addSubview:bottomPaddle.view];
  [self.view addSubview:ball.view];

  self.gameLoopTimer = [NSTimer scheduledTimerWithTimeInterval:0.033 target:self selector:@selector(gameLoop) userInfo:nil repeats:YES];
}

Once again, the idea is pretty straightforward: Every time our paddle changes its position due to touch events, we send the updated position over to our peer so that it can be reflected on his screen. (Keep in mind that we don't let the paddle move off visible part of the application window completely, which means that sometimes we must leave the object stationary, even though the player is still dragging her finger across the screen.)

float previousX = bottomPaddle.center.x;
    [bottomPaddle moveHorizontallyByDistance:distance inViewFrame:self.view.frame];
    if ( bottomPaddle.center.x != previousX ) {
      [self paddleMoved];
    }

But the way we are sending the position updates is a bit different from the other messages that we've been using so far. We are using GKSendDataUnreliable, as opposed to GKSendDataReliable data mode:

- (void)paddleMoved {
  NSMutableData *data = [NSMutableData dataWithCapacity:1+sizeof(int)+sizeof(float)];
  char messageType = gkMessagePaddleMoved;
  [data appendBytes:&messageType length:1];
  myLastPaddleUpdateID++;
  [data appendBytes:&myLastPaddleUpdateID length:sizeof(int)];
  float x = bottomPaddle.center.x;
  [data appendBytes:&x length:sizeof(float)];

  [gkSession sendDataToAllPeers:data withDataMode:GKSendDataUnreliable error:nil];
}

What we are telling GKSession here is that we don't really care whether or not this message gets delivered to the other side. It might get there, or it might not. It might also get delivered out of order, which means that if we send message A followed by message B, there is a possibility that our peer will see message B first and then message A. Or one of those messages might not arrive at all. In the worst case, neither message A nor message B will get there.

You might be wondering why would we want to use such a sloppy delivery method. Why don't we care about our data being transmitted safely and reliably? Why don't we use same delivery method that we've used before? What's so different about this particular kind of message? The answer to all of those questions consists of two parts.

First of all, reliable delivery of packets is an expensive operation. Instead of just sending one packet and moving on to the next, we now must wait for a confirmation that the data arrived safely to the other side. We must also be prepared to retransmit the message in case we aren't sure that the delivery happened successfully. All of that takes time. In a game where objects can move fairly often, usage of such an expensive transmission method might lead to performance problems.

The other part of the answer is that we don't really rely on the paddle position updates for anything important. Even if one of the messages doesn't arrive, we will most likely receive another one in the future—whenever the paddle moves again. The worst thing that can happen is the user noticing the opponent's paddle moving a bit erratically. And even that is not such a big deal. More than half of the time, our players will focus their eyes on the ball; otherwise, it would be pretty darn difficult to bounce it. Think about what would happen if gkMessageDiceRolled, gkMessageBallServed. or gkMessageBallBounced didn't arrive successfully. Such failure would result in a disconnected gaming experience, where the two players might see very different things on their respective screens. By comparison, missing one or two paddle position updates every so often is a small issue.

This is a vast and interesting topic that can easily consume a chapter all by itself. But the bottom line is that we are using the fastest possible delivery method for sending data that is not very important to guaranteeing our game play experience.

As I mentioned before, whenever GKSendDataUnreliable is used, messages can get lost, but they also can arrive out of order. Think about what this means for the paddle position updates. As you're moving your finger from right to left, your peer is constantly getting updates as to the new position of my paddle, and the object gets redrawn on their screen accordingly. If everything is going fine, paddle's X coordinate is always increasing. Even if some messages are lost, all that means is that the paddle object pauses briefly, and then resumes its movement after fresh packets arrive. But whenever a message is delayed and arrives later than the ones that were sent after it, as far as receiving end is concerned, the information that that message contains is already out of date. If our peer did move the paddle on the screen in response to that message, it would have jumped backward, as depicted in Figure 14-10.

Device A sends out "Paddle position: 65" after "Paddle position: 49," but the order gets mixed up during unreliable transmission. This results in device B recording that paddle jumped from position 100 to 65, which could produce a confusing, erratic animation.

Figure 14.10. Device A sends out "Paddle position: 65" after "Paddle position: 49," but the order gets mixed up during unreliable transmission. This results in device B recording that paddle jumped from position 100 to 65, which could produce a confusing, erratic animation.

What is a good way to protect our app from this? Changing the data mode to "reliable" would certainly fix this issue, but the cost of that solution is more than we are willing to pay. The approach that we are going to take is to drop the outdated packets. Each message will carry a sequence number in it. Every time a message is sent, the sequence number is incremented:

myLastPaddleUpdateID++;
  [data appendBytes:&myLastPaddleUpdateID length:sizeof(int)];

Whenever we receive a message with a sequence number lower than the last one, we discard it:

int paddleUpdateID = *(int *)(incomingPacket + 1);
  if ( paddleUpdateID <= peerLastPaddleUpdateID ) {
    return;
  }
  peerLastPaddleUpdateID = paddleUpdateID;

In order to avoid reusing sequence numbers and getting out of sync with our peer, we will reset them every time we start playing the game:

myLastPaddleUpdateID = 0;
  peerLastPaddleUpdateID = 0;

Now, messages that arrive late will get ignored, as seen in Figure 14-11.

It is easy to discard outdated messages, since each message now carries a sequence number. Here, device B will no longer render the paddle jumping incorrectly from position 100 to 65.

Figure 14.11. It is easy to discard outdated messages, since each message now carries a sequence number. Here, device B will no longer render the paddle jumping incorrectly from position 100 to 65.

The game is now fully functional. However, we can't call it a finished product just yet. We need to take care of one more thing.

Game Over: Handling Disconnects

Run the game once again. This time, observe what happens when one player closes the app while his opponent keeps on playing. That's right—nothing happens. The other app doesn't really do anything about it and keeps on running, completely oblivious. Sure, you and your opponent are very likely to be right next to each other and are perfectly capable of communicating whenever one of you exits the game (the limited range of Bluetooth won't let you roam too far apart). But the game should still detect and appropriately handle such scenario.

It turns out that we can ask GKSession to let us know when our peer gets disconnected, by providing a delegate. This gives us a chance to notify the player that the game is over and go back to the welcome screen. Let's make GKPongViewController implement the GKSessionDelegate protocol and add the necessary handler.

Modify GKPongViewController.h first:

@interface GKPongViewController : UIViewController <BallDelegate,
    GKPeerPickerControllerDelegate> {
    GKPeerPickerControllerDelegate, GKSessionDelegate> {

Then switch to GKPongViewController.m and add the following code:

- (void)peerPickerController:(GKPeerPickerController *)picker
    didConnectPeer:(NSString *)peerID toSession:(GKSession *)session {

  self.gkPeerID = peerID;
  self.gkSession = session;
  [gkSession setDataReceiveHandler:self withContext:NULL];
  gkSession.delegate = self;
[picker dismiss];
  picker.delegate = nil;
  [picker autorelease];

  gameState = gameStateRollingDice;
  [self diceRolled];
}

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

  if ( [gkPeerID isEqualToString:peerID] && state == GKPeerStateDisconnected ) {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Disconnection"
      message:@"Your opponent seems to have disconnected. Game is over."
      delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil];
    [alertView show];

    [gkSession disconnectFromAllPeers];
    gkSession.delegate = nil;
    self.gkSession = nil;
    self.gkPeerID = nil;

    [bottomPaddle.view removeFromSuperview];
    [topPaddle.view removeFromSuperview];
    [ball.view removeFromSuperview];
    [gameLoopTimer invalidate];

    [self showAnnouncement:@"Welcome to GKPong!

Please tap to begin."];
    gameState = gameStateLaunched;
  }
}

Let's go through the changes that we just implemented. First, whenever our peer gets disconnected, we let our player know by displaying a pop-up message. Then we must make sure that our GKSession object is cleaned up and released.

Before we can return to the welcome screen, both paddles and the ball get removed from the view. Also, in order to prevent CPU from doing unnecessary work while there is no game being played, we stop the gameLoopTimer. It will be scheduled again the next time startGame gets called.

Summary

Well, what do you think about the transformation that our little app just went through? We started with a game that was confined to one device, and gave it the ability to communicate with other copies of itself, turning the player's gaming experience from solitary and mechanical into social and competitive.

In the process, you learned how to use GameKit to establish Bluetooth connections between apps and allow them to exchange messages. As you probably realized by now, there is a lot more that happens behind those simple method calls that deliver your data or create sessions.

Once you are sure that you have a pretty good handle on everything that we just did, feel free to move on to the next chapter. There, we will dive deeper into the networking frameworks, and you'll learn how to create more complex multiplayer games.

Let's also not forget to enjoy the fruits of our labor. So, what are you waiting for? Call your friends—it's time to play Pong!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.221.61